file manager: path-based selection + ADF AID normalization

Make the DF context from the resolver actually reach the live views:

- pysimFsSelected now stores the canonical path (pysimFsNodePath), not
  the node name; pysimFsRenderNode emits the path in the click/toggle
  handlers; every consumer resolves via pysimFsFindNodeByPath.  The
  name-based pysimFsFindNode is removed - two same-named files under
  different DFs (EF.ECC/EF.AD, DF.PHONEBOOK copies) are now distinct.
- efDfFromPath maps ADF-rooted parent segments by RID (A0000000871002
  -> ADF.USIM, A0000000871004 -> ADF.ISIM), so snapshot/profile paths
  rooted by AID resolve to the same DF key as the named form.
- efDecEcc shares efEccCategory with efDecEccUsim (TS 24.008
  Table 10.5.135d), so the SIM and USIM category displays can't drift.

Frontend 1087 + Python 1090 tests green.  (Version/sw bump deferred -
  this is part of the unreleased decoded-editing batch.)
This commit is contained in:
catarrh committed 2026-10-25 21:01:11 +03:00
1 parent 8e0a97f025
commit 5ebcd0773d
7 files changed
+237 -91

No files matched your search

+85 -63
View File
@@ -16216,6 +16216,10 @@ function pysimFsRenderNode(node, depth) {
const italic = node.custom ? ' italic' : ''; const italic = node.custom ? ' italic' : '';
const fidStr = node.fid ? ' <span class="text-gray-400 dark:text-slate-500">(' + node.fid.toUpperCase() + ')</span>' : ''; const fidStr = node.fid ? ' <span class="text-gray-400 dark:text-slate-500">(' + node.fid.toUpperCase() + ')</span>' : '';
const aidStr = node.aid ? ' <span class="text-gray-400 dark:text-slate-500">[' + node.aid + ']</span>' : ''; const aidStr = node.aid ? ' <span class="text-gray-400 dark:text-slate-500">[' + node.aid + ']</span>' : '';
// The handlers carry the canonical path, not the display name: names are
// not unique (EF.ECC/EF.AD exist in DF.GSM and ADF.USIM with the same FID,
// and DF.PHONEBOOK is copied per DF), while a path resolves one node.
const nodePath = pysimFsNodePath(node);
let html = '<div class="' + color + italic + '">'; let html = '<div class="' + color + italic + '">';
html += pad; html += pad;
if (verified && !exists) { if (verified && !exists) {
@@ -16224,15 +16228,15 @@ function pysimFsRenderNode(node, depth) {
if (isDir) { if (isDir) {
if ((verified && exists) || !verified) { if ((verified && exists) || !verified) {
const toggle = node.children && node.expanded ? '▼' : '▶'; const toggle = node.children && node.expanded ? '▼' : '▶';
html += '<span class="cursor-pointer select-none' + italic + '" onclick="pysimFsToggleDir(\'' + esc(node.name) + '\')">' + toggle + '</span> '; html += '<span class="cursor-pointer select-none' + italic + '" onclick="pysimFsToggleDir(\'' + esc(nodePath) + '\')">' + toggle + '</span> ';
html += '<span class="cursor-pointer hover:underline' + italic + '" onclick="pysimFsToggleDir(\'' + esc(node.name) + '\')">' + esc(node.name) + fidStr + aidStr + '</span>'; html += '<span class="cursor-pointer hover:underline' + italic + '" onclick="pysimFsToggleDir(\'' + esc(nodePath) + '\')">' + esc(node.name) + fidStr + aidStr + '</span>';
} else { } else {
html += esc(node.name) + fidStr + aidStr + '</div>'; html += esc(node.name) + fidStr + aidStr + '</div>';
return html; return html;
} }
} else { } else {
html += '<span class="cursor-pointer hover:underline ml-4' + italic + '"' + html += '<span class="cursor-pointer hover:underline ml-4' + italic + '"' +
((verified && exists) ? ' onclick="pysimFsClickFile(\'' + esc(node.name) + '\')"' : (!verified ? ' onclick="pysimFsClickFile(\'' + esc(node.name) + '\')"' : '')) + '>' + esc(node.name) + fidStr + aidStr + '</span>'; ((verified && exists) ? ' onclick="pysimFsClickFile(\'' + esc(nodePath) + '\')"' : (!verified ? ' onclick="pysimFsClickFile(\'' + esc(nodePath) + '\')"' : '')) + '>' + esc(node.name) + fidStr + aidStr + '</span>';
} }
html += '</div>'; html += '</div>';
if (isDir && node.children && node.expanded) { if (isDir && node.children && node.expanded) {
@@ -16247,27 +16251,16 @@ function pysimFsRenderNode(node, depth) {
return html; return html;
} }
function pysimFsFindNode(name, root) {
if (!root) return null;
if (root.name === name) return root;
if (root.children) {
for (const c of root.children) {
const found = pysimFsFindNode(name, c);
if (found) return found;
}
}
return null;
}
// Decoder availability for the selected file, resolved exactly like the // Decoder availability for the selected file, resolved exactly like the
// decoded view does (name from the tree node, FID fallback, and the parent DF // decoded view does (the node from the selected canonical path, the path's
// when the node's path is known — EF.ECC/EF.AD differ between DFs) — the // FID as the fallback, and the parent DF from that same path — EF.ECC/EF.AD
// content is not needed, so the Read decoded pill can be gated before any read. // differ between DFs) — the content is not needed, so the Read decoded pill
// can be gated before any read.
function pysimFsHasDecoder() { function pysimFsHasDecoder() {
if (!pysimFsSelected) return false; if (!pysimFsSelected) return false;
const node = pysimFsFindNode(pysimFsSelected, pysimFsTreeRoot); const node = pysimFsFindNodeByPath(pysimFsSelected);
const fid = (node && node.fid) || (/^[0-9a-fA-F]{4}$/.test(pysimFsSelected) ? pysimFsSelected : null); const fid = (node && node.fid) || efFidFromPath(pysimFsSelected);
const df = node ? efDfFromPath(pysimFsNodePath(node)) : ''; const df = node ? efDfFromPath(pysimFsNodePath(node)) : efDfFromPath(pysimFsSelected);
return !!efFindDecoder(node && node.name, fid, df); return !!efFindDecoder(node && node.name, fid, df);
} }
@@ -16275,15 +16268,16 @@ function pysimFsHasDecoder() {
// same way as the decoder above. // same way as the decoder above.
function pysimFsHasEncoder() { function pysimFsHasEncoder() {
if (!pysimFsSelected) return false; if (!pysimFsSelected) return false;
const node = pysimFsFindNode(pysimFsSelected, pysimFsTreeRoot); const node = pysimFsFindNodeByPath(pysimFsSelected);
const fid = (node && node.fid) || (/^[0-9a-fA-F]{4}$/.test(pysimFsSelected) ? pysimFsSelected : null); const fid = (node && node.fid) || efFidFromPath(pysimFsSelected);
const df = node ? efDfFromPath(pysimFsNodePath(node)) : ''; const df = node ? efDfFromPath(pysimFsNodePath(node)) : efDfFromPath(pysimFsSelected);
return !!efFindEncoder(node && node.name, fid, df); return !!efFindEncoder(node && node.name, fid, df);
} }
// Find a node by its canonical path ("MF/7F20/6F46", "ADF.USIM/6F07"), so // Find a node by its canonical path ("MF/7F20/6F46", "ADF.USIM/6F07") — the
// custom-file parents can be resolved against the loaded tree (standard DFs // file manager's selection key, and how custom-file parents resolve against
// included, at any depth). Returns null when the path is not loaded yet. // the loaded tree (standard DFs included, at any depth). A path is unique,
// a display name is not. Returns null when the path is not loaded yet.
function pysimFsFindNodeByPath(path) { function pysimFsFindNodeByPath(path) {
const want = pysimCustomNormPath(path); const want = pysimCustomNormPath(path);
if (!want || !pysimFsTreeRoot) return null; if (!want || !pysimFsTreeRoot) return null;
@@ -16298,8 +16292,15 @@ function pysimFsFindNodeByPath(path) {
return walk(pysimFsTreeRoot); return walk(pysimFsTreeRoot);
} }
async function pysimFsToggleDir(name) { // The select body of a selected path whose tree node is not loaded (a branch
const node = pysimFsFindNode(name, pysimFsTreeRoot); // refreshed while it was selected): the last segment names the file.
function pysimFsFallbackNode(path) {
const segs = String(path || '').split('/').filter(Boolean);
return { name: segs.length ? segs[segs.length - 1] : path, fid: efFidFromPath(path) };
}
async function pysimFsToggleDir(path) {
const node = pysimFsFindNodeByPath(path);
if (!node || !node.isDir) return; if (!node || !node.isDir) return;
if (!node.children) { if (!node.children) {
await pysimFsLoadChildren(node); await pysimFsLoadChildren(node);
@@ -16352,18 +16353,20 @@ async function pysimArrEnsure() {
return _pysimArrCache.arrs; return _pysimArrCache.arrs;
} }
async function pysimFsClickFile(name) { async function pysimFsClickFile(path) {
pysimFsResetEdit(); pysimFsResetEdit();
pysimFsSetMode('raw'); pysimFsSetMode('raw');
const node = pysimFsFindNode(name, pysimFsTreeRoot); const node = pysimFsFindNodeByPath(path);
const sel = await pysimFetch('/api/select', pysimFsSelectBody(node || { name: name, fid: null })); const sel = await pysimFetch('/api/select', pysimFsSelectBody(node || pysimFsFallbackNode(path)));
const exists = !!(sel && sel.exists !== false && !sel.error); const exists = !!(sel && sel.exists !== false && !sel.error);
if (node) { if (node) {
node.exists = exists; node.exists = exists;
if (!exists) node.children = []; if (!exists) node.children = [];
pysimFsRenderTree(); pysimFsRenderTree();
} }
pysimFsSelected = name; // the selection key is the canonical path (a display name is not unique)
pysimFsSelected = node ? pysimFsNodePath(node) : path;
const name = node ? node.name : path.split('/').pop();
const arrs = await pysimArrEnsure(); const arrs = await pysimArrEnsure();
document.getElementById('pysim-fs-info').innerHTML = exists ? pysimFsInfoHtml(sel, name, { arr: arrs, path: pysimFsNodePath(node) }) : ''; document.getElementById('pysim-fs-info').innerHTML = exists ? pysimFsInfoHtml(sel, name, { arr: arrs, path: pysimFsNodePath(node) }) : '';
document.getElementById('pysim-fs-content').innerHTML = ''; document.getElementById('pysim-fs-content').innerHTML = '';
@@ -16393,8 +16396,8 @@ async function pysimFsRead() {
out.innerHTML = ''; out.innerHTML = '';
statusEl.textContent = t('Loading...'); statusEl.textContent = t('Loading...');
try { try {
const node = pysimFsFindNode(rawName, pysimFsTreeRoot); const node = pysimFsFindNodeByPath(rawName);
const body = pysimFsSelectBody(node || { name: rawName, fid: null }); const body = pysimFsSelectBody(node || pysimFsFallbackNode(rawName));
body.mode = 'raw'; // decoding is client-side (works offline in snapshots too) body.mode = 'raw'; // decoding is client-side (works offline in snapshots too)
const data = await pysimFetch('/api/read', body); const data = await pysimFetch('/api/read', body);
if (!data.success) { if (!data.success) {
@@ -16403,7 +16406,7 @@ async function pysimFsRead() {
} }
statusEl.textContent = 'SW: ' + data.sw + ' OK'; statusEl.textContent = 'SW: ' + data.sw + ' OK';
if (pysimFsDecodedMode) { if (pysimFsDecodedMode) {
out.innerHTML = pysimFsDecodedHtml(node || { name: rawName, fid: null }, data); out.innerHTML = pysimFsDecodedHtml(node || pysimFsFallbackNode(rawName), data);
} else if (data.records) { } else if (data.records) {
let html = ''; let html = '';
for (const r of data.records) { for (const r of data.records) {
@@ -16432,9 +16435,9 @@ async function pysimFsRead() {
// read of the card). // read of the card).
function pysimFsDecodedHtml(node, data) { function pysimFsDecodedHtml(node, data) {
const name = (node && node.name) || null; const name = (node && node.name) || null;
const fid = (node && node.fid) || (/^[0-9a-fA-F]{4}$/.test(pysimFsSelected || '') ? pysimFsSelected : null); const fid = (node && node.fid) || efFidFromPath(pysimFsSelected);
const content = data.records ? { kind: 'record', records: data.records } : { kind: 'transparent', data: data.data }; const content = data.records ? { kind: 'record', records: data.records } : { kind: 'transparent', data: data.data };
const df = node ? efDfFromPath(pysimFsNodePath(node)) : ''; const df = node ? efDfFromPath(pysimFsNodePath(node)) : efDfFromPath(pysimFsSelected);
const dec = efDecodeFile(node && node.name, fid, content, df); const dec = efDecodeFile(node && node.name, fid, content, df);
let html = ''; let html = '';
if (!dec || !dec.fn) { if (!dec || !dec.fn) {
@@ -16517,8 +16520,8 @@ async function pysimFsDecEdit() {
// edit mode (they stay disabled) or bail out (re-enabled below) // edit mode (they stay disabled) or bail out (re-enabled below)
pysimFsPillsEnabled(false); pysimFsPillsEnabled(false);
try { try {
const node = pysimFsFindNode(rawName, pysimFsTreeRoot); const node = pysimFsFindNodeByPath(rawName);
const body = pysimFsSelectBody(node || { name: rawName, fid: null }); const body = pysimFsSelectBody(node || pysimFsFallbackNode(rawName));
body.mode = 'raw'; body.mode = 'raw';
const data = await pysimFetch('/api/read', body); const data = await pysimFetch('/api/read', body);
if (rawName !== pysimFsSelected) { if (rawName !== pysimFsSelected) {
@@ -16532,11 +16535,11 @@ async function pysimFsDecEdit() {
pysimFsPillsEnabled(true); pysimFsPillsEnabled(true);
return; return;
} }
const fid = (node && node.fid) || (/^[0-9a-fA-F]{4}$/.test(rawName || '') ? rawName : null); const fid = (node && node.fid) || efFidFromPath(rawName);
const content = data.records ? { kind: 'record', records: data.records } const content = data.records ? { kind: 'record', records: data.records }
: { kind: 'transparent', data: data.data }; : { kind: 'transparent', data: data.data };
const model = efDecEditModel(node && node.name, fid, content, const model = efDecEditModel(node && node.name, fid, content,
node ? efDfFromPath(pysimFsNodePath(node)) : ''); node ? efDfFromPath(pysimFsNodePath(node)) : efDfFromPath(rawName));
if (!model) { if (!model) {
statusEl.textContent = t('No decoded editor for this file'); statusEl.textContent = t('No decoded editor for this file');
pysimFsPillsEnabled(true); pysimFsPillsEnabled(true);
@@ -16588,10 +16591,10 @@ async function pysimFsSave() {
const rawName = pysimFsSelected; const rawName = pysimFsSelected;
const statusEl = document.getElementById('pysim-fs-status'); const statusEl = document.getElementById('pysim-fs-status');
const out = document.getElementById('pysim-fs-content'); const out = document.getElementById('pysim-fs-content');
const node = pysimFsFindNode(rawName, pysimFsTreeRoot); const node = pysimFsFindNodeByPath(rawName);
// An EF.ARR write changes the referenced access rules: drop the client // 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. // 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 wroteFid = String((node && node.fid) || efFidFromPath(rawName) || '').toUpperCase();
const arrTouched = wroteFid === '2F06' || wroteFid === '6F06'; const arrTouched = wroteFid === '2F06' || wroteFid === '6F06';
statusEl.textContent = 'Saving...'; statusEl.textContent = 'Saving...';
if (pysimFsDecEditMode) { if (pysimFsDecEditMode) {
@@ -16602,7 +16605,7 @@ async function pysimFsSave() {
const textareas = out.querySelectorAll('textarea'); const textareas = out.querySelectorAll('textarea');
if (textareas.length === 1) { if (textareas.length === 1) {
// Transparent file // Transparent file
const body = pysimFsSelectBody(node || { name: rawName, fid: null }); const body = pysimFsSelectBody(node || pysimFsFallbackNode(rawName));
body.data = textareas[0].value.replace(/[^0-9a-fA-F]/g, ''); body.data = textareas[0].value.replace(/[^0-9a-fA-F]/g, '');
const data = await pysimFetch('/api/write', body); const data = await pysimFetch('/api/write', body);
if (data.success) { if (data.success) {
@@ -16624,7 +16627,7 @@ async function pysimFsSave() {
wroteAny = true; wroteAny = true;
const num = row?.querySelector('span')?.textContent; const num = row?.querySelector('span')?.textContent;
if (num) { if (num) {
const body = pysimFsSelectBody(node || { name: rawName, fid: null }); const body = pysimFsSelectBody(node || pysimFsFallbackNode(rawName));
body.data = inp.value.replace(/[^0-9a-fA-F]/g, ''); body.data = inp.value.replace(/[^0-9a-fA-F]/g, '');
body.record_nr = parseInt(num); body.record_nr = parseInt(num);
const data = await pysimFetch('/api/write', body); const data = await pysimFetch('/api/write', body);
@@ -16661,8 +16664,8 @@ async function pysimFsDecSave(node, rawName, statusEl, out, arrTouched) {
return; return;
} }
const entry = model.entry || efFindEncoder(node && node.name, const entry = model.entry || efFindEncoder(node && node.name,
(node && node.fid) || (/^[0-9a-fA-F]{4}$/.test(rawName || '') ? rawName : null), (node && node.fid) || efFidFromPath(rawName),
node ? efDfFromPath(pysimFsNodePath(node)) : ''); node ? efDfFromPath(pysimFsNodePath(node)) : efDfFromPath(rawName));
if (!entry) { statusEl.textContent = t('No decoded editor for this file'); return; } if (!entry) { statusEl.textContent = t('No decoded editor for this file'); return; }
const root = out.querySelector('.pysim-dec-edit'); const root = out.querySelector('.pysim-dec-edit');
try { try {
@@ -16672,7 +16675,7 @@ async function pysimFsDecSave(node, rawName, statusEl, out, arrTouched) {
? entry.custom.collect(root) ? entry.custom.collect(root)
: (entry.list ? efDecCollectRows(root, entry.list.itemFields) : (entry.list ? efDecCollectRows(root, entry.list.itemFields)
: efDecCollectValues(root, entry.fields)); : efDecCollectValues(root, entry.fields));
const body = pysimFsSelectBody(node || { name: rawName, fid: null }); const body = pysimFsSelectBody(node || pysimFsFallbackNode(rawName));
body.data = entry.enc(values, orig); body.data = entry.enc(values, orig);
// identity writes ask once, and only when something actually changes // identity writes ask once, and only when something actually changes
if (entry.confirm && String(body.data).toUpperCase() !== String(orig).toUpperCase()) { if (entry.confirm && String(body.data).toUpperCase() !== String(orig).toUpperCase()) {
@@ -16697,7 +16700,7 @@ async function pysimFsDecSave(node, rawName, statusEl, out, arrTouched) {
if (cb && !cb.checked) continue; if (cb && !cb.checked) continue;
const num = block.getAttribute('data-rec'); const num = block.getAttribute('data-rec');
if (!num) continue; if (!num) continue;
const body = pysimFsSelectBody(node || { name: rawName, fid: null }); const body = pysimFsSelectBody(node || pysimFsFallbackNode(rawName));
const collected = entry.custom const collected = entry.custom
? entry.custom.collect(block) ? entry.custom.collect(block)
: (entry.items ? efDecCollectItems(block) : efDecCollectValues(block, entry.fields)); : (entry.items ? efDecCollectItems(block) : efDecCollectValues(block, entry.fields));
@@ -23556,6 +23559,14 @@ function efDecCbmir(b) {
return ranges.length ? { ranges: ranges } : { raw: efHex(b) }; return ranges.length ? { ranges: ranges } : { raw: efHex(b) };
} }
// The Emergency Service Category byte is the same coding in both files
// (TS 24.008 Table 10.5.135d: bit n = service n, TS 51.011 10.3.27 refers to
// it); a byte without a known bit keeps its raw hex.
function efEccCategory(cat) {
const kinds = EF_ECC_CATEGORIES.filter(c => cat & c[0]).map(c => c[1]);
return kinds.length ? kinds.join(', ') : '0x' + cat.toString(16).toUpperCase().padStart(2, '0');
}
// TS 51.011 10.3.27 EF.ECC (DF.GSM): transparent, 3-byte records of the // TS 51.011 10.3.27 EF.ECC (DF.GSM): transparent, 3-byte records of the
// emergency call code (2 bytes BCD) + the emergency category. The ADF.USIM // emergency call code (2 bytes BCD) + the emergency category. The ADF.USIM
// file of the same name/FID is a different format - efDecEccUsim below - so // file of the same name/FID is a different format - efDecEccUsim below - so
@@ -23567,7 +23578,7 @@ function efDecEcc(b) {
const rec = b.subarray(i, i + 3); const rec = b.subarray(i, i + 3);
if (efAllFf(rec)) continue; if (efAllFf(rec)) continue;
const digits = efSwapNibbles(efHex(rec.subarray(0, 2))).replace(/[fF]+$/, ''); const digits = efSwapNibbles(efHex(rec.subarray(0, 2))).replace(/[fF]+$/, '');
recs.push({ number: digits || null, esc: '0x' + efHex(rec.subarray(2, 3)) }); recs.push({ number: digits || null, esc: efEccCategory(rec[2]) });
} }
if (!recs.length) return { raw: efHex(b) }; if (!recs.length) return { raw: efHex(b) };
return recs.length === 1 ? recs[0] : { numbers: recs }; return recs.length === 1 ? recs[0] : { numbers: recs };
@@ -23591,11 +23602,9 @@ function efDecEccUsim(b) {
// EF_ECC decodes such a record as None too) // EF_ECC decodes such a record as None too)
if (efAllFf(b.subarray(0, 3))) return { empty: true }; if (efAllFf(b.subarray(0, 3))) return { empty: true };
const cat = b[b.length - 1]; const cat = b[b.length - 1];
const kinds = EF_ECC_CATEGORIES.filter(c => cat & c[0]).map(c => c[1]);
const out = { const out = {
code: efSwapNibbles(efHex(b.subarray(0, 3))).replace(/[fF]+$/, ''), code: efSwapNibbles(efHex(b.subarray(0, 3))).replace(/[fF]+$/, ''),
category: kinds.length ? kinds.join(', ') category: efEccCategory(cat),
: '0x' + cat.toString(16).toUpperCase().padStart(2, '0'),
}; };
const alpha = efAnnexA(efRstripFf(b.subarray(3, b.length - 1))); const alpha = efAnnexA(efRstripFf(b.subarray(3, b.length - 1)));
if (alpha) out.alpha = alpha; if (alpha) out.alpha = alpha;
@@ -24066,13 +24075,26 @@ function efFidFromPath(path) {
return /^[0-9a-fA-F]{4}$/.test(seg) ? seg.toUpperCase() : null; return /^[0-9a-fA-F]{4}$/.test(seg) ? seg.toUpperCase() : null;
} }
// The ADF roots a snapshot/profile path uses are the DF name (an AID) the card
// reports in its FCI, e.g. A0000000871002FF49FF0589 - but the decoders key
// their DF-specific entries by the symbolic name. The two standard
// application RIDs (TS 101 220; ADF.USIM / ADF.ISIM per TS 31.102/31.103 §4)
// fold onto those names, whatever the AID's PIX is.
const EF_ADF_RIDS = [
['A0000000871002', 'ADF.USIM'],
['A0000000871004', 'ADF.ISIM'],
];
// The parent DF of a canonical path: "MF/7F20/6FAD" -> "7F20", // The parent DF of a canonical path: "MF/7F20/6FAD" -> "7F20",
// "ADF.USIM/6FAD" -> "ADF.USIM", "MF/ADF.USIM/6FAD" -> "ADF.USIM"; a bare FID // "ADF.USIM/6FAD" -> "ADF.USIM", "MF/ADF.USIM/6FAD" -> "ADF.USIM",
// (no parent segment) has no DF context and yields ''. // "A0000000871002FF49FF0589/6F07" -> "ADF.USIM"; a bare FID (no parent
// segment) has no DF context and yields ''.
function efDfFromPath(path) { function efDfFromPath(path) {
const segs = String(path || '').split('/').filter(Boolean); const segs = String(path || '').split('/').filter(Boolean);
if (segs.length < 2) return ''; if (segs.length < 2) return '';
return segs[segs.length - 2].toUpperCase(); const seg = segs[segs.length - 2].toUpperCase();
for (const [rid, name] of EF_ADF_RIDS) if (seg.startsWith(rid)) return name;
return seg;
} }
// ---- content encoders (the decoded editor) ---- // ---- content encoders (the decoded editor) ----
@@ -25670,10 +25692,10 @@ function pysimFsDecBitsFilter(input) {
function pysimFsDecAddRow() { function pysimFsDecAddRow() {
if (!pysimFsSelected) return; if (!pysimFsSelected) return;
const node = pysimFsFindNode(pysimFsSelected, pysimFsTreeRoot); const node = pysimFsFindNodeByPath(pysimFsSelected);
const fid = (node && node.fid) || (/^[0-9a-fA-F]{4}$/.test(pysimFsSelected) ? pysimFsSelected : null); const fid = (node && node.fid) || efFidFromPath(pysimFsSelected);
const entry = efFindEncoder(node && node.name, fid, const entry = efFindEncoder(node && node.name, fid,
node ? efDfFromPath(pysimFsNodePath(node)) : ''); node ? efDfFromPath(pysimFsNodePath(node)) : efDfFromPath(pysimFsSelected));
if (!entry || !entry.list) return; if (!entry || !entry.list) return;
const out = document.getElementById('pysim-fs-content'); const out = document.getElementById('pysim-fs-content');
const rows = out && out.querySelector('.pysim-dec-rows'); const rows = out && out.querySelector('.pysim-dec-rows');
@@ -25702,10 +25724,10 @@ function pysimFsDecAddItem(btn) {
const rec = btn && btn.closest ? btn.closest('.pysim-dec-rec') : null; const rec = btn && btn.closest ? btn.closest('.pysim-dec-rec') : null;
const box = rec ? rec.querySelector('.pysim-dec-items') : null; const box = rec ? rec.querySelector('.pysim-dec-items') : null;
if (!box || !pysimFsSelected) return; if (!box || !pysimFsSelected) return;
const node = pysimFsFindNode(pysimFsSelected, pysimFsTreeRoot); const node = pysimFsFindNodeByPath(pysimFsSelected);
const fid = (node && node.fid) || (/^[0-9a-fA-F]{4}$/.test(pysimFsSelected) ? pysimFsSelected : null); const fid = (node && node.fid) || efFidFromPath(pysimFsSelected);
const entry = efFindEncoder(node && node.name, fid, const entry = efFindEncoder(node && node.name, fid,
node ? efDfFromPath(pysimFsNodePath(node)) : ''); node ? efDfFromPath(pysimFsNodePath(node)) : efDfFromPath(pysimFsSelected));
if (!entry || !entry.items) return; if (!entry || !entry.items) return;
const first = (entry.items.tags || [])[0]; const first = (entry.items.tags || [])[0];
box.insertAdjacentHTML('beforeend', efDecItemRowHtml(entry, { tag: first ? first.tag : '43' })); box.insertAdjacentHTML('beforeend', efDecItemRowHtml(entry, { tag: first ? first.tag : '43' }));
+70 -4
View File
@@ -26,13 +26,13 @@ const FNS = [
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct', 'efPnnText', 'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct', 'efPnnText',
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecSst', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact', 'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecSst', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact',
'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc', 'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc',
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecHpplmn', 'efDecEcc', 'efDecEccUsim', 'efDecOpl', 'efDecAd', 'efDecAcl', 'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecHpplmn', 'efDecEcc', 'efDecEccUsim', 'efEccCategory', 'efDecOpl', 'efDecAd', 'efDecAcl',
'efSmspDaNumber', 'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn', 'efSmspDaNumber', 'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn',
'pinKeyRefName', 'efArrAmLabel', 'efArrSc', 'efArrAmDo', 'pinKeyRefName', 'efArrAmLabel', 'efArrSc', 'efArrAmDo',
'efDecExt1', 'efDecSms', 'efDecSume', 'efPbrHex', 'efDecPbr', 'efFindDecoder', 'efFidFromPath', 'efDfFromPath', 'efDecodeBytes', 'efDecExt1', 'efDecSms', 'efDecSume', 'efPbrHex', 'efDecPbr', 'efFindDecoder', 'efFidFromPath', 'efDfFromPath', 'efDecodeBytes',
'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten', 'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten',
'efDiffData', 'efDataSummary', 'efContentDiff', 'gsm7Decode', 'efRenderFieldsHtml', 'efDiffData', 'efDataSummary', 'efContentDiff', 'gsm7Decode', 'efRenderFieldsHtml',
'pysimFsDecodedHtml', 'pysimFsNodePath', 'profilerRenderReport', 'profilerLabelText', 'profilerResultAspects', 'pysimFsDecodedHtml', 'pysimFsNodePath', 'pysimFsFindNodeByPath', 'profilerRenderReport', 'profilerLabelText', 'profilerResultAspects',
'profilerAspectSummary', 'profilerMatchedRecordsText', 'profilerRawDataCheck', 'profilerCustomNameForPath', 'profilerAspectSummary', 'profilerMatchedRecordsText', 'profilerRawDataCheck', 'profilerCustomNameForPath',
'pysimCustomNormPath', 'pysimCustomNormPath',
]; ];
@@ -46,9 +46,10 @@ for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_ES
code += html.match(new RegExp('const ' + c + ' = \\{.*\\};'))[0].replace('const ', 'var ') + '\n'; code += html.match(new RegExp('const ' + c + ' = \\{.*\\};'))[0].replace('const ', 'var ') + '\n';
} }
code += html.match(/const EF_ECC_CATEGORIES = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n'; code += html.match(/const EF_ECC_CATEGORIES = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
code += html.match(/const EF_ADF_RIDS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
code += html.match(/const EF_FIELD_LABELS = \{[\s\S]*?\n\};/)[0].replace('const ', 'var ') + '\n'; code += html.match(/const EF_FIELD_LABELS = \{[\s\S]*?\n\};/)[0].replace('const ', 'var ') + '\n';
code += html.match(/const EF_DECODERS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n'; code += html.match(/const EF_DECODERS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
code += 'var pysimCustomFiles = [];\nvar pysimFsSelected = \'EF.IMSI\';\n'; code += 'var pysimCustomFiles = [];\nvar pysimFsSelected = \'EF.IMSI\';\nvar pysimFsTreeRoot = null;\n';
code += 'function esc(s){return String(s);}\nfunction escHtml(s){return String(s);}\nfunction t(k){return k;}\n'; code += 'function esc(s){return String(s);}\nfunction escHtml(s){return String(s);}\nfunction t(k){return k;}\n';
eval(code); eval(code);
@@ -107,7 +108,7 @@ test('EF decoders match the pinned spec / pySim test vectors', () => {
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] }); assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] });
assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] }); assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] });
assert.deepStrictEqual(efDecAcc(efBytes('8001')), { access_classes: [15, 0] }); assert.deepStrictEqual(efDecAcc(efBytes('8001')), { access_classes: [15, 0] });
assert.deepStrictEqual(efDecEcc(efBytes('19f101')), { number: '911', esc: '0x01' }); assert.deepStrictEqual(efDecEcc(efBytes('19f101')), { number: '911', esc: 'police' });
assert.deepStrictEqual(efDecKc(efBytes('837d783609a3858f05')), { kc: '837D783609A3858F', ksi: 0 }); assert.deepStrictEqual(efDecKc(efBytes('837d783609a3858f05')), { kc: '837D783609A3858F', ksi: 0 });
assert.deepStrictEqual(efDecPlmnList(efBytes('22F860')), { plmns: [{ mcc: '228', mnc: '06' }] }); assert.deepStrictEqual(efDecPlmnList(efBytes('22F860')), { plmns: [{ mcc: '228', mnc: '06' }] });
// EF.FPLMN: a FFFFFF gap in any position is skipped, not a terminator // EF.FPLMN: a FFFFFF gap in any position is skipped, not a terminator
@@ -358,6 +359,23 @@ test('decoder registry resolves by name first, then FID; unknown returns null',
assert.strictEqual(efFidFromPath('ADF.USIM'), null); assert.strictEqual(efFidFromPath('ADF.USIM'), null);
}); });
test('EF.ECC (DF.GSM) decodes the Emergency Service Category like the USIM file', () => {
// TS 51.011 10.3.27: the category byte is the TS 24.008 10.5.135d coding,
// the same table the ADF.USIM file uses (bit n = service n)
assert.deepStrictEqual(efDecEcc(efBytes('19F104')), { number: '911', esc: 'fire brigade' });
assert.deepStrictEqual(efDecEcc(efBytes('19F102')), { number: '911', esc: 'ambulance' });
assert.deepStrictEqual(efDecEcc(efBytes('19F109')), { number: '911', esc: 'police, marine guard' });
// a byte without a known bit keeps its raw hex value
assert.deepStrictEqual(efDecEcc(efBytes('19F180')), { number: '911', esc: '0x80' });
// a record whose code is FF is still decoded (only an all-FF record is empty)
assert.deepStrictEqual(efDecEcc(efBytes('FFFF00')), { number: null, esc: '0x00' });
// several 3-byte records
assert.deepStrictEqual(efDecEcc(efBytes('19F10411F220')), {
numbers: [{ number: '911', esc: 'fire brigade' }, { number: '112', esc: 'manual eCall' }],
});
assert.deepStrictEqual(efDecEcc(efBytes('FFFFFFFF')), { empty: true });
});
test('efDfFromPath reads the parent DF of a canonical path', () => { test('efDfFromPath reads the parent DF of a canonical path', () => {
assert.strictEqual(efDfFromPath('MF/7F20/6FAD'), '7F20'); assert.strictEqual(efDfFromPath('MF/7F20/6FAD'), '7F20');
assert.strictEqual(efDfFromPath('ADF.USIM/6FAD'), 'ADF.USIM'); assert.strictEqual(efDfFromPath('ADF.USIM/6FAD'), 'ADF.USIM');
@@ -374,6 +392,22 @@ test('efDfFromPath reads the parent DF of a canonical path', () => {
assert.strictEqual(efFidFromPath('MF/7F20/6FAD'), '6FAD'); assert.strictEqual(efFidFromPath('MF/7F20/6FAD'), '6FAD');
}); });
test('efDfFromPath folds an ADF AID root onto its symbolic ADF name', () => {
// a snapshot/profile path roots an ADF by the DF name (the AID) the card
// reports, not by "ADF.USIM" - the RID decides, whatever the PIX is
assert.strictEqual(efDfFromPath('A0000000871002/6F07'), 'ADF.USIM');
assert.strictEqual(efDfFromPath('A0000000871002FF49FF0589/6F07'), 'ADF.USIM');
assert.strictEqual(efDfFromPath('a0000000871002ff49ff0589/6fad'), 'ADF.USIM');
assert.strictEqual(efDfFromPath('A0000000871004/6F07'), 'ADF.ISIM');
assert.strictEqual(efDfFromPath('A0000000871004FF49FF0589/6F07'), 'ADF.ISIM');
// an unrelated AID is not an ADF name: it passes through as itself
assert.strictEqual(efDfFromPath('A0000000090003FFFF/6F07'), 'A0000000090003FFFF');
assert.strictEqual(efDfFromPath('F0414C46416001/6F07'), 'F0414C46416001');
// the AID root reads the same as the symbolic name and the plain FID do
assert.strictEqual(efFindDecoder('EF.ECC', '6FB7', efDfFromPath('A0000000871002/6FB7')).fn, efDecEccUsim);
assert.strictEqual(efFindDecoder('EF.ECC', '6FB7', efDfFromPath('MF/7F20/6FB7')).fn, efDecEcc);
});
test('the decoder resolver is DF-first: EF.ECC is the SIM or the USIM file', () => { test('the decoder resolver is DF-first: EF.ECC is the SIM or the USIM file', () => {
// EF.ECC collides in name AND FID: only the parent DF separates the // EF.ECC collides in name AND FID: only the parent DF separates the
// TS 51.011 transparent form from the TS 31.102 linear-fixed one // TS 51.011 transparent form from the TS 31.102 linear-fixed one
@@ -463,6 +497,38 @@ test('efFlatten skips the all-FF empty marker', () => {
assert.deepStrictEqual(efFlatten({ empty: true }), []); assert.deepStrictEqual(efFlatten({ empty: true }), []);
}); });
test('the file manager resolves two same-named files by their parent DF', () => {
// EF.ECC exists in DF.GSM and ADF.USIM with the same name and FID; the
// selection key is the canonical path, so each resolves to its own node
const root = { name: 'MF', fid: '3F00', children: [], parent: null };
const gsm = { name: 'DF.GSM', fid: '7F20', children: [], parent: root };
const usim = { name: 'ADF.USIM', fid: '7FFF', children: [], parent: root };
const gsmEcc = { name: 'EF.ECC', fid: '6FB7', children: null, parent: gsm };
const usimEcc = { name: 'EF.ECC', fid: '6FB7', children: null, parent: usim };
root.children = [gsm, usim];
gsm.children = [gsmEcc];
usim.children = [usimEcc];
pysimFsTreeRoot = root;
assert.strictEqual(pysimFsFindNodeByPath('MF/7F20/6FB7'), gsmEcc);
assert.strictEqual(pysimFsFindNodeByPath('ADF.USIM/6FB7'), usimEcc);
// a path that is not loaded resolves to nothing (the callers fall back to
// the path's own last segment / FID)
assert.strictEqual(pysimFsFindNodeByPath('MF/7F10/6FB7'), null);
// the decoded view's DF follows the resolved node: the USIM file is the
// linear-fixed X+4 form, the DF.GSM copy the SIM transparent one
pysimFsSelected = 'ADF.USIM/6FB7';
const usimHtml = pysimFsDecodedHtml(pysimFsFindNodeByPath(pysimFsSelected),
{ success: true, records: [{ num: 1, data: '19F1FF04' }] });
assert.ok(usimHtml.includes('Emergency call code'));
assert.ok(usimHtml.includes('fire brigade'));
pysimFsSelected = 'MF/7F20/6FB7';
const gsmHtml = pysimFsDecodedHtml(pysimFsFindNodeByPath(pysimFsSelected),
{ success: true, data: '19F104' });
assert.ok(gsmHtml.includes('Emergency category'));
assert.ok(gsmHtml.includes('fire brigade'));
assert.ok(!gsmHtml.includes('Emergency call code'));
});
test('pysimFsDecodedHtml decodes EF.ECC by the selected node\'s parent DF', () => { test('pysimFsDecodedHtml decodes EF.ECC by the selected node\'s parent DF', () => {
// ADF.USIM's EF.ECC is the linear-fixed X+4 form (code/alpha/category) // ADF.USIM's EF.ECC is the linear-fixed X+4 form (code/alpha/category)
const usim = { name: 'ADF.USIM', children: [] }; const usim = { name: 'ADF.USIM', children: [] };
+27 -15
View File
@@ -52,7 +52,7 @@ const FNS = [
'efDecRecordsHtml', 'efDecEditHtml', 'efDecRecordsHtml', 'efDecEditHtml',
'efDecCollectValues', 'efDecCollectRows', 'efDecCollectItems', 'efDecCollectValues', 'efDecCollectRows', 'efDecCollectItems',
'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem', 'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem',
'pysimFsDecRemoveItem', 'pysimFsFindNode', 'pysimFsNodePath', 'efDfFromPath', 'pysimFsDecRemoveItem', 'pysimFsFindNodeByPath', 'pysimCustomNormPath', 'pysimFsNodePath', 'pysimFsFallbackNode', 'efFidFromPath', 'efDfFromPath',
'pysimFsHasDecoder', 'pysimFsHasEncoder', 'pysimFsDecSave', 'pysimFsDecEdit', 'pysimFsHasDecoder', 'pysimFsHasEncoder', 'pysimFsDecSave', 'pysimFsDecEdit',
'pysimFsPillsEnabled', 'pysimFsSyncContentVisibility', 'gsm7Decode', 'pysimFsPillsEnabled', 'pysimFsSyncContentVisibility', 'gsm7Decode',
'gsm7TextToSeptets', 'gsm7Encode', 'gsm7TextToSeptets', 'gsm7Encode',
@@ -63,7 +63,7 @@ let code = 'var pysimFsSelected = null;\nvar pysimFsTreeRoot = null;\nvar pysimF
+ 'var pysimFsDecEditMode = false;\nvar pysimFsEditMode = false;\n'; + 'var pysimFsDecEditMode = false;\nvar pysimFsEditMode = false;\n';
for (const f of FNS) code += extractFunc(html, f) + '\n'; for (const f of FNS) code += extractFunc(html, f) + '\n';
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS', for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS']) { 'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS', 'EF_ADF_RIDS']) {
code += extractConst(html, c); code += extractConst(html, c);
} }
eval(code); eval(code);
@@ -253,30 +253,41 @@ test('changing a record input checks its block', () => {
assert.strictEqual(cb.checked, true); assert.strictEqual(cb.checked, true);
}); });
test('pysimFsHasEncoder resolves the selected file by name or FID', () => { test('pysimFsHasEncoder resolves the selected file by its path or FID', () => {
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.SPN', fid: '6f46' }, { name: 'EF.ALIAS', fid: '6f3a' }] }; pysimFsTreeRoot = { name: 'MF', children: [] };
pysimFsSelected = 'EF.SPN'; const add = (name, fid) => {
const n = { name: name, fid: fid, parent: pysimFsTreeRoot };
pysimFsTreeRoot.children.push(n);
return n;
};
add('EF.SPN', '6f46');
add('EF.ALIAS', '6f3a');
pysimFsSelected = 'MF/6F46';
assert.strictEqual(pysimFsHasEncoder(), true); assert.strictEqual(pysimFsHasEncoder(), true);
pysimFsSelected = 'EF.ALIAS'; pysimFsSelected = 'MF/6F3A';
assert.strictEqual(pysimFsHasEncoder(), true); assert.strictEqual(pysimFsHasEncoder(), true);
// a bare FID selection still resolves through the FID fallback
pysimFsSelected = '6F46'; pysimFsSelected = '6F46';
assert.strictEqual(pysimFsHasEncoder(), true); assert.strictEqual(pysimFsHasEncoder(), true);
pysimFsSelected = 'EF.LOCI'; pysimFsSelected = 'MF/6F7E';
assert.strictEqual(pysimFsHasEncoder(), false); assert.strictEqual(pysimFsHasEncoder(), false);
// the SIM service table shares 6F38 with UST but has no editor // the SIM service table shares 6F38 with UST but has no editor
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.SST', fid: '6f38' }] }; pysimFsTreeRoot = { name: 'MF', children: [] };
pysimFsSelected = 'EF.SST'; const sst = { name: 'EF.SST', fid: '6f38', parent: pysimFsTreeRoot };
pysimFsTreeRoot.children.push(sst);
pysimFsSelected = 'MF/6F38';
assert.strictEqual(pysimFsHasEncoder(), false); assert.strictEqual(pysimFsHasEncoder(), false);
pysimFsSelected = null; pysimFsSelected = null;
assert.strictEqual(pysimFsHasEncoder(), false); assert.strictEqual(pysimFsHasEncoder(), false);
}); });
test('the file-manager resolver call sites pass the node\'s parent DF', () => { test('the file-manager resolver call sites pass the parent DF', () => {
// the DF context comes from the node's canonical path; these five sites // the DF context comes from the selected node's canonical path (or the
// selected path itself when the node is not loaded); these sites
// gate/read/edit a selected file and must resolve the same entry // gate/read/edit a selected file and must resolve the same entry
assert.ok(html.includes('return !!efFindDecoder(node && node.name, fid, df);')); assert.ok(html.includes('return !!efFindDecoder(node && node.name, fid, df);'));
assert.ok(html.includes('return !!efFindEncoder(node && node.name, fid, df);')); assert.ok(html.includes('return !!efFindEncoder(node && node.name, fid, df);'));
assert.ok(html.includes("const df = node ? efDfFromPath(pysimFsNodePath(node)) : '';")); assert.ok(html.includes("const df = node ? efDfFromPath(pysimFsNodePath(node)) : efDfFromPath(pysimFsSelected);"));
assert.ok(html.includes('const model = efDecEditModel(node && node.name, fid, content,')); assert.ok(html.includes('const model = efDecEditModel(node && node.name, fid, content,'));
for (const fn of ['pysimFsDecSave', 'pysimFsDecAddRow', 'pysimFsDecAddItem']) { for (const fn of ['pysimFsDecSave', 'pysimFsDecAddRow', 'pysimFsDecAddItem']) {
assert.ok(/efFindEncoder\([^;]*efDfFromPath\(pysimFsNodePath\(node\)\)/s.test(extractFunc(html, fn)), assert.ok(/efFindEncoder\([^;]*efDfFromPath\(pysimFsNodePath\(node\)\)/s.test(extractFunc(html, fn)),
@@ -647,8 +658,9 @@ test('F2: untouched PNN items keep their TLV bytes through the DOM and the save'
}); });
test('F12: a selection change during the read abandons the decoded edit', async () => { test('F12: a selection change during the read abandons the decoded edit', async () => {
pysimFsSelected = 'EF.SPN'; pysimFsSelected = 'MF/6F46';
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.SPN', fid: '6f46' }] }; pysimFsTreeRoot = { name: 'MF', children: [] };
pysimFsTreeRoot.children.push({ name: 'EF.SPN', fid: '6f46', parent: pysimFsTreeRoot });
const fakeEl = () => { const fakeEl = () => {
const classes = new Set(); const classes = new Set();
return { return {
@@ -681,7 +693,7 @@ test('F12: a selection change during the read abandons the decoded edit', async
let resets = 0; let resets = 0;
globalThis.pysimFsResetEdit = () => { resets++; }; globalThis.pysimFsResetEdit = () => { resets++; };
const pending = pysimFsDecEdit(); const pending = pysimFsDecEdit();
pysimFsSelected = 'EF.ADN'; pysimFsSelected = 'MF/6F3A';
release(); release();
await pending; await pending;
assert.strictEqual(resets, 1); assert.strictEqual(resets, 1);
+12 -6
View File
@@ -29,11 +29,12 @@ let code = 'var pysimFsDecodedMode = false;\n'
+ 'var pysimFsSelected = null;\n' + 'var pysimFsSelected = null;\n'
+ 'var pysimFsTreeRoot = null;\n'; + 'var pysimFsTreeRoot = null;\n';
for (const fn of ['pysimFsSetMode', 'pysimFsPillsEnabled', 'pysimFsEdit', for (const fn of ['pysimFsSetMode', 'pysimFsPillsEnabled', 'pysimFsEdit',
'pysimFsCancel', 'pysimFsResetEdit', 'pysimFsFindNode', 'pysimFsNodePath', 'pysimFsCancel', 'pysimFsResetEdit', 'pysimFsFindNodeByPath', 'pysimCustomNormPath',
'efDfFromPath', 'pysimFsHasDecoder', 'pysimFsNodePath', 'efFidFromPath', 'efDfFromPath', 'pysimFsHasDecoder',
'pysimFsSyncContentVisibility']) { 'pysimFsSyncContentVisibility']) {
code += extractFunc(html, fn) + '\n'; code += extractFunc(html, fn) + '\n';
} }
code += html.match(/const EF_ADF_RIDS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
eval(code); eval(code);
let events = []; let events = [];
@@ -84,8 +85,9 @@ function setup(opts) {
pysimFsDecodedMode = !!opts.decoded; pysimFsDecodedMode = !!opts.decoded;
pysimFsEditMode = false; pysimFsEditMode = false;
pysimFsEditData = null; pysimFsEditData = null;
pysimFsSelected = 'EF.IMSI'; pysimFsSelected = 'MF/6F07';
pysimFsTreeRoot = { name: 'MF', fid: '3f00', children: [{ name: 'EF.IMSI', fid: '6f07' }] }; pysimFsTreeRoot = { name: 'MF', fid: '3f00', children: [] };
pysimFsTreeRoot.children.push({ name: 'EF.IMSI', fid: '6f07', parent: pysimFsTreeRoot });
globalThis.efFindDecoder = (name, fid) => { globalThis.efFindDecoder = (name, fid) => {
const n = (name || '').toUpperCase(); const n = (name || '').toUpperCase();
const f = (fid || '').toLowerCase(); const f = (fid || '').toLowerCase();
@@ -206,12 +208,16 @@ test('Read decoded is ignored when the file has no decoder', () => {
assert.ok(!h.pill('dec').classList.contains('bg-blue-600')); assert.ok(!h.pill('dec').classList.contains('bg-blue-600'));
}); });
test('pysimFsHasDecoder resolves the selected file by name or FID', () => { test('pysimFsHasDecoder resolves the selected file by its path or FID', () => {
setup(); setup();
assert.strictEqual(pysimFsHasDecoder(), true, 'EF.IMSI is in the tree'); assert.strictEqual(pysimFsHasDecoder(), true, 'EF.IMSI is in the tree');
// alias name: the decoder is matched by the FID fallback // alias name: the decoder is matched by the FID fallback
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.ALIAS', fid: '6f07' }] }; pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.ALIAS', fid: '6f07' }] };
pysimFsSelected = 'EF.ALIAS'; pysimFsTreeRoot.children[0].parent = pysimFsTreeRoot;
pysimFsSelected = 'MF/6F07';
assert.strictEqual(pysimFsHasDecoder(), true);
// a bare FID selection (an API caller) resolves by the FID fallback
pysimFsSelected = '6F07';
assert.strictEqual(pysimFsHasDecoder(), true); assert.strictEqual(pysimFsHasDecoder(), true);
// unknown file -> no decoder // unknown file -> no decoder
globalThis.efFindDecoder = () => null; globalThis.efFindDecoder = () => null;
+16 -2
View File
@@ -24,6 +24,9 @@ function extractFunc(src, name) {
let code = 'var pysimFsSelected = null;\n' let code = 'var pysimFsSelected = null;\n'
+ 'var pysimFsDecodedMode = false;\n' + 'var pysimFsDecodedMode = false;\n'
+ 'var pysimFsTreeRoot = null;\n'; + 'var pysimFsTreeRoot = null;\n';
code += extractFunc(html, 'efFidFromPath') + '\n';
code += extractFunc(html, 'efDfFromPath') + '\n';
code += extractFunc(html, 'pysimFsFallbackNode') + '\n';
code += extractFunc(html, 'pysimFsSyncContentVisibility') + '\n'; code += extractFunc(html, 'pysimFsSyncContentVisibility') + '\n';
code += extractFunc(html, 'pysimFsRead') + '\n'; code += extractFunc(html, 'pysimFsRead') + '\n';
eval(code); eval(code);
@@ -53,7 +56,7 @@ function setup() {
}; };
globalThis.t = s => s; globalThis.t = s => s;
globalThis.esc = s => s; globalThis.esc = s => s;
globalThis.pysimFsFindNode = () => ({ name: 'EF.IMSI', fid: '6f07' }); globalThis.pysimFsFindNodeByPath = () => ({ name: 'EF.IMSI', fid: '6f07' });
globalThis.pysimFsSelectBody = () => ({}); globalThis.pysimFsSelectBody = () => ({});
globalThis.pysimFsShowError = (el, sw, err) => { globalThis.pysimFsShowError = (el, sw, err) => {
errorShown = { sw, err }; errorShown = { sw, err };
@@ -63,7 +66,7 @@ function setup() {
if (fetchResult instanceof Error) throw fetchResult; if (fetchResult instanceof Error) throw fetchResult;
return fetchResult; return fetchResult;
}; };
pysimFsSelected = 'EF.IMSI'; pysimFsSelected = 'ADF.USIM/6F07';
pysimFsDecodedMode = false; pysimFsDecodedMode = false;
pysimFsTreeRoot = { name: 'MF' }; pysimFsTreeRoot = { name: 'MF' };
fetchResult = { success: true, sw: '9000', data: 'AABB' }; fetchResult = { success: true, sw: '9000', data: 'AABB' };
@@ -92,6 +95,17 @@ test('a successful read fills and shows the content pane', async () => {
assert.strictEqual(status.textContent, 'SW: 9000 OK'); assert.strictEqual(status.textContent, 'SW: 9000 OK');
}); });
test('a path whose node is not loaded selects by its last segment and FID', async () => {
setup();
globalThis.pysimFsFindNodeByPath = () => null;
let body = null;
globalThis.pysimFsSelectBody = node => { body = node; return {}; };
pysimFsSelected = 'MF/7F20/6FB7';
const ok = await pysimFsRead();
assert.strictEqual(ok, true);
assert.deepStrictEqual(body, { name: '6FB7', fid: '6FB7' });
});
test('a failed read keeps the content pane hidden', async () => { test('a failed read keeps the content pane hidden', async () => {
setup(); setup();
fetchResult = { success: false, sw: '6982', error: 'Security status not satisfied' }; fetchResult = { success: false, sw: '6982', error: 'Security status not satisfied' };
+25
View File
@@ -23,6 +23,7 @@ function extractFunc(src, name) {
let code = 'var pysimFsSort = "fid";\n'; let code = 'var pysimFsSort = "fid";\n';
code += extractFunc(html, 'pysimFsSortChildren') + '\n'; code += extractFunc(html, 'pysimFsSortChildren') + '\n';
code += extractFunc(html, 'pysimFsNodePath') + '\n';
code += extractFunc(html, 'pysimFsRenderNode') + '\n'; code += extractFunc(html, 'pysimFsRenderNode') + '\n';
code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\n'; code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\n';
eval(code); eval(code);
@@ -65,3 +66,27 @@ test('non-existing directory hides previously loaded children', () => {
assert.ok(!out.includes('(empty)'), out); assert.ok(!out.includes('(empty)'), out);
assert.ok(!out.includes('pysimFsToggleDir'), out); assert.ok(!out.includes('pysimFsToggleDir'), out);
}); });
test('the row handlers carry the canonical path, so same-named files differ', () => {
// EF.ECC lives in DF.GSM and ADF.USIM with the same name AND FID: the
// onclick argument must be the path, never the ambiguous display name
const root = { name: 'MF', fid: '3F00', isDir: true, exists: true, children: [], expanded: true, parent: null };
const gsm = { name: 'DF.GSM', fid: '7F20', isDir: true, exists: true, children: [], expanded: true, parent: root };
const usim = { name: 'ADF.USIM', fid: '7FFF', isDir: true, exists: true, children: [], expanded: true, parent: root };
const gsmEcc = { name: 'EF.ECC', fid: '6FB7', isDir: false, exists: true, children: null, expanded: false, parent: gsm };
const usimEcc = { name: 'EF.ECC', fid: '6FB7', isDir: false, exists: true, children: null, expanded: false, parent: usim };
root.children = [gsm, usim];
gsm.children = [gsmEcc];
usim.children = [usimEcc];
assert.ok(pysimFsRenderNode(gsmEcc, 0).includes("pysimFsClickFile('MF/7F20/6FB7')"));
assert.ok(pysimFsRenderNode(usimEcc, 0).includes("pysimFsClickFile('ADF.USIM/6FB7')"));
assert.ok(pysimFsRenderNode(gsm, 0).includes("pysimFsToggleDir('MF/7F20')"));
assert.ok(pysimFsRenderNode(usim, 0).includes("pysimFsToggleDir('ADF.USIM')"));
});
test('pysimFsRenderNode passes pysimFsNodePath(node) to the click/toggle handlers', () => {
const body = extractFunc(html, 'pysimFsRenderNode');
assert.ok(body.includes('const nodePath = pysimFsNodePath(node);'));
assert.match(body, /pysimFsToggleDir\(\\'' \+ esc\(nodePath\)/);
assert.match(body, /pysimFsClickFile\(\\'' \+ esc\(nodePath\)/);
});
+2 -1
View File
@@ -446,7 +446,8 @@ test('file manager shows FCI info and keeps the selection in state, not the DOM'
assert.ok(html.includes('pysimFsInfoHtml(sel, name, { arr: arrs, path: pysimFsNodePath(node) })')); assert.ok(html.includes('pysimFsInfoHtml(sel, name, { arr: arrs, path: pysimFsNodePath(node) })'));
assert.ok(!html.includes('pysim-fs-filename')); assert.ok(!html.includes('pysim-fs-filename'));
assert.ok(html.includes('let pysimFsSelected = null;')); assert.ok(html.includes('let pysimFsSelected = null;'));
assert.ok(html.includes('pysimFsSelected = name;')); assert.ok(html.includes('pysimFsSelected = node ? pysimFsNodePath(node) : path;'),
'the selection is keyed by the canonical path, not the display name');
assert.ok(!html.includes('pysimFsSelect()')); assert.ok(!html.includes('pysimFsSelect()'));
}); });