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:
catarrh committed 2026-10-18 01:18:26 +03:00
1 parent 5dea6bceb8
commit ac424e67e3
13 files changed
+283 -9

No files matched your search

+7 -2
View File
@@ -1472,8 +1472,13 @@ def _decode_fcp(data):
elif tag == 0xAB:
result['security_attr_template'] = value.hex().upper()
if not sfi_seen and fid:
sfi = int(fid, 16) & 0x1F
result['sfi'] = sfi if 1 <= sfi <= 30 else None
# TS 102 221 §11.1.1.4.8: without the '88' DO the SFI is the file
# identifier's five least-significant bits — an EF rule; a DF/ADF's
# FCP has no SFI at all.
ft = (result.get('file_descriptor') or {}).get('file_type')
if ft != 'DF or ADF':
sfi = int(fid, 16) & 0x1F
result['sfi'] = sfi if 1 <= sfi <= 30 else None
return result
+32 -1
View File
@@ -42,6 +42,7 @@ from .database import _SUCCESS_SW1
from .decode import (DF_CHILDREN, KNOWN_FIDS, _file_name, _is_df_fid,
select_target_fid, selected_df_fid, selected_df_state,
sfi_table)
from .pbr import parse_pbr_record
IMAGE_CAP = 64 * 1024 # observed bytes kept per file
EVENTS_CAP = 1000 # change events kept per file
@@ -250,6 +251,7 @@ class FileTracker:
self.files = {} # key -> _FileEntry
self.timeline = [] # one item per read/update/card change
self.sfi_relations = {} # df -> {sfi: fid}
self.pb_names = {} # df -> {fid: EF_*} from the card's EF_PBR
self.aid_labels = {} # AID (upper hex) -> application label
self._index = 0
self._pending_read = {} # channel -> {'d', 'df'} read awaiting its data
@@ -287,7 +289,8 @@ class FileTracker:
# A file first seen via its FCP (or an operation whose decode had
# no name) still gets its DF-scoped dictionary name.
entry = _FileEntry(key, df, fid,
(name or _file_name(fid, df)) if fid else '?',
(name or self.pb_names.get(df, {}).get(fid)
or _file_name(fid, df)) if fid else '?',
image_cap=self._image_cap, df_path=df_path)
self.files[key] = entry
elif df_path and len(df_path) > len(entry.df_path) and \
@@ -476,8 +479,35 @@ class FileTracker:
if idx is not None else None)
else:
self._push('read', entry, index, msg, record=record)
self._observe_pbr(df, fid, data)
self._note_dir_content(d)
def _observe_pbr(self, df, fid, data):
"""Learn the phonebook structure from an EF_PBR record.
The card declares each phonebook file's FID, its role (EF_ADN, EF_PBC,
…) and — when the object is three bytes long — its SFI
(TS 31.102 §4.4.2.1); the files then get their symbolic names and the
SFI relations resolve without waiting for an FCP.
"""
if fid != '4f30' or not data:
return
parsed = parse_pbr_record(bytes(data))
if not parsed['groups']:
return
names = self.pb_names.setdefault(df, {})
for group in parsed['groups']:
for item in group['files']:
if 'fid' not in item:
continue
names[item['fid']] = item['name']
if item.get('sfi'):
self._learn_sfi(df, item['sfi'], item['fid'])
entry = self.files.get((df, item['fid']))
if entry is not None and entry.name in (entry.fid,
entry.fid.upper()):
entry.name = item['name']
def _observe_write(self, d, df, index, msg, df_path=None):
data = _body_bytes(d)
if not data or not _success(d):
@@ -502,6 +532,7 @@ class FileTracker:
change_ref = {'file': _key_str(entry.key), 'idx': idx}
self._push('update', entry, index, msg, record=record, change_ref=change_ref,
no_change=(not diffs and compared_all))
self._observe_pbr(df, fid, data)
# -- helpers ---------------------------------------------------------
+107
View File
@@ -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
+1 -1
View File
@@ -1,3 +1,3 @@
"""Single source of truth for the SIMtrace Analyser version."""
__version__ = '2.7.6'
__version__ = '2.7.7'