Files view: the full DF chain in the file path (v2.7.6)
- the tracker kept only the immediate parent DF, so a phonebook file read as
"DF_PHONEBOOK/4FA1" — the trace's own selection path (00a4080406
7F105F3A4FA1) spells out the whole chain; the channel state now carries it
(selected_df_state() returns (df, path), e.g. ('5f3a', ['7f10', '5f3a'])),
the tracker stores it per file and _file_path() renders it:
"DF_TELECOM/DF_PHONEBOOK/4FA1"
- chain rules: a child DF selected by FID appends to the current chain, MF and
the ADF restart it, an AID is its own root, a path selection uses the DFs
in the path (an explicit '7fff' collapsing to the ADF), and an EF selects
keeps it. A leading MF is implicit in a longer path; MF-only files keep
"MF/…", ADF files "ADF/…" (the previous labels unchanged)
- the chain is learned from the FCP sighting too and extends a shorter path
recorded earlier, so entries created from a GET RESPONSE still get the full
path
- verified on the live traces (sessions 47/193): 5f3a/4fa1 ->
DF_TELECOM/DF_PHONEBOOK/4FA1, 5f3a/4f30 -> DF_TELECOM/DF_PHONEBOOK/EF_PBR
(4F30), while 3f00/2fe2 -> MF/EF_ICCID (2FE2) and 7fff/6f07 ->
ADF/EF_IMSI (6F07) stay as before
- tests: selected_df_state (path/FID/MF/ADF/AID/EF cases) and a tracker flow
with a path selection asserting the full path; 529 Python + 76 Node tests
- README/README_RUS show the chain; PWA_VERSION/pyproject/version.py 2.7.6,
SW cache simtrace-analyser-v86
This commit is contained in:
1 parent
1df52a34e5
commit
5dea6bceb8
12 files changed
+184
-52
No files matched your search
@@ -216,8 +216,10 @@ new messages pin the view to the bottom only while the reader is already
|
|||||||
there, so a jump is never pulled away or un-highlighted by the live poll). Rows that carry a recorded change (a
|
there, so a jump is never pulled away or un-highlighted by the live poll). Rows that carry a recorded change (a
|
||||||
card-side change, or an update that wrote or first observed bytes) also offer
|
card-side change, or an update that wrote or first observed bytes) also offer
|
||||||
**Show diff**: a modal showing the full file path (`MF/EF_ICCID (2FE2)`,
|
**Show diff**: a modal showing the full file path (`MF/EF_ICCID (2FE2)`,
|
||||||
`ADF/EF_IMSI (6F07)`, `DF_GSM_ACCESS/EF_KcGPRS (4F52)`, `AID …/4F09` — the DF
|
`ADF/EF_IMSI (6F07)`, `DF_TELECOM/DF_PHONEBOOK/4FA1`, `AID …/4F09` — the whole
|
||||||
named from the EF.DIR labels when known) and the affected unit — the whole
|
DF chain the trace's selection path spells out, each DF named from the
|
||||||
|
specification dictionary and the AID from the EF.DIR labels when known) and
|
||||||
|
the affected unit — the whole
|
||||||
file, or the single record that changed — **before and after** the change, as
|
file, or the single record that changed — **before and after** the change, as
|
||||||
hex views (up to 16 bytes per row — a shorter file/record ends its row there
|
hex views (up to 16 bytes per row — a shorter file/record ends its row there
|
||||||
instead of padding with `--`, and the offset column only appears when a view
|
instead of padding with `--`, and the offset column only appears when a view
|
||||||
|
|||||||
+4
-2
@@ -221,8 +221,10 @@ Files — это таймлайн файловых операций по все
|
|||||||
поэтому переход не срывается живым опросом и не снимает выделение). У строк с записанным изменением
|
поэтому переход не срывается живым опросом и не снимает выделение). У строк с записанным изменением
|
||||||
(изменение со стороны карты либо запись, изменившая или впервые показавшая
|
(изменение со стороны карты либо запись, изменившая или впервые показавшая
|
||||||
байты) есть кнопка **Show diff** («Изменения»): окно показывает полный путь файла (`MF/EF_ICCID (2FE2)`,
|
байты) есть кнопка **Show diff** («Изменения»): окно показывает полный путь файла (`MF/EF_ICCID (2FE2)`,
|
||||||
`ADF/EF_IMSI (6F07)`, `DF_GSM_ACCESS/EF_KcGPRS (4F52)`, `AID …/4F09` — DF
|
`ADF/EF_IMSI (6F07)`, `DF_TELECOM/DF_PHONEBOOK/4FA1`, `AID …/4F09` — вся
|
||||||
называется по меткам из EF.DIR, если они есть) и затронутую единицу — файл
|
цепочка DF из пути выбора в трассе, каждый DF назван по словарю
|
||||||
|
спецификаций, а AID — по меткам из EF.DIR, если они есть) и затронутую
|
||||||
|
единицу — файл
|
||||||
целиком или изменившуюся запись — **до и после** изменения в виде hex-вида
|
целиком или изменившуюся запись — **до и после** изменения в виде hex-вида
|
||||||
(до 16 байт в строке: короткий файл/запись заканчивает строку, а не
|
(до 16 байт в строке: короткий файл/запись заканчивает строку, а не
|
||||||
дополняется `--`, и столбец смещений появляется, только когда представление
|
дополняется `--`, и столбец смещений появляется, только когда представление
|
||||||
|
|||||||
+1
-1
@@ -3386,7 +3386,7 @@ function onSearchChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────── Versioning ────────────────────
|
// ──────────────────── Versioning ────────────────────
|
||||||
const PWA_VERSION = '2.7.5';
|
const PWA_VERSION = '2.7.6';
|
||||||
const MIN_SERVER_VERSION = '2.3.0';
|
const MIN_SERVER_VERSION = '2.3.0';
|
||||||
|
|
||||||
function parseVersion(v) {
|
function parseVersion(v) {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simtrace-analyser-v85';
|
const CACHE = 'simtrace-analyser-v86';
|
||||||
|
|
||||||
const PRECACHE = [
|
const PRECACHE = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "simtrace-analyser"
|
name = "simtrace-analyser"
|
||||||
version = "2.7.5"
|
version = "2.7.6"
|
||||||
description = "SIMtrace Analyser — APDU trace analyser for GSMTAP/PCAP captures (bundles the simtrace2-pysniff utility)"
|
description = "SIMtrace Analyser — APDU trace analyser for GSMTAP/PCAP captures (bundles the simtrace2-pysniff utility)"
|
||||||
readme = {file = "README.md", content-type = "text/markdown"}
|
readme = {file = "README.md", content-type = "text/markdown"}
|
||||||
license = {text = "GPL-2.0-or-later"}
|
license = {text = "GPL-2.0-or-later"}
|
||||||
|
|||||||
@@ -274,7 +274,8 @@ class Database:
|
|||||||
|
|
||||||
def _new_channel_state(self):
|
def _new_channel_state(self):
|
||||||
from .decode import sfi_table
|
from .decode import sfi_table
|
||||||
return {'prev': None, 'sel': None, 'df': '3f00', 'sfi_map': dict(sfi_table('3f00'))}
|
return {'prev': None, 'sel': None, 'df': '3f00', 'df_path': ['3f00'],
|
||||||
|
'sfi_map': dict(sfi_table('3f00'))}
|
||||||
|
|
||||||
def _channel_state(self, states, channel):
|
def _channel_state(self, states, channel):
|
||||||
st = states.get(channel)
|
st = states.get(channel)
|
||||||
@@ -312,22 +313,26 @@ class Database:
|
|||||||
ctx = dict(st['prev']) if st['prev'] else {}
|
ctx = dict(st['prev']) if st['prev'] else {}
|
||||||
ctx['sel'] = st['sel']
|
ctx['sel'] = st['sel']
|
||||||
ctx['df'] = st['df']
|
ctx['df'] = st['df']
|
||||||
|
ctx['df_path'] = st['df_path']
|
||||||
ctx['sfi_map'] = st['sfi_map']
|
ctx['sfi_map'] = st['sfi_map']
|
||||||
d = decode_sniff_msg(data, 'tpdu', 0, prev=ctx)
|
d = decode_sniff_msg(data, 'tpdu', 0, prev=ctx)
|
||||||
if not d or not d.get('ins_hex'):
|
if not d or not d.get('ins_hex'):
|
||||||
continue
|
continue
|
||||||
st['sel'], st['df'], st['sfi_map'] = self._advance_state(d, st['sel'], st['df'], st['sfi_map'])
|
(st['sel'], st['df'], st['sfi_map'],
|
||||||
|
st['df_path']) = self._advance_state(d, st['sel'], st['df'],
|
||||||
|
st['sfi_map'], st['df_path'])
|
||||||
st['prev'] = self._context_from_decoded(d)
|
st['prev'] = self._context_from_decoded(d)
|
||||||
return states, clk_hz
|
return states, clk_hz
|
||||||
|
|
||||||
def _advance_state(self, d, sel, df, sfi_map):
|
def _advance_state(self, d, sel, df, sfi_map, df_path=None):
|
||||||
"""Update (sel, df, sfi_map) for one decoded TPDU."""
|
"""Update (sel, df, sfi_map, df_path) for one decoded TPDU."""
|
||||||
from .decode import selected_df_fid, sfi_table, KNOWN_FIDS
|
from .decode import selected_df_state, sfi_table, KNOWN_FIDS
|
||||||
new_df = selected_df_fid(d, df)
|
new_df, new_path = selected_df_state(d, df, df_path)
|
||||||
if new_df is not None and new_df != df:
|
if new_df is not None and new_df != df:
|
||||||
# Only a successful SELECT changes the current DF.
|
# Only a successful SELECT changes the current DF.
|
||||||
if (d.get('sw') or {}).get('sw1') in _SUCCESS_SW1:
|
if (d.get('sw') or {}).get('sw1') in _SUCCESS_SW1:
|
||||||
df = new_df
|
df = new_df
|
||||||
|
df_path = new_path
|
||||||
sfi_map = dict(sfi_table(new_df))
|
sfi_map = dict(sfi_table(new_df))
|
||||||
# Learn SFI from the FCP of the last SELECT (GET RESPONSE).
|
# Learn SFI from the FCP of the last SELECT (GET RESPONSE).
|
||||||
if d.get('ins_hex') == 'c0' and d.get('response_for') == 'SELECT' and sel:
|
if d.get('ins_hex') == 'c0' and d.get('response_for') == 'SELECT' and sel:
|
||||||
@@ -343,7 +348,7 @@ class Database:
|
|||||||
fid = sfi_map.get(sfi)
|
fid = sfi_map.get(sfi)
|
||||||
if fid:
|
if fid:
|
||||||
sel = {'fid': fid, 'name': KNOWN_FIDS.get(fid), 'structure': None}
|
sel = {'fid': fid, 'name': KNOWN_FIDS.get(fid), 'structure': None}
|
||||||
return sel, df, sfi_map
|
return sel, df, sfi_map, (df_path or [df])
|
||||||
|
|
||||||
def _context_from_decoded(self, d):
|
def _context_from_decoded(self, d):
|
||||||
ins_hex = d.get('ins_hex')
|
ins_hex = d.get('ins_hex')
|
||||||
@@ -400,6 +405,7 @@ class Database:
|
|||||||
ctx = dict(st['prev']) if st['prev'] else {}
|
ctx = dict(st['prev']) if st['prev'] else {}
|
||||||
ctx['sel'] = st['sel']
|
ctx['sel'] = st['sel']
|
||||||
ctx['df'] = st['df']
|
ctx['df'] = st['df']
|
||||||
|
ctx['df_path'] = st['df_path']
|
||||||
ctx['sfi_map'] = st['sfi_map']
|
ctx['sfi_map'] = st['sfi_map']
|
||||||
msg = self._row_to_message(row, prev=ctx, clk_hz=clk_hz)
|
msg = self._row_to_message(row, prev=ctx, clk_hz=clk_hz)
|
||||||
d = msg.get('decoded')
|
d = msg.get('decoded')
|
||||||
@@ -408,7 +414,9 @@ class Database:
|
|||||||
# channel state as it was *before* this message.
|
# channel state as it was *before* this message.
|
||||||
on_message(msg, st, channel)
|
on_message(msg, st, channel)
|
||||||
if msg['type'] == 'tpdu' and d and d.get('ins_hex'):
|
if msg['type'] == 'tpdu' and d and d.get('ins_hex'):
|
||||||
st['sel'], st['df'], st['sfi_map'] = self._advance_state(d, st['sel'], st['df'], st['sfi_map'])
|
(st['sel'], st['df'], st['sfi_map'],
|
||||||
|
st['df_path']) = self._advance_state(d, st['sel'], st['df'],
|
||||||
|
st['sfi_map'], st['df_path'])
|
||||||
st['prev'] = self._context_from_decoded(d)
|
st['prev'] = self._context_from_decoded(d)
|
||||||
elif msg['type'] == 'atr':
|
elif msg['type'] == 'atr':
|
||||||
states = {}
|
states = {}
|
||||||
|
|||||||
@@ -298,6 +298,44 @@ def selected_df_fid(d, current_df=None):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def selected_df_state(d, current_df=None, current_path=None):
|
||||||
|
"""(current DF, DF path) after a SELECT, or ``(None, None)``.
|
||||||
|
|
||||||
|
The path is the chain of DFs from the root (MF) or from the running
|
||||||
|
application's ADF to the current DF: selecting ``7F10/5F3A/4FA1`` leaves
|
||||||
|
``('5f3a', ['7f10', '5f3a'])``. It lets the Files view show a file's full
|
||||||
|
path (``DF_TELECOM/DF_PHONEBOOK/4FA1``) instead of its parent alone.
|
||||||
|
"""
|
||||||
|
df = selected_df_fid(d, current_df)
|
||||||
|
if df is None:
|
||||||
|
return None, None
|
||||||
|
p1 = (d.get('p1') or {}).get('raw')
|
||||||
|
h = (d.get('body') or {}).get('hex') or ''
|
||||||
|
base = list(current_path or ([current_df] if current_df else []))
|
||||||
|
if p1 in ('00', '01'):
|
||||||
|
# A child DF of the current one; MF and an ADF start a fresh path.
|
||||||
|
if df == '3f00' or df == '7fff' or df.startswith('aid:'):
|
||||||
|
return df, [df]
|
||||||
|
return df, base + [df] if base else ['3f00', df]
|
||||||
|
if p1 in ('08', '09'):
|
||||||
|
comps = [h[i:i + 4] for i in range(0, len(h) - 3, 4)]
|
||||||
|
chain = [] if p1 == '08' else list(base)
|
||||||
|
for comp in comps:
|
||||||
|
if comp == '7fff':
|
||||||
|
# The ADF: the path restarts there (an AID identity stays).
|
||||||
|
chain = [df] if df.startswith('aid:') else ['7fff']
|
||||||
|
elif _is_df_fid(comp):
|
||||||
|
chain.append(comp)
|
||||||
|
if not chain:
|
||||||
|
chain = ['3f00'] if p1 == '08' else (list(base) or ['3f00'])
|
||||||
|
if chain[-1] != df:
|
||||||
|
chain.append(df)
|
||||||
|
return df, chain
|
||||||
|
if p1 == '04': # by AID: the ADF is the path root
|
||||||
|
return df, [df]
|
||||||
|
return df, (base or [df])
|
||||||
|
|
||||||
|
|
||||||
def sfi_table(df):
|
def sfi_table(df):
|
||||||
"""Return the SFI→FID table for a DF FID, or an empty dict."""
|
"""Return the SFI→FID table for a DF FID, or an empty dict."""
|
||||||
return _SFI_BY_DF.get(df or '', {})
|
return _SFI_BY_DF.get(df or '', {})
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ keeps counting).
|
|||||||
|
|
||||||
from .database import _SUCCESS_SW1
|
from .database import _SUCCESS_SW1
|
||||||
from .decode import (DF_CHILDREN, KNOWN_FIDS, _file_name, _is_df_fid,
|
from .decode import (DF_CHILDREN, KNOWN_FIDS, _file_name, _is_df_fid,
|
||||||
select_target_fid, selected_df_fid, sfi_table)
|
select_target_fid, selected_df_fid, selected_df_state,
|
||||||
|
sfi_table)
|
||||||
|
|
||||||
IMAGE_CAP = 64 * 1024 # observed bytes kept per file
|
IMAGE_CAP = 64 * 1024 # observed bytes kept per file
|
||||||
EVENTS_CAP = 1000 # change events kept per file
|
EVENTS_CAP = 1000 # change events kept per file
|
||||||
@@ -73,20 +74,35 @@ def _file_label(d):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _file_path(df, fid, name, aid_labels=None):
|
def _file_path(df, fid, name, aid_labels=None, df_path=None):
|
||||||
"""A human-readable path for a tracked file: the DF (named when known),
|
"""A human-readable path for a tracked file: the DF chain (named when
|
||||||
the EF and the FID — e.g. ``MF/EF_ICCID (2FE2)``,
|
known), the EF and the FID — e.g. ``MF/EF_ICCID (2FE2)``,
|
||||||
``ADF/EF_IMSI (6F07)``, ``DF_GSM_ACCESS/EF_KcGPRS (4F52)``,
|
``ADF/EF_IMSI (6F07)``, ``DF_TELECOM/DF_PHONEBOOK/4FA1``,
|
||||||
``AID A00000015141434C00/4F09``."""
|
``AID A00000015141434C00/4F09``. Without *df_path* the current DF is
|
||||||
|
named alone (the previous behaviour)."""
|
||||||
ef = (DF_CHILDREN.get(df, {}) or {}).get(fid) or name or fid.upper()
|
ef = (DF_CHILDREN.get(df, {}) or {}).get(fid) or name or fid.upper()
|
||||||
if df.startswith('aid:'):
|
labels = []
|
||||||
aid = df[4:].upper()
|
for comp in df_path or []:
|
||||||
label = (aid_labels or {}).get(aid)
|
if comp == '3f00' and len(df_path) > 1:
|
||||||
df_label = f'AID {aid}' + (f' ({label})' if label else '')
|
continue # MF is implicit in a longer path
|
||||||
elif df == '7fff':
|
if comp.startswith('aid:'):
|
||||||
df_label = 'ADF'
|
aid = comp[4:].upper()
|
||||||
else:
|
label = (aid_labels or {}).get(aid)
|
||||||
df_label = KNOWN_FIDS.get(df) or df.upper()
|
labels.append(f'AID {aid}' + (f' ({label})' if label else ''))
|
||||||
|
elif comp == '7fff':
|
||||||
|
labels.append('ADF')
|
||||||
|
else:
|
||||||
|
labels.append(KNOWN_FIDS.get(comp) or comp.upper())
|
||||||
|
if not labels:
|
||||||
|
if df.startswith('aid:'):
|
||||||
|
aid = df[4:].upper()
|
||||||
|
label = (aid_labels or {}).get(aid)
|
||||||
|
labels = [f'AID {aid}' + (f' ({label})' if label else '')]
|
||||||
|
elif df == '7fff':
|
||||||
|
labels = ['ADF']
|
||||||
|
else:
|
||||||
|
labels = [KNOWN_FIDS.get(df) or df.upper()]
|
||||||
|
df_label = '/'.join(labels)
|
||||||
if ef.upper() == fid.upper():
|
if ef.upper() == fid.upper():
|
||||||
return f'{df_label}/{ef.upper()}'
|
return f'{df_label}/{ef.upper()}'
|
||||||
return f'{df_label}/{ef} ({fid.upper()})'
|
return f'{df_label}/{ef} ({fid.upper()})'
|
||||||
@@ -131,11 +147,12 @@ def _segments(buf, mask):
|
|||||||
class _FileEntry:
|
class _FileEntry:
|
||||||
"""One tracked file: counters, sparse image, change log."""
|
"""One tracked file: counters, sparse image, change log."""
|
||||||
|
|
||||||
def __init__(self, key, df, fid, name, image_cap=IMAGE_CAP):
|
def __init__(self, key, df, fid, name, image_cap=IMAGE_CAP, df_path=None):
|
||||||
self.key = key
|
self.key = key
|
||||||
self.df = df
|
self.df = df
|
||||||
self.fid = fid
|
self.fid = fid
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.df_path = list(df_path or [])
|
||||||
self.image_cap = image_cap
|
self.image_cap = image_cap
|
||||||
self.sfi = None
|
self.sfi = None
|
||||||
self.structure = None
|
self.structure = None
|
||||||
@@ -248,21 +265,22 @@ class FileTracker:
|
|||||||
return
|
return
|
||||||
ins_hex = d['ins_hex']
|
ins_hex = d['ins_hex']
|
||||||
df = state.get('df') or '3f00'
|
df = state.get('df') or '3f00'
|
||||||
|
df_path = state.get('df_path') or [df]
|
||||||
if ins_hex == 'c0':
|
if ins_hex == 'c0':
|
||||||
self._observe_get_response(d, state, channel, index, msg)
|
self._observe_get_response(d, state, channel, index, msg)
|
||||||
return
|
return
|
||||||
self._pending_read.pop(channel, None)
|
self._pending_read.pop(channel, None)
|
||||||
ins = int(ins_hex, 16)
|
ins = int(ins_hex, 16)
|
||||||
if ins == 0xA4:
|
if ins == 0xA4:
|
||||||
self._observe_select(d, df)
|
self._observe_select(d, df, df_path)
|
||||||
elif ins in _UPDATE_INS:
|
elif ins in _UPDATE_INS:
|
||||||
self._observe_write(d, df, index, msg)
|
self._observe_write(d, df, index, msg, df_path)
|
||||||
elif ins in _READ_INS:
|
elif ins in _READ_INS:
|
||||||
self._observe_read(d, df, channel, index, msg)
|
self._observe_read(d, df, channel, index, msg, df_path)
|
||||||
|
|
||||||
# -- file registry ---------------------------------------------------
|
# -- file registry ---------------------------------------------------
|
||||||
|
|
||||||
def _entry(self, df, fid, name=None):
|
def _entry(self, df, fid, name=None, df_path=None):
|
||||||
key = (df, fid)
|
key = (df, fid)
|
||||||
entry = self.files.get(key)
|
entry = self.files.get(key)
|
||||||
if entry is None:
|
if entry is None:
|
||||||
@@ -270,9 +288,14 @@ class FileTracker:
|
|||||||
# no name) still gets its DF-scoped dictionary name.
|
# no name) still gets its DF-scoped dictionary name.
|
||||||
entry = _FileEntry(key, df, fid,
|
entry = _FileEntry(key, df, fid,
|
||||||
(name or _file_name(fid, df)) if fid else '?',
|
(name or _file_name(fid, df)) if fid else '?',
|
||||||
image_cap=self._image_cap)
|
image_cap=self._image_cap, df_path=df_path)
|
||||||
self.files[key] = entry
|
self.files[key] = entry
|
||||||
elif name:
|
elif df_path and len(df_path) > len(entry.df_path) and \
|
||||||
|
list(df_path[:len(entry.df_path)]) == entry.df_path:
|
||||||
|
# A fuller path learned later (e.g. the DF was entered by a path
|
||||||
|
# selection after the first FCP-only sighting).
|
||||||
|
entry.df_path = list(df_path)
|
||||||
|
if name:
|
||||||
placeholders = {entry.fid, entry.fid.upper(), '', '?'}
|
placeholders = {entry.fid, entry.fid.upper(), '', '?'}
|
||||||
if entry.sfi is not None:
|
if entry.sfi is not None:
|
||||||
placeholders.add(f'SFI {entry.sfi}')
|
placeholders.add(f'SFI {entry.sfi}')
|
||||||
@@ -371,15 +394,18 @@ class FileTracker:
|
|||||||
|
|
||||||
# -- operations ------------------------------------------------------
|
# -- operations ------------------------------------------------------
|
||||||
|
|
||||||
def _observe_select(self, d, df):
|
def _observe_select(self, d, df, df_path=None):
|
||||||
fid = select_target_fid(d)
|
fid = select_target_fid(d)
|
||||||
if not fid or _is_df_fid(fid) or not _success(d):
|
if not fid or _is_df_fid(fid) or not _success(d):
|
||||||
return
|
return
|
||||||
# A path selection of an EF also enters the EF's parent DF
|
# A path selection of an EF also enters the EF's parent DF
|
||||||
# (TS 102 221 §8.4.1) — key the file by that DF, not the one the
|
# (TS 102 221 §8.4.1) — key the file by that DF, not the one the
|
||||||
# channel was in before the SELECT.
|
# channel was in before the SELECT, and keep the DF chain the
|
||||||
|
# selection's own path spells out (DF_TELECOM/DF_PHONEBOOK/…).
|
||||||
effective = selected_df_fid(d, df) or df
|
effective = selected_df_fid(d, df) or df
|
||||||
self._entry(effective, fid).counters['select'] += 1
|
_new_df, new_path = selected_df_state(d, df, df_path)
|
||||||
|
self._entry(effective, fid,
|
||||||
|
df_path=new_path or df_path).counters['select'] += 1
|
||||||
|
|
||||||
def _observe_get_response(self, d, state, channel, index, msg):
|
def _observe_get_response(self, d, state, channel, index, msg):
|
||||||
response = d.get('response') or {}
|
response = d.get('response') or {}
|
||||||
@@ -390,7 +416,7 @@ class FileTracker:
|
|||||||
if not fid or _is_df_fid(fid):
|
if not fid or _is_df_fid(fid):
|
||||||
return
|
return
|
||||||
df = state.get('df') or '3f00'
|
df = state.get('df') or '3f00'
|
||||||
entry = self._entry(df, fid)
|
entry = self._entry(df, fid, df_path=state.get('df_path'))
|
||||||
fd = response.get('file_descriptor')
|
fd = response.get('file_descriptor')
|
||||||
if isinstance(fd, dict):
|
if isinstance(fd, dict):
|
||||||
entry.structure = fd.get('structure', entry.structure)
|
entry.structure = fd.get('structure', entry.structure)
|
||||||
@@ -409,28 +435,31 @@ class FileTracker:
|
|||||||
# SFI-addressed command keeps its context (the response's
|
# SFI-addressed command keeps its context (the response's
|
||||||
# attribution resolves against the current selection, not the SFI).
|
# attribution resolves against the current selection, not the SFI).
|
||||||
src, src_df = d, state.get('df') or '3f00'
|
src, src_df = d, state.get('df') or '3f00'
|
||||||
|
src_path = state.get('df_path')
|
||||||
if pending:
|
if pending:
|
||||||
_fid, sfi = self._file_ref(pending['d'], pending['df'])
|
_fid, sfi = self._file_ref(pending['d'], pending['df'])
|
||||||
if sfi is not None:
|
if sfi is not None:
|
||||||
src, src_df = pending['d'], pending['df']
|
src, src_df = pending['d'], pending['df']
|
||||||
self._apply_read(src, src_df, index, msg, _body_bytes(d))
|
src_path = pending.get('df_path')
|
||||||
|
self._apply_read(src, src_df, index, msg, _body_bytes(d), src_path)
|
||||||
|
|
||||||
def _observe_read(self, d, df, channel, index, msg):
|
def _observe_read(self, d, df, channel, index, msg, df_path=None):
|
||||||
if not _success(d):
|
if not _success(d):
|
||||||
return
|
return
|
||||||
if not _body_bytes(d):
|
if not _body_bytes(d):
|
||||||
# Data pending (T=0): the GET RESPONSE will carry it.
|
# Data pending (T=0): the GET RESPONSE will carry it.
|
||||||
self._pending_read[channel] = {'d': d, 'df': df}
|
self._pending_read[channel] = {'d': d, 'df': df,
|
||||||
|
'df_path': df_path}
|
||||||
return
|
return
|
||||||
self._apply_read(d, df, index, msg, _body_bytes(d))
|
self._apply_read(d, df, index, msg, _body_bytes(d), df_path)
|
||||||
|
|
||||||
def _apply_read(self, d, df, index, msg, data):
|
def _apply_read(self, d, df, index, msg, data, df_path=None):
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
fid, sfi = self._file_ref(d, df)
|
fid, sfi = self._file_ref(d, df)
|
||||||
if fid is None:
|
if fid is None:
|
||||||
return
|
return
|
||||||
entry = self._entry(df, fid, _file_label(d))
|
entry = self._entry(df, fid, _file_label(d), df_path)
|
||||||
if sfi is not None and entry.sfi is None:
|
if sfi is not None and entry.sfi is None:
|
||||||
entry.sfi = sfi
|
entry.sfi = sfi
|
||||||
entry.counters['read'] += 1
|
entry.counters['read'] += 1
|
||||||
@@ -449,14 +478,14 @@ class FileTracker:
|
|||||||
self._push('read', entry, index, msg, record=record)
|
self._push('read', entry, index, msg, record=record)
|
||||||
self._note_dir_content(d)
|
self._note_dir_content(d)
|
||||||
|
|
||||||
def _observe_write(self, d, df, index, msg):
|
def _observe_write(self, d, df, index, msg, df_path=None):
|
||||||
data = _body_bytes(d)
|
data = _body_bytes(d)
|
||||||
if not data or not _success(d):
|
if not data or not _success(d):
|
||||||
return
|
return
|
||||||
fid, sfi = self._file_ref(d, df)
|
fid, sfi = self._file_ref(d, df)
|
||||||
if fid is None:
|
if fid is None:
|
||||||
return
|
return
|
||||||
entry = self._entry(df, fid, _file_label(d))
|
entry = self._entry(df, fid, _file_label(d), df_path)
|
||||||
if sfi is not None and entry.sfi is None:
|
if sfi is not None and entry.sfi is None:
|
||||||
entry.sfi = sfi
|
entry.sfi = sfi
|
||||||
entry.counters['update'] += 1
|
entry.counters['update'] += 1
|
||||||
@@ -540,7 +569,7 @@ class FileTracker:
|
|||||||
'key': _key_str(entry.key),
|
'key': _key_str(entry.key),
|
||||||
'name': entry.name,
|
'name': entry.name,
|
||||||
'path': _file_path(entry.df, entry.fid, entry.name,
|
'path': _file_path(entry.df, entry.fid, entry.name,
|
||||||
self.aid_labels),
|
self.aid_labels, entry.df_path),
|
||||||
'df': entry.df,
|
'df': entry.df,
|
||||||
'fid': entry.fid,
|
'fid': entry.fid,
|
||||||
'structure': entry.structure,
|
'structure': entry.structure,
|
||||||
@@ -567,7 +596,7 @@ class FileTracker:
|
|||||||
files[_key_str(key)] = {
|
files[_key_str(key)] = {
|
||||||
'name': entry.name,
|
'name': entry.name,
|
||||||
'path': _file_path(entry.df, entry.fid, entry.name,
|
'path': _file_path(entry.df, entry.fid, entry.name,
|
||||||
self.aid_labels),
|
self.aid_labels, entry.df_path),
|
||||||
'df': entry.df,
|
'df': entry.df,
|
||||||
'fid': entry.fid,
|
'fid': entry.fid,
|
||||||
'sfi': entry.sfi,
|
'sfi': entry.sfi,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""Single source of truth for the SIMtrace Analyser version."""
|
"""Single source of truth for the SIMtrace Analyser version."""
|
||||||
|
|
||||||
__version__ = '2.7.5'
|
__version__ = '2.7.6'
|
||||||
@@ -1814,6 +1814,33 @@ class TestSfiResolution(unittest.TestCase):
|
|||||||
self.assertEqual(r['file']['ef'], 'EF_LOCI')
|
self.assertEqual(r['file']['ef'], 'EF_LOCI')
|
||||||
self.assertEqual(r['file']['tmsi'], '1E731E57')
|
self.assertEqual(r['file']['tmsi'], '1E731E57')
|
||||||
|
|
||||||
|
def test_selected_df_state(self):
|
||||||
|
from simtrace_analyser.decode import selected_df_state
|
||||||
|
|
||||||
|
def sel(p1, body):
|
||||||
|
return {'ins_hex': 'a4', 'p1': {'raw': p1}, 'body': {'hex': body}}
|
||||||
|
|
||||||
|
# A path selection spells out the DF chain: 7F10/5F3A/4FA1.
|
||||||
|
self.assertEqual(selected_df_state(sel('08', '7f105f3a4fa1')),
|
||||||
|
('5f3a', ['7f10', '5f3a']))
|
||||||
|
# A child DF selected by FID appends to the current chain.
|
||||||
|
self.assertEqual(selected_df_state(sel('00', '5f3a'), '7f10', ['7f10']),
|
||||||
|
('5f3a', ['7f10', '5f3a']))
|
||||||
|
# MF and the ADF start a fresh chain; an AID is the root.
|
||||||
|
self.assertEqual(selected_df_state(sel('00', '3f00'), '7f10',
|
||||||
|
['7f10', '5f3a']),
|
||||||
|
('3f00', ['3f00']))
|
||||||
|
self.assertEqual(selected_df_state(sel('08', '7fff6f07'), '3f00',
|
||||||
|
['3f00']), ('7fff', ['7fff']))
|
||||||
|
aid = 'aid:a00000015141434c00'
|
||||||
|
self.assertEqual(selected_df_state(sel('04', 'A00000015141434C00'),
|
||||||
|
'3f00', ['3f00']), (aid, [aid]))
|
||||||
|
# An EF selection leaves the chain in place; non-SELECTs never move it.
|
||||||
|
self.assertEqual(selected_df_state(sel('09', '4fa1'), '5f3a',
|
||||||
|
['7f10', '5f3a']),
|
||||||
|
('5f3a', ['7f10', '5f3a']))
|
||||||
|
self.assertEqual(selected_df_state({'ins_hex': 'b0'}), (None, None))
|
||||||
|
|
||||||
def test_selected_df_fid(self):
|
def test_selected_df_fid(self):
|
||||||
from simtrace_analyser.decode import selected_df_fid, sfi_table
|
from simtrace_analyser.decode import selected_df_fid, sfi_table
|
||||||
self.assertEqual(selected_df_fid({'ins_hex': 'a4', 'p1': {'raw': '08'},
|
self.assertEqual(selected_df_fid({'ins_hex': 'a4', 'p1': {'raw': '08'},
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ def sel_fid(fid, le='02'):
|
|||||||
return f'00a4000002{fid}61{le}'
|
return f'00a4000002{fid}61{le}'
|
||||||
|
|
||||||
|
|
||||||
|
def sel_path(path_hex, le='02'):
|
||||||
|
"""SELECT a path from the MF (P1=08)."""
|
||||||
|
return f'00a40804{len(path_hex) // 2:02x}{path_hex}61{le}'
|
||||||
|
|
||||||
|
|
||||||
def gr(data_hex, total=None):
|
def gr(data_hex, total=None):
|
||||||
"""GET RESPONSE carrying *data_hex* (the FCP or the read data)."""
|
"""GET RESPONSE carrying *data_hex* (the FCP or the read data)."""
|
||||||
n = len(data_hex) // 2 if total is None else total
|
n = len(data_hex) // 2 if total is None else total
|
||||||
@@ -541,6 +546,20 @@ class TestNaming(TrackerCase):
|
|||||||
'DF_GSM/EF_SST (6F38)')
|
'DF_GSM/EF_SST (6F38)')
|
||||||
|
|
||||||
|
|
||||||
|
class TestDfChain(TrackerCase):
|
||||||
|
def test_path_selection_keeps_the_full_df_chain(self):
|
||||||
|
# The trace's own path selection spells out DF_TELECOM/DF_PHONEBOOK;
|
||||||
|
# the Files view used to show only the parent DF.
|
||||||
|
data = self.track(
|
||||||
|
sel_path('7f105f3a4fa1'),
|
||||||
|
gr(fcp('4fa1', rec_len=44, n_rec=100)),
|
||||||
|
)
|
||||||
|
entry = data['files']['5f3a/4fa1']
|
||||||
|
self.assertEqual(entry['path'], 'DF_TELECOM/DF_PHONEBOOK/4FA1')
|
||||||
|
self.assertEqual(entry['record_len'], 44)
|
||||||
|
self.assertEqual(entry['n_records'], 100)
|
||||||
|
|
||||||
|
|
||||||
class TestFilePath(unittest.TestCase):
|
class TestFilePath(unittest.TestCase):
|
||||||
def test_paths(self):
|
def test_paths(self):
|
||||||
from simtrace_analyser.filetrack import _file_path
|
from simtrace_analyser.filetrack import _file_path
|
||||||
@@ -556,6 +575,13 @@ class TestFilePath(unittest.TestCase):
|
|||||||
# The DF_TELECOM files are named from TS 51.011 10.5 / TS 31.102 4.5.
|
# The DF_TELECOM files are named from TS 51.011 10.5 / TS 31.102 4.5.
|
||||||
self.assertEqual(_file_path('7f10', '6f3a', '6F3A'),
|
self.assertEqual(_file_path('7f10', '6f3a', '6F3A'),
|
||||||
'DF_TELECOM/EF_ADN (6F3A)')
|
'DF_TELECOM/EF_ADN (6F3A)')
|
||||||
|
# The full DF chain (learned from the selection path) is part of the
|
||||||
|
# path — the immediate DF alone hid DF_TELECOM for the phonebook.
|
||||||
|
self.assertEqual(_file_path('5f3a', '4fa1', '4FA1', None,
|
||||||
|
['7f10', '5f3a']),
|
||||||
|
'DF_TELECOM/DF_PHONEBOOK/4FA1')
|
||||||
|
self.assertEqual(_file_path('7fff', '6f07', 'EF_IMSI', None, ['7fff']),
|
||||||
|
'ADF/EF_IMSI (6F07)')
|
||||||
# An application selected by an unknown AID: the AID is the DF name,
|
# An application selected by an unknown AID: the AID is the DF name,
|
||||||
# with the EF.DIR label when the trace surfaced it.
|
# with the EF.DIR label when the trace surfaced it.
|
||||||
self.assertEqual(_file_path('aid:a00000015141434c00', '4f09', '4F09'),
|
self.assertEqual(_file_path('aid:a00000015141434c00', '4f09', '4F09'),
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ class TestImportEndpoint(unittest.TestCase):
|
|||||||
class TestVersion(unittest.TestCase):
|
class TestVersion(unittest.TestCase):
|
||||||
def test_package_version(self):
|
def test_package_version(self):
|
||||||
from simtrace2_pysniff import __version__
|
from simtrace2_pysniff import __version__
|
||||||
self.assertEqual(__version__, '2.7.5')
|
self.assertEqual(__version__, '2.7.6')
|
||||||
|
|
||||||
def test_status_includes_version(self):
|
def test_status_includes_version(self):
|
||||||
import os
|
import os
|
||||||
|
|||||||
Reference in new issue
Block a user