Files view: the change-details modal shows the full file path (v2.4.2)
- the modal header showed only the flat name (EF_ICCID, 4F09); it now shows the path built from data already available: the tracker's DF identity named via KNOWN_FIDS (MF, DF_GSM_ACCESS, DF_TELECOM, …), 'ADF' for the current application (7FFF), DF_CHILDREN for DF-qualified EF names (4F52 → EF_KcGPRS) and the EF.DIR labels for AID-selected applications — e.g. `MF/EF_ICCID (2FE2)`, `ADF/EF_IMSI (6F07)`, `DF_GSM_ACCESS/EF_KcGPRS (4F52)`, `AID A00000015141434C00/4F09` - exposed as `path` in the change-details payload and in the files entries (the timeline rows keep the short name — they have no room) - the colour legend (changed / newly observed / never observed) moved below the two hex views - tests: `_file_path` cases (MF, ADF, DF-qualified EF, AID with and without an EF.DIR label, FID-as-name) plus the payload field, and the Node modal test asserts the path header and that the legend follows the views - v2.4.2; SW cache simtrace-analyser-v68
This commit is contained in:
1 parent
cbb73536f4
commit
5809092175
9 files changed
+77
-21
No files matched your search
@@ -194,11 +194,14 @@ trace-wide timeline of file operations — every **Read**, **Update** and
|
||||
**Changed on card** row shows the same file name as the Map view, and clicking
|
||||
a row jumps to the APDU in the trace. Rows that carry a recorded change (a
|
||||
card-side change, or an update that wrote or first observed bytes) also offer
|
||||
**Details**: a modal showing the affected unit — the whole file, or the single
|
||||
record that changed — **before and after** the change, as hex views (16 bytes
|
||||
per row, never-observed bytes drawn as `--`) with the bytes that changed
|
||||
highlighted in one colour and the bytes the operation first observed in
|
||||
another, plus the changed/new byte counts and the APDU reference.
|
||||
**Details**: 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
|
||||
named 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
|
||||
hex views (16 bytes per row, never-observed bytes drawn as `--`) with the bytes
|
||||
that changed highlighted in one colour and the bytes the operation first
|
||||
observed in another, the changed/new byte counts, the APDU reference and the
|
||||
colour legend below the views.
|
||||
|
||||
The images come from `GET /api/sessions/<id>/files/image?key=…&change=…` and
|
||||
are reconstructed from the current image by undoing the later changes
|
||||
|
||||
+5
-2
@@ -199,11 +199,14 @@ Files — это таймлайн файловых операций по все
|
||||
**Update** и **Changed on card** показывают те же имена файлов, что и вид Map,
|
||||
а щелчок по строке переходит к APDU в трассе. У строк с записанным изменением
|
||||
(изменение со стороны карты либо запись, изменившая или впервые показавшая
|
||||
байты) есть кнопка **Details**: окно показывает затронутую единицу — файл
|
||||
байты) есть кнопка **Details**: окно показывает полный путь файла (`MF/EF_ICCID (2FE2)`,
|
||||
`ADF/EF_IMSI (6F07)`, `DF_GSM_ACCESS/EF_KcGPRS (4F52)`, `AID …/4F09` — DF
|
||||
называется по меткам из EF.DIR, если они есть) и затронутую единицу — файл
|
||||
целиком или изменившуюся запись — **до и после** изменения в виде hex-вида
|
||||
(16 байт в строке, никогда не наблюдавшиеся байты — `--`), причём изменённые
|
||||
байты подсвечены одним цветом, а впервые наблюдаемые — другим; ниже —
|
||||
счётчики изменённых/новых байт и ссылка на APDU.
|
||||
счётчики изменённых/новых байт, ссылка на APDU и легенда цветов под
|
||||
представлениями.
|
||||
|
||||
Изображения отдаёт `GET /api/sessions/<id>/files/image?key=…&change=…`; они
|
||||
восстанавливаются из текущего образа отменой более поздних изменений
|
||||
|
||||
+9
-9
@@ -192,7 +192,7 @@ body { font: 14px/1.5 system-ui, sans-serif; background: var(--bg); color: var(-
|
||||
.file-op .file-op-diff:hover { color: var(--warn); }
|
||||
.file-diff-modal { max-width: 760px; }
|
||||
.file-diff-body { max-height: 60vh; overflow-y: auto; margin-bottom: 16px; }
|
||||
.hex-legend { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted); margin-bottom: 6px; flex-wrap: wrap; }
|
||||
.hex-legend { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted); margin-top: 10px; flex-wrap: wrap; }
|
||||
.hex-summary { font-size: 12px; color: var(--text-muted); margin-bottom: 10px; }
|
||||
.hex-pair { display: flex; gap: 16px; flex-wrap: wrap; }
|
||||
.hex-pair > div { flex: 1 1 320px; min-width: 0; }
|
||||
@@ -2244,18 +2244,18 @@ function renderFileDiff(image) {
|
||||
}
|
||||
if (image.index !== null && image.index !== undefined) meta.push(`#${image.index}`);
|
||||
if (image.elapsed !== null && image.elapsed !== undefined) meta.push(fmt_elapsed(image.elapsed));
|
||||
el('file-diff-sub').textContent = `${image.name || ''} — ${meta.join(' · ')}`;
|
||||
el('file-diff-sub').textContent = `${image.path || image.name || ''} — ${meta.join(' · ')}`;
|
||||
el('file-diff-body').innerHTML =
|
||||
`<div class="hex-legend">
|
||||
<span class="hex-cell hex-changed">aa</span> ${t('changed')}
|
||||
<span class="hex-cell hex-new">aa</span> ${t('newly observed')}
|
||||
<span class="hex-cell hex-unknown">--</span> ${t('never observed')}
|
||||
</div>
|
||||
<div class="hex-summary">${t('{c} bytes changed, {n} bytes newly observed')
|
||||
`<div class="hex-summary">${t('{c} bytes changed, {n} bytes newly observed')
|
||||
.replace('{c}', changed).replace('{n}', fresh)}</div>
|
||||
<div class="hex-pair">
|
||||
<div><h4>${t('Before')}</h4>${hexImageHtml(image, 'before')}</div>
|
||||
<div><h4>${t('After')}</h4>${hexImageHtml(image, 'after')}</div>
|
||||
</div>
|
||||
<div class="hex-legend">
|
||||
<span class="hex-cell hex-changed">aa</span> ${t('changed')}
|
||||
<span class="hex-cell hex-new">aa</span> ${t('newly observed')}
|
||||
<span class="hex-cell hex-unknown">--</span> ${t('never observed')}
|
||||
</div>` +
|
||||
(image.image_truncated ? `<p class="hex-note">${t('The image is frozen (size cap reached) — later changes are not tracked.')}</p>` : '');
|
||||
}
|
||||
@@ -3167,7 +3167,7 @@ function onFilterChange() {
|
||||
}
|
||||
|
||||
// ──────────────────── Versioning ────────────────────
|
||||
const PWA_VERSION = '2.4.1';
|
||||
const PWA_VERSION = '2.4.2';
|
||||
const MIN_SERVER_VERSION = '2.3.0';
|
||||
|
||||
function parseVersion(v) {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simtrace-analyser-v67';
|
||||
const CACHE = 'simtrace-analyser-v68';
|
||||
|
||||
const PRECACHE = [
|
||||
'index.html',
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
|
||||
|
||||
[project]
|
||||
name = "simtrace-analyser"
|
||||
version = "2.4.1"
|
||||
version = "2.4.2"
|
||||
description = "SIMtrace Analyser — APDU trace analyser for GSMTAP/PCAP captures (bundles the simtrace2-pysniff utility)"
|
||||
readme = {file = "README.md", content-type = "text/markdown"}
|
||||
license = {text = "GPL-2.0-or-later"}
|
||||
|
||||
@@ -39,7 +39,8 @@ keeps counting).
|
||||
"""
|
||||
|
||||
from .database import _SUCCESS_SW1
|
||||
from .decode import (_is_df_fid, select_target_fid, selected_df_fid, sfi_table)
|
||||
from .decode import (DF_CHILDREN, KNOWN_FIDS, _is_df_fid, select_target_fid,
|
||||
selected_df_fid, sfi_table)
|
||||
|
||||
IMAGE_CAP = 64 * 1024 # observed bytes kept per file
|
||||
EVENTS_CAP = 1000 # change events kept per file
|
||||
@@ -72,6 +73,25 @@ def _file_label(d):
|
||||
return None
|
||||
|
||||
|
||||
def _file_path(df, fid, name, aid_labels=None):
|
||||
"""A human-readable path for a tracked file: the DF (named when known),
|
||||
the EF and the FID — e.g. ``MF/EF_ICCID (2FE2)``,
|
||||
``ADF/EF_IMSI (6F07)``, ``DF_GSM_ACCESS/EF_KcGPRS (4F52)``,
|
||||
``AID A00000015141434C00/4F09``."""
|
||||
ef = (DF_CHILDREN.get(df, {}) or {}).get(fid) or name or fid.upper()
|
||||
if df.startswith('aid:'):
|
||||
aid = df[4:].upper()
|
||||
label = (aid_labels or {}).get(aid)
|
||||
df_label = f'AID {aid}' + (f' ({label})' if label else '')
|
||||
elif df == '7fff':
|
||||
df_label = 'ADF'
|
||||
else:
|
||||
df_label = KNOWN_FIDS.get(df) or df.upper()
|
||||
if ef.upper() == fid.upper():
|
||||
return f'{df_label}/{ef.upper()}'
|
||||
return f'{df_label}/{ef} ({fid.upper()})'
|
||||
|
||||
|
||||
def _key_str(key):
|
||||
df, fid = key
|
||||
return f'{df}/{fid}'
|
||||
@@ -515,6 +535,8 @@ class FileTracker:
|
||||
return {
|
||||
'key': _key_str(entry.key),
|
||||
'name': entry.name,
|
||||
'path': _file_path(entry.df, entry.fid, entry.name,
|
||||
self.aid_labels),
|
||||
'df': entry.df,
|
||||
'fid': entry.fid,
|
||||
'structure': entry.structure,
|
||||
@@ -540,6 +562,8 @@ class FileTracker:
|
||||
for key, entry in self.files.items():
|
||||
files[_key_str(key)] = {
|
||||
'name': entry.name,
|
||||
'path': _file_path(entry.df, entry.fid, entry.name,
|
||||
self.aid_labels),
|
||||
'df': entry.df,
|
||||
'fid': entry.fid,
|
||||
'sfi': entry.sfi,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""Single source of truth for the SIMtrace Analyser version."""
|
||||
|
||||
__version__ = '2.4.1'
|
||||
__version__ = '2.4.2'
|
||||
@@ -96,6 +96,7 @@ class TestCountersAndRegistration(TrackerCase):
|
||||
entry = data['files']['3f00/2fe2']
|
||||
self.assertEqual(entry['counters'], {'select': 2, 'read': 1, 'update': 1})
|
||||
self.assertEqual(entry['name'], 'EF_ICCID')
|
||||
self.assertEqual(entry['path'], 'MF/EF_ICCID (2FE2)')
|
||||
self.assertEqual(entry['size'], 10)
|
||||
|
||||
def test_failed_select_does_not_register(self):
|
||||
@@ -419,6 +420,7 @@ class TestChangeImages(TrackerCase):
|
||||
update_binary(0, 'AABB0000'),
|
||||
)
|
||||
self.assertEqual(result['kind'], 'device')
|
||||
self.assertEqual(result['path'], 'MF/EF_ICCID (2FE2)')
|
||||
self.assertEqual(result['record'], None)
|
||||
self.assertEqual(result['ranges'], [[2, 2]])
|
||||
self.assertEqual(result['observed_ranges'], [])
|
||||
@@ -497,6 +499,30 @@ class TestChangeImages(TrackerCase):
|
||||
{'offset': 6, 'hex': 'cc00'}])
|
||||
|
||||
|
||||
class TestFilePath(unittest.TestCase):
|
||||
def test_paths(self):
|
||||
from simtrace_analyser.filetrack import _file_path
|
||||
# MF-level EF: the DF is named, the EF is named, the FID is shown.
|
||||
self.assertEqual(_file_path('3f00', '2fe2', 'EF_ICCID'),
|
||||
'MF/EF_ICCID (2FE2)')
|
||||
# The ADF of the current application.
|
||||
self.assertEqual(_file_path('7fff', '6f07', 'EF_IMSI'),
|
||||
'ADF/EF_IMSI (6F07)')
|
||||
# A DF-qualified name from DF_CHILDREN wins over the flat FID.
|
||||
self.assertEqual(_file_path('5f3b', '4f52', '4F52'),
|
||||
'DF_GSM_ACCESS/EF_KcGPRS (4F52)')
|
||||
# An application selected by an unknown AID: the AID is the DF name,
|
||||
# with the EF.DIR label when the trace surfaced it.
|
||||
self.assertEqual(_file_path('aid:a00000015141434c00', '4f09', '4F09'),
|
||||
'AID A00000015141434C00/4F09')
|
||||
self.assertEqual(_file_path('aid:a0000000871002ff49ff0589', '6f07',
|
||||
'EF_IMSI',
|
||||
{'A0000000871002FF49FF0589': 'USIM'}),
|
||||
'AID A0000000871002FF49FF0589 (USIM)/EF_IMSI (6F07)')
|
||||
# An unnamed file: the FID alone, no duplicated parentheses.
|
||||
self.assertEqual(_file_path('7f10', '6f3a', '6F3A'), 'DF_TELECOM/6F3A')
|
||||
|
||||
|
||||
class TestEndpoint(unittest.TestCase):
|
||||
def test_session_files_endpoint(self):
|
||||
from simtrace_analyser.server import RequestHandler
|
||||
|
||||
@@ -224,7 +224,7 @@ class TestImportEndpoint(unittest.TestCase):
|
||||
class TestVersion(unittest.TestCase):
|
||||
def test_package_version(self):
|
||||
from simtrace2_pysniff import __version__
|
||||
self.assertEqual(__version__, '2.4.1')
|
||||
self.assertEqual(__version__, '2.4.2')
|
||||
|
||||
def test_status_includes_version(self):
|
||||
import os
|
||||
|
||||
Reference in new issue
Block a user