v3.31.0: the EF_PBR (phonebook reference) support
EF_PBR ('4F30', TS 31.102 4.4.2.1) lists the phonebook's files as
BER-TLVs, and those files are not in pySim's model - so they were
invisible in the file tree.
- The PWA gains the decoder (efDecPbr, registered as EF.PBR/4f30): the
'A8'/'A9'/'AA' type-1/2/3 containers, the 'C0'-'CC' kinds, the 02/03
value lengths (FID / FID+SFI) and the master EF (the first FID after
the first 'A8'), tolerant of FF padding, truncation, unknown tags and
the spec's vague 'AA' path form (kept raw + noted, never a throw).
- The server reads the three PBR copies once per card session
(best-effort, exactly like EF.ARR) into server.pbr and serves the raw
records from the card-free GET /api/pbr.
- The file tree is populated SERVER-SIDE (no frontend involvement): a
small Python parser reads the cached records and /api/tree appends the
referenced files as children of the DF that holds the PBR
({'name','fid','isDir':false,'pbr':true,'sfi'}, deduplicated against
the model), and /api/tree|select|read|write auto-allow the probe for
exactly those FIDs, so the injected nodes are clickable without any
PWA flag. The node-to-PBR match is by the requested node's normalised
path: a leading MF before an ADF segment is dropped and an ADF
addressed by AID (the Profiler's walk) folds onto its name, so both
DF_PHONEBOOK copies resolve to their own PBR; a full path never falls
back to another DF's copy, and an ambiguous bare 5F3A injects nothing.
- Drift guard: the JS and Python parsers share
frontend/tests/pbr_vectors.json, which includes the TS 31.102 Annex G
record (its printed A8 length is internally inconsistent - 10
primitives x 5 bytes = 0x32, not the printed 0x2D - so the corrected
length is used, noted in the case name).
Docs: api.md (/api/pbr, the tree children, the auto-probe, the AA path
form), the repo AGENTS. 953 frontend + 989 Python; sw simple-v455.
This commit is contained in:
@@ -29,7 +29,7 @@ const FNS = [
|
||||
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecEcc', 'efDecOpl', 'efDecAd', 'efDecAcl',
|
||||
'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn',
|
||||
'pinKeyRefName', 'efArrAmLabel', 'efArrSc', 'efArrAmDo',
|
||||
'efDecExt1', 'efDecSms', 'efDecSume', 'efFindDecoder', 'efFidFromPath', 'efDecodeBytes',
|
||||
'efDecExt1', 'efDecSms', 'efDecSume', 'efPbrHex', 'efDecPbr', 'efFindDecoder', 'efFidFromPath', 'efDecodeBytes',
|
||||
'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten',
|
||||
'efDiffData', 'efDataSummary', 'efContentDiff', 'gsm7Decode', 'efRenderFieldsHtml',
|
||||
'pysimFsDecodedHtml', 'profilerRenderReport', 'profilerLabelText', 'profilerResultAspects',
|
||||
@@ -42,7 +42,7 @@ for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'FCP_ARR_AM_BITS']) {
|
||||
code += html.match(new RegExp('const ' + c + ' = [\\s\\S]*?\\n\\];'))[0].replace('const ', 'var ') + '\n';
|
||||
}
|
||||
for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_PHASE_NAMES']) {
|
||||
for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_PHASE_NAMES', 'EF_PBR_KINDS']) {
|
||||
code += html.match(new RegExp('const ' + c + ' = \\{.*\\};'))[0].replace('const ', 'var ') + '\n';
|
||||
}
|
||||
code += html.match(/const EF_FIELD_LABELS = \{[\s\S]*?\n\};/)[0].replace('const ', 'var ') + '\n';
|
||||
@@ -132,6 +132,106 @@ test('EF.SUME decodes the SET UP MENU title and icon TLVs (TS 51.011 10.3.34)',
|
||||
{ title: 'Menu' });
|
||||
});
|
||||
|
||||
test('EF.PBR decodes the phonebook structure containers (TS 31.102 4.4.2.1)', () => {
|
||||
// pySim's EF_PBR fixture: A8 { C0 03 4F3A 02, C5 03 4F09 04 }
|
||||
// + AA { CB 03 4F3D 07, C2 03 4F4A 06 } - all with an SFI.
|
||||
const d = efDecPbr(efBytes('A80AC0034F3A02C5034F0904AA0ACB034F3D07C2034F4A06'));
|
||||
assert.strictEqual(d.master, '4F3A');
|
||||
assert.strictEqual(d.iap, null);
|
||||
assert.strictEqual(d.note, undefined);
|
||||
assert.deepStrictEqual(d.links, [
|
||||
{ link: 1, tag: 'C0', kind: 'EF.ADN', fid: '4F3A', sfi: '02' },
|
||||
{ link: 1, tag: 'C5', kind: 'EF.PBC', fid: '4F09', sfi: '04' },
|
||||
{ link: 3, tag: 'CB', kind: 'EF.CCP1', fid: '4F3D', sfi: '07' },
|
||||
{ link: 3, tag: 'C2', kind: 'EF.EXT1', fid: '4F4A', sfi: '06' },
|
||||
]);
|
||||
assert.deepStrictEqual(d.files, ['4F3A', '4F09', '4F3D', '4F4A']);
|
||||
});
|
||||
|
||||
test('EF.PBR: the no-SFI form and the type-2 (A9) container with EF_IAP', () => {
|
||||
// A8 { C0 02 4F3A (FID only, no SFI), C1 02 4F09 (EF_IAP) }
|
||||
// + A9 { C6 03 4F0B 06, C7 03 4F0C 07 } - the type-2 files are addressed
|
||||
// through the EF_IAP pointers, in the order of the FIDs after 'A9'.
|
||||
const d = efDecPbr(efBytes('A808C0024F3AC1024F09A90AC6034F0B06C7034F0C07'));
|
||||
assert.strictEqual(d.master, '4F3A');
|
||||
assert.strictEqual(d.iap, '4F09');
|
||||
assert.deepStrictEqual(d.links, [
|
||||
{ link: 1, tag: 'C0', kind: 'EF.ADN', fid: '4F3A', sfi: null },
|
||||
{ link: 1, tag: 'C1', kind: 'EF.IAP', fid: '4F09', sfi: null },
|
||||
{ link: 2, tag: 'C6', kind: 'EF.GRP', fid: '4F0B', sfi: '06' },
|
||||
{ link: 2, tag: 'C7', kind: 'EF.AAS', fid: '4F0C', sfi: '07' },
|
||||
]);
|
||||
assert.deepStrictEqual(d.files, ['4F3A', '4F09', '4F0B', '4F0C']);
|
||||
assert.strictEqual(d.note, undefined);
|
||||
});
|
||||
|
||||
test('EF.PBR: FF padding is not a bogus link; a truncated TLV is a note, not a throw', () => {
|
||||
const padded = efDecPbr(efBytes('A80AC0034F3A02C5034F0904AA0ACB034F3D07C2034F4A06' + 'FF'.repeat(8)));
|
||||
assert.strictEqual(padded.links.length, 4);
|
||||
assert.strictEqual(padded.note, undefined);
|
||||
assert.deepStrictEqual(padded.files, ['4F3A', '4F09', '4F3D', '4F4A']);
|
||||
// a trailing 'C5' tag with no length byte: the parsed links are kept
|
||||
const trunc = efDecPbr(efBytes('A80AC0034F3A02C5034F0904C5'));
|
||||
assert.strictEqual(trunc.master, '4F3A');
|
||||
assert.strictEqual(trunc.links.length, 2);
|
||||
assert.match(trunc.note, /truncated TLV/);
|
||||
// a container whose value runs past the record: the earlier links are kept
|
||||
const bad = efDecPbr(efBytes('A80AC0034F3A02C5034F0904AA0ACB034F3D07'));
|
||||
assert.strictEqual(bad.links.length, 2);
|
||||
assert.match(bad.note, /truncated TLV/);
|
||||
});
|
||||
|
||||
test('EF.PBR: unknown primitive tags are preserved raw with a note', () => {
|
||||
// A8 { C0 03 4F3A 02 (master), CE 03 4F09 04 (not a phonebook kind) }
|
||||
const d = efDecPbr(efBytes('A80AC0034F3A02CE034F0904'));
|
||||
assert.strictEqual(d.master, '4F3A');
|
||||
assert.deepStrictEqual(d.links, [
|
||||
{ link: 1, tag: 'C0', kind: 'EF.ADN', fid: '4F3A', sfi: '02' },
|
||||
{ link: 1, tag: 'CE', kind: null, fid: '4F09', sfi: '04', raw: '4F0904' },
|
||||
]);
|
||||
assert.deepStrictEqual(d.files, ['4F3A']);
|
||||
assert.match(d.note, /CE/);
|
||||
});
|
||||
|
||||
test('EF.PBR: the AA non-primitive fallback keeps the raw bytes and notes it', () => {
|
||||
const d = efDecPbr(efBytes('AA03C0FF01'));
|
||||
assert.deepStrictEqual(d.links, [
|
||||
{ link: 3, tag: 'AA', kind: null, fid: null, sfi: null, raw: 'C0FF01' },
|
||||
]);
|
||||
assert.match(d.note, /not a primitive list/);
|
||||
// an all-FF record is the unused marker
|
||||
assert.deepStrictEqual(efDecPbr(efBytes('FFFFFFFF')), { empty: true });
|
||||
});
|
||||
|
||||
test('EF.PBR matches the shared decode vectors (drift guard with the server parser)', () => {
|
||||
// frontend/tests/pbr_vectors.json is asserted by tests/test_pbr.py too:
|
||||
// the PWA decoder and the server-side PBR parser must agree.
|
||||
const vectors = JSON.parse(fs.readFileSync(path.join(__dirname, 'pbr_vectors.json'), 'utf8'));
|
||||
assert.ok(vectors.cases.length >= 5);
|
||||
for (const c of vectors.cases) {
|
||||
const d = efDecPbr(efBytes(c.records[0]));
|
||||
assert.strictEqual(d.master, c.master, c.name + ': master');
|
||||
assert.deepStrictEqual(d.links, c.links, c.name + ': links');
|
||||
assert.deepStrictEqual(d.files, c.files, c.name + ': files');
|
||||
}
|
||||
});
|
||||
|
||||
test('EF.PBR is registered by name and FID; efFlatten renders link rows', () => {
|
||||
assert.strictEqual(efFindDecoder('EF.PBR', null).fid, '4f30');
|
||||
assert.strictEqual(efFindDecoder(null, '4f30').name, 'EF.PBR');
|
||||
assert.strictEqual(efFindDecoder('MF/7F10/5F3A/EF.PBR', null).fn, efDecPbr);
|
||||
assert.deepStrictEqual(efFlatten({ master: '4F3A', iap: '4F09', links: [
|
||||
{ link: 1, tag: 'C0', kind: 'EF.ADN', fid: '4F3A', sfi: '02' },
|
||||
{ link: 3, tag: 'AA', kind: null, fid: null, sfi: null, raw: 'C0FF01' },
|
||||
], files: ['4F3A'] }), [
|
||||
['Master EF', '4F3A'],
|
||||
['IAP (EF.IAP)', '4F09'],
|
||||
['Links 1', 'Type 1 \u00b7 EF.ADN \u2014 4F3A (SFI 02)'],
|
||||
['Links 2', 'Type 3 \u00b7 tag AA \u2014 C0FF01'],
|
||||
['Files', '4F3A'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('ADN-format records: alpha + BCD number + CCP/EXT1', () => {
|
||||
const d = efDecAdn(efBytes('42204841203120536963FFFFFFFFFFFF06810628560810FFFFFFFFFFFFFF'));
|
||||
assert.strictEqual(d.name, 'B HA 1 Sic');
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
{
|
||||
"_comment": "EF.PBR (TS 31.102 4.4.2.1) decode vectors, shared by the PWA decoder (frontend/tests/ef_decode.test.js, efDecPbr) and the server parser (tests/test_pbr.py, _pbr_parse) - the drift guard for the two implementations. 'records' are raw EF_PBR record hex strings (as /api/pbr serves them); 'master' is the first FID of the first 'A8' container (the PWA decoder only - the server parser does not need it); 'links' is the PWA decoder's link list (tag/kind/fid/sfi, plus 'raw' for a preserved unknown tag); 'files' is the referenced FID list in record order, deduplicated by FID - the shape the server parser returns.",
|
||||
"cases": [
|
||||
{
|
||||
"name": "pySim's EF_PBR fixture: type 1 and type 3 containers, every primitive with an SFI",
|
||||
"records": [
|
||||
"A80AC0034F3A02C5034F0904AA0ACB034F3D07C2034F4A06"
|
||||
],
|
||||
"master": "4F3A",
|
||||
"links": [
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C0",
|
||||
"kind": "EF.ADN",
|
||||
"fid": "4F3A",
|
||||
"sfi": "02"
|
||||
},
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C5",
|
||||
"kind": "EF.PBC",
|
||||
"fid": "4F09",
|
||||
"sfi": "04"
|
||||
},
|
||||
{
|
||||
"link": 3,
|
||||
"tag": "CB",
|
||||
"kind": "EF.CCP1",
|
||||
"fid": "4F3D",
|
||||
"sfi": "07"
|
||||
},
|
||||
{
|
||||
"link": 3,
|
||||
"tag": "C2",
|
||||
"kind": "EF.EXT1",
|
||||
"fid": "4F4A",
|
||||
"sfi": "06"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"4F3A",
|
||||
"4F09",
|
||||
"4F3D",
|
||||
"4F4A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "type 2 ('A9') addressed through EF_IAP (a 'C1' inside the 'A8'); FID-only primitives (no SFI)",
|
||||
"records": [
|
||||
"A808C0024F3AC1024F09A90AC6034F0B06C7034F0C07"
|
||||
],
|
||||
"master": "4F3A",
|
||||
"links": [
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C0",
|
||||
"kind": "EF.ADN",
|
||||
"fid": "4F3A",
|
||||
"sfi": null
|
||||
},
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C1",
|
||||
"kind": "EF.IAP",
|
||||
"fid": "4F09",
|
||||
"sfi": null
|
||||
},
|
||||
{
|
||||
"link": 2,
|
||||
"tag": "C6",
|
||||
"kind": "EF.GRP",
|
||||
"fid": "4F0B",
|
||||
"sfi": "06"
|
||||
},
|
||||
{
|
||||
"link": 2,
|
||||
"tag": "C7",
|
||||
"kind": "EF.AAS",
|
||||
"fid": "4F0C",
|
||||
"sfi": "07"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"4F3A",
|
||||
"4F09",
|
||||
"4F0B",
|
||||
"4F0C"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "unused trailing bytes are FF: the padding is not a link",
|
||||
"records": [
|
||||
"A80AC0034F3A02C5034F0904AA0ACB034F3D07C2034F4A06FFFFFFFFFFFFFFFF"
|
||||
],
|
||||
"master": "4F3A",
|
||||
"links": [
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C0",
|
||||
"kind": "EF.ADN",
|
||||
"fid": "4F3A",
|
||||
"sfi": "02"
|
||||
},
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C5",
|
||||
"kind": "EF.PBC",
|
||||
"fid": "4F09",
|
||||
"sfi": "04"
|
||||
},
|
||||
{
|
||||
"link": 3,
|
||||
"tag": "CB",
|
||||
"kind": "EF.CCP1",
|
||||
"fid": "4F3D",
|
||||
"sfi": "07"
|
||||
},
|
||||
{
|
||||
"link": 3,
|
||||
"tag": "C2",
|
||||
"kind": "EF.EXT1",
|
||||
"fid": "4F4A",
|
||||
"sfi": "06"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"4F3A",
|
||||
"4F09",
|
||||
"4F3D",
|
||||
"4F4A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "a truncated trailing TLV (a lone 'C5' tag): the links before it are kept",
|
||||
"records": [
|
||||
"A80AC0034F3A02C5034F0904C5"
|
||||
],
|
||||
"master": "4F3A",
|
||||
"links": [
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C0",
|
||||
"kind": "EF.ADN",
|
||||
"fid": "4F3A",
|
||||
"sfi": "02"
|
||||
},
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C5",
|
||||
"kind": "EF.PBC",
|
||||
"fid": "4F09",
|
||||
"sfi": "04"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"4F3A",
|
||||
"4F09"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "an unknown primitive tag ('CE') is preserved raw and is not a phonebook file",
|
||||
"records": [
|
||||
"A80AC0034F3A02CE034F0904"
|
||||
],
|
||||
"master": "4F3A",
|
||||
"links": [
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C0",
|
||||
"kind": "EF.ADN",
|
||||
"fid": "4F3A",
|
||||
"sfi": "02"
|
||||
},
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "CE",
|
||||
"kind": null,
|
||||
"fid": "4F09",
|
||||
"sfi": "04",
|
||||
"raw": "4F0904"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"4F3A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TS 31.102 Annex G Table G.2 Rec 1 (every kind tag; the printed A8 length is internally inconsistent - the table lists 10 primitives of 5 bytes = 50 = 0x32 - so the corrected length is used)",
|
||||
"records": [
|
||||
"A832C0034F3A01C5034F0902C6034F2603C4034F1104C4034F1305C4034F1506C3034F1907C9034F2112CA034F5009CC034F5416AA0FC7034F4B14C8034F4C15C2034F4A08"
|
||||
],
|
||||
"master": "4F3A",
|
||||
"links": [
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C0",
|
||||
"kind": "EF.ADN",
|
||||
"fid": "4F3A",
|
||||
"sfi": "01"
|
||||
},
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C5",
|
||||
"kind": "EF.PBC",
|
||||
"fid": "4F09",
|
||||
"sfi": "02"
|
||||
},
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C6",
|
||||
"kind": "EF.GRP",
|
||||
"fid": "4F26",
|
||||
"sfi": "03"
|
||||
},
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C4",
|
||||
"kind": "EF.ANR",
|
||||
"fid": "4F11",
|
||||
"sfi": "04"
|
||||
},
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C4",
|
||||
"kind": "EF.ANR",
|
||||
"fid": "4F13",
|
||||
"sfi": "05"
|
||||
},
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C4",
|
||||
"kind": "EF.ANR",
|
||||
"fid": "4F15",
|
||||
"sfi": "06"
|
||||
},
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C3",
|
||||
"kind": "EF.SNE",
|
||||
"fid": "4F19",
|
||||
"sfi": "07"
|
||||
},
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "C9",
|
||||
"kind": "EF.UID",
|
||||
"fid": "4F21",
|
||||
"sfi": "12"
|
||||
},
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "CA",
|
||||
"kind": "EF.EMAIL",
|
||||
"fid": "4F50",
|
||||
"sfi": "09"
|
||||
},
|
||||
{
|
||||
"link": 1,
|
||||
"tag": "CC",
|
||||
"kind": "EF.PURI",
|
||||
"fid": "4F54",
|
||||
"sfi": "16"
|
||||
},
|
||||
{
|
||||
"link": 3,
|
||||
"tag": "C7",
|
||||
"kind": "EF.AAS",
|
||||
"fid": "4F4B",
|
||||
"sfi": "14"
|
||||
},
|
||||
{
|
||||
"link": 3,
|
||||
"tag": "C8",
|
||||
"kind": "EF.GAS",
|
||||
"fid": "4F4C",
|
||||
"sfi": "15"
|
||||
},
|
||||
{
|
||||
"link": 3,
|
||||
"tag": "C2",
|
||||
"kind": "EF.EXT1",
|
||||
"fid": "4F4A",
|
||||
"sfi": "08"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"4F3A",
|
||||
"4F09",
|
||||
"4F26",
|
||||
"4F11",
|
||||
"4F13",
|
||||
"4F15",
|
||||
"4F19",
|
||||
"4F21",
|
||||
"4F50",
|
||||
"4F54",
|
||||
"4F4B",
|
||||
"4F4C",
|
||||
"4F4A"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user