b0b8d02fe7
The parent field was a select over custom DFs only, so a standard DF (e.g. MF/7F20) could not be a parent and canonical entries under one were dropped on import. It is now a free-text path with suggestions from the root, custom DFs and DFs of the loaded tree (any depth); a bare FID chain is completed from the root selector. Validation accepts standard or not-yet-seen parents (flagged in the list), rejects a parent known to be an EF, and import keeps such entries. Custom-file edits re-inject the loaded tree: new nodes appear at once, renamed model nodes are restored and injected nodes removed when their entry goes away. Help updated (the form lives in Profiler, not Card reader). SW cache otaman-v176.
126 lines
4.7 KiB
JavaScript
126 lines
4.7 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');
|
|
|
|
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);
|
|
}
|
|
|
|
let code = 'var pysimCustomFiles = [];\nvar pysimFsTreeRoot = null;\n';
|
|
for (const fn of ['pysimFsNodePath', 'pysimCustomInject', 'pysimCustomRefreshTree', 'pysimCustomParent', 'pysimCustomFid']) {
|
|
code += extractFunc(html, fn) + '\n';
|
|
}
|
|
code += 'globalThis.pysimFsRenderTree = () => {};\n';
|
|
eval(code);
|
|
|
|
function node(name, fid, parent, children) {
|
|
return { name, fid, parent: parent || null, children: children || null, isDir: true };
|
|
}
|
|
|
|
function tree() {
|
|
const mf = node('MF', '3F00', null, []);
|
|
const dfA = node('DF.A', '5F01', mf);
|
|
const dfB = node('DF.B', '5F02', mf);
|
|
const dfC = node('DF.C', '5F03', mf, [node('EF.X', '6F46', null)]);
|
|
dfC.children[0].parent = dfC;
|
|
const adf = node('ADF.USIM', '7FFF', mf);
|
|
const efImsi = node('EF.IMSI', '6F07', adf);
|
|
adf.children = [efImsi];
|
|
mf.children = [dfA, dfB, dfC, adf];
|
|
return { mf, dfA, dfB, dfC, adf, efImsi };
|
|
}
|
|
|
|
test('pysimFsNodePath builds canonical paths from MF and ADF roots', () => {
|
|
const t = tree();
|
|
assert.strictEqual(pysimFsNodePath(t.mf), 'MF');
|
|
assert.strictEqual(pysimFsNodePath(t.dfA), 'MF/5F01');
|
|
assert.strictEqual(pysimFsNodePath(t.dfC.children[0]), 'MF/5F03/6F46');
|
|
assert.strictEqual(pysimFsNodePath(t.adf), 'ADF.USIM');
|
|
assert.strictEqual(pysimFsNodePath(t.efImsi), 'ADF.USIM/6F07');
|
|
});
|
|
|
|
test('injection matches full paths, so same-FID DFs stay apart', () => {
|
|
const t = tree();
|
|
pysimCustomFiles = [
|
|
{ path: 'MF/5F01/6F46', name: 'EF.ONLY-A', kind: 'ef' },
|
|
{ path: 'MF/5F02/6F46', name: 'EF.ONLY-B', kind: 'ef' },
|
|
{ path: 'ADF.USIM/6F07', name: 'EF.MY-IMSI', kind: 'ef' },
|
|
];
|
|
pysimCustomInject(t.mf);
|
|
assert.strictEqual(t.mf.children.length, 4, 'MF-level injection adds nothing');
|
|
pysimCustomInject(t.dfA);
|
|
assert.strictEqual(t.dfA.children.length, 1);
|
|
assert.strictEqual(t.dfA.children[0].name, 'EF.ONLY-A');
|
|
assert.strictEqual(t.dfA.children[0].customPath, 'MF/5F01/6F46');
|
|
pysimCustomInject(t.dfB);
|
|
assert.strictEqual(t.dfB.children[0].name, 'EF.ONLY-B');
|
|
pysimCustomInject(t.dfC);
|
|
assert.strictEqual(t.dfC.children.length, 1, 'unrelated DF is untouched');
|
|
pysimCustomInject(t.adf);
|
|
assert.strictEqual(t.adf.children.length, 1);
|
|
assert.strictEqual(t.adf.children[0].name, 'EF.MY-IMSI');
|
|
assert.strictEqual(t.adf.children[0].customPath, 'ADF.USIM/6F07');
|
|
});
|
|
|
|
test('injection renames and marks an existing model node', () => {
|
|
const t = tree();
|
|
pysimCustomFiles = [{ path: 'MF/5F03/6F46', name: 'EF.RENAMED', kind: 'ef' }];
|
|
pysimCustomInject(t.dfC);
|
|
assert.strictEqual(t.dfC.children.length, 1);
|
|
assert.strictEqual(t.dfC.children[0].name, 'EF.RENAMED');
|
|
assert.strictEqual(t.dfC.children[0].custom, true);
|
|
});
|
|
|
|
test('injected DFs are directories and can host their own children', () => {
|
|
const t = tree();
|
|
pysimCustomFiles = [
|
|
{ path: 'MF/5F10', name: 'DF.NEW', kind: 'df' },
|
|
{ path: 'MF/5F10/6F46', name: 'EF.UNDER-NEW', kind: 'ef' },
|
|
];
|
|
pysimCustomInject(t.mf);
|
|
const df = t.mf.children.find(c => c.fid === '5F10');
|
|
assert.ok(df, 'custom DF injected into MF');
|
|
assert.strictEqual(df.isDir, true);
|
|
assert.strictEqual(pysimFsNodePath(df), 'MF/5F10');
|
|
pysimCustomInject(df);
|
|
assert.strictEqual(df.children.length, 1);
|
|
assert.strictEqual(df.children[0].name, 'EF.UNDER-NEW');
|
|
});
|
|
|
|
test('refresh restores renamed model nodes and drops injected ones', () => {
|
|
const t = tree();
|
|
pysimFsTreeRoot = t.mf;
|
|
pysimCustomFiles = [{ path: 'MF/5F03/6F46', name: 'EF.RENAMED', kind: 'ef' }];
|
|
pysimCustomRefreshTree();
|
|
assert.strictEqual(t.dfC.children[0].name, 'EF.RENAMED');
|
|
assert.strictEqual(t.dfC.children[0].custom, true);
|
|
// deleting the entry brings the model name back
|
|
pysimCustomFiles = [];
|
|
pysimCustomRefreshTree();
|
|
assert.strictEqual(t.dfC.children[0].name, 'EF.X');
|
|
assert.ok(!t.dfC.children[0].custom);
|
|
assert.strictEqual(t.dfC.children[0].modelName, undefined);
|
|
// injected nodes disappear together with their entry
|
|
pysimCustomFiles = [{ path: 'MF/5F10', name: 'DF.NEW', kind: 'df' }];
|
|
pysimCustomRefreshTree();
|
|
assert.ok(t.mf.children.some(c => c.fid === '5F10'));
|
|
pysimCustomFiles = [];
|
|
pysimCustomRefreshTree();
|
|
assert.ok(!t.mf.children.some(c => c.fid === '5F10'));
|
|
});
|