From a2937eef8b02402a0672cebb3842b6d386cdd1bb Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Wed, 15 Jul 2026 10:16:11 -0400 Subject: [PATCH 1/8] fix: skip unknown chunk types instead of aborting the whole read readChunk() threw on any chunk type it didn't recognize. A future PCM version adding a new chunk type to the save format would make this library unable to read those saves at all, even for tables/chunks it otherwise understands. Since every chunk header carries its own chunkSize, an unknown chunk can be skipped by byte count (with a warning) instead of aborting, preserving forward compatibility. Co-Authored-By: Claude Sonnet 5 --- src/reader.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/reader.ts b/src/reader.ts index 910e864..b14492b 100644 --- a/src/reader.ts +++ b/src/reader.ts @@ -248,9 +248,17 @@ export class CDBReader { break; default: - throw new Error( - `Unknown chunk type: 0x${(header.chunkType as number).toString(16)}`, - ); + { + console.warn( + `Skipping unknown chunk type: 0x${(header.chunkType as number).toString(16)} at position ${chunkStartPos}`, + ); + const skippedBytes = chunkEndPos - this.pos - 4; + result = { + type: header.chunkType, + value: this.readBytes(skippedBytes), + }; + } + break; } this.readPadding(); From 159b1ae1add409d7f4b8e2efd5a23edc975be48b Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Mon, 20 Jul 2026 21:45:57 -0400 Subject: [PATCH 2/8] fix: handle unknown chunk types gracefully without aborting --- src/reader.ts | 13 +++++++++--- test/reader.test.ts | 49 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/reader.ts b/src/reader.ts index b14492b..7370bd5 100644 --- a/src/reader.ts +++ b/src/reader.ts @@ -249,10 +249,17 @@ export class CDBReader { default: { - console.warn( - `Skipping unknown chunk type: 0x${(header.chunkType as number).toString(16)} at position ${chunkStartPos}`, - ); + if (typeof console !== "undefined") { + console.warn( + `Skipping unknown chunk type: 0x${(header.chunkType as number).toString(16)} at position ${chunkStartPos}`, + ); + } const skippedBytes = chunkEndPos - this.pos - 4; + if (skippedBytes < 0) { + throw new Error( + `Invalid chunk size for unknown chunk type 0x${(header.chunkType as number).toString(16)} at position ${chunkStartPos}`, + ); + } result = { type: header.chunkType, value: this.readBytes(skippedBytes), diff --git a/test/reader.test.ts b/test/reader.test.ts index 7f2add2..ceae126 100644 --- a/test/reader.test.ts +++ b/test/reader.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { CDBReader } from "../src/reader"; import { CHUNK_TYPE, DATA_TYPE, MAGIC } from "../src/tableMetadata"; import { CDBWriter } from "../src/writer"; @@ -59,6 +59,22 @@ function createWrapperWithMissingColumnDescription(): Uint8Array { return writer.getData(); } +function createUnknownChunkBuffer(payloadLength: number): Uint8Array { + const chunkSize = 28 + payloadLength; + const buffer = new ArrayBuffer(chunkSize); + const view = new DataView(buffer); + + view.setUint32(0, MAGIC.CHUNK_BEGIN, true); + view.setUint32(4, chunkSize, true); + view.setUint32(8, 0x99, true); // unknown chunk type + view.setUint32(12, 0, true); + view.setUint32(16, 0, true); + view.setUint32(20, MAGIC.CHUNK_SEPARATOR, true); + view.setUint32(24 + payloadLength, MAGIC.CHUNK_END, true); + + return new Uint8Array(buffer); +} + describe("CDBReader", () => { it("throws when CHUNK_BEGIN magic is invalid", () => { const reader = new CDBReader(createChunkBuffer({ chunkBegin: 0x12345678 })); @@ -85,4 +101,35 @@ describe("CDBReader", () => { "Invalid column chunk: missing column description", ); }); + + it("skips an unknown chunk type instead of throwing", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const reader = new CDBReader(createUnknownChunkBuffer(8)); + + const chunk = reader.readChunk(); + + expect(chunk.type).toBe(0x99); + expect(chunk.value).toBeInstanceOf(Uint8Array); + expect((chunk.value as Uint8Array).length).toBe(8); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("Skipping unknown chunk type: 0x99"), + ); + + warnSpy.mockRestore(); + }); + + it("throws when an unknown chunk declares an impossibly small size", () => { + const buffer = createUnknownChunkBuffer(0); + // Shrink the declared chunkSize below what's already been consumed + // by the header/separator, forcing a negative skip length. + new DataView(buffer.buffer).setUint32(4, 4, true); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const reader = new CDBReader(buffer); + + expect(() => reader.readChunk()).toThrowError( + "Invalid chunk size for unknown chunk type 0x99 at position 0", + ); + + warnSpy.mockRestore(); + }); }); From e6bcdbe110a92e897740078ef91a22e113ec32b1 Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Mon, 20 Jul 2026 21:48:17 -0400 Subject: [PATCH 3/8] fix: remove unnecessary comments in CDBReader test for unknown chunk size --- test/reader.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/reader.test.ts b/test/reader.test.ts index 3801399..63d1605 100644 --- a/test/reader.test.ts +++ b/test/reader.test.ts @@ -168,8 +168,6 @@ describe("CDBReader", () => { it("throws when an unknown chunk declares an impossibly small size", () => { const buffer = createUnknownChunkBuffer(0); - // Shrink the declared chunkSize below what's already been consumed - // by the header/separator, forcing a negative skip length. new DataView(buffer.buffer).setUint32(4, 4, true); const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const reader = new CDBReader(buffer); From 05638eac63305c8d8f885b2ae422fbe380890972 Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Mon, 20 Jul 2026 21:51:00 -0400 Subject: [PATCH 4/8] fix: refactor round-trip test for improved readability and consistency --- test/roundtrip.test.ts | 65 +++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/test/roundtrip.test.ts b/test/roundtrip.test.ts index a405023..2dc30bb 100644 --- a/test/roundtrip.test.ts +++ b/test/roundtrip.test.ts @@ -63,42 +63,43 @@ function snapshot(db: SqlDatabase): TableSnapshot[] { } describe("cdb <-> sql round-trip (no data loss)", () => { - it.each( - saveFixtures, - )("preserves all data and table flags for %s", (_label, fixturePath) => { - const original = readFileSync(fixturePath); + it.each(saveFixtures)( + "preserves all data and table flags for %s", + (_label, fixturePath) => { + const original = readFileSync(fixturePath); - // 1. cdb -> sql - const db1 = cdbToSql(original, SQL); - const before = snapshot(db1); + // 1. cdb -> sql + const db1 = cdbToSql(original, SQL); + const before = snapshot(db1); - // 2. Serialize to SQLite bytes and reopen — mirrors the CLI writing a .sqlite - // file and reading it back, dropping any in-memory-only state. - const db2 = new SQL.Database(db1.export()) as SqlDatabase; + // 2. Serialize to SQLite bytes and reopen — mirrors the CLI writing a .sqlite + // file and reading it back, dropping any in-memory-only state. + const db2 = new SQL.Database(db1.export()) as SqlDatabase; - // 3. sql -> cdb -> sql - const db3 = cdbToSql(sqlToCdb(db2), SQL); - const after = snapshot(db3); + // 3. sql -> cdb -> sql + const db3 = cdbToSql(sqlToCdb(db2), SQL); + const after = snapshot(db3); - try { - // Same tables, in the same order, with the same flags. - expect(after.map((t) => `${t.id}:${t.name}:${t.flags}`)).toEqual( - before.map((t) => `${t.id}:${t.name}:${t.flags}`), - ); - - // Same schema and the same row data, table by table. - for (let i = 0; i < before.length; i++) { - expect(after[i].columns, `columns of ${before[i].name}`).toEqual( - before[i].columns, - ); - expect(after[i].rows, `rows of ${before[i].name}`).toEqual( - before[i].rows, + try { + // Same tables, in the same order, with the same flags. + expect(after.map((t) => `${t.id}:${t.name}:${t.flags}`)).toEqual( + before.map((t) => `${t.id}:${t.name}:${t.flags}`), ); + + // Same schema and the same row data, table by table. + for (let i = 0; i < before.length; i++) { + expect(after[i].columns, `columns of ${before[i].name}`).toEqual( + before[i].columns, + ); + expect(after[i].rows, `rows of ${before[i].name}`).toEqual( + before[i].rows, + ); + } + } finally { + db1.close(); + db2.close(); + db3.close(); } - } finally { - db1.close(); - db2.close(); - db3.close(); - } - }); + }, + ); }); From bfc94d205124ed00288dc9f98f1bdebc5cf445d4 Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Mon, 20 Jul 2026 21:56:02 -0400 Subject: [PATCH 5/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- test/reader.test.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/test/reader.test.ts b/test/reader.test.ts index 63d1605..0e0a125 100644 --- a/test/reader.test.ts +++ b/test/reader.test.ts @@ -152,18 +152,20 @@ describe("CDBReader", () => { it("skips an unknown chunk type instead of throwing", () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); - const reader = new CDBReader(createUnknownChunkBuffer(8)); - - const chunk = reader.readChunk(); - - expect(chunk.type).toBe(0x99); - expect(chunk.value).toBeInstanceOf(Uint8Array); - expect((chunk.value as Uint8Array).length).toBe(8); - expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining("Skipping unknown chunk type: 0x99"), - ); - - warnSpy.mockRestore(); + try { + const reader = new CDBReader(createUnknownChunkBuffer(8)); + + const chunk = reader.readChunk(); + + expect(chunk.type).toBe(0x99); + expect(chunk.value).toBeInstanceOf(Uint8Array); + expect((chunk.value as Uint8Array).length).toBe(8); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("Skipping unknown chunk type: 0x99"), + ); + } finally { + warnSpy.mockRestore(); + } }); it("throws when an unknown chunk declares an impossibly small size", () => { From 888ba554ebb252d90b0c443b46cc263e308e4f09 Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Mon, 20 Jul 2026 21:56:14 -0400 Subject: [PATCH 6/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- test/reader.test.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/test/reader.test.ts b/test/reader.test.ts index 0e0a125..9e1322e 100644 --- a/test/reader.test.ts +++ b/test/reader.test.ts @@ -169,16 +169,18 @@ describe("CDBReader", () => { }); it("throws when an unknown chunk declares an impossibly small size", () => { - const buffer = createUnknownChunkBuffer(0); - new DataView(buffer.buffer).setUint32(4, 4, true); const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); - const reader = new CDBReader(buffer); - - expect(() => reader.readChunk()).toThrowError( - "Invalid chunk size for unknown chunk type 0x99 at position 0", - ); + try { + const buffer = createUnknownChunkBuffer(0); + new DataView(buffer.buffer).setUint32(4, 4, true); + const reader = new CDBReader(buffer); - warnSpy.mockRestore(); + expect(() => reader.readChunk()).toThrowError( + "Invalid chunk size for unknown chunk type 0x99 at position 0", + ); + } finally { + warnSpy.mockRestore(); + } }); describe("FLOAT_LIST formatting", () => { From 8f8b500e78202ce19a38aa55b94645f4b014bf94 Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Mon, 20 Jul 2026 22:02:34 -0400 Subject: [PATCH 7/8] chore: bump version to 0.2.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 082195b..02e3ba5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cdb-converter", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cdb-converter", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "dependencies": { "@types/sql.js": "^1.4.11", diff --git a/package.json b/package.json index 6ab12ac..e90a58e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cdb-converter", - "version": "0.2.0", + "version": "0.2.1", "description": "Convert Pro Cycling Manager CDB files to/from SQLite and other formats. TypeScript library with zero configuration.", "license": "MIT", "author": "mpicciolli", From 0208a52e992e2288b762b89fec1a025ad45f173f Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Mon, 20 Jul 2026 22:02:56 -0400 Subject: [PATCH 8/8] fix: bump version to 0.3.0 (minor) instead of patch --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 02e3ba5..287c145 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cdb-converter", - "version": "0.2.1", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cdb-converter", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "dependencies": { "@types/sql.js": "^1.4.11", diff --git a/package.json b/package.json index e90a58e..88898a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cdb-converter", - "version": "0.2.1", + "version": "0.3.0", "description": "Convert Pro Cycling Manager CDB files to/from SQLite and other formats. TypeScript library with zero configuration.", "license": "MIT", "author": "mpicciolli",