EF resolver: resolve by (DF, FID); split EF.ECC and EF.AD per DF
The decoder/encoder lookup now takes an optional DF context and resolves DF-first, then name, then FID (the spec keys files by FID within their DF; the name is a display label). efDfFromPath derives the parent DF segment of a canonical path. EF.ECC is split into its two DF-specific decoders: DF.GSM (7F20) transparent 3n (existing) and ADF.USIM linear-fixed X+4 - the new efDecEccUsim reads the 3-byte Emergency Call Code, the Annex A alpha and the Emergency Service Category (decoded per TS 24.008 Table 10.5.135d). EF.AD is registered as DF-keyed entries (7F20 / ADF.USIM / ADF.ISIM), reusing the existing encoder. The df is threaded through the file-manager decoded-view/edit/save and the snapshot/compare decode. No ECC editor yet (read-only). Frontend 1081 + Python 1090 tests green.
This commit is contained in:
1 parent
6d762b9d9c
commit
8e0a97f025
5 files changed
+247
-36
No files matched your search
@@ -26,13 +26,13 @@ const FNS = [
|
||||
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct', 'efPnnText',
|
||||
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecSst', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact',
|
||||
'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc',
|
||||
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecHpplmn', 'efDecEcc', 'efDecOpl', 'efDecAd', 'efDecAcl',
|
||||
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecHpplmn', 'efDecEcc', 'efDecEccUsim', 'efDecOpl', 'efDecAd', 'efDecAcl',
|
||||
'efSmspDaNumber', 'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn',
|
||||
'pinKeyRefName', 'efArrAmLabel', 'efArrSc', 'efArrAmDo',
|
||||
'efDecExt1', 'efDecSms', 'efDecSume', 'efPbrHex', 'efDecPbr', 'efFindDecoder', 'efFidFromPath', 'efDecodeBytes',
|
||||
'efDecExt1', 'efDecSms', 'efDecSume', 'efPbrHex', 'efDecPbr', 'efFindDecoder', 'efFidFromPath', 'efDfFromPath', 'efDecodeBytes',
|
||||
'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten',
|
||||
'efDiffData', 'efDataSummary', 'efContentDiff', 'gsm7Decode', 'efRenderFieldsHtml',
|
||||
'pysimFsDecodedHtml', 'profilerRenderReport', 'profilerLabelText', 'profilerResultAspects',
|
||||
'pysimFsDecodedHtml', 'pysimFsNodePath', 'profilerRenderReport', 'profilerLabelText', 'profilerResultAspects',
|
||||
'profilerAspectSummary', 'profilerMatchedRecordsText', 'profilerRawDataCheck', 'profilerCustomNameForPath',
|
||||
'pysimCustomNormPath',
|
||||
];
|
||||
@@ -45,6 +45,7 @@ for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'FCP_ARR_AM_BITS']) {
|
||||
for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_PBR_KINDS']) {
|
||||
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_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 += 'var pysimCustomFiles = [];\nvar pysimFsSelected = \'EF.IMSI\';\n';
|
||||
@@ -357,6 +358,70 @@ test('decoder registry resolves by name first, then FID; unknown returns null',
|
||||
assert.strictEqual(efFidFromPath('ADF.USIM'), null);
|
||||
});
|
||||
|
||||
test('efDfFromPath reads the parent DF of a canonical path', () => {
|
||||
assert.strictEqual(efDfFromPath('MF/7F20/6FAD'), '7F20');
|
||||
assert.strictEqual(efDfFromPath('ADF.USIM/6FAD'), 'ADF.USIM');
|
||||
assert.strictEqual(efDfFromPath('MF/ADF.USIM/6FAD'), 'ADF.USIM');
|
||||
assert.strictEqual(efDfFromPath('mf/7f20/6fad'), '7F20');
|
||||
assert.strictEqual(efDfFromPath('MF/7F10/5F3A/4F30'), '5F3A');
|
||||
// a bare FID (and a bare root) carries no DF context
|
||||
assert.strictEqual(efDfFromPath('6FAD'), '');
|
||||
assert.strictEqual(efDfFromPath('MF'), '');
|
||||
assert.strictEqual(efDfFromPath('ADF.USIM'), '');
|
||||
assert.strictEqual(efDfFromPath(''), '');
|
||||
assert.strictEqual(efDfFromPath(null), '');
|
||||
// the two halves of the spec identity come from the same path
|
||||
assert.strictEqual(efFidFromPath('MF/7F20/6FAD'), '6FAD');
|
||||
});
|
||||
|
||||
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
|
||||
// TS 51.011 transparent form from the TS 31.102 linear-fixed one
|
||||
assert.strictEqual(efFindDecoder('EF.ECC', '6fb7', '7F20').fn, efDecEcc);
|
||||
assert.strictEqual(efFindDecoder('EF.ECC', '6fb7', 'ADF.USIM').fn, efDecEccUsim);
|
||||
assert.strictEqual(efFindDecoder('EF.ECC', '6fb7', 'adf.usim').fn, efDecEccUsim);
|
||||
assert.strictEqual(efFindDecoder(null, '6FB7', 'ADF.USIM').fn, efDecEccUsim);
|
||||
// without a DF the historical name-then-FID lookup answers (the first
|
||||
// entry, the DF.GSM form, is unchanged behaviour)
|
||||
assert.strictEqual(efFindDecoder('EF.ECC', '6fb7').fn, efDecEcc);
|
||||
assert.strictEqual(efFindDecoder('EF.ECC', null).fn, efDecEcc);
|
||||
// a DF carrying no entry of its own falls back to the generic lookup
|
||||
assert.strictEqual(efFindDecoder('EF.ECC', '6fb7', 'ADF.ISIM').fn, efDecEcc);
|
||||
// the FID fallback stays inside the DF
|
||||
assert.strictEqual(efFindDecoder('EF.NOPE', '6FB7', 'ADF.USIM').fn, efDecEccUsim);
|
||||
assert.strictEqual(efFindDecoder('EF.NOPE', 'abcd', 'ADF.USIM'), null);
|
||||
// every EF.AD form has its own DF-keyed entry over one decoder
|
||||
assert.strictEqual(efFindDecoder('EF.AD', '6fad', '7F20').fn, efDecAd);
|
||||
assert.strictEqual(efFindDecoder('EF.AD', '6fad', 'ADF.USIM').fn, efDecAd);
|
||||
assert.strictEqual(efFindDecoder('EF.AD', '6fad', 'ADF.ISIM').fn, efDecAd);
|
||||
assert.strictEqual(efFindDecoder('EF.AD', '6fad', 'ADF.ISIM').df, 'ADF.ISIM');
|
||||
// files without a collision are unaffected by the DF context
|
||||
assert.strictEqual(efFindDecoder('EF.IMSI', '6f07', 'ADF.USIM').name, 'EF.IMSI');
|
||||
assert.strictEqual(efFindDecoder(null, '2fe2', 'MF').name, 'EF.ICCID');
|
||||
});
|
||||
|
||||
test('EF.ECC (USIM) decodes a X+4 record: code, alpha and service category', () => {
|
||||
// pySim's EF_ECC vectors (TS 31.102 4.2.21): 3 BCD code bytes, the
|
||||
// optional annex A alpha, then the category byte
|
||||
assert.deepStrictEqual(efDecEccUsim(efBytes('19F1FF01')), { code: '911', category: 'police' });
|
||||
assert.deepStrictEqual(efDecEccUsim(efBytes('19F3FF02')), { code: '913', category: 'ambulance' });
|
||||
// code 112 = 11 F2 FF (odd digit count, F padded)
|
||||
assert.deepStrictEqual(efDecEccUsim(efBytes('11F2FF40')), { code: '112', category: 'automatic eCall' });
|
||||
// with the optional alpha identifier (X = 3, GSM 7-bit octets)
|
||||
assert.deepStrictEqual(efDecEccUsim(efBytes('19F1FF424F4204')),
|
||||
{ code: '911', category: 'fire brigade', alpha: 'BOB' });
|
||||
// a UCS-2 (0x80-prefixed) alpha, and two category bits at once
|
||||
assert.deepStrictEqual(efDecEccUsim(efBytes('19F1FF800042004F004205')),
|
||||
{ code: '911', category: 'police, fire brigade', alpha: 'BOB' });
|
||||
// an unmodelled category byte stays visible as its hex value
|
||||
assert.deepStrictEqual(efDecEccUsim(efBytes('19F1FF80')), { code: '911', category: '0x80' });
|
||||
// unused records: all FF, or the mandatory code all FF
|
||||
assert.deepStrictEqual(efDecEccUsim(efBytes('FFFFFFFFFFFF')), { empty: true });
|
||||
assert.deepStrictEqual(efDecEccUsim(efBytes('FFFFFF02')), { empty: true });
|
||||
// shorter than the mandatory fields: raw fallback, never a throw
|
||||
assert.deepStrictEqual(efDecEccUsim(efBytes('19F1FF')), { raw: '19F1FF' });
|
||||
});
|
||||
|
||||
test('efDecodeFile handles transparent and record content', () => {
|
||||
const t = efDecodeFile('EF.IMSI', null, { kind: 'transparent', data: '082982608200002080' });
|
||||
assert.strictEqual(typeof t.fn, 'function');
|
||||
@@ -398,6 +463,26 @@ test('efFlatten skips the all-FF empty marker', () => {
|
||||
assert.deepStrictEqual(efFlatten({ empty: true }), []);
|
||||
});
|
||||
|
||||
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)
|
||||
const usim = { name: 'ADF.USIM', children: [] };
|
||||
const usimEcc = { name: 'EF.ECC', fid: '6FB7', parent: usim };
|
||||
usim.children.push(usimEcc);
|
||||
const usimHtml = pysimFsDecodedHtml(usimEcc, { success: true, records: [{ num: 1, data: '19F1FF01' }] });
|
||||
assert.ok(usimHtml.includes('Emergency call code'));
|
||||
assert.ok(usimHtml.includes('911'));
|
||||
assert.ok(usimHtml.includes('police'));
|
||||
// DF.GSM's EF.ECC keeps the transparent code + category decode
|
||||
const gsm = { name: 'DF.GSM', fid: '7F20', children: [] };
|
||||
const gsmEcc = { name: 'EF.ECC', fid: '6FB7', parent: gsm };
|
||||
gsm.children.push(gsmEcc);
|
||||
const gsmHtml = pysimFsDecodedHtml(gsmEcc, { success: true, data: '19F101' });
|
||||
assert.ok(gsmHtml.includes('Number'));
|
||||
assert.ok(gsmHtml.includes('911'));
|
||||
assert.ok(gsmHtml.includes('Emergency category'));
|
||||
assert.ok(!gsmHtml.includes('Emergency call code'));
|
||||
});
|
||||
|
||||
test('efDiffData names an all-FF side Empty (all FF)', () => {
|
||||
assert.deepStrictEqual(efDiffData({ empty: true }, { full: 'Miran' }),
|
||||
[['Full name', 'Empty (all FF)', 'Miran']]);
|
||||
|
||||
@@ -218,6 +218,36 @@ test('EF.SPN and the ADN family share the encoder registry with the PLMN list',
|
||||
assert.strictEqual(efFindEncoder('EF.PLMNwAcT', null).list, efFindEncoder('EF.HPLMNwAcT', null).list);
|
||||
});
|
||||
|
||||
test('the encoder resolver is DF-first: every EF.AD form shares one encoder', () => {
|
||||
// EF.AD collides in name AND FID: one entry per DF (DF.GSM 3+X,
|
||||
// ADF.USIM 4+X, ADF.ISIM 3+X), all over the same length-preserving codec
|
||||
const sim = efFindEncoder('EF.AD', '6fad', '7F20');
|
||||
const usim = efFindEncoder('EF.AD', '6fad', 'ADF.USIM');
|
||||
const isim = efFindEncoder('EF.AD', '6fad', 'ADF.ISIM');
|
||||
for (const e of [sim, usim, isim]) {
|
||||
assert.strictEqual(e.enc, efEncAd);
|
||||
assert.strictEqual(e.model, efAdModel);
|
||||
assert.strictEqual(e.fields, EF_AD_FIELDS);
|
||||
}
|
||||
assert.notStrictEqual(sim, isim);
|
||||
// without a DF the historical name-first lookup answers (DF.GSM's entry)
|
||||
assert.strictEqual(efFindEncoder('EF.AD', '6fad'), sim);
|
||||
assert.strictEqual(efFindEncoder('EF.AD', '6fad', 'ADF.NOPE'), sim);
|
||||
// EF.ECC stays read-only in either DF, EF.SST in the SIM layout, and the
|
||||
// shared 6F38 FID must not hand SST the UST encoder
|
||||
assert.strictEqual(efHasEncoder('EF.ECC', '6fb7', '7F20'), false);
|
||||
assert.strictEqual(efHasEncoder('EF.ECC', '6fb7', 'ADF.USIM'), false);
|
||||
assert.strictEqual(efHasEncoder('EF.SST', '6f38', '7F20'), false);
|
||||
assert.strictEqual(efHasEncoder('EF.UST', '6f38', 'ADF.USIM'), true);
|
||||
assert.strictEqual(efFindEncoder('EF.NOPE', 'abcd', 'ADF.USIM'), null);
|
||||
// the SIM 3+X and USIM 4+X forms keep their length on an untouched save
|
||||
for (const hex of ['00' + '0000', '00' + '0000' + '02' + 'AABB', AD_HEX, 'FF'.repeat(6)]) {
|
||||
for (const e of [sim, usim]) {
|
||||
assert.strictEqual(e.enc(e.model(efDecAd(efBytes(hex))), hex), hex.toUpperCase(), hex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('encoder validation errors go through t() when the runtime provides it', () => {
|
||||
globalThis.t = k => (k === 'Select at least one access technology' ? 'Выберите технологию' : k);
|
||||
try {
|
||||
|
||||
@@ -52,7 +52,7 @@ const FNS = [
|
||||
'efDecRecordsHtml', 'efDecEditHtml',
|
||||
'efDecCollectValues', 'efDecCollectRows', 'efDecCollectItems',
|
||||
'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem',
|
||||
'pysimFsDecRemoveItem', 'pysimFsFindNode',
|
||||
'pysimFsDecRemoveItem', 'pysimFsFindNode', 'pysimFsNodePath', 'efDfFromPath',
|
||||
'pysimFsHasDecoder', 'pysimFsHasEncoder', 'pysimFsDecSave', 'pysimFsDecEdit',
|
||||
'pysimFsPillsEnabled', 'pysimFsSyncContentVisibility', 'gsm7Decode',
|
||||
'gsm7TextToSeptets', 'gsm7Encode',
|
||||
@@ -271,6 +271,19 @@ test('pysimFsHasEncoder resolves the selected file by name or FID', () => {
|
||||
assert.strictEqual(pysimFsHasEncoder(), false);
|
||||
});
|
||||
|
||||
test('the file-manager resolver call sites pass the node\'s parent DF', () => {
|
||||
// the DF context comes from the node's canonical path; these five sites
|
||||
// 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 !!efFindEncoder(node && node.name, fid, df);'));
|
||||
assert.ok(html.includes("const df = node ? efDfFromPath(pysimFsNodePath(node)) : '';"));
|
||||
assert.ok(html.includes('const model = efDecEditModel(node && node.name, fid, content,'));
|
||||
for (const fn of ['pysimFsDecSave', 'pysimFsDecAddRow', 'pysimFsDecAddItem']) {
|
||||
assert.ok(/efFindEncoder\([^;]*efDfFromPath\(pysimFsNodePath\(node\)\)/s.test(extractFunc(html, fn)),
|
||||
fn + ' resolves the entry with the node DF');
|
||||
}
|
||||
});
|
||||
|
||||
test('the Edit decoded button is encoder-gated next to Edit raw', () => {
|
||||
const btn = /<button[^>]*id="pysim-fs-edit-dec-btn"[^>]*>/.exec(html);
|
||||
assert.ok(btn, 'Edit decoded button');
|
||||
|
||||
@@ -29,7 +29,8 @@ let code = 'var pysimFsDecodedMode = false;\n'
|
||||
+ 'var pysimFsSelected = null;\n'
|
||||
+ 'var pysimFsTreeRoot = null;\n';
|
||||
for (const fn of ['pysimFsSetMode', 'pysimFsPillsEnabled', 'pysimFsEdit',
|
||||
'pysimFsCancel', 'pysimFsResetEdit', 'pysimFsFindNode', 'pysimFsHasDecoder',
|
||||
'pysimFsCancel', 'pysimFsResetEdit', 'pysimFsFindNode', 'pysimFsNodePath',
|
||||
'efDfFromPath', 'pysimFsHasDecoder',
|
||||
'pysimFsSyncContentVisibility']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user