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:
@@ -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>«Профайлер»</strong> и <strong>«Симулятор»</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
@@ -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’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’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’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
@@ -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
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v422';
|
||||
const CACHE = 'simple-v423';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -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;'));
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user