From 6ee1d85ce19a2511d684be8545cc95c3d7b28d70 Mon Sep 17 00:00:00 2001 From: fuleinist Date: Sat, 25 Jul 2026 11:06:09 +1000 Subject: [PATCH] fix(result_formatter): preserve full hex for checksums wider than 16 bits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ResultFormatter.checksum used ('0000' + hex).slice(-4) which truncates any checksum > 0xFFFF to its last 4 hex digits. For example, 0xAFF5C displayed as 0xFF5C and 0xDEADBEEF as 0xBEEF — silently wrong values in formatted.items that downstream consumers render to users. Replace with (value >>> 0).toString(16).padStart(4, '0') which: - Pads values < 0x1000 to 4 hex digits (preserving prior behaviour) - Preserves the full hex representation for wider checksums - Coerces to unsigned via >>> 0, avoiding negative-hex display for 32-bit CRCs whose top bit is set (companion to #458) Add result_formatter.test.ts with 6 unit tests covering 12-bit, 16-bit, 20-bit, and 32-bit checksum round-trips. Fixes #484 --- lib/utils/result_formatter.test.ts | 64 ++++++++++++++++++++++++++++++ lib/utils/result_formatter.ts | 2 +- 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 lib/utils/result_formatter.test.ts diff --git a/lib/utils/result_formatter.test.ts b/lib/utils/result_formatter.test.ts new file mode 100644 index 0000000..8944e4c --- /dev/null +++ b/lib/utils/result_formatter.test.ts @@ -0,0 +1,64 @@ +import { DecodeResult } from '../DecoderPluginInterface'; +import { ResultFormatter } from './result_formatter'; + +function makeDecodeResult(): DecodeResult { + return { + decoded: true, + decoder: { name: 'test', type: 'pattern-match', decodeLevel: 'full' }, + formatted: { description: 'Test', items: [] }, + raw: {}, + remaining: {}, + }; +} + +describe('ResultFormatter.checksum', () => { + test('formats 16-bit checksum with 4-hex-digit padding', () => { + const dr = makeDecodeResult(); + ResultFormatter.checksum(dr, 0x1234); + expect(dr.raw.checksum).toBe(0x1234); + const item = dr.formatted.items[dr.formatted.items.length - 1]; + expect(item.type).toBe('message_checksum'); + expect(item.value).toBe('0x1234'); + }); + + test('pads small values to at least 4 hex digits', () => { + const dr = makeDecodeResult(); + ResultFormatter.checksum(dr, 0x003b); + const item = dr.formatted.items[dr.formatted.items.length - 1]; + expect(item.value).toBe('0x003b'); + }); + + test('preserves full hex for 20-bit checksum (no truncation)', () => { + // 0xaff5c is 5 hex digits — old code truncated to 0xff5c + const dr = makeDecodeResult(); + ResultFormatter.checksum(dr, 0xaff5c); + expect(dr.raw.checksum).toBe(0xaff5c); + const item = dr.formatted.items[dr.formatted.items.length - 1]; + expect(item.value).toBe('0xaff5c'); + }); + + test('preserves full hex for 32-bit checksum (no truncation)', () => { + const dr = makeDecodeResult(); + ResultFormatter.checksum(dr, 0x123456); + expect(dr.raw.checksum).toBe(0x123456); + const item = dr.formatted.items[dr.formatted.items.length - 1]; + expect(item.value).toBe('0x123456'); + }); + + test('displays large 32-bit CRC without sign artifacts', () => { + // 0xDEADBEEF has the top bit set; toString(16) on a signed int would + // produce a negative string. >>> 0 coerces to unsigned first. + const dr = makeDecodeResult(); + ResultFormatter.checksum(dr, 0xdeadbeef); + expect(dr.raw.checksum).toBe(0xdeadbeef); + const item = dr.formatted.items[dr.formatted.items.length - 1]; + expect(item.value).toBe('0xdeadbeef'); + }); + + test('pads 12-bit value to 4 digits', () => { + const dr = makeDecodeResult(); + ResultFormatter.checksum(dr, 0xabc); + const item = dr.formatted.items[dr.formatted.items.length - 1]; + expect(item.value).toBe('0x0abc'); + }); +}); diff --git a/lib/utils/result_formatter.ts b/lib/utils/result_formatter.ts index d66500d..0864cf9 100644 --- a/lib/utils/result_formatter.ts +++ b/lib/utils/result_formatter.ts @@ -294,7 +294,7 @@ export class ResultFormatter { type: 'message_checksum', code: 'CHECKSUM', label: 'Message Checksum', - value: '0x' + ('0000' + decodeResult.raw.checksum.toString(16)).slice(-4), + value: '0x' + (decodeResult.raw.checksum >>> 0).toString(16).padStart(4, '0'), }); }