Detail view: P3 says Lc or Le, and SEARCH RECORD names its target (v2.7.9)
- the P3 row read "0x02 — 2 bytes" for both kinds of P3; the decode now marks it (p3_kind): a command that carries data has Lc (the command-data length), a case-2 command (APDU_SPEC 'le', e.g. READ BINARY/RECORD, GET RESPONSE, SEARCH…) has Le (the expected response length). The detail row appends "· Lc (command data length)" / "· Le (expected response length)" («Lc (длина данных команды)» / «Le (ожидаемая длина ответа)») - a SEARCH RECORD without an SFI searched the current EF (TS 102 221 §11.1.7.1) but showed no file at all; it now resolves like READ/UPDATE: the SFI's declared file when SFI-addressed (an unresolved SFI stays marked "SFI n"), otherwise the current selection — with the stale-selection note when that EF is not a record file. Verified on session 193: the 5.371 s search (P2=0x04, no SFI) now shows 4FA1, the 5.434 s one (SFI 2) shows 4F3A - tests: p3_kind cases (Le/Lc/none) and the SEARCH target cases (current EF, SFI-resolved, unknown SFI, transparent selection); a Node pin for the P3 row and the two new Russian strings; 540 Python + 77 Node tests - PWA_VERSION/pyproject/version.py 2.7.9, SW cache simtrace-analyser-v89
This commit is contained in:
1 parent
824405f936
commit
b743c6621d
8 files changed
+72
-12
No files matched your search
+4
-2
@@ -1925,7 +1925,7 @@ function showDetail(msg, entry) {
|
|||||||
<tr><td class="label">INS</td><td class="name">0x${(decoded.ins_hex || '').toUpperCase()} — ${decoded.ins_name}</td></tr>
|
<tr><td class="label">INS</td><td class="name">0x${(decoded.ins_hex || '').toUpperCase()} — ${decoded.ins_name}</td></tr>
|
||||||
${catRow}${respRow}${trRow}
|
${catRow}${respRow}${trRow}
|
||||||
${p1p2}${p1}${p2}
|
${p1p2}${p1}${p2}
|
||||||
<tr><td class="label">P3</td><td class="value">0x${hx(decoded.p3, 2)} — ${decoded.p3} bytes${lmNote}</td></tr>
|
<tr><td class="label">P3</td><td class="value">0x${hx(decoded.p3, 2)} — ${decoded.p3} bytes${decoded.p3_kind === 'le' ? ' · ' + t('Le (expected response length)') : decoded.p3_kind === 'lc' ? ' · ' + t('Lc (command data length)') : ''}${lmNote}</td></tr>
|
||||||
${body}
|
${body}
|
||||||
${cmdRows(decoded.cmd)}
|
${cmdRows(decoded.cmd)}
|
||||||
${responseRows(decoded.response)}
|
${responseRows(decoded.response)}
|
||||||
@@ -3386,7 +3386,7 @@ function onSearchChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────── Versioning ────────────────────
|
// ──────────────────── Versioning ────────────────────
|
||||||
const PWA_VERSION = '2.7.8';
|
const PWA_VERSION = '2.7.9';
|
||||||
const MIN_SERVER_VERSION = '2.3.0';
|
const MIN_SERVER_VERSION = '2.3.0';
|
||||||
|
|
||||||
function parseVersion(v) {
|
function parseVersion(v) {
|
||||||
@@ -3453,6 +3453,8 @@ const LANG_RU = {
|
|||||||
'Close': 'Закрыть',
|
'Close': 'Закрыть',
|
||||||
'Offset': 'Смещение',
|
'Offset': 'Смещение',
|
||||||
'Record': 'Запись №',
|
'Record': 'Запись №',
|
||||||
|
'Lc (command data length)': 'Lc (длина данных команды)',
|
||||||
|
'Le (expected response length)': 'Le (ожидаемая длина ответа)',
|
||||||
'Before': 'Было',
|
'Before': 'Было',
|
||||||
'After': 'Стало',
|
'After': 'Стало',
|
||||||
'Show the change': 'Показать изменение',
|
'Show the change': 'Показать изменение',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simtrace-analyser-v88';
|
const CACHE = 'simtrace-analyser-v89';
|
||||||
|
|
||||||
const PRECACHE = [
|
const PRECACHE = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ test('no per-element scrollbar overrides or thin default remain', () => {
|
|||||||
assert.doesNotMatch(src, /::-webkit-scrollbar \{ width: 6px/);
|
assert.doesNotMatch(src, /::-webkit-scrollbar \{ width: 6px/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the P3 row labels Lc and Le', () => {
|
||||||
|
assert.match(src, /decoded\.p3_kind === 'le' \? ' · ' \+ t\('Le \(expected response length\)'\)/);
|
||||||
|
assert.match(src, /t\('Lc \(command data length\)'\)/);
|
||||||
|
});
|
||||||
|
|
||||||
// ── trace topbar layout ────────────────────────────────────────────────
|
// ── trace topbar layout ────────────────────────────────────────────────
|
||||||
|
|
||||||
test('the view selector sits over the side panel at the panel width', () => {
|
test('the view selector sits over the side panel at the panel width', () => {
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "simtrace-analyser"
|
name = "simtrace-analyser"
|
||||||
version = "2.7.8"
|
version = "2.7.9"
|
||||||
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"}
|
||||||
|
|||||||
@@ -3942,6 +3942,14 @@ def decode_message(raw_data, prev=None):
|
|||||||
elif cmd_body_len < p3 and not (spec and spec.get('le')):
|
elif cmd_body_len < p3 and not (spec and spec.get('le')):
|
||||||
result['length_mismatch'] = {'kind': 'truncated', 'expected': p3, 'actual': cmd_body_len}
|
result['length_mismatch'] = {'kind': 'truncated', 'expected': p3, 'actual': cmd_body_len}
|
||||||
|
|
||||||
|
# P3 is the length of the command data (Lc) when the command carries data
|
||||||
|
# and the expected response length (Le) for a case-2 command — the two are
|
||||||
|
# otherwise easy to confuse in the detailed view.
|
||||||
|
if spec and spec.get('le'):
|
||||||
|
result['p3_kind'] = 'le'
|
||||||
|
elif cmd_body_len > 0:
|
||||||
|
result['p3_kind'] = 'lc'
|
||||||
|
|
||||||
if cmd_body_len > 0:
|
if cmd_body_len > 0:
|
||||||
body = remaining[:cmd_body_len]
|
body = remaining[:cmd_body_len]
|
||||||
result['body'] = {'hex': body.hex(), 'size': cmd_body_len}
|
result['body'] = {'hex': body.hex(), 'size': cmd_body_len}
|
||||||
@@ -4020,14 +4028,23 @@ def decode_message(raw_data, prev=None):
|
|||||||
'raw': body.hex().upper()}
|
'raw': body.hex().upper()}
|
||||||
else:
|
else:
|
||||||
result['file'] = {'sfi': rec_sfi, 'unknown': True, 'ef': f'SFI {rec_sfi}'}
|
result['file'] = {'sfi': rec_sfi, 'unknown': True, 'ef': f'SFI {rec_sfi}'}
|
||||||
elif ins == 0xA2 and prev and rec_sfi:
|
elif ins == 0xA2 and prev and (rec_sfi or prev.get('sel')):
|
||||||
# SEARCH RECORD with SFI → attach the target file name (the body
|
# SEARCH RECORD: the body is a search pattern, not file data. An
|
||||||
# is a search pattern, not file data).
|
# SFI addresses the file it names; without one the search runs in
|
||||||
fid = (prev.get('sfi_map') or {}).get(rec_sfi)
|
# the current EF (TS 102 221 §11.1.7.1) — show the target like
|
||||||
|
# READ/UPDATE do.
|
||||||
|
sel = prev.get('sel') or {}
|
||||||
|
fid = (prev.get('sfi_map') or {}).get(rec_sfi) if rec_sfi \
|
||||||
|
else sel.get('fid')
|
||||||
if fid:
|
if fid:
|
||||||
result['file'] = {'fid': fid, 'ef': _file_name(fid, prev.get('df'))}
|
if not rec_sfi and not _is_record_file(sel):
|
||||||
|
result['file'] = _stale_file_note(sel)
|
||||||
else:
|
else:
|
||||||
result['file'] = {'sfi': rec_sfi, 'unknown': True, 'ef': f'SFI {rec_sfi}'}
|
result['file'] = {'fid': fid,
|
||||||
|
'ef': _file_name(fid, prev.get('df'))}
|
||||||
|
elif rec_sfi:
|
||||||
|
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'):
|
elif ins in (0xB0, 0xB2, 0xD6, 0xDC) and prev and prev.get('sel'):
|
||||||
offset = p1p2.get('offset', 0)
|
offset = p1p2.get('offset', 0)
|
||||||
if ins in (0xB2, 0xDC) and not _is_record_file(prev['sel']):
|
if ins in (0xB2, 0xDC) and not _is_record_file(prev['sel']):
|
||||||
|
|||||||
@@ -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.8'
|
__version__ = '2.7.9'
|
||||||
@@ -1967,6 +1967,42 @@ class TestP1P2(unittest.TestCase):
|
|||||||
r = decode_message(bytes.fromhex('0020008108' + '12' * 8))
|
r = decode_message(bytes.fromhex('0020008108' + '12' * 8))
|
||||||
self.assertEqual(r['p2']['name'], 'Second PIN Appl 1')
|
self.assertEqual(r['p2']['name'], 'Second PIN Appl 1')
|
||||||
|
|
||||||
|
def test_p3_kind_lc_le(self):
|
||||||
|
# P3 is the command-data length (Lc) when the command carries data and
|
||||||
|
# the expected response length (Le) for a case-2 command.
|
||||||
|
from simtrace_analyser.decode import decode_message
|
||||||
|
self.assertEqual(decode_message(bytes.fromhex('00b000000a'))['p3_kind'],
|
||||||
|
'le') # READ BINARY: Le
|
||||||
|
self.assertEqual(decode_message(
|
||||||
|
bytes.fromhex('00a40000023f00'))['p3_kind'], 'lc') # SELECT: Lc
|
||||||
|
self.assertEqual(decode_message(
|
||||||
|
bytes.fromhex('00a201040200006164'))['p3_kind'], 'lc') # pattern
|
||||||
|
# A zero-length SELECT carries neither (no data, no Le).
|
||||||
|
self.assertNotIn('p3_kind',
|
||||||
|
decode_message(bytes.fromhex('00a4000000')))
|
||||||
|
|
||||||
|
def test_search_record_target_file(self):
|
||||||
|
# A SEARCH RECORD without an SFI searches the current EF
|
||||||
|
# (TS 102 221 §11.1.7.1) — show the target like READ RECORD does.
|
||||||
|
from simtrace_analyser.decode import decode_message
|
||||||
|
prev = {'sel': {'fid': '4fa1', 'structure': 'linear fixed'},
|
||||||
|
'df': '5f3a'}
|
||||||
|
r = decode_message(bytes.fromhex('00a201040200006164'), prev=prev)
|
||||||
|
self.assertEqual(r['file'], {'fid': '4fa1', 'ef': '4FA1'})
|
||||||
|
# An SFI-addressed search resolves through the SFI map…
|
||||||
|
prev['sfi_map'] = {2: '4f3a'}
|
||||||
|
r = decode_message(bytes.fromhex('00a201142c' + 'ff' * 44), prev=prev)
|
||||||
|
self.assertEqual(r['file'], {'fid': '4f3a', 'ef': '4F3A'})
|
||||||
|
# …and an unknown SFI stays marked as such.
|
||||||
|
prev['sfi_map'] = {}
|
||||||
|
r = decode_message(bytes.fromhex('00a201142c' + 'ff' * 44), prev=prev)
|
||||||
|
self.assertTrue(r['file']['unknown'])
|
||||||
|
self.assertEqual(r['file']['sfi'], 2)
|
||||||
|
# A record search on a transparent selection is a stale-selection note.
|
||||||
|
prev2 = {'sel': {'fid': '2fe2', 'structure': 'transparent'}, 'df': '3f00'}
|
||||||
|
r = decode_message(bytes.fromhex('00a201040200006164'), prev=prev2)
|
||||||
|
self.assertTrue(r['file'].get('stale'))
|
||||||
|
|
||||||
def test_manage_channel_p2(self):
|
def test_manage_channel_p2(self):
|
||||||
r = decode_message(bytes.fromhex('0070000300'))
|
r = decode_message(bytes.fromhex('0070000300'))
|
||||||
self.assertEqual(r['p1']['name'], 'Open channel')
|
self.assertEqual(r['p1']['name'], 'Open channel')
|
||||||
|
|||||||
@@ -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.8')
|
self.assertEqual(__version__, '2.7.9')
|
||||||
|
|
||||||
def test_status_includes_version(self):
|
def test_status_includes_version(self):
|
||||||
import os
|
import os
|
||||||
|
|||||||
Reference in new issue
Block a user