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:
2026-10-18 01:47:04 +03:00
parent 30a742618e
commit 9ae9c39e55
9 changed files with 1237 additions and 10 deletions
+80 -2
View File
@@ -1943,7 +1943,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.30.4';
const SIMPLE_VERSION = '3.31.0';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
@@ -22553,6 +22553,8 @@ const EF_UST_SERVICES = {1: "Local Phone Book", 2: "Fixed Dialling Numbers (FDN)
const EF_SST_SERVICES = {1: "CHV1 disable function", 2: "Abbreviated Dialling Numbers (ADN)", 3: "Fixed Dialling Numbers (FDN)", 4: "Short Message Storage (SMS)", 5: "Advice of Charge (AoC)", 6: "Capability Configuration Parameters (CCP)", 7: "PLMN selector", 8: "RFU", 9: "MSISDN", 10: "Extension1", 11: "Extension2", 12: "SMS Parameters", 13: "Last Number Dialled (LND)", 14: "Cell Broadcast Message Identifier", 15: "Group Identifier Level 1", 16: "Group Identifier Level 2", 17: "Service Provider Name", 18: "Service Dialling Numbers (SDN)", 19: "Extension3", 20: "RFU", 21: "VGCS Group Identifier List (EFVGCS and EFVGCSS)", 22: "VBS Group Identifier List (EFVBS and EFVBSS)", 23: "enhanced Multi-Level Precedence and Pre-emption Service", 24: "Automatic Answer for eMLPP", 25: "Data download via SMS-CB", 26: "Data download via SMS-PP", 27: "Menu selection", 28: "Call control", 29: "Proactive SIM", 30: "Cell Broadcast Message Identifier Ranges", 31: "Barred Dialling Numbers (BDN)", 32: "Extension4", 33: "De-personalization Control Keys", 34: "Co-operative Network List", 35: "Short Message Status Reports", 36: "Network's indication of alerting in the MS", 37: "Mobile Originated Short Message control by SIM", 38: "GPRS", 39: "Image (IMG)", 40: "SoLSA (Support of Local Service Area)", 41: "USSD string data object supported in Call Control", 42: "RUN AT COMMAND command", 43: "User controlled PLMN Selector with Access Technology", 44: "Operator controlled PLMN Selector with Access Technology", 45: "HPLMN Selector with Access Technology", 46: "CPBCCH Information", 47: "Investigation Scan", 48: "Extended Capability Configuration Parameters", 49: "MExE", 50: "Reserved and shall be ignored", 51: "PLMN Network Name", 52: "Operator PLMN List", 53: "Mailbox Dialling Numbers", 54: "Message Waiting Indication Status", 55: "Call Forwarding Indication Status", 56: "Service Provider Display Information", 57: "Multimedia Messaging Service (MMS)", 58: "Extension 8", 59: "MMS User Connectivity Parameters"};
const EF_IST_SERVICES = {1: "P-CSCF address", 2: "Generic Bootstrapping Architecture (GBA)", 3: "HTTP Digest", 4: "GBA-based Local Key Establishment Mechanism", 5: "Support of P-CSCF discovery for IMS Local Break Out", 6: "Short Message Storage (SMS)", 7: "Short Message Status Reports (SMSR)", 8: "Support for SM-over-IP including data download via SMS-PP as defined in TS 31.111 [31]", 9: "Communication Control for IMS by ISIM", 10: "Support of UICC access to IMS", 11: "URI support by UICC", 12: "Media Type support", 13: "IMS call disconnection cause", 14: "URI support for MO SHORT MESSAGE CONTROL", 15: "MCPTT", 16: "URI support for SMS-PP DOWNLOAD as defined in 3GPP TS 31.111 [31]", 17: "From Preferred", 18: "IMS configuration data", 19: "XCAP Configuration Data", 20: "WebRTC URI", 21: "MuD and MiD configuration data", 22: "IMS Data Channel indication"};
const EF_PHASE_NAMES = {0: "phase 1", 2: "phase 2", 3: "phase 2 and higher"};
// TS 31.102 §4.4.2.1 EF_PBR primitive tags (the phonebook kinds).
const EF_PBR_KINDS = { 0xC0: 'EF.ADN', 0xC1: 'EF.IAP', 0xC2: 'EF.EXT1', 0xC3: 'EF.SNE', 0xC4: 'EF.ANR', 0xC5: 'EF.PBC', 0xC6: 'EF.GRP', 0xC7: 'EF.AAS', 0xC8: 'EF.GAS', 0xC9: 'EF.UID', 0xCA: 'EF.EMAIL', 0xCB: 'EF.CCP1', 0xCC: 'EF.PURI' };
// ===== EF content decoders (client-side, offline) =====
// Decode the contents of common EFs for the file manager, the snapshot view,
@@ -22583,6 +22585,7 @@ const EF_FIELD_LABELS = {
lac_range_high: 'LAC range high', show_in_hplmn: 'Show in HPLMN', hide_in_oplmn: 'Hide in OPLMN',
location_update_status: 'Location update status', eps_update_status: 'EPS update status',
update_status: 'Update status', access_tech: 'Access technology', p_tmsi_signature: 'P-TMSI signature',
master: 'Master EF', iap: 'IAP (EF.IAP)', links: 'Links', files: 'Files', note: 'Note',
};
const EF_GSM7_EXT = { 0x0A: '\n', 0x14: '^', 0x28: '{', 0x29: '}', 0x2F: '\\', 0x3C: '[', 0x3D: '~', 0x3E: ']', 0x40: '|', 0x65: '\u20ac' };
@@ -22618,6 +22621,8 @@ function efRstripFf(b) {
return b.subarray(0, end);
}
// Returns {tag, value, end} - `end` is the index just past the TLV, so a
// caller can tell a complete walk from a truncated tail (efDecPbr).
function efTlv(data) {
const out = [];
let i = 0;
@@ -22631,7 +22636,7 @@ function efTlv(data) {
for (let k = 0; k < n; k++) len = (len << 8) | data[i++];
}
if (i + len > data.length) break;
out.push({ tag: tag, value: data.slice(i, i + len) });
out.push({ tag: tag, value: data.slice(i, i + len), end: i + len });
i += len;
}
return out;
@@ -23280,6 +23285,77 @@ function efDecSume(b) {
return Object.keys(out).length ? out : { raw: efHex(b) };
}
// TS 31.102 §4.4.2.1 EF_PBR (4F30): the phonebook reference record - a
// sequence of BER-TLVs describing the phonebook structure (unused trailing
// bytes are FF). Three constructed containers: 'A8' type 1 (one record per
// master-EF record, mapped 1:1), 'A9' type 2 (mapped through the pointers in
// EF_IAP, in the order of the FIDs after the tag) and 'AA' type 3 (addressed
// by record identifiers). Each holds primitive tags whose value length says
// whether an SFI is present (02 = FID, 03 = FID + SFI). The first FID after
// the first 'A8' is the master EF; all linked files inherit its access
// conditions. One record covers at most 254 entries - a larger phonebook
// uses further records, decoded one at a time by the caller.
function efPbrHex(tag) {
return tag.toString(16).toUpperCase().padStart(2, '0');
}
function efDecPbr(b) {
if (efAllFf(b)) return { empty: true };
const data = efRstripFf(b);
const out = { master: null, iap: null, links: [], files: [] };
const notes = [];
const tlvs = efTlv(data);
// A trailing TLV whose length runs past the record: keep what parsed and
// say so instead of throwing (a corrupt/partial PBR must stay readable).
if (data.length && (!tlvs.length || tlvs[tlvs.length - 1].end !== data.length)) {
notes.push('truncated TLV at the end of the record');
}
let seenA8 = false;
for (const t of tlvs) {
if (t.tag !== 0xA8 && t.tag !== 0xA9 && t.tag !== 0xAA) {
notes.push('unexpected top-level tag ' + efPbrHex(t.tag));
continue;
}
const link = t.tag === 0xA8 ? 1 : (t.tag === 0xA9 ? 2 : 3);
const firstA8 = t.tag === 0xA8 && !seenA8;
if (t.tag === 0xA8) seenA8 = true;
const inner = efTlv(t.value);
const complete = t.value.length === 0
|| (inner.length > 0 && inner[inner.length - 1].end === t.value.length);
if (t.tag === 0xAA && !complete) {
// The spec's tag length may point at a path instead of a primitive
// list (TS 31.102 4.4.2.1): keep the raw bytes, never throw.
out.links.push({ link: link, tag: 'AA', kind: null, fid: null, sfi: null, raw: efHex(t.value) });
notes.push('the AA (type 3) container is not a primitive list - kept raw');
continue;
}
if (!complete) notes.push('truncated TLV inside the ' + efPbrHex(t.tag) + ' container');
for (const p of inner) {
const kind = EF_PBR_KINDS[p.tag] || null;
let fid = null, sfi = null;
if (p.value.length === 2) fid = efHex(p.value);
else if (p.value.length === 3) {
fid = efHex(p.value.subarray(0, 2));
sfi = efHex(p.value.subarray(2, 3));
}
if (!kind || fid === null) {
// An unknown or malformed primitive tag is preserved raw.
out.links.push({ link: link, tag: efPbrHex(p.tag), kind: kind, fid: fid, sfi: sfi, raw: efHex(p.value) });
notes.push('unknown or malformed primitive tag ' + efPbrHex(p.tag)
+ ' (' + efHex(p.value) + ') - kept raw');
continue;
}
out.links.push({ link: link, tag: efPbrHex(p.tag), kind: kind, fid: fid, sfi: sfi });
if (!out.files.includes(fid)) out.files.push(fid);
if (firstA8 && !out.master) out.master = fid;
if (p.tag === 0xC1 && !out.iap) out.iap = fid;
}
}
if (!out.links.length) return { raw: efHex(b), note: notes.join('; ') || undefined };
if (notes.length) out.note = notes.join('; ');
return out;
}
// ---- registry ----
const EF_DECODERS = [
@@ -23336,6 +23412,7 @@ const EF_DECODERS = [
{ name: 'EF.EXT4', fid: '6f4e', fn: efDecExt1 },
{ name: 'EF.EXT5', fid: '6f55', fn: efDecExt1 },
{ name: 'EF.SUME', fid: '6f54', fn: efDecSume },
{ name: 'EF.PBR', fid: '4f30', fn: efDecPbr },
];
function efFindDecoder(name, fid) {
@@ -23417,6 +23494,7 @@ function efFlatten(data) {
else if (it && it.n !== undefined && it.name !== undefined) rows.push(['Service ' + it.n, String(it.name)]);
else if (it && it.mcc !== undefined) rows.push(['PLMN ' + (i + 1), it.mcc + '-' + it.mnc + (it.access_tech ? ' (' + it.access_tech + ')' : '')]);
else if (it && it.aid !== undefined) rows.push(['Application ' + (i + 1), (it.label ? it.label + ' \u2014 ' : '') + it.aid + (it.eap ? ' [' + (it.eap.eap_types || []).join(', ') + ']' : '')]);
else if (it && it.link !== undefined && it.tag !== undefined) rows.push([label + ' ' + (i + 1), 'Type ' + it.link + ' \u00b7 ' + (it.kind || ('tag ' + it.tag)) + (it.fid ? ' \u2014 ' + it.fid + (it.sfi ? ' (SFI ' + it.sfi + ')' : '') : (it.raw ? ' \u2014 ' + it.raw : ''))]);
else rows.push([label + ' ' + (i + 1), JSON.stringify(it)]);
});
};
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v454';
const CACHE = 'simple-v455';
const URLS = [
'index.html',
'help.html',
+102 -2
View File
@@ -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');
+303
View File
@@ -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"
]
}
]
}