Files
simple/frontend/tests/uicc_files.test.js
T
catarrh 1035bf069d ui: show SELECT picker paths from the root instead of "fid — parent"
The picker option label read backwards, e.g. "EF.IMSI — 6F07 — 7F20".
It now shows the path within the optgroup's root with the FIDs in order,
matching the path-field convention: "EF.IMSI — 7F20/6F07",
"EF.ICCID — 2FE2", "EF.PSC — 5F3A/4F22" (ADF session).  Switch markers and
the filter (which also matches the symbolic path) are unchanged.
sw cache -> simple-v243; version stays 3.4.0.
2026-09-23 08:30:12 +03:00

243 lines
12 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
const asset = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'uicc_files.json'), 'utf8'));
function extractFunc(src, name) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
// The RFM SELECT file picker: merge of the card tree / custom files / the
// shipped standard list, and the per-method fill logic (relative FID chains
// per TS 102 221 11.1.1.2, path from MF without the MF identifier per
// ISO 7816-4, path from the current DF per TS 102 221 11.1.1.2 P1=09).
const FNS = ['chainKind', 'rfmRowKey', 'rfmParentPath', 'rfmEntryByPath', 'rfmRoots',
'rfmFileEntries', 'rfmStartDf', 'rfmSetStartDf', 'rfmNoticeClear', 'rfmApplySelect',
'rfmCurrentDf', 'rfmFidsFrom', 'rfmRelativeChain', 'rfmSelectFill', 'rfmFileOptionsHtml',
'escHtml', 'esc', 'pysimFsNodePath', 'pysimCustomFid'];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
for (const c of ['const _chains = \\{\\};', 'let _uiccStdFiles = null;', 'let _uiccStdLoad = null;',
'let _rfmShowAll = false;', 'const _rfmFilter = \\{\\};', 'const _rfmNotice = \\{\\};',
'const _rfmStartDf = \\{\\};']) {
const m = html.match(new RegExp(c));
if (!m) throw new Error('declaration not found: ' + c);
code += m[0].replace(/^(const|let) /, 'var ') + '\n';
}
code += 'var pysimCustomFiles = [];\n';
code += 'var pysimFsTreeRoot = null;\n';
code += 'function chainRender() {}\n';
eval(code);
// The shipped asset stands in for the fetched standard list.
_uiccStdFiles = asset.files;
function entry(p) {
const e = _uiccStdFiles.find(f => f.path === p);
assert.ok(e, 'asset entry missing: ' + p);
return e;
}
function row(cmd, fields) { return { cmd: cmd, fields: fields || {} }; }
function setChain(chainId, rows) {
_chains[chainId] = { rows: rows || [] };
return chainId;
}
test('the shipped asset is well formed and covers the standard trees', () => {
assert.ok(asset.files.length > 300, 'entries: ' + asset.files.length);
const paths = asset.files.map(f => f.path);
assert.strictEqual(paths.length, new Set(paths).size, 'duplicate paths');
assert.strictEqual(entry('MF/2FE2').name, 'EF.ICCID');
assert.strictEqual(entry('MF/7F10').name, 'DF.TELECOM');
assert.strictEqual(entry('MF/7F10/6F40').name, 'EF.MSISDN');
assert.strictEqual(entry('MF/7F20/6F46').name, 'EF.SPN');
assert.strictEqual(entry('ADF.USIM/6F07').name, 'EF.IMSI');
assert.strictEqual(entry('ADF.USIM').kind, 'adf');
assert.strictEqual(entry('ADF.USIM').aid, 'A0000000871002');
assert.strictEqual(entry('ADF.ISIM').aid, 'A0000000871004');
});
test('rfmFidsFrom walks a path down to the FIDs', () => {
const list = rfmFileEntries();
assert.strictEqual(rfmFidsFrom(list, 'MF', 'MF/7F10/6F40'), '7F106F40');
assert.strictEqual(rfmFidsFrom(list, 'MF/7F10', 'MF/7F10/6F40'), '6F40');
assert.strictEqual(rfmFidsFrom(list, 'MF/7F10', 'MF/7F20'), '');
assert.strictEqual(rfmFidsFrom(list, 'MF', 'ADF.USIM/6F07'), '');
});
test('rfmRelativeChain uses the FID search order (children/parent/siblings)', () => {
const list = rfmFileEntries();
assert.strictEqual(rfmRelativeChain(list, 'MF', 'MF/7F10/6F40'), '7F10,6F40');
// Sibling branch: from DF.TELECOM to EF.SPN under DF.GSM.
assert.strictEqual(rfmRelativeChain(list, 'MF/7F10', 'MF/7F20/6F46'), '7F20,6F46');
// One level up then sideways.
assert.strictEqual(rfmRelativeChain(list, 'MF/7F10/5F3A', 'MF/7F20'), '7F10,7F20');
assert.strictEqual(rfmRelativeChain(list, 'ADF.USIM', 'ADF.USIM/6F07'), '6F07');
assert.strictEqual(rfmRelativeChain(list, 'ADF.USIM/5F3A', 'ADF.USIM/6F07'), '6F07');
// Different roots: a FID select cannot cross into another tree.
assert.strictEqual(rfmRelativeChain(list, 'MF', 'ADF.USIM/6F07'), '');
assert.strictEqual(rfmRelativeChain(list, 'ADF.USIM', 'MF/2FE2'), '');
});
test('rfmSelectFill keeps a method that can express the pick', () => {
setChain('chain-sim', [row('select', { method: 'fid' })]);
let fill = rfmSelectFill('chain-sim', 0, entry('MF/7F10'), rfmFileEntries());
assert.deepStrictEqual(fill, { method: 'fid', fields: { fid: '7F10' }, switched: false });
setChain('chain-sim', [row('select', { method: 'chain' })]);
fill = rfmSelectFill('chain-sim', 0, entry('MF/7F10/6F40'), rfmFileEntries());
assert.deepStrictEqual(fill, { method: 'chain', fields: { chain: '7F10,6F40' }, switched: false });
});
test('rfmSelectFill prefers path when the method has to switch', () => {
// SIM RFM: path is P1=08 (from MF) only.
setChain('chain-sim', [row('select', { method: 'fid' })]);
let fill = rfmSelectFill('chain-sim', 0, entry('MF/7F10/6F40'), rfmFileEntries());
assert.deepStrictEqual(fill, { method: 'path', fields: { path: '7F106F40', base: 'mf' }, switched: true });
// ADF entry cannot be reached from MF by FID/path/chain.
assert.strictEqual(rfmSelectFill('chain-sim', 0, entry('ADF.USIM/6F07'), rfmFileEntries()), null);
// USIM RFM starts in ADF.USIM: its files are direct children, deep ones use
// the relative path from the current DF.
setChain('chain-usim', [row('select', { method: 'fid' })]);
fill = rfmSelectFill('chain-usim', 0, entry('ADF.USIM/6F07'), rfmFileEntries());
assert.deepStrictEqual(fill, { method: 'fid', fields: { fid: '6F07' }, switched: false });
setChain('chain-usim', [row('select', { method: 'fid' })]);
fill = rfmSelectFill('chain-usim', 0, entry('ADF.USIM/5F3A/4F22'), rfmFileEntries());
assert.deepStrictEqual(fill, { method: 'path', fields: { path: '5F3A4F22', base: 'df' }, switched: true });
// Cross-root: an MF file is reachable from the ADF session by path from MF.
setChain('chain-usim', [row('select', { method: 'fid' })]);
fill = rfmSelectFill('chain-usim', 0, entry('MF/2FE2'), rfmFileEntries());
assert.deepStrictEqual(fill, { method: 'path', fields: { path: '2FE2', base: 'mf' }, switched: true });
});
test('rfmSelectFill falls back to chain when path cannot express it', () => {
// Session in ADF.USIM, currently inside DF.PHONEBOOK: a target in the
// DF.GSM-ACCESS branch is not under the current DF and not in the MF tree,
// so only the relative chain works.
const list = rfmFileEntries();
setChain('chain-usim', [row('select', { method: 'path', path: '5F3A', base: 'df' })]);
const fill = rfmSelectFill('chain-usim', 1, entry('ADF.USIM/5F3B/4F20'), list);
assert.strictEqual(fill.method, 'chain');
assert.strictEqual(fill.fields.chain, '5F3B,4F20');
assert.strictEqual(fill.switched, true);
// ADF roots are selected by AID, not by the picker.
assert.strictEqual(rfmSelectFill('chain-usim', 1, entry('ADF.USIM'), list), null);
});
test('rfmCurrentDf tracks the session context across rows', () => {
const list = rfmFileEntries();
setChain('chain-sim', [
row('select', { method: 'fid', fid: '7F10' }),
row('select', { method: 'fid', fid: '6F40' }),
row('select', { method: 'path', path: '7F106F40' }),
]);
assert.strictEqual(rfmCurrentDf('chain-sim', 0, list), 'MF');
assert.strictEqual(rfmCurrentDf('chain-sim', 1, list), 'MF/7F10');
// Selecting an EF keeps the current DF.
assert.strictEqual(rfmCurrentDf('chain-sim', 2, list), 'MF/7F10');
// A path select sets the DF to the file's parent.
assert.strictEqual(rfmCurrentDf('chain-sim', 3, list), 'MF/7F10');
setChain('chain-usim', [
row('select', { method: 'path', path: '5F3A', base: 'df' }),
row('select', { method: 'chain', chain: '4F22' }),
]);
assert.strictEqual(rfmCurrentDf('chain-usim', 0, list), 'ADF.USIM');
assert.strictEqual(rfmCurrentDf('chain-usim', 1, list), 'ADF.USIM/5F3A');
assert.strictEqual(rfmCurrentDf('chain-usim', 2, list), 'ADF.USIM/5F3A');
// The picker can start the session elsewhere (TAR decides the implicit DF).
rfmSetStartDf('chain-sim', 'ADF.USIM');
assert.strictEqual(rfmCurrentDf('chain-sim', 0, list), 'ADF.USIM');
rfmSetStartDf('chain-sim', 'MF');
});
test('rfmFileEntries merges card tree, custom files and the standard list', () => {
const mf = { name: 'MF', fid: '3F00', isDir: true, parent: null, children: [] };
const gsm = { name: 'DF.GSM', fid: '7F20', isDir: true, parent: mf, exists: true, children: [] };
const adn = { name: 'EF.ADN', fid: '6F3A', isDir: false, parent: gsm, exists: false, children: null };
const iccid = { name: 'EF.ICCID', fid: '2FE2', isDir: false, parent: mf, exists: true, children: null };
const customNode = { name: 'EF.MYFILE', fid: '6F99', isDir: false, parent: mf, custom: true, customPath: 'MF/6F99', exists: null, children: null };
gsm.children.push(adn);
mf.children.push(gsm, iccid, customNode);
pysimFsTreeRoot = mf;
pysimCustomFiles = [{ path: 'MF/6F99', name: 'EF.MYFILE', kind: 'ef' }];
const list = rfmFileEntries();
const byPath = {};
list.forEach(e => { byPath[e.path] = e; });
// The card tree wins over the standard entry with the same path and hides
// probed-absent files.
assert.strictEqual(byPath['MF/7F20'].source, 'card');
assert.strictEqual(byPath['MF/2FE2'].exists, true);
assert.ok(!byPath['MF/7F20/6F3A'], 'probed-absent file must be dropped');
assert.strictEqual(byPath['MF/6F99'].source, 'custom');
// The standard list still covers branches the card tree has not loaded.
assert.ok(byPath['MF/7F10/6F40'], 'standard entry missing');
assert.strictEqual(byPath['MF/7F10/6F40'].source, 'standard');
// ADF roots are group headers, not entries.
assert.ok(!list.some(e => e.kind === 'adf'));
pysimFsTreeRoot = null;
pysimCustomFiles = [];
});
test('rfmFileOptionsHtml labels files with their path within the root', () => {
const list = rfmFileEntries();
setChain('chain-usim', [row('select', { method: 'fid' })]);
const opts = rfmFileOptionsHtml('chain-usim', 0, list);
// ADF.USIM session: EF.IMSI is a direct child of the root.
assert.ok(opts.includes('>EF.IMSI — 6F07<'), opts.match(/>EF\.IMSI[^<]*</)[0]);
// The root is the optgroup, the path is relative to it (FIDs in order).
assert.ok(opts.includes('<optgroup label="ADF.USIM">'));
assert.ok(!opts.includes('6F07 — '), 'no reversed "fid — parent" label');
// Deep file under a DF: the label reads like the path field.
rfmSetStartDf('chain-usim', 'MF');
setChain('chain-sim', [row('select', { method: 'path', path: '7F10' })]);
const deep = rfmFileOptionsHtml('chain-sim', 1, list);
assert.ok(deep.includes('>EF.MSISDN — 7F10/6F40<'), deep.match(/>EF\.MSISDN[^<]*</)[0]);
rfmSetStartDf('chain-usim', 'ADF.USIM');
});
test('rfmFileOptionsHtml groups by root and marks method switches', () => {
const list = rfmFileEntries();
setChain('chain-sim', [row('select', { method: 'fid' })]);
// Default: only what by-FID from MF can express (its direct children).
const opts = rfmFileOptionsHtml('chain-sim', 0, list);
assert.ok(opts.includes('<optgroup label="MF">'));
assert.ok(opts.includes('MF/7F10'), 'DF.TELECOM is a direct child');
assert.ok(!opts.includes('MF/7F10/6F40'), 'deep files need a switch');
assert.ok(!opts.includes('<optgroup label="ADF.USIM">'), 'ADF files are not reachable from MF');
// "all files" reveals the switchable entries, marked with the method they
// would switch to; files in another root stay hidden (the session start
// selector covers those).
_rfmShowAll = true;
const all = rfmFileOptionsHtml('chain-sim', 0, list);
assert.ok(all.includes('MF/7F10/6F40'));
assert.ok(all.includes('→ path'), 'switching entries are marked');
assert.ok(!all.includes('<optgroup label="ADF.USIM">'));
// Starting the session in the ADF makes its files pickable instead.
_rfmShowAll = false;
rfmSetStartDf('chain-sim', 'ADF.USIM');
const adf = rfmFileOptionsHtml('chain-sim', 0, list);
assert.ok(adf.includes('<optgroup label="ADF.USIM">'));
assert.ok(adf.includes('ADF.USIM/6F07'), 'EF.IMSI is a direct child of ADF.USIM');
rfmSetStartDf('chain-sim', 'MF');
_rfmFilter['chain-sim#0'] = 'imsi';
const filtered = rfmFileOptionsHtml('chain-sim', 0, list);
assert.ok(!filtered.includes('EF.ADN'));
delete _rfmFilter['chain-sim#0'];
});