Change details: no filler past a short file, no offsets for a single row (v2.7.3)
- the hex views padded every line to 16 bytes with '--', so a 2-byte file looked like it was missing data; the row now ends at the known file/record length (from the FCP — hexImageRows gained the size parameter), while '--' still marks bytes *inside* the file that were never observed (an unknown length keeps the old row padding, nothing better is available) - the offset column is shown only when a view spans more than one row — a four-hex-digit 0000 above two bytes was noise - the legend lists only the marks actually rendered: 'never observed' is gone from a short/fully-observed view, and 'newly observed' when nothing was first observed (the summary line still counts both) - tests: pure hexImageRows cases (the 2-byte file, a 9-byte record with an unobserved tail, the unknown-size fallback) plus two renderFileDiff cases pinning the missing filler/offset and the trimmed legend; 527 Python + 75 Node tests - README/README_RUS describe the new hex-view behaviour; PWA_VERSION/pyproject/version.py 2.7.3, SW cache simtrace-analyser-v83
This commit is contained in:
1 parent
bc6fcc02c2
commit
71a94f5f50
8 files changed
+121
-25
No files matched your search
@@ -219,10 +219,12 @@ card-side change, or an update that wrote or first observed bytes) also offer
|
|||||||
`ADF/EF_IMSI (6F07)`, `DF_GSM_ACCESS/EF_KcGPRS (4F52)`, `AID …/4F09` — the DF
|
`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
|
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
|
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
|
hex views (up to 16 bytes per row — a shorter file/record ends its row there
|
||||||
that changed highlighted in one colour and the bytes the operation first
|
instead of padding with `--`, and the offset column only appears when a view
|
||||||
observed in another, the changed/new byte counts, the APDU reference and the
|
spans more than one row) with the bytes that changed highlighted in one colour
|
||||||
colour legend below the views.
|
and the bytes the operation first observed in another, the changed/new byte
|
||||||
|
counts, the APDU reference and a colour legend below the views that lists only
|
||||||
|
the marks actually shown.
|
||||||
|
|
||||||
The images come from `GET /api/sessions/<id>/files/image?key=…&change=…` and
|
The images come from `GET /api/sessions/<id>/files/image?key=…&change=…` and
|
||||||
are reconstructed from the current image by undoing the later changes
|
are reconstructed from the current image by undoing the later changes
|
||||||
|
|||||||
+4
-2
@@ -224,10 +224,12 @@ Files — это таймлайн файловых операций по все
|
|||||||
`ADF/EF_IMSI (6F07)`, `DF_GSM_ACCESS/EF_KcGPRS (4F52)`, `AID …/4F09` — DF
|
`ADF/EF_IMSI (6F07)`, `DF_GSM_ACCESS/EF_KcGPRS (4F52)`, `AID …/4F09` — DF
|
||||||
называется по меткам из EF.DIR, если они есть) и затронутую единицу — файл
|
называется по меткам из EF.DIR, если они есть) и затронутую единицу — файл
|
||||||
целиком или изменившуюся запись — **до и после** изменения в виде hex-вида
|
целиком или изменившуюся запись — **до и после** изменения в виде hex-вида
|
||||||
(16 байт в строке, никогда не наблюдавшиеся байты — `--`), причём изменённые
|
(до 16 байт в строке: короткий файл/запись заканчивает строку, а не
|
||||||
|
дополняется `--`, и столбец смещений появляется, только когда представление
|
||||||
|
занимает больше одной строки), причём изменённые
|
||||||
байты подсвечены одним цветом, а впервые наблюдаемые — другим; ниже —
|
байты подсвечены одним цветом, а впервые наблюдаемые — другим; ниже —
|
||||||
счётчики изменённых/новых байт, ссылка на APDU и легенда цветов под
|
счётчики изменённых/новых байт, ссылка на APDU и легенда цветов под
|
||||||
представлениями.
|
представлениями, перечисляющая только реально показанные пометки.
|
||||||
|
|
||||||
Изображения отдаёт `GET /api/sessions/<id>/files/image?key=…&change=…`; они
|
Изображения отдаёт `GET /api/sessions/<id>/files/image?key=…&change=…`; они
|
||||||
восстанавливаются из текущего образа отменой более поздних изменений
|
восстанавливаются из текущего образа отменой более поздних изменений
|
||||||
|
|||||||
+47
-15
@@ -2171,7 +2171,7 @@ function fileOpLabel(op) {
|
|||||||
// Hex view of an image: 16 bytes per row across the observed segments, with
|
// Hex view of an image: 16 bytes per row across the observed segments, with
|
||||||
// gaps drawn as unknown cells. *ranges* (bytes that changed) and *observed*
|
// gaps drawn as unknown cells. *ranges* (bytes that changed) and *observed*
|
||||||
// (bytes first observed by this change) mark the cells.
|
// (bytes first observed by this change) mark the cells.
|
||||||
function hexImageRows(segments, ranges, observed, limit) {
|
function hexImageRows(segments, ranges, observed, limit, size) {
|
||||||
const maxBytes = limit || 2048;
|
const maxBytes = limit || 2048;
|
||||||
const bytes = new Map();
|
const bytes = new Map();
|
||||||
let end = 0;
|
let end = 0;
|
||||||
@@ -2184,6 +2184,7 @@ function hexImageRows(segments, ranges, observed, limit) {
|
|||||||
}
|
}
|
||||||
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);
|
||||||
const rows = [];
|
const rows = [];
|
||||||
|
const used = {changed: false, fresh: false, unknown: false};
|
||||||
let truncated = false;
|
let truncated = false;
|
||||||
for (let off = 0; off < end; off += 16) {
|
for (let off = 0; off < end; off += 16) {
|
||||||
if (off >= maxBytes) {
|
if (off >= maxBytes) {
|
||||||
@@ -2193,30 +2194,51 @@ function hexImageRows(segments, ranges, observed, limit) {
|
|||||||
const cells = [];
|
const cells = [];
|
||||||
for (let i = 0; i < 16; i++) {
|
for (let i = 0; i < 16; i++) {
|
||||||
const pos = off + i;
|
const pos = off + i;
|
||||||
|
// Never pad past the known file/record length: the filler would
|
||||||
|
// read as missing data when the file is simply short. '--'
|
||||||
|
// still marks bytes inside the file that were never observed.
|
||||||
|
if (size && pos >= size) 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'});
|
||||||
|
used.unknown = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const cls = covered(ranges, pos) ? 'hex-changed'
|
const cls = covered(ranges, pos) ? 'hex-changed'
|
||||||
: covered(observed, pos) ? 'hex-new' : '';
|
: covered(observed, pos) ? 'hex-new' : '';
|
||||||
|
if (cls === 'hex-changed') used.changed = true;
|
||||||
|
else if (cls === 'hex-new') used.fresh = true;
|
||||||
cells.push({hex: hex, cls: cls});
|
cells.push({hex: hex, cls: cls});
|
||||||
}
|
}
|
||||||
rows.push({offset: off, cells: cells});
|
rows.push({offset: off, cells: cells});
|
||||||
}
|
}
|
||||||
return {rows: rows, truncated: truncated, end: end};
|
return {rows: rows, truncated: truncated, end: end, used: used};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One side of the change view: the hex rows plus the marks actually used, so
|
||||||
|
// the caller can show only the legend entries that apply.
|
||||||
function hexImageHtml(image, key) {
|
function hexImageHtml(image, key) {
|
||||||
const segments = image[key] || [];
|
const segments = image[key] || [];
|
||||||
const view = hexImageRows(segments, image.ranges, image.observed_ranges);
|
const asRecord = image.record !== null && image.record !== undefined;
|
||||||
const body = view.rows.map(r =>
|
const size = asRecord ? image.record_len : image.size;
|
||||||
`<div class="hex-row"><span class="hex-addr">${r.offset.toString(16).toUpperCase().padStart(4, '0')}</span>` +
|
const view = hexImageRows(segments, image.ranges, image.observed_ranges,
|
||||||
|
undefined, size);
|
||||||
|
// The offset column only helps when the view spans more than one row.
|
||||||
|
const addr = view.rows.length > 1;
|
||||||
|
const used = view.used;
|
||||||
|
let body = view.rows.map(r =>
|
||||||
|
`<div class="hex-row">` +
|
||||||
|
(addr ? `<span class="hex-addr">${r.offset.toString(16).toUpperCase().padStart(4, '0')}</span>` : '') +
|
||||||
r.cells.map(c => `<span class="hex-cell ${c.cls}">${c.hex}</span>`).join('') +
|
r.cells.map(c => `<span class="hex-cell ${c.cls}">${c.hex}</span>`).join('') +
|
||||||
'</div>'
|
'</div>'
|
||||||
).join('') || `<div class="hex-row hex-unknown">${t('never observed')}</div>`;
|
).join('');
|
||||||
return `<div class="hex-view">${body}</div>` +
|
if (!view.rows.length) {
|
||||||
|
body = `<div class="hex-row hex-unknown">${t('never observed')}</div>`;
|
||||||
|
used.unknown = true;
|
||||||
|
}
|
||||||
|
const html = `<div class="hex-view">${body}</div>` +
|
||||||
(view.truncated ? `<div class="hex-note">${t('Showing the first {n} bytes of the image').replace('{n}', 2048)}</div>` : '');
|
(view.truncated ? `<div class="hex-note">${t('Showing the first {n} bytes of the image').replace('{n}', 2048)}</div>` : '');
|
||||||
|
return {html: html, used: used};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderFileView() {
|
async function renderFileView() {
|
||||||
@@ -2301,18 +2323,28 @@ function renderFileDiff(image) {
|
|||||||
if (image.index !== null && image.index !== undefined) meta.push(`#${image.index}`);
|
if (image.index !== null && image.index !== undefined) meta.push(`#${image.index}`);
|
||||||
if (image.elapsed !== null && image.elapsed !== undefined) meta.push(fmt_elapsed(image.elapsed));
|
if (image.elapsed !== null && image.elapsed !== undefined) meta.push(fmt_elapsed(image.elapsed));
|
||||||
el('file-diff-sub').textContent = `${image.path || image.name || ''} — ${meta.join(' · ')}`;
|
el('file-diff-sub').textContent = `${image.path || image.name || ''} — ${meta.join(' · ')}`;
|
||||||
|
const before = hexImageHtml(image, 'before');
|
||||||
|
const after = hexImageHtml(image, 'after');
|
||||||
|
const used = {
|
||||||
|
changed: before.used.changed || after.used.changed,
|
||||||
|
fresh: before.used.fresh || after.used.fresh,
|
||||||
|
unknown: before.used.unknown || after.used.unknown,
|
||||||
|
};
|
||||||
|
// Only the marks the views actually show get a legend entry (a short
|
||||||
|
// file has no '--' filler, so 'never observed' would be misleading).
|
||||||
|
const legend = [
|
||||||
|
used.changed ? `<span class="hex-legend-item"><span class="hex-cell hex-changed">aa</span> ${t('changed')}</span>` : '',
|
||||||
|
used.fresh ? `<span class="hex-legend-item"><span class="hex-cell hex-new">aa</span> ${t('newly observed')}</span>` : '',
|
||||||
|
used.unknown ? `<span class="hex-legend-item"><span class="hex-cell hex-unknown">--</span> ${t('never observed')}</span>` : '',
|
||||||
|
].join('');
|
||||||
el('file-diff-body').innerHTML =
|
el('file-diff-body').innerHTML =
|
||||||
`<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>
|
.replace('{c}', changed).replace('{n}', fresh)}</div>
|
||||||
<div class="hex-pair">
|
<div class="hex-pair">
|
||||||
<div><h4>${t('Before')}</h4>${hexImageHtml(image, 'before')}</div>
|
<div><h4>${t('Before')}</h4>${before.html}</div>
|
||||||
<div><h4>${t('After')}</h4>${hexImageHtml(image, 'after')}</div>
|
<div><h4>${t('After')}</h4>${after.html}</div>
|
||||||
</div>
|
|
||||||
<div class="hex-legend">
|
|
||||||
<span class="hex-legend-item"><span class="hex-cell hex-changed">aa</span> ${t('changed')}</span>
|
|
||||||
<span class="hex-legend-item"><span class="hex-cell hex-new">aa</span> ${t('newly observed')}</span>
|
|
||||||
<span class="hex-legend-item"><span class="hex-cell hex-unknown">--</span> ${t('never observed')}</span>
|
|
||||||
</div>` +
|
</div>` +
|
||||||
|
(legend ? `<div class="hex-legend">${legend}</div>` : '') +
|
||||||
(image.image_truncated ? `<p class="hex-note">${t('The image is frozen (size cap reached) — later changes are not tracked.')}</p>` : '');
|
(image.image_truncated ? `<p class="hex-note">${t('The image is frozen (size cap reached) — later changes are not tracked.')}</p>` : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3341,7 +3373,7 @@ function onSearchChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────── Versioning ────────────────────
|
// ──────────────────── Versioning ────────────────────
|
||||||
const PWA_VERSION = '2.7.2';
|
const PWA_VERSION = '2.7.3';
|
||||||
const MIN_SERVER_VERSION = '2.3.0';
|
const MIN_SERVER_VERSION = '2.3.0';
|
||||||
|
|
||||||
function parseVersion(v) {
|
function parseVersion(v) {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simtrace-analyser-v82';
|
const CACHE = 'simtrace-analyser-v83';
|
||||||
|
|
||||||
const PRECACHE = [
|
const PRECACHE = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
@@ -319,6 +319,24 @@ test('Files view: the hex image rows mark changed, new and unknown bytes', () =>
|
|||||||
assert.equal(view.rows[0].ascii, undefined);
|
assert.equal(view.rows[0].ascii, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Files view: the hex rows stop at the known length (no filler)', () => {
|
||||||
|
// A 2-byte file: two cells, no '--' filler that would look like data loss.
|
||||||
|
const short = hexImageRows([{offset: 0, hex: 'aabb'}], [], [], undefined, 2);
|
||||||
|
assert.equal(short.rows.length, 1);
|
||||||
|
assert.equal(short.rows[0].cells.length, 2);
|
||||||
|
assert.deepEqual(short.used, {changed: false, fresh: false, unknown: false});
|
||||||
|
// A 9-byte record with 4 bytes seen: the row ends at 9, and '--' marks the
|
||||||
|
// bytes inside the record that were never observed.
|
||||||
|
const rec = hexImageRows([{offset: 0, hex: '01020304'}], [[0, 2]], [],
|
||||||
|
undefined, 9);
|
||||||
|
assert.equal(rec.rows[0].cells.length, 9);
|
||||||
|
assert.equal(rec.rows[0].cells.filter(c => c.cls === 'hex-unknown').length, 5);
|
||||||
|
assert.deepEqual(rec.used, {changed: true, fresh: false, unknown: true});
|
||||||
|
// Without a known length the old row padding stays (nothing better to do).
|
||||||
|
const noSize = hexImageRows([{offset: 0, hex: 'aabb'}], [], [], undefined, null);
|
||||||
|
assert.equal(noSize.rows[0].cells.length, 16);
|
||||||
|
});
|
||||||
|
|
||||||
test('Files view: the hex image respects the row limit', () => {
|
test('Files view: the hex image respects the row limit', () => {
|
||||||
const view = hexImageRows([{offset: 0, hex: 'aa'.repeat(64)}], [], [], 32);
|
const view = hexImageRows([{offset: 0, hex: 'aa'.repeat(64)}], [], [], 32);
|
||||||
assert.equal(view.truncated, true);
|
assert.equal(view.truncated, true);
|
||||||
@@ -513,6 +531,48 @@ test('Change details modal: header, summary, legend and both views', () => {
|
|||||||
assert.match(el('file-diff-body').innerHTML, /image is frozen/);
|
assert.match(el('file-diff-body').innerHTML, /image is frozen/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Change details: a short file shows no filler, no offset, no stray legend', () => {
|
||||||
|
renderFileDiff({
|
||||||
|
name: 'EF_Kc', path: 'DF_GSM/EF_Kc (6F20)', op: 'UPDATE BINARY',
|
||||||
|
record: null, size: 2, record_len: null, n_records: null,
|
||||||
|
index: 3, elapsed: 1.25, ranges: [[1, 1]], observed_ranges: [],
|
||||||
|
before: [{offset: 0, hex: 'aabb'}], after: [{offset: 0, hex: 'aacc'}],
|
||||||
|
image_truncated: false,
|
||||||
|
});
|
||||||
|
const body = el('file-diff-body').innerHTML;
|
||||||
|
assert.doesNotMatch(body, /hex-unknown/); // no '--' cells at all
|
||||||
|
assert.doesNotMatch(body, /hex-addr/); // a single row: no offset column
|
||||||
|
assert.match(body, /hex-cell hex-changed/);
|
||||||
|
// The legend lists only what the views show: 'changed' (the summary line
|
||||||
|
// above still counts the zero "newly observed" bytes).
|
||||||
|
const legend = body.slice(body.indexOf('hex-legend'));
|
||||||
|
assert.equal((legend.match(/class="hex-legend-item"/g) || []).length, 1);
|
||||||
|
assert.match(legend, /changed</);
|
||||||
|
assert.doesNotMatch(legend, /newly observed|never observed/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Change details: a short record keeps its never-observed tail', () => {
|
||||||
|
// A 9-byte record with 4 bytes observed: the row ends at 9 (no filler
|
||||||
|
// past it) and '--' marks the bytes that were never observed.
|
||||||
|
renderFileDiff({
|
||||||
|
name: 'EF_IMSI', path: 'ADF/EF_IMSI (6F07)', op: 'READ RECORD',
|
||||||
|
record: 1, record_len: 9, n_records: 2, size: 18,
|
||||||
|
index: 5, elapsed: 2, ranges: [[0, 2]], observed_ranges: [],
|
||||||
|
before: [{offset: 0, hex: 'ffffffff'}],
|
||||||
|
after: [{offset: 0, hex: 'ffffffff'}],
|
||||||
|
image_truncated: false,
|
||||||
|
});
|
||||||
|
const body = el('file-diff-body').innerHTML;
|
||||||
|
assert.match(body, /hex-cell hex-changed/);
|
||||||
|
assert.match(body, /hex-cell hex-unknown">--<\/span>\s*<\/div>\s*<\/div>/);
|
||||||
|
assert.doesNotMatch(body, /hex-addr/); // one row: no offset column
|
||||||
|
const legend = body.slice(body.indexOf('hex-legend'));
|
||||||
|
assert.equal((legend.match(/class="hex-legend-item"/g) || []).length, 2);
|
||||||
|
assert.match(legend, /changed</);
|
||||||
|
assert.match(legend, /never observed/);
|
||||||
|
assert.doesNotMatch(legend, /newly observed/);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Session loading must survive "no active capture" ───────────────────
|
// ── Session loading must survive "no active capture" ───────────────────
|
||||||
eval(extractFunc(src, 'stopCaptureIfRunning'));
|
eval(extractFunc(src, 'stopCaptureIfRunning'));
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "simtrace-analyser"
|
name = "simtrace-analyser"
|
||||||
version = "2.7.2"
|
version = "2.7.3"
|
||||||
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,3 +1,3 @@
|
|||||||
"""Single source of truth for the SIMtrace Analyser version."""
|
"""Single source of truth for the SIMtrace Analyser version."""
|
||||||
|
|
||||||
__version__ = '2.7.2'
|
__version__ = '2.7.3'
|
||||||
@@ -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.2')
|
self.assertEqual(__version__, '2.7.3')
|
||||||
|
|
||||||
def test_status_includes_version(self):
|
def test_status_includes_version(self):
|
||||||
import os
|
import os
|
||||||
|
|||||||
Reference in new issue
Block a user