v3.24.0: the EF.ARR access rules in the FCP view

The file manager's decoded FCI shows the '8B' security attribute
reference (EF_ARR 2F06, record 23).  The referenced record's access
rules are now rendered as indented muted lines under that row, decoded
with the existing EF.ARR record decoder (AM_DO/SC_DO conditions).

The server reads the card's ARR copies (MF/2F06 and ADF.USIM/6F06,
best effort) once per card session in _apply_equipped_card and keeps
them as server.arr; GET /api/arr serves the cache without the card
lock, a disconnect clears it, and a write to an ARR copy through
/api/write refreshes it.  The PWA fetches it lazily per card session
(pysimArrEnsure, keyed by card_session) and resolves the reference with
the selected file's DF context: an ADF.USIM file uses the ADF's 6F06
copy, the implicit reference form too (ISO 7816-4 5.4.3.3 lookup
order).  Unresolvable references stay silent.

The '8B' decode now covers all three ISO 7816-4 Table 25 forms:
implicit ARR byte / FID + record / FID + (SEID, ARR) pairs - the pairs
form was mis-decoded as a record before.  A snapshot resolves the same
references offline from its own captured EF.ARR copy.

Tests: the three forms, the resolution (DF preference, out-of-range
silence), the snapshot path, the server read/cache/route/write-refresh.
868 frontend + 856 Python; sw cache simple-v423.
This commit is contained in:
2026-10-10 00:30:45 +03:00
parent 9b4114012b
commit b6537bd54b
10 changed files with 521 additions and 14 deletions
+1 -1
View File
@@ -397,7 +397,7 @@
<p class="mb-3">Подключение к локальному <a href="https://github.com/anttro/simple" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-simple-server</a> для работы с картой: введите URL сервера (по умолчанию <code class="font-mono text-sm">http://127.0.0.1:8080</code>) и нажмите <strong>Подключиться</strong>. Область статуса показывает состояние ридера/карты, а <strong>Подключить карту</strong> (пере)инициализирует карту после вставки. Подвкладки: <strong>Файловый менеджер</strong>, <strong>Командная строка pySim</strong> и <strong>Отправка APDU</strong>. <strong>&laquo;Профайлер&raquo;</strong> и <strong>&laquo;Симулятор&raquo;</strong> — отдельные вкладки верхнего уровня.</p>
<h3 id="file-manager" class="text-lg font-medium mb-2">6.1 Файловый менеджер</h3>
<p class="text-sm mb-2">Дерево файловой системы отображается слева; выбор файла открывает панель деталей справа. Элементы сгруппированы: DF выше EF, сортировка по <strong>FID</strong> или символьному <strong>имени</strong> (пиллы и кнопка <strong>«Проверить все файлы»</strong> закреплены над прокручиваемым деревом; выбор сохраняется в <code class="font-mono text-sm">localStorage</code>). При выборе файла над содержимым также показываются FID, тип файла, размер / структура записей и декодированный FCI — в рамке с символьным именем файла.</p>
<p class="text-sm mb-2">Дерево файловой системы отображается слева; выбор файла открывает панель деталей справа. Элементы сгруппированы: DF выше EF, сортировка по <strong>FID</strong> или символьному <strong>имени</strong> (пиллы и кнопка <strong>«Проверить все файлы»</strong> закреплены над прокручиваемым деревом; выбор сохраняется в <code class="font-mono text-sm">localStorage</code>). При выборе файла над содержимым также показываются FID, тип файла, размер / структура записей и декодированный FCI — в рамке с символьным именем файла. Если FCI ссылается на свои правила доступа через запись <code class="font-mono text-sm">EF.ARR</code> (атрибут безопасности <code class="font-mono text-sm">8B</code>), правила указанной записи показываются с отступом под этой строкой: они берутся из копий <code class="font-mono text-sm">EF.ARR</code> карты (читаются один раз за сессию при equip, обновляются при сбросе/equip и после записи <code class="font-mono text-sm">EF.ARR</code>); снимок разрешает те же ссылки из собственной захваченной копии.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Читать сырые данные / Декодировать</strong> — чтение выбранного файла в виде hex-дампа или таблицы декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); серверный pySim JSON того же чтения остаётся доступен в свёрнутом блоке <em>pySim JSON (сервер)</em>. Подсвеченная кнопка — текущий вид; нажатие любой из них (пере)читывает файл. «Декодировать» недоступна, если для файла нет клиентского декодера (проверка по имени/FID до чтения).</li>
<li><strong>Редактировать сырые данные</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>); transparent-файлы редактируются в textarea, record-файлы — по одному полю на запись с флажками выбора. Декодированный вид доступен только для чтения — «Редактировать сырые данные» сначала переключает на hex-вид и при необходимости читает файл.</li>
+1 -1
View File
@@ -396,7 +396,7 @@
<p class="mb-3">Connects to a local <a href="https://github.com/anttro/simple" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-simple-server</a> for live card operations: enter the server URL (default <code class="font-mono text-sm">http://127.0.0.1:8080</code>) and press <strong>Connect</strong>. The status area shows the reader/card state, and <strong>Equip card</strong> (re)initializes the card after insertion. Sub-tabs: <strong>File manager</strong>, <strong>pySim command line</strong>, and <strong>Raw APDU</strong>. The <strong>Profiler</strong> and <strong>Simulator</strong> are separate top-level tabs.</p>
<h3 id="file-manager" class="text-lg font-medium mb-2">6.1 File manager</h3>
<p class="text-sm mb-2">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 <strong>FID</strong> or symbolic <strong>Name</strong> (pills pinned above the scrolling tree together with <strong>Probe all files</strong>; the choice is remembered in <code class="font-mono text-sm">localStorage</code>). Selecting a file also shows its FID, file type, size / record layout and the decoded FCI — in a bordered block labelled with the file&rsquo;s symbolic name — above the content pane.</p>
<p class="text-sm mb-2">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 <strong>FID</strong> or symbolic <strong>Name</strong> (pills pinned above the scrolling tree together with <strong>Probe all files</strong>; the choice is remembered in <code class="font-mono text-sm">localStorage</code>). Selecting a file also shows its FID, file type, size / record layout and the decoded FCI — in a bordered block labelled with the file&rsquo;s symbolic name — above the content pane. When the FCI references its access rules through an <code class="font-mono text-sm">EF.ARR</code> record (security attribute <code class="font-mono text-sm">8B</code>), the rules of the referenced record are shown as indented lines under that row: they are resolved from the card&rsquo;s <code class="font-mono text-sm">EF.ARR</code> copies (read once per card session at equip, refreshed on reset/equip and after an <code class="font-mono text-sm">EF.ARR</code> write); a snapshot resolves the same references from its own captured copy.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Read raw / Read decoded</strong> — 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 <em>pySim JSON (server)</em> disclosure. The highlighted pill is the current view; clicking either pill (re-)reads the file. <strong>Read decoded</strong> is disabled when the client-side decoders do not cover the file (name/FID check, before reading).</li>
<li><strong>Edit raw</strong> — edit the raw hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>); transparent files use a textarea, record files one input per record with selection checkboxes. The decoded view is read-only — <strong>Edit raw</strong> switches to the hex view first and reads the file if needed.</li>
+125 -7
View File
@@ -1905,7 +1905,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.23.9';
const SIMPLE_VERSION = '3.24.0';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
@@ -15060,8 +15060,9 @@ async function pysimFsToggleDir(name) {
let pysimFsSelected = null;
function pysimFsInfoHtml(sel, name) {
function pysimFsInfoHtml(sel, name, opts) {
if (!sel) return '';
opts = opts || {};
const num = v => (v === null || v === undefined) ? null : String(v);
const attrs = [];
if (sel.fid) attrs.push('FID: ' + esc(String(sel.fid).toUpperCase()));
@@ -15079,10 +15080,26 @@ function pysimFsInfoHtml(sel, name) {
(name ? '<legend class="px-1 text-xs font-mono font-medium text-gray-500 dark:text-slate-400">' + esc(name) + '</legend>' : '') +
(attrs.length ? '<div class="' + metaCls + ' mb-0.5">' + attrs.join(' · ') + '</div>' : '') +
'<div class="text-xs font-mono leading-relaxed text-gray-600 dark:text-slate-400 break-all">' +
profilerFciPreviewItems(sel.fci_hex, { hideFileSize: num(sel.file_size) !== null }) +
profilerFciPreviewItems(sel.fci_hex, { hideFileSize: num(sel.file_size) !== null, arr: opts.arr, path: opts.path }) +
'</div></fieldset>';
}
// EF.ARR cache for the file manager's FCP view (v3.24.0): the server reads
// the card's ARR copies once per session (GET /api/arr is itself cache-only);
// a card-session change drops this copy.
let _pysimArrCache = null;
async function pysimArrEnsure() {
if (_pysimArrCache && _pysimArrCache.session === _pysimCardSession) return _pysimArrCache.arrs;
try {
const data = await pysimFetch('/api/arr');
_pysimArrCache = { session: _pysimCardSession, arrs: (data && data.arrs) || [] };
} catch (e) {
_pysimArrCache = { session: _pysimCardSession, arrs: [] };
}
return _pysimArrCache.arrs;
}
async function pysimFsClickFile(name) {
pysimFsResetEdit();
pysimFsSetMode('raw');
@@ -15095,7 +15112,8 @@ async function pysimFsClickFile(name) {
pysimFsRenderTree();
}
pysimFsSelected = name;
document.getElementById('pysim-fs-info').innerHTML = exists ? pysimFsInfoHtml(sel, name) : '';
const arrs = await pysimArrEnsure();
document.getElementById('pysim-fs-info').innerHTML = exists ? pysimFsInfoHtml(sel, name, { arr: arrs, path: pysimFsNodePath(node) }) : '';
document.getElementById('pysim-fs-content').innerHTML = '';
pysimFsSyncContentVisibility();
document.getElementById('pysim-fs-status').textContent = exists ? '' : '✗ File not found on card';
@@ -15257,6 +15275,10 @@ async function pysimFsSave() {
const statusEl = document.getElementById('pysim-fs-status');
const out = document.getElementById('pysim-fs-content');
const node = pysimFsFindNode(rawName, pysimFsTreeRoot);
// An EF.ARR write changes the referenced access rules: drop the client
// ARR cache so the next FCP view refetches the server's refreshed copy.
const wroteFid = String((node && node.fid) || (/^[0-9a-fA-F]{4}$/.test(rawName || '') ? rawName : '')).toUpperCase();
const arrTouched = wroteFid === '2F06' || wroteFid === '6F06';
statusEl.textContent = 'Saving...';
try {
const textareas = out.querySelectorAll('textarea');
@@ -15267,6 +15289,7 @@ async function pysimFsSave() {
const data = await pysimFetch('/api/write', body);
if (data.success) {
statusEl.textContent = 'SW: ' + data.sw + ' OK';
if (arrTouched) _pysimArrCache = null;
pysimFsCancel();
pysimFsRead();
} else {
@@ -15298,6 +15321,7 @@ async function pysimFsSave() {
return;
}
statusEl.textContent = 'SW: 9000 OK';
if (arrTouched) _pysimArrCache = null;
pysimFsCancel();
pysimFsRead();
}
@@ -19616,6 +19640,8 @@ function snapshotSaveName() {
function profilerRenderSnapshotFiles(s) {
if (!s.files.length) return '<span class="text-gray-400">' + t('No files captured.') + '</span>';
// the snapshot's own EF.ARR copies resolve the FCP '8B' references offline
const snapArr = profilerSnapshotArr(s);
let html = '';
for (const f of s.files) {
const name = profilerCustomNameForPath(f.path) || f.name;
@@ -19644,7 +19670,7 @@ function profilerRenderSnapshotFiles(s) {
html += '<div class="flex-1 min-w-0"><div class="text-xs text-gray-500 dark:text-slate-400 mb-0.5">' + esc(t('FCI hex (raw SELECT response)')) + '</div>' +
'<textarea readonly rows="2" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-100 dark:bg-slate-800 break-all">' + escHtml(f.fciHex) + '</textarea></div>';
html += '<div class="flex-1 min-w-0"><div class="text-xs text-gray-500 dark:text-slate-400 mb-0.5">' + esc(t('Decoded FCI')) + '</div>' +
'<div class="text-xs font-mono leading-relaxed text-gray-600 dark:text-slate-400 break-all">' + profilerFciPreviewItems(f.fciHex) + '</div></div>';
'<div class="text-xs font-mono leading-relaxed text-gray-600 dark:text-slate-400 break-all">' + profilerFciPreviewItems(f.fciHex, { arr: snapArr, path: f.path }) + '</div></div>';
html += '</div>';
}
const dec = f.content ? efDecodeFile(f.name, efFidFromPath(f.path), f.content) : null;
@@ -20357,6 +20383,65 @@ function fcpPsTemplate(v) {
return pins.length ? pins.join('; ') : (v || '');
}
// FCP '8B' security attribute referencing (ISO 7816-4 5.4.3.3, Table 25):
// one ARR byte (a record in the implicitly known EF.ARR), a file identifier +
// ARR byte (the record number), or a file identifier followed by (SEID, ARR)
// pairs. Returns null for a malformed length (the raw value is shown then).
function fcpArrRef(value) {
const v = (value || '').toUpperCase();
if (!/^[0-9A-F]*$/.test(v)) return null;
if (v.length === 2) return { implicit: true, record: parseInt(v, 16) };
if (v.length === 6) return { fid: v.substr(0, 4), record: parseInt(v.substr(4, 2), 16) };
if (v.length >= 8 && v.length % 2 === 0) {
const pairs = [];
for (let i = 4; i + 4 <= v.length; i += 4) {
pairs.push({ seid: parseInt(v.substr(i, 2), 16), record: parseInt(v.substr(i + 2, 2), 16) });
}
return { fid: v.substr(0, 4), pairs: pairs };
}
return null;
}
// SEID label (TS 102 221 9.3.1 / Table 9.0d: SE00 and SE01 are defined; the
// same pattern names any other value).
function fcpSeidName(seid) {
return 'SE' + (Number(seid) || 0).toString(16).toUpperCase().padStart(2, '0');
}
// 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).
// Returns [] when nothing resolves - the caller stays silent (v3.24.0).
function fcpArrRules(ref, arrs, path) {
if (!ref || !arrs || !arrs.length) return [];
const df = (((path || '').split('/')[0]) || 'MF').toUpperCase();
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];
};
const out = [];
const add = (label, fid, record) => {
const arr = pick(fid);
const recs = (arr && arr.records) || [];
if (!(record >= 1) || record > recs.length) return;
const dec = efDecArr(efBytes(recs[record - 1] || ''));
for (const rule of ((dec && dec.rules) || [])) {
out.push((label ? label + ': ' : '') + rule);
}
};
if (ref.implicit) {
add('', df.startsWith('ADF.') ? '6F06' : '2F06', ref.record);
} else if (ref.pairs) {
for (const p of ref.pairs) add(fcpSeidName(p.seid), ref.fid, p.record);
} else {
add('', ref.fid, ref.record);
}
return out;
}
function fcpDo(key, value) {
if (/^[0-9A-F]+$/.test(value) === false) value = value || '';
switch (key) {
@@ -20370,7 +20455,13 @@ function fcpDo(key, value) {
case '87': return { name: 'FCI extension EF', decoded: value };
case '88': return { name: 'Short file identifier', decoded: fcpSfi(value) };
case '8A': return { name: 'Life cycle status', decoded: fcpLifeCycle(value) };
case '8B': return { name: 'Security attributes (referenced)', decoded: value.length >= 6 ? 'EF_ARR ' + value.substr(0, 4) + ', record ' + fcpInt(value.substr(4, 2)) : value };
case '8B': {
const ref = fcpArrRef(value);
if (!ref) return { name: 'Security attributes (referenced)', decoded: value };
if (ref.implicit) return { name: 'Security attributes (referenced)', decoded: 'record ' + ref.record + ' (implicit EF_ARR)' };
if (ref.pairs) return { name: 'Security attributes (referenced)', decoded: 'EF_ARR ' + ref.fid + ', ' + ref.pairs.map(p => fcpSeidName(p.seid) + ' \u2192 record ' + p.record).join(', ') };
return { name: 'Security attributes (referenced)', decoded: 'EF_ARR ' + ref.fid + ', record ' + ref.record };
}
case '8C': return { name: 'Security attributes (compact)', decoded: value };
case '8D': return { name: 'Security env. template EF', decoded: value };
case '8E': return { name: 'Channel security attribute', decoded: value };
@@ -20562,6 +20653,26 @@ function fcpDiffHtml(expectedHex, actualHex, labels) {
return html;
}
// EF.ARR copies captured inside a snapshot (the scan stores every readable
// file): the snapshot's FCI view resolves '8B' references offline from them,
// shaped like the server's /api/arr entries (v3.24.0).
function profilerSnapshotArr(snapshot) {
const out = [];
for (const f of ((snapshot && snapshot.files) || [])) {
const fid = efFidFromPath(f.path);
if (!fid || (fid !== '2F06' && fid !== '6F06')) continue;
if (!f.content || f.content.kind !== 'record') continue;
out.push({
fid: fid,
path: f.path,
record_len: f.recordLen,
num_records: f.numRecords,
records: (f.content.records || []).map(r => r.data || ''),
});
}
return out;
}
// Decoded FCI rows. The short file identifier belongs to the file
// identifier, so it is merged into that row. `opts.hideFileSize` drops the
// File size DO where the caller already shows the size (file manager header).
@@ -20573,7 +20684,11 @@ function profilerFciPreviewItems(hex, opts) {
for (const it of d.items) {
if (it.decoded === null) continue;
if (opts.hideFileSize && it.key === '80') continue;
rows.push({ key: it.key, name: it.name, text: it.decoded || it.value });
// the referenced EF.ARR record's access rules, when the caller has the
// ARR content (the file manager's live cache, or a snapshot's copy)
const ref = it.key === '8B' ? fcpArrRef(it.value) : null;
rows.push({ key: it.key, name: it.name, text: it.decoded || it.value,
sub: ref ? fcpArrRules(ref, opts.arr, opts.path) : [] });
}
const fi = rows.find(r => r.key === '83');
const sfi = rows.find(r => r.key === '88');
@@ -20584,6 +20699,9 @@ function profilerFciPreviewItems(hex, opts) {
let html = '';
for (const r of rows) {
html += '<div>' + esc(r.name) + ': <span class="text-gray-800 dark:text-slate-200">' + esc(r.text) + '</span></div>';
for (const s of (r.sub || [])) {
html += '<div class="pl-2 text-gray-500 dark:text-slate-400">\u21b3 ' + esc(s) + '</div>';
}
}
if (d.error) {
html += '<div class="text-red-600">' + esc(t('Decode failed')) + ': ' + esc(d.error) + '</div>';
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v422';
const CACHE = 'simple-v423';
const URLS = [
'index.html',
'help.html',
+1 -1
View File
@@ -443,7 +443,7 @@ test('file manager shows FCI info and keeps the selection in state, not the DOM'
const content = html.indexOf('id="pysim-fs-content"');
assert.ok(detail !== -1 && info > detail && info < content, 'pysim-fs-info must sit above the content');
assert.ok(html.includes('function pysimFsInfoHtml'));
assert.ok(html.includes('pysimFsInfoHtml(sel, name)'));
assert.ok(html.includes('pysimFsInfoHtml(sel, name, { arr: arrs, path: pysimFsNodePath(node) })'));
assert.ok(!html.includes('pysim-fs-filename'));
assert.ok(html.includes('let pysimFsSelected = null;'));
assert.ok(html.includes('pysimFsSelected = name;'));
+89
View File
@@ -22,6 +22,7 @@ function extractFunc(src, name, asyncFn) {
}
const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'pysimCustomNormPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'profilerMatchedRecordsText', 'profilerCloneName', 'profilerClone', 'profilerNewId', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport', 'parseBerLen', 'parseTlvList', 'fcpInt', 'fcpParseTlvs', 'fcpFileDescriptor', 'fcpLifeCycle', 'fcpSfi', 'fcpDo', 'fcpDecode',
'fcpArrRef', 'fcpSeidName', 'fcpArrRules', 'profilerSnapshotArr', 'efBytes', 'efHex', 'efTlv', 'efDecArr', 'efArrAmLabel', 'efArrSc', 'efArrAmDo', 'efFidFromPath',
'pinKeyRefName', 'fcpPsTemplate', 'fcpDiffHtml', 'profilerFciPreviewItems', 'profilerUpdateFciPreview', 'profilerUpdateRule', 'profilerFciInput', 'profilerScanToggleAll', 'profilerScanIgnoreAllState', 'swapNibbles', 'decIccid', 'profilerSnapshotIccid', 'profilerValidateSnapshot', 'profilerListSwitch', 'profilerScanRefreshOptions', 'profilerLiveSource', 'profilerSnapshotSource', 'profilerVisibleResults', 'profilerRulesFromSnapshot', 'profilerExtraFileResults', 'profilerScanNameKeydown', 'profilerTimingStats', 'profilerTimingAccumulator', 'profilerFormatMs', 'profilerRenderSnapshotSummary', 'profilerSnapshotCountLabel', 'pysimFsInfoHtml', 'profilerLabelText', 'profilerResultsHeaderText', 'profilerRenderResultsView', 'profilerBuildFileRuleFromSnapshot', 'profilerSnapshotPickListHtml', 'profilerScanSetTarget'];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
@@ -813,6 +814,94 @@ test('pysimFsInfoHtml keeps the FCI File size when the header has none', () => {
delete global.t;
});
test('fcpArrRef parses the three 8B referencing forms', () => {
assert.deepStrictEqual(fcpArrRef('17'), { implicit: true, record: 23 });
assert.deepStrictEqual(fcpArrRef('2F0617'), { fid: '2F06', record: 23 });
assert.deepStrictEqual(fcpArrRef('2f0600170101'), {
fid: '2F06', pairs: [{ seid: 0, record: 23 }, { seid: 1, record: 1 }] });
assert.strictEqual(fcpArrRef('2F060'), null); // odd length
assert.strictEqual(fcpArrRef('ZZZZZZ'), null); // not hex
});
test('fcpDo decodes every 8B referencing form', () => {
global.t = s => s;
assert.strictEqual(fcpDo('8B', '2F0617').decoded, 'EF_ARR 2F06, record 23');
assert.strictEqual(fcpDo('8B', '17').decoded, 'record 23 (implicit EF_ARR)');
assert.strictEqual(fcpDo('8B', '2F0600170101').decoded,
'EF_ARR 2F06, SE00 \u2192 record 23, SE01 \u2192 record 1');
assert.strictEqual(fcpDo('8B', '2F06').decoded, '2F06');
delete global.t;
});
test('the FCI view shows the referenced EF.ARR rules when the ARR is known', () => {
global.t = s => s;
// the stk_params fixture FCI with 8B 03 2F06 02 (record 2)
const hex = '621A82054221000F0583026F4F8A01058B032F06028802004B8801B0';
const arrs = [{ fid: '2F06', path: 'MF/2F06', record_len: 6, num_records: 2,
records: ['FFFFFFFFFFFFFFFFFFFFFFFF', '800101A40683010A950108'] }];
const out = profilerFciPreviewItems(hex, { arr: arrs, path: 'MF/7F20/6F07' });
assert.ok(out.includes('EF_ARR 2F06, record 2'), out);
assert.ok(out.includes('AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): ADM1 (verify)'), out);
assert.ok(out.includes('\u21b3'), out);
// without ARR data the row is unchanged
const plain = profilerFciPreviewItems(hex);
assert.ok(plain.includes('EF_ARR 2F06, record 2'), plain);
assert.ok(!plain.includes('ADM1'), plain);
// an out-of-range record stays silent
const hex9 = '621A82054221000F0583026F4F8A01058B032F06098802004B8801B0';
const oor = profilerFciPreviewItems(hex9, { arr: arrs, path: 'MF/2F06' });
assert.ok(oor.includes('EF_ARR 2F06, record 9'), oor);
assert.ok(!oor.includes('ADM1'), oor);
delete global.t;
});
test('the implicit 8B form uses the ARR of the file\'s DF', () => {
global.t = s => s;
// 8B 01 02: implicit EF_ARR, record 2
const hex = '621882054221000F0583026F4F8A01058B01028002004B8801B0';
const arrs = [
{ fid: '2F06', path: 'MF/2F06', records: ['FFFFFFFF', '800101A40683010A950108'] },
{ fid: '6F06', path: 'ADF.USIM/6F06', records: ['FFFFFFFF', '8001019000'] },
];
// an ADF.USIM file resolves against the ADF's 6F06 copy
const adf = profilerFciPreviewItems(hex, { arr: arrs, path: 'ADF.USIM/6F07' });
assert.ok(adf.includes('record 2 (implicit EF_ARR)'), adf);
assert.ok(adf.includes('AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): always'), adf);
assert.ok(!adf.includes('ADM1'), adf);
// an MF-tree file resolves against MF/2F06
const mf = profilerFciPreviewItems(hex, { arr: arrs, path: 'MF/7F20/6F07' });
assert.ok(mf.includes('AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): ADM1 (verify)'), mf);
delete global.t;
});
test('the snapshot FCI view resolves references from the snapshot ARR', () => {
global.t = s => s;
const snap = { files: [
{ path: 'MF/2F06', name: 'EF.ARR', fileType: 'linear_fixed', recordLen: 6, numRecords: 2,
content: { kind: 'record', records: [{ num: 1, data: 'FFFFFFFFFFFFFFFFFFFFFFFF' },
{ num: 2, data: '800101A40683010A950108' }] } },
{ path: 'MF/7F20/6F07', name: 'EF.IMSI',
fciHex: '621A82054221000F0583026F4F8A01058B032F06028802004B8801B0' },
]};
const arrs = profilerSnapshotArr(snap);
assert.deepStrictEqual(arrs.map(a => a.fid), ['2F06']);
const out = profilerFciPreviewItems(snap.files[1].fciHex, { arr: arrs, path: snap.files[1].path });
assert.ok(out.includes('AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): ADM1 (verify)'), out);
delete global.t;
});
test('pysimFsInfoHtml shows the referenced ARR rules when given the cache', () => {
global.t = s => s;
const hex = '621A82054221000F0583026F4F8A01058B032F06028802004B8801B0';
const arrs = [{ fid: '2F06', path: 'MF/2F06', records: ['', '800101a40683010a950108'] }];
const out = pysimFsInfoHtml({ fid: '6f4f', file_type: 'linear_fixed', file_size: 75,
record_len: 15, num_of_rec: 5, fci_hex: hex }, 'EF.SAMPLE',
{ arr: arrs, path: 'MF/7F20/6F4F' });
assert.ok(out.includes('EF_ARR 2F06, record 2'), out);
assert.ok(out.includes('ADM1 (verify)'), out);
delete global.t;
});
test('pysimFsInfoHtml embeds the symbolic name in the FCI border', () => {
global.t = s => s;
const hex = '6212800200098202412183026F078A0105880110';