Change details: the length cut never hides observed bytes (v2.7.5)

- a record whose FCP understates the layout (the reported case: record_len 2
  while the READ RECORD carried P3=0x2C and returned 44 bytes) had its rows
  cut to two cells by the v2.7.3 length rule, hiding 42 newly-observed bytes
  while the summary still counted them
- the filler now stops at the known length only while the data fits inside
  it; beyond that — and when the length is unknown or absent — the row ends
  at the data, so nothing observed is ever hidden and no bytes are invented
- verified on the live payload (session 55, 5f3a/4f09 change 0): before = 2
  cells, both changed; after = 16+16+12 cells with 2 changed and 42 newly
  observed, matching the summary line
- tests: the regression case (a 2-byte record_len with a 44-byte read) and
  the unknown-length case, whose row now ends at the data; 527 Python +
  76 Node tests
- PWA_VERSION/pyproject/version.py 2.7.5, SW cache simtrace-analyser-v85
This commit is contained in:
catarrh committed 2026-10-18 00:50:55 +03:00
1 parent 06c6e952a9
commit 1df52a34e5
6 files changed
+23 -8

No files matched your search

+8 -2
View File
@@ -2183,6 +2183,12 @@ function hexImageRows(segments, ranges, observed, limit, size) {
end = Math.max(end, seg.offset + Math.floor(hex.length / 2)); end = Math.max(end, seg.offset + Math.floor(hex.length / 2));
} }
const covered = (list, pos) => (list || []).some(([p, n]) => pos >= p && pos < p + n); const covered = (list, pos) => (list || []).some(([p, n]) => pos >= p && pos < p + n);
// The filler stops at the known file/record length (a short file must not
// look like it has missing data) — and never hides observed bytes: when
// the FCP understates the layout (a 2-byte record_len for a 44-byte
// record read) the data's own end is the limit. Without a known length
// the row also ends at the data instead of inventing bytes.
const stopAt = size && size >= end ? size : end;
const rows = []; const rows = [];
const used = {changed: false, fresh: false, unknown: false}; const used = {changed: false, fresh: false, unknown: false};
let truncated = false; let truncated = false;
@@ -2197,7 +2203,7 @@ function hexImageRows(segments, ranges, observed, limit, size) {
// Never pad past the known file/record length: the filler would // Never pad past the known file/record length: the filler would
// read as missing data when the file is simply short. '--' // read as missing data when the file is simply short. '--'
// still marks bytes inside the file that were never observed. // still marks bytes inside the file that were never observed.
if (size && pos >= size) break; if (stopAt && pos >= stopAt) break;
const hex = bytes.get(pos); const hex = bytes.get(pos);
if (hex === undefined) { if (hex === undefined) {
cells.push({hex: '--', cls: 'hex-unknown'}); cells.push({hex: '--', cls: 'hex-unknown'});
@@ -3380,7 +3386,7 @@ function onSearchChange() {
} }
// ──────────────────── Versioning ──────────────────── // ──────────────────── Versioning ────────────────────
const PWA_VERSION = '2.7.4'; const PWA_VERSION = '2.7.5';
const MIN_SERVER_VERSION = '2.3.0'; const MIN_SERVER_VERSION = '2.3.0';
function parseVersion(v) { function parseVersion(v) {
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simtrace-analyser-v84'; const CACHE = 'simtrace-analyser-v85';
const PRECACHE = [ const PRECACHE = [
'index.html', 'index.html',
+11 -2
View File
@@ -332,9 +332,18 @@ test('Files view: the hex rows stop at the known length (no filler)', () => {
assert.equal(rec.rows[0].cells.length, 9); assert.equal(rec.rows[0].cells.length, 9);
assert.equal(rec.rows[0].cells.filter(c => c.cls === 'hex-unknown').length, 5); assert.equal(rec.rows[0].cells.filter(c => c.cls === 'hex-unknown').length, 5);
assert.deepEqual(rec.used, {changed: true, fresh: false, unknown: true}); assert.deepEqual(rec.used, {changed: true, fresh: false, unknown: true});
// Without a known length the old row padding stays (nothing better to do). // Without a known length the row ends at the data — no invented bytes.
const noSize = hexImageRows([{offset: 0, hex: 'aabb'}], [], [], undefined, null); const noSize = hexImageRows([{offset: 0, hex: 'aabb'}], [], [], undefined, null);
assert.equal(noSize.rows[0].cells.length, 16); assert.equal(noSize.rows[0].cells.length, 2);
// A card whose FCP understates the layout (a 2-byte record_len for a
// 44-byte read) must still show every observed byte, not two per row.
const wide = hexImageRows(
[{offset: 0, hex: 'aa'.repeat(44)}], [[0, 2]], [[2, 42]], undefined, 2);
assert.deepEqual(wide.rows.map(r => r.cells.length), [16, 16, 12]);
assert.equal(wide.rows[0].cells[0].cls, 'hex-changed');
assert.equal(wide.rows[0].cells[2].cls, 'hex-new');
assert.equal(wide.used.changed, true);
assert.equal(wide.used.fresh, true);
}); });
test('Files view: the hex image respects the row limit', () => { test('Files view: the hex image respects the row limit', () => {
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
[project] [project]
name = "simtrace-analyser" name = "simtrace-analyser"
version = "2.7.4" version = "2.7.5"
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"}
+1 -1
View File
@@ -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.4' __version__ = '2.7.5'
+1 -1
View File
@@ -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.4') self.assertEqual(__version__, '2.7.5')
def test_status_includes_version(self): def test_status_includes_version(self):
import os import os