diff --git a/frontend/index.html b/frontend/index.html
index 209d2c7..f21df42 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1881,7 +1881,7 @@ function onFilterChange() {
}
// ──────────────────── Versioning ────────────────────
-const PWA_VERSION = '1.16.0';
+const PWA_VERSION = '1.16.1';
const MIN_SERVER_VERSION = '1.0.0';
function parseVersion(v) {
diff --git a/frontend/sw.js b/frontend/sw.js
index d5f8cd5..f6ba6c8 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simtrace-analyser-v22';
+const CACHE = 'simtrace-analyser-v23';
const PRECACHE = [
'index.html',
diff --git a/pyproject.toml b/pyproject.toml
index 2b20a1e..5c5de61 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
[project]
name = "simtrace2-pysniff"
-version = "1.16.0"
+version = "1.16.1"
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"}
diff --git a/simtrace2_pysniff/__init__.py b/simtrace2_pysniff/__init__.py
index 2682846..5b297ff 100644
--- a/simtrace2_pysniff/__init__.py
+++ b/simtrace2_pysniff/__init__.py
@@ -6,7 +6,7 @@ from .gsmtap import GsmtapSender, GsmtapReceiver
from .pcap import PcapWriter
from .dump import FileDumper, format_message
-__version__ = '1.16.0'
+__version__ = '1.16.1'
__all__ = [
'SniffSession',
'DeviceDisconnected',
diff --git a/simtrace2_pysniff/server/decode.py b/simtrace2_pysniff/server/decode.py
index 6e06b7d..98d9125 100644
--- a/simtrace2_pysniff/server/decode.py
+++ b/simtrace2_pysniff/server/decode.py
@@ -3858,13 +3858,22 @@ def decode_message(raw_data, prev=None):
result['file'] = {'sfi': rec_sfi, 'unknown': True, 'ef': f'SFI {rec_sfi}'}
elif ins in (0xB0, 0xB2, 0xD6, 0xDC) and prev and prev.get('sel'):
offset = p1p2.get('offset', 0)
- if ins in (0xB2, 0xDC) or offset == 0:
- if ins in (0xB2, 0xDC) and not _is_record_file(prev['sel']):
- result['file'] = _stale_file_note(prev['sel'])
- else:
+ if ins in (0xB2, 0xDC) and not _is_record_file(prev['sel']):
+ result['file'] = _stale_file_note(prev['sel'])
+ else:
+ file_dec = None
+ if ins in (0xB2, 0xDC) or offset == 0:
file_dec = _decode_file_data(prev['sel']['fid'], body, p1=p1)
- if file_dec:
- result['file'] = file_dec
+ if file_dec:
+ result['file'] = file_dec
+ else:
+ # Known file, no content decoder (or non-zero offset) —
+ # still attribute the operation to the selected file.
+ result['file'] = {
+ 'fid': prev['sel']['fid'],
+ 'ef': KNOWN_FIDS.get(prev['sel']['fid'],
+ prev['sel']['fid'].upper()),
+ }
# SW
if sw_bytes:
@@ -3884,6 +3893,13 @@ def decode_message(raw_data, prev=None):
file_dec = _decode_file_data(prev['sel']['fid'], remaining[:cmd_body_len])
if file_dec:
result['file'] = file_dec
+ else:
+ # Known file, no content decoder — attribute anyway.
+ fid = prev['sel']['fid']
+ result['file'] = {
+ 'fid': fid,
+ 'ef': KNOWN_FIDS.get(fid, fid.upper()),
+ }
elif prev_ins == 0xA2 and prev.get('sel') and prev.get('file_ok'):
if not _is_record_file(prev['sel']):
result['file'] = _stale_file_note(prev['sel'])
diff --git a/tests/test_decode.py b/tests/test_decode.py
index 98a5f0e..7cad17b 100644
--- a/tests/test_decode.py
+++ b/tests/test_decode.py
@@ -1143,10 +1143,13 @@ class TestFileDecoders(unittest.TestCase):
self.assertIn('IMSI 250011234567890', r['summary'])
def test_read_binary_offset_skip(self):
+ # Non-zero offset: content decode is skipped (data starts mid-file)
+ # but the operation is still attributed to the selected file.
r = decode_message(bytes.fromhex('00b00100') +
bytes.fromhex('082905102143658709') + bytes.fromhex('9000'),
prev={'sel': {'fid': '6f07'}})
- self.assertNotIn('file', r)
+ self.assertEqual(r['file'], {'fid': '6f07', 'ef': 'EF_IMSI'})
+ self.assertNotIn('imsi', r['file'])
def test_read_binary_offset_form(self):
# b8 of P1 = 0 → offset = b7..b1 << 8 | P2.
@@ -2129,6 +2132,47 @@ class TestApduSpecCoverage(unittest.TestCase):
self.assertEqual(r, 'MANAGE LSI')
+class TestFileAttribution(unittest.TestCase):
+ """Non-SFI READ/UPDATE attribute to the last file selected in the
+ channel — even when the file has no content decoder (mobileid
+ session 63 regression: UPDATE BINARY after a proactive group)."""
+
+ SEL_SMSS = {'sel': {'fid': '6f43', 'name': 'EF_SMSS', 'structure': 'transparent'}}
+
+ def _upd(self, prev):
+ raw = bytes.fromhex('00d6000004aabbccdd' + '9000')
+ return decode_message(raw, prev=prev)
+
+ def test_update_atributed_without_content_decoder(self):
+ # EF_SMSS (6F43) has no FILE_DECODERS entry — attribution must
+ # still carry the file name.
+ r = self._upd(dict(self.SEL_SMSS, ins=0xB0, ins_name='SELECT', sw1='61'))
+ self.assertEqual(r['file'], {'fid': '6f43', 'ef': 'EF_SMSS'})
+
+ def test_update_with_content_decoder_wins(self):
+ prev = dict(self.SEL_SMSS, ins=0xB0, ins_name='SELECT', sw1='61')
+ prev['sel'] = {'fid': '6f7e', 'name': 'EF_LOCI', 'structure': 'transparent'}
+ raw = bytes.fromhex('00d600000b' + '00' * 11 + '9000')
+ r = decode_message(raw, prev=prev)
+ self.assertEqual(r['file']['ef'], 'EF_LOCI')
+ self.assertIn('mcc', r['file'])
+
+ def test_update_nonzero_offset_attributed(self):
+ # Content decode skipped for non-zero offset, attribution kept.
+ raw = bytes.fromhex('00d6001004aabbccdd' + '9000')
+ r = decode_message(raw, prev=dict(self.SEL_SMSS, ins=0xB0,
+ ins_name='SELECT', sw1='61'))
+ self.assertEqual(r['file']['ef'], 'EF_SMSS')
+
+ def test_get_response_attribution(self):
+ # READ BINARY (61 XX) → GET RESPONSE on a decoder-less file
+ prev = dict(self.SEL_SMSS, ins=0xB0, ins_name='READ BINARY', sw1='61',
+ file_ok=True)
+ raw = bytes.fromhex('00c0000002' + 'cafe' + '9000')
+ r = decode_message(raw, prev=prev)
+ self.assertEqual(r['file'], {'fid': '6f43', 'ef': 'EF_SMSS'})
+
+
class TestClaDecode(unittest.TestCase):
"""ISO 7816-4 §5.1.1 first/further interindustry CLA coding."""
diff --git a/tests/test_pcap_import.py b/tests/test_pcap_import.py
index 7fae3de..d5df490 100644
--- a/tests/test_pcap_import.py
+++ b/tests/test_pcap_import.py
@@ -197,7 +197,7 @@ class TestImportEndpoint(unittest.TestCase):
class TestVersion(unittest.TestCase):
def test_package_version(self):
from simtrace2_pysniff import __version__
- self.assertEqual(__version__, '1.16.0')
+ self.assertEqual(__version__, '1.16.1')
def test_status_includes_version(self):
import os