STATUS list entries: suppress raw hex when empty, describe FCP data
- decode.py: _build_summary emits 'Data: N bytes — FCP of <file> (...)' for STATUS responses with data (FCP already decoded into decoded.response); _fcp_brief builds the 'FCP of MF (DF or ADF)' / 'FCP of EF_xxx (...)' fragment - index.html: cmd_description suppresses the raw-hex fallback for body-less STATUS rows (nothing to describe beyond type + SW) - tests: no-data STATUS summary None; DF/EF FCP summaries; pluralization - Bump 1.22.19, SW cache v50->v51
This commit is contained in:
1 parent
39ee173a7d
commit
ccbb3f231b
7 files changed
+87
-6
No files matched your search
+6
-2
@@ -1047,7 +1047,11 @@ function data_text(m) {
|
||||
|
||||
function cmd_description(m) {
|
||||
if (m.type === 'gap') return t('device disconnected — messages may be missing');
|
||||
if (m.decoded && m.decoded.summary) return m.decoded.summary;
|
||||
const d = m.decoded;
|
||||
// STATUS has no command body: without response data there is nothing
|
||||
// to describe beyond the type and SW (no raw-hex fallback).
|
||||
if (d && d.ins_hex === 'f2' && !d.body) return d.summary || '';
|
||||
if (d && d.summary) return d.summary;
|
||||
return data_text(m);
|
||||
}
|
||||
|
||||
@@ -2629,7 +2633,7 @@ function onFilterChange() {
|
||||
}
|
||||
|
||||
// ──────────────────── Versioning ────────────────────
|
||||
const PWA_VERSION = '1.22.18';
|
||||
const PWA_VERSION = '1.22.19';
|
||||
const MIN_SERVER_VERSION = '1.0.0';
|
||||
|
||||
function parseVersion(v) {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simtrace-analyser-v50';
|
||||
const CACHE = 'simtrace-analyser-v51';
|
||||
|
||||
const PRECACHE = [
|
||||
'index.html',
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
|
||||
|
||||
[project]
|
||||
name = "simtrace2-pysniff"
|
||||
version = "1.22.18"
|
||||
version = "1.22.19"
|
||||
description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)"
|
||||
readme = {file = "README.md", content-type = "text/markdown"}
|
||||
license = {text = "GPL-2.0-or-later"}
|
||||
|
||||
@@ -6,7 +6,7 @@ from .gsmtap import GsmtapSender, GsmtapReceiver
|
||||
from .pcap import PcapWriter
|
||||
from .dump import FileDumper, format_message
|
||||
|
||||
__version__ = '1.22.18'
|
||||
__version__ = '1.22.19'
|
||||
__all__ = [
|
||||
'SniffSession',
|
||||
'DeviceDisconnected',
|
||||
|
||||
@@ -3629,6 +3629,39 @@ def _fcp_summary(response_for, fd, response):
|
||||
return label
|
||||
|
||||
|
||||
def _fcp_brief(response):
|
||||
"""Build a short "what this FCP describes" fragment for STATUS entries.
|
||||
|
||||
E.g. ``FCP of MF (DF or ADF)`` or ``FCP of EF_ICCID (Working EF,
|
||||
transparent, 10 B)``.
|
||||
"""
|
||||
fd = response.get('file_descriptor') or {}
|
||||
fid = response.get('file_id')
|
||||
name = response.get('file_id_name')
|
||||
what = name or (fid.upper() if fid else None)
|
||||
if not what and not fd:
|
||||
return None
|
||||
label = f'FCP of {what}' if what else 'FCP'
|
||||
details = []
|
||||
ft = fd.get('file_type')
|
||||
if ft:
|
||||
details.append(ft)
|
||||
if ft and ft != 'DF or ADF':
|
||||
struct = fd.get('structure')
|
||||
if struct:
|
||||
details.append(struct)
|
||||
nrec = fd.get('num_records')
|
||||
rlen = fd.get('record_length')
|
||||
fsize = response.get('file_size') or response.get('total_file_size')
|
||||
if nrec and rlen:
|
||||
details.append(f'{nrec} rec \u00d7 {rlen} B')
|
||||
elif fsize:
|
||||
details.append(f'{fsize} B')
|
||||
if details:
|
||||
label += f" ({', '.join(details)})"
|
||||
return label
|
||||
|
||||
|
||||
def _build_summary(result):
|
||||
"""Build a concise human-readable description of a decoded command.
|
||||
|
||||
@@ -3685,6 +3718,16 @@ def _build_summary(result):
|
||||
if bodytxt:
|
||||
parts.append(bodytxt)
|
||||
|
||||
if ins == 'f2' and body:
|
||||
# STATUS response data is the FCP of the current DF/EF (seen in a
|
||||
# TPDU capture as the bytes after the 5-byte command header).
|
||||
size = body.get('size') or 0
|
||||
txt = f"Data: {size} byte{'s' if size != 1 else ''}"
|
||||
brief = _fcp_brief(result.get('response') or {})
|
||||
if brief:
|
||||
txt += f' \u2014 {brief}'
|
||||
parts.append(txt)
|
||||
|
||||
cmd = result.get('cmd')
|
||||
if cmd:
|
||||
if cmd.get('context'):
|
||||
|
||||
@@ -1586,6 +1586,40 @@ class TestStatusFcp(unittest.TestCase):
|
||||
self.assertEqual(resp['df_name'], 'A0000000871002FF45FF018902011100')
|
||||
self.assertEqual(resp['file_descriptor']['file_type'], 'DF or ADF')
|
||||
self.assertEqual(resp['life_cycle'], 'operational state (activated)')
|
||||
self.assertEqual(r['summary'], 'Data: 56 bytes \u2014 FCP of 7FF0 (DF or ADF)')
|
||||
|
||||
def test_status_no_data_has_no_summary(self):
|
||||
# 80 F2 00 00 00 6C 25 — P3=0, card answers "wrong length"; no data.
|
||||
r = decode_message(bytes.fromhex('80f20000006c25'))
|
||||
self.assertEqual(r['ins_name'], 'STATUS')
|
||||
self.assertNotIn('body', r)
|
||||
self.assertNotIn('response', r)
|
||||
self.assertIsNone(r['summary'])
|
||||
|
||||
def test_status_mf_summary(self):
|
||||
raw = bytes.fromhex(
|
||||
'80f200000d'
|
||||
'620b8202782183023f008a0105'
|
||||
'9000')
|
||||
r = decode_message(raw)
|
||||
self.assertEqual(r['response']['file_id'], '3F00')
|
||||
self.assertEqual(r['response']['file_id_name'], 'MF')
|
||||
self.assertEqual(r['summary'], 'Data: 13 bytes \u2014 FCP of MF (DF or ADF)')
|
||||
|
||||
def test_status_ef_summary(self):
|
||||
raw = bytes.fromhex(
|
||||
'80f2000024'
|
||||
'62228202412183026f73a509c0018091047f206f538a01058b036f06098002000e880160'
|
||||
'9000')
|
||||
r = decode_message(raw)
|
||||
self.assertEqual(r['response']['file_id_name'], 'EF_PSLOCI')
|
||||
self.assertEqual(
|
||||
r['summary'],
|
||||
'Data: 36 bytes \u2014 FCP of EF_PSLOCI (Working EF, transparent, 14 B)')
|
||||
|
||||
def test_status_single_byte_pluralization(self):
|
||||
r = decode_message(bytes.fromhex('80f2000001009000'))
|
||||
self.assertEqual(r['summary'], 'Data: 1 byte')
|
||||
|
||||
|
||||
class TestLengthMismatch(unittest.TestCase):
|
||||
|
||||
@@ -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__, '1.22.18')
|
||||
self.assertEqual(__version__, '1.22.19')
|
||||
|
||||
def test_status_includes_version(self):
|
||||
import os
|
||||
|
||||
Reference in new issue
Block a user