Files view: name the phonebook files from the card's EF_PBR (v2.7.7)
- EF_PBR (TS 31.102 §4.4.2.1) is the card's declaration of its phonebook:
the constructed tags A8/A9/AA announce the type-1/2/3 groups and each nested
primitive tag C0…CC names the file kind plus its FID — and, in a three-byte
object, its SFI (length 2 = FID only). pySim has no parser for it (its
EF_PBR class still carries a "# TODO: a80ac0034f3a…" stub), so the decoder
follows the spec: new simtrace_analyser/pbr.py
- the tracker decodes every EF_PBR record it observes (a READ RECORD's data
or an UPDATE RECORD's body) and uses it for the DF it belongs to: the
phonebook files get their symbolic names (4FA1 = EF_PBC, 4F3A = EF_ADN,
4F01 = EF_IAP, 4F4A = EF_EXT1, 4F3D = EF_CCP1, 4F61 = EF_EMAIL, …), an
earlier placeholder name is upgraded when the PBR arrives later, and the
declared SFIs feed the DF's SFI→FID map (so SFI-addressed reads resolve
without waiting for an FCP)
- verified on the live sessions 47/193:
DF_TELECOM/DF_PHONEBOOK/EF_PBC (4FA1) and the SFI map
{2→4F3A, 4→4FA1, 6→4F4A, 7→4F4F}
- also fixed: the "SFI = FID's five low bits" fallback (TS 102 221
§11.1.1.4.8) no longer applies to a DF/ADF's FCP, which has no SFI
- tests: tests/test_pbr.py (the two real records, SFI-less objects, padding,
truncation, merging, the shape check) and tracker cases (names, path,
SFI map, the later-PBR upgrade); 537 Python + 76 Node tests
- README/README_RUS document the PBR naming; PWA_VERSION/pyproject/
version.py 2.7.7, SW cache simtrace-analyser-v87
This commit is contained in:
1 parent
5dea6bceb8
commit
ac424e67e3
13 files changed
+283
-9
No files matched your search
@@ -0,0 +1,107 @@
|
||||
"""EF_PBR (Phone Book Reference file) record decoding — TS 31.102 §4.4.2.1.
|
||||
|
||||
The phonebook's files (ADN, IAP, ANR, PBC, …) have issuer-assigned FIDs, so
|
||||
the static EF dictionary cannot name them. The card declares them here:
|
||||
|
||||
* the constructed tags 'A8', 'A9' and 'AA' announce a type-1/2/3 file group
|
||||
(the first FID under 'A8' is the master EF, normally the ADN file);
|
||||
* each nested primitive tag ('C0' … 'CC') names the kind of file and carries
|
||||
its FID and — when the object is three bytes long — its SFI
|
||||
(length '02' = FID, length '03' = FID + SFI);
|
||||
* a record is padded with 'FF' after the last object.
|
||||
|
||||
The decoder is deliberately tolerant: unknown primitives are reported raw and
|
||||
a truncated object ends the record (real cards pad and occasionally carry
|
||||
longer, path-shaped type-3 references).
|
||||
"""
|
||||
import collections
|
||||
|
||||
# Table 4.2 — the primitive tags naming the phonebook files.
|
||||
PBR_NAMES = {
|
||||
'C0': 'EF_ADN', 'C1': 'EF_IAP', 'C2': 'EF_EXT1', 'C3': 'EF_SNE',
|
||||
'C4': 'EF_ANR', 'C5': 'EF_PBC', 'C6': 'EF_GRP', 'C7': 'EF_AAS',
|
||||
'C8': 'EF_GAS', 'C9': 'EF_UID', 'CA': 'EF_EMAIL', 'CB': 'EF_CCP1',
|
||||
'CC': 'EF_PURI',
|
||||
}
|
||||
# Table 4.1 — the constructed tags: the file-linking type.
|
||||
PBR_TYPES = {'A8': 1, 'A9': 2, 'AA': 3}
|
||||
|
||||
|
||||
def _objects(data):
|
||||
"""Iterate (tag, value) pairs; stop at the 'FF' padding or a truncation."""
|
||||
i = 0
|
||||
while i + 1 < len(data):
|
||||
tag = data[i]
|
||||
if tag == 0xFF:
|
||||
return
|
||||
length = data[i + 1]
|
||||
j = i + 2
|
||||
if length & 0x80: # BER long form (defensive)
|
||||
octets = length & 0x7F
|
||||
if j + octets > len(data):
|
||||
return
|
||||
length = int.from_bytes(data[j:j + octets], 'big')
|
||||
j += octets
|
||||
value = data[j:j + length]
|
||||
if len(value) < length:
|
||||
return
|
||||
yield tag, value
|
||||
i = j + length
|
||||
|
||||
|
||||
def _files(value):
|
||||
out = []
|
||||
for tag, v in _objects(value):
|
||||
name = PBR_NAMES.get(f'{tag:02X}')
|
||||
if name is None:
|
||||
continue
|
||||
if len(v) == 2: # FID only
|
||||
item = {'tag': f'{tag:02X}', 'name': name, 'fid': v.hex()}
|
||||
elif len(v) == 3: # FID + SFI
|
||||
item = {'tag': f'{tag:02X}', 'name': name, 'fid': v[:2].hex(),
|
||||
'sfi': v[2]}
|
||||
else: # a path-shaped reference
|
||||
item = {'tag': f'{tag:02X}', 'name': name, 'path': v.hex()}
|
||||
out.append(item)
|
||||
return out
|
||||
|
||||
|
||||
def parse_pbr_record(raw):
|
||||
"""Decode one EF_PBR record into its type groups (and the master EF)."""
|
||||
if not raw:
|
||||
return {'groups': [], 'master': None}
|
||||
groups = []
|
||||
master = None
|
||||
for tag, value in _objects(raw):
|
||||
kind = PBR_TYPES.get(f'{tag:02X}')
|
||||
if kind is None:
|
||||
continue
|
||||
files = _files(value)
|
||||
groups.append({'type': kind, 'tag': f'{tag:02X}', 'files': files})
|
||||
if master is None and kind == 1 and files and 'fid' in files[0]:
|
||||
master = files[0]['fid']
|
||||
return {'groups': groups, 'master': master}
|
||||
|
||||
|
||||
def parse_pbr(records):
|
||||
"""Merge several EF_PBR records (the structure repeats per record)."""
|
||||
out = collections.OrderedDict()
|
||||
master = None
|
||||
for raw in records or []:
|
||||
parsed = parse_pbr_record(raw)
|
||||
if master is None:
|
||||
master = parsed['master']
|
||||
for group in parsed['groups']:
|
||||
for item in group['files']:
|
||||
if 'fid' not in item:
|
||||
continue
|
||||
prev = out.setdefault(item['fid'], dict(item))
|
||||
prev.setdefault('types', [])
|
||||
if group['type'] not in prev['types']:
|
||||
prev['types'].append(group['type'])
|
||||
return {'files': out, 'master': master}
|
||||
|
||||
|
||||
def looks_like_pbr(raw):
|
||||
"""Cheap shape check: the record starts with a constructed A8/A9/AA tag."""
|
||||
return bool(raw) and f'{raw[0]:02X}' in PBR_TYPES and len(raw) >= 4
|
||||
Reference in new issue
Block a user