diff --git a/docs/api.md b/docs/api.md
index 9b44219..ec9b93d 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1403,8 +1403,8 @@ is not initialized.
Returns the card's cached **EF.ARR copies** — the access-rule files the FCP
security attribute DO `'8B'` references (ISO 7816-4 Table 25). The copies
-are read once per card session when a card is equipped (`MF/2F06` and
-`ADF.USIM/6F06`, best effort) and served from memory, so this endpoint
+are read once per card session when a card is equipped (`MF/2F06`,
+`MF/7F10/6F06`, `MF/7F20/6F06` and `ADF.USIM/6F06`, best effort) and served from memory, so this endpoint
performs no card I/O; a disconnect clears the cache, and an `EF.ARR` write
through `/api/write` refreshes it.
diff --git a/frontend/help-ru.html b/frontend/help-ru.html
index 2e46d5b..d261f94 100644
--- a/frontend/help-ru.html
+++ b/frontend/help-ru.html
@@ -397,7 +397,7 @@
Подключение к локальному pysim-simple-server для работы с картой: введите URL сервера (по умолчанию http://127.0.0.1:8080) и нажмите Подключиться. Область статуса показывает состояние ридера/карты, а Подключить карту (пере)инициализирует карту после вставки. Подвкладки: Файловый менеджер, Командная строка pySim и Отправка APDU. «Профайлер» и «Симулятор» — отдельные вкладки верхнего уровня.
6.1 Файловый менеджер
- Дерево файловой системы отображается слева; выбор файла открывает панель деталей справа. Элементы сгруппированы: DF выше EF, сортировка по FID или символьному имени (пиллы и кнопка «Проверить все файлы» закреплены над прокручиваемым деревом; выбор сохраняется в localStorage). При выборе файла над содержимым также показываются FID, тип файла, размер / структура записей и декодированный FCI — в рамке с символьным именем файла. Если FCI ссылается на свои правила доступа через запись EF.ARR (атрибут безопасности 8B), правила указанной записи показываются с отступом под этой строкой: они берутся из копий EF.ARR карты — копии MF (2F06) и копии ADF.USIM (6F06): читаются один раз за сессию при equip, обновляются при сбросе/equip и после записи EF.ARR; снимок разрешает те же ссылки из собственной захваченной копии. Ссылка на копию ARR другого DF не разрешается (строка остаётся как есть).
+ Дерево файловой системы отображается слева; выбор файла открывает панель деталей справа. Элементы сгруппированы: DF выше EF, сортировка по FID или символьному имени (пиллы и кнопка «Проверить все файлы» закреплены над прокручиваемым деревом; выбор сохраняется в localStorage). При выборе файла над содержимым также показываются FID, тип файла, размер / структура записей и декодированный FCI — в рамке с символьным именем файла. Если FCI ссылается на свои правила доступа через запись EF.ARR (атрибут безопасности 8B), правила указанной записи показываются с отступом под этой строкой: они берутся из копий EF.ARR карты — копии MF (2F06), копий DF.TELECOM/DF.GSM (6F06) и копии ADF.USIM (6F06): читаются один раз за сессию при equip, обновляются при сбросе/equip и после записи EF.ARR; снимок разрешает те же ссылки из собственной захваченной копии. Побеждает копия собственного DF файла, с подъёмом до MF; ссылка, для которой в этой цепочке копии нет, остаётся неразрешённой, если только кандидат не единственный.
- Читать сырые данные / Декодировать — чтение выбранного файла в виде hex-дампа или таблицы декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); серверный pySim JSON того же чтения остаётся доступен в свёрнутом блоке pySim JSON (сервер). Подсвеченная кнопка — текущий вид; нажатие любой из них (пере)читывает файл. «Декодировать» недоступна, если для файла нет клиентского декодера (проверка по имени/FID до чтения).
- Редактировать сырые данные — изменение hex-данных, Сохранить для записи (или Отмена); transparent-файлы редактируются в textarea, record-файлы — по одному полю на запись с флажками выбора. Декодированный вид доступен только для чтения — «Редактировать сырые данные» сначала переключает на hex-вид и при необходимости читает файл.
diff --git a/frontend/help.html b/frontend/help.html
index a6ce0be..0c03b6b 100644
--- a/frontend/help.html
+++ b/frontend/help.html
@@ -396,7 +396,7 @@
Connects to a local pysim-simple-server for live card operations: enter the server URL (default http://127.0.0.1:8080) and press Connect. The status area shows the reader/card state, and Equip card (re)initializes the card after insertion. Sub-tabs: File manager, pySim command line, and Raw APDU. The Profiler and Simulator are separate top-level tabs.
6.1 File manager
- The file system tree is displayed on the left; selecting a file opens its detail pane on the right. Entries are grouped with DFs above EFs and sorted by FID or symbolic Name (pills pinned above the scrolling tree together with Probe all files; the choice is remembered in localStorage). Selecting a file also shows its FID, file type, size / record layout and the decoded FCI — in a bordered block labelled with the file’s symbolic name — above the content pane. When the FCI references its access rules through an EF.ARR record (security attribute 8B), the rules of the referenced record are shown as indented lines under that row: they are resolved from the card’s EF.ARR copies — the MF copy (2F06) and the ADF.USIM copy (6F06): read once per card session at equip, refreshed on reset/equip and after an EF.ARR write; a snapshot resolves the same references from its own captured copy. A reference to another DF’s ARR copy is not resolved (the row stays as it is).
+ The file system tree is displayed on the left; selecting a file opens its detail pane on the right. Entries are grouped with DFs above EFs and sorted by FID or symbolic Name (pills pinned above the scrolling tree together with Probe all files; the choice is remembered in localStorage). Selecting a file also shows its FID, file type, size / record layout and the decoded FCI — in a bordered block labelled with the file’s symbolic name — above the content pane. When the FCI references its access rules through an EF.ARR record (security attribute 8B), the rules of the referenced record are shown as indented lines under that row: they are resolved from the card’s EF.ARR copies — the MF copy (2F06), the DF.TELECOM/DF.GSM copies (6F06) and the ADF.USIM copy (6F06): read once per card session at equip, refreshed on reset/equip and after an EF.ARR write; a snapshot resolves the same references from its own captured copy. The copy of the file’s own DF wins, walking up to MF; a reference with no copy in that chain stays unresolved unless a single candidate exists.
- Read raw / Read decoded — read the selected file as a hex dump or as a decoded field table (client-side EF decoders: IMSI, ICCID, SPN, PLMN lists, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the server-side pySim JSON of the same read stays available in the collapsed pySim JSON (server) disclosure. The highlighted pill is the current view; clicking either pill (re-)reads the file. Read decoded is disabled when the client-side decoders do not cover the file (name/FID check, before reading).
- Edit raw — edit the raw hex data, Save to write back (or Cancel); transparent files use a textarea, record files one input per record with selection checkboxes. The decoded view is read-only — Edit raw switches to the hex view first and reads the file if needed.
diff --git a/frontend/index.html b/frontend/index.html
index 4fe5027..11294fd 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -20545,33 +20545,53 @@ function fcpWithoutArrRef(hex) {
// Access rules of the record(s) an FCP '8B' reference points at, resolved
// against a set of EF.ARR copies ({fid, path, records[]} - the server's
// /api/arr cache or a snapshot's own EF.ARR files). `path` is the tree path
-// of the file whose FCI is shown: it picks the ARR copy of that DF (the
-// ISO 7816-4 5.4.3.3 lookup order - ADF files use the ADF's 6F06 copy).
+// of the file whose FCI is shown: the ARR copy is looked up per the ISO
+// 7816-4 5.4.3.3 / TS 102 221 9.2.7 order - the file's own DF first, then
+// walking up to the ADF/MF (a DF/ADF level holds 6F06, MF holds 2F06).
// Returns {found, rules}: found = at least one referenced record exists;
// rules = the decoded rule lines (empty when nothing resolves - the caller
-// stays silent, v3.24.0).
+// stays silent, v3.24.0; v3.25.0 walk-up fix).
function fcpArrRules(ref, arrs, path) {
if (!ref || !arrs || !arrs.length) return { found: false, rules: [] };
- const df = (((path || '').split('/')[0]) || 'MF').toUpperCase();
+ // the file's DF chain, own DF first: MF/7F20/6F07 -> ['MF/7F20', 'MF'];
+ // ADF.USIM/6F07 -> ['ADF.USIM', 'MF']
+ const segs = ((path || '').toUpperCase().split('/')).filter(s => s);
+ const fileDf = segs.slice(0, -1);
+ const chain = [];
+ for (let i = fileDf.length; i > 0; i--) chain.push(fileDf.slice(0, i).join('/'));
+ if (!chain.includes('MF')) chain.push('MF');
+ const dfOf = a => String(a.path || '').toUpperCase().split('/').slice(0, -1).join('/');
+ const pickAt = (df, fid) => arrs.find(a =>
+ String(a.fid || '').toUpperCase() === fid && dfOf(a) === df) || null;
const pick = fid => {
const hits = arrs.filter(a => String(a.fid || '').toUpperCase() === fid);
if (!hits.length) return null;
- return hits.find(a => String(a.path || '').split('/')[0].toUpperCase() === df) || hits[0];
+ for (const df of chain) {
+ const hit = hits.find(a => dfOf(a) === df);
+ if (hit) return hit;
+ }
+ // no copy in the file's DF chain: only a single candidate is safe
+ return hits.length === 1 ? hits[0] : null;
};
const out = [];
let found = false;
- const add = (label, fid, record) => {
- const arr = pick(fid);
+ const add = (label, fid, record, arrOverride) => {
+ const arr = arrOverride !== undefined ? arrOverride : pick(fid);
const recs = (arr && arr.records) || [];
- if (!(record >= 1) || record > recs.length) return;
+ if (!(record >= 1) || record > recs.length) return false;
found = true;
const dec = efDecArr(efBytes(recs[record - 1] || ''));
for (const rule of ((dec && dec.rules) || [])) {
out.push((label ? label + ': ' : '') + rule);
}
+ return true;
};
if (ref.implicit) {
- add('', df.startsWith('ADF.') ? '6F06' : '2F06', ref.record);
+ // the implicitly known EF.ARR of the file's DF, walking up
+ for (const df of chain) {
+ const fid = df === 'MF' ? '2F06' : '6F06';
+ if (add('', fid, ref.record, pickAt(df, fid))) break;
+ }
} else if (ref.pairs) {
for (const p of ref.pairs) add(fcpSeidName(p.seid), ref.fid, p.record);
} else {
diff --git a/frontend/tests/profiler.test.js b/frontend/tests/profiler.test.js
index 774a208..682e145 100644
--- a/frontend/tests/profiler.test.js
+++ b/frontend/tests/profiler.test.js
@@ -1021,6 +1021,43 @@ test('ignoreFciRef compares the FCI without the EF.ARR reference', async () => {
assert.strictEqual(strict.checks.find(c => c.label === 'fci').ok, false);
});
+test('a 6F06 reference resolves to the file DF\'s own ARR copy', () => {
+ global.t = s => s;
+ // 8B 03 6F06 01: record 1 of the DF-tree copy
+ const hex = '621A82054221000F0583026F4F8A01058B036F06018802004B8801B0';
+ const dfCopy = { fid: '6F06', path: 'MF/7F20/6F06', records: ['8001019000'] };
+ const adfCopy = { fid: '6F06', path: 'ADF.USIM/6F06', records: ['800101A40683010A950108'] };
+ // the file's own DF copy wins (ISO 7816-4 5.4.3.3 walk-up)
+ const df = profilerFciPreviewItems(hex, { arr: [dfCopy, adfCopy], path: 'MF/7F20/6F07' });
+ assert.ok(df.includes(': always'), df);
+ assert.ok(!df.includes('ADM1'), df);
+ // an ADF.USIM file uses the ADF's copy
+ const adf = profilerFciPreviewItems(hex, { arr: [dfCopy, adfCopy], path: 'ADF.USIM/6F07' });
+ assert.ok(adf.includes(': ADM1'), adf);
+ // two candidates outside the file's chain stay unresolved (silent)
+ const none = profilerFciPreviewItems(hex, { arr: [dfCopy, adfCopy], path: 'MF/7F10/6F07' });
+ assert.ok(!none.includes('ADM1') && !none.includes(': always'), none);
+ // a single candidate outside the chain still resolves
+ const uniq = profilerFciPreviewItems(hex, { arr: [adfCopy], path: 'MF/7F10/6F07' });
+ assert.ok(uniq.includes(': ADM1'), uniq);
+ delete global.t;
+});
+
+test('the implicit form prefers the file DF\'s own ARR copy', () => {
+ global.t = s => s;
+ // 8B 01 01: implicit EF_ARR, record 1
+ const hex = '621882054221000F0583026F4F8A01058B01018002004B8801B0';
+ const dfCopy = { fid: '6F06', path: 'MF/7F20/6F06', records: ['8001019000'] };
+ const mfCopy = { fid: '2F06', path: 'MF/2F06', records: ['800101A40683010A950108'] };
+ // the DF's own copy wins
+ const df = profilerFciPreviewItems(hex, { arr: [dfCopy, mfCopy], path: 'MF/7F20/6F07' });
+ assert.ok(df.includes(': always'), df);
+ // without it the walk-up reaches the MF copy
+ const up = profilerFciPreviewItems(hex, { arr: [mfCopy], path: 'MF/7F20/6F07' });
+ assert.ok(up.includes(': ADM1'), up);
+ delete global.t;
+});
+
test('profilerResultAspects and the summary include Access rules', () => {
const res = { checks: [
{ label: 'exists', ok: true },
diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py
index fcdb3b8..aafadee 100644
--- a/pysim_simple_server/server.py
+++ b/pysim_simple_server/server.py
@@ -486,10 +486,11 @@ def _read_iccid(app):
# EF.ARR copies referenced by the FCP security attribute DO '8B' (ISO 7816-4
-# Table 25): the MF copy and the ADF.USIM copy (TS 102 221 13.4). The read
-# runs once per card session (_refresh_arr_cache); the PWA's file manager
-# resolves the referenced record's access rules against the cache.
-ARR_READ_PATHS = ('MF/2F06', 'ADF.USIM/6F06')
+# Table 25): the MF copy, the DF.TELECOM/DF.GSM copies (TS 51.011 7F10/7F20)
+# and the ADF.USIM copy (TS 102 221 13.4). The read runs once per card
+# session (_refresh_arr_cache); the PWA's file manager resolves the referenced
+# record's access rules against the cache (the file's DF chain wins).
+ARR_READ_PATHS = ('MF/2F06', 'MF/7F10/6F06', 'MF/7F20/6F06', 'ADF.USIM/6F06')
ARR_RECORD_LIMIT = 64
diff --git a/tests/test_arr.py b/tests/test_arr.py
index 0eba3c0..5fdea80 100644
--- a/tests/test_arr.py
+++ b/tests/test_arr.py
@@ -74,6 +74,13 @@ class ReadArrTests(unittest.TestCase):
self.assertEqual(arrs[0]['record_len'], 6)
self.assertEqual(arrs[1]['path'], 'ADF.USIM/6F06')
+ def test_reads_the_df_tree_copies(self):
+ app, _ = _app({'MF/2F06': ['9000'], 'MF/7F10/6F06': ['9000'],
+ 'MF/7F20/6F06': ['9000'], 'ADF.USIM/6F06': ['9000']})
+ arrs = _read(app)
+ self.assertEqual([a['path'] for a in arrs],
+ ['MF/2F06', 'MF/7F10/6F06', 'MF/7F20/6F06', 'ADF.USIM/6F06'])
+
def test_a_missing_copy_is_skipped(self):
app, _ = _app({'ADF.USIM/6F06': ['9000']})
arrs = _read(app)