Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e4d828f
test(archive-codec): expose md5's 64-bit bit-length split for direct …
Mearman Sep 12, 2026
25f4dcd
refactor(archive-codec): drop startsWithMagic's redundant length pre-…
Mearman Sep 12, 2026
5c028c0
refactor(archive-codec): use indexOf and DataView writes in the OLE P…
Mearman Sep 12, 2026
df7bf50
refactor(archive-codec): write RC4 password bytes through a DataView
Mearman Sep 12, 2026
3fe2ef2
refactor(archive-codec): drop bytesEqual's dead length check in RC4 C…
Mearman Sep 12, 2026
8099b32
refactor(archive-codec): write XOR obfuscation bytes through DataViews
Mearman Sep 12, 2026
2d9dbd0
test(archive-codec): give sha1's million-repetition vector a generous…
Mearman Sep 12, 2026
9b5c30d
refactor(archive-codec): simplify the OLEPS property-set writer's byt…
Mearman Sep 12, 2026
b98a432
test(archive-codec): drop endian-invariant writes and add direct OLEP…
Mearman Sep 12, 2026
71d1f20
test(archive-codec): read zip test-support integers through DataView
Mearman Sep 12, 2026
f3ad36a
refactor(archive-codec): drop equivalent zero-padding writes in the O…
Mearman Sep 12, 2026
8adfebd
fix(archive-codec): stop writeGuid's Data4 byte extraction discarding…
Mearman Sep 12, 2026
a44374f
fix(archive-codec): stop asciiZeroTerminated's loop bound hiding an e…
Mearman Sep 12, 2026
888f889
test(archive-codec): make md5's 64-bit length write's high half direc…
Mearman Sep 12, 2026
fe85dad
test(archive-codec): prove localHeaderCompressionMethod stops at a si…
Mearman Sep 12, 2026
cb063a4
test(archive-codec): fix walkArchive's ancestor-chain order assertion
Mearman Sep 12, 2026
f8696e3
test(archive-codec): assert exact SummaryInformation error messages a…
Mearman Sep 12, 2026
253a804
refactor(archive-codec): remove requireBytes' dead negative-offset gu…
Mearman Sep 12, 2026
c574468
test(archive-codec): assert exact requireBytes boundary messages thro…
Mearman Sep 12, 2026
23e11d6
refactor(archive-codec): remove write.ts's dead surrogate case-mappin…
Mearman Sep 12, 2026
4c721c9
test(archive-codec): cover writeCompoundFile's sector-arithmetic and …
Mearman Sep 12, 2026
a24109f
refactor(archive-codec): drop remaining equivalent zero-padding write…
Mearman Sep 12, 2026
336b080
refactor(archive-codec): remove test-support/cfb.ts's redundant array…
Mearman Sep 12, 2026
067fd0c
test(archive-codec): add multi-sector and validation coverage for rea…
Mearman Sep 12, 2026
c714fc0
refactor(archive-codec): remove fatEntry's dead negative-offset guard
Mearman Sep 12, 2026
3fbe553
test(archive-codec): assert exact messages across readCompoundFile's …
Mearman Sep 12, 2026
0860f29
refactor(archive-codec): remove compoundFile's redundant mini-FAT zer…
Mearman Sep 13, 2026
e054bca
refactor(archive-codec): detect FAT and mini-FAT cycles by visited-se…
Mearman Sep 13, 2026
7b34ac0
test(archive-codec): assert exact writeCompoundFile error messages an…
Mearman Sep 13, 2026
06f475e
test(archive-codec): add direct coverage for the compoundFile test-fi…
Mearman Sep 13, 2026
7c1d58a
refactor(archive-codec): link compoundFile's directory siblings from …
Mearman Sep 13, 2026
17f60e7
test(archive-codec): give the multi-FAT-sector fixture a generous tim…
Mearman Sep 13, 2026
89eb512
test(archive-codec): move the DIFAT fixture into beforeAll for correc…
Mearman Sep 13, 2026
667b4c9
fix(archive-codec): build the DIFAT fixture inside each test, not a s…
Mearman Sep 13, 2026
33d8c32
refactor(archive-codec): store each directory record's own child link…
Mearman Sep 13, 2026
f95b9b5
refactor(archive-codec): drop the writer's redundant DIFAT-count conv…
Mearman Sep 13, 2026
0b998c8
refactor(archive-codec): iterate a DIFAT sector's own entries by boun…
Mearman Sep 13, 2026
65b0a0e
test(archive-codec): build the header/sector-layout fixture per test,…
Mearman Sep 13, 2026
97eb40f
test(archive-codec): assert DIFAT tail padding and unallocated size f…
Mearman Sep 13, 2026
147cf7c
test(archive-codec): raise the mutation break threshold to 100
Mearman Sep 13, 2026
5bb6f7a
Merge branch 'main' into feat/100-percent-mutation-archive-codec
Mearman Sep 13, 2026
9a3ac90
Merge branch 'main' into feat/100-percent-mutation-archive-codec
Mearman Sep 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 81 additions & 11 deletions packages/archive-codec/src/cfb/ole-package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,19 +79,58 @@ describe("readOlePackage", () => {

it("throws OlePackageFormatError when a string never terminates", () => {
const unterminated = enc("\x02\x00Book1.xlsx");
let caught: unknown;
try {
readOlePackage(unterminated);
throw new Error("expected readOlePackage to throw OlePackageFormatError");
} catch (error) {
expect(error).toBeInstanceOf(OlePackageFormatError);
caught = error;
}
expect(caught).toBeInstanceOf(OlePackageFormatError);
expect((caught as Error).name).toBe("OlePackageFormatError");
expect((caught as Error).message).toBe(
"Package stream ends inside its label string with no terminator",
);
});

it("names the field that never terminates, when it is the source path rather than the label", () => {
const unterminated = enc("\x02\x00a\0Book1.xlsx");
expect(() => readOlePackage(unterminated)).toThrow(
"Package stream ends inside its source path string with no terminator",
);
});

it("names the field that never terminates, when it is the temp path rather than the label or source path", () => {
const unterminated = new Uint8Array([
...enc("\x02\x00a\0b\0"),
...new Uint8Array(8), // the 8 opaque bytes between sourcePath and tempPath
...enc("Book1.xlsx"),
]);
expect(() => readOlePackage(unterminated)).toThrow(
"Package stream ends inside its temp path string with no terminator",
);
});

it("throws OlePackageFormatError when fewer than 4 bytes remain for the packaged file's own size field", () => {
// The three strings and the 8 opaque bytes are all present and well-formed; only the trailing size field itself is truncated.
const bytes = new Uint8Array([
...enc("\x02\x00a\0b\0"),
...new Uint8Array(8), // the 8 opaque bytes
...enc("c\0"),
0x00,
0x00,
]); // 2 bytes where the 4-byte size field belongs
expect(() => readOlePackage(bytes)).toThrow(
"Package stream ends before its packaged file size field",
);
});

it("throws OlePackageFormatError when the declared file size exceeds the remaining bytes", () => {
const wrapped = packageStream("a", "b", "c", enc("payload"));
const view = new DataView(wrapped.buffer);
view.setUint32(wrapped.length - "payload".length - 4, 0x00ffffff, true);
expect(() => readOlePackage(wrapped)).toThrow(OlePackageFormatError);
expect(() => readOlePackage(wrapped)).toThrow(
"Package stream declares 16777215 packaged-file bytes but holds only 7",
);
});

it("throws OlePackageFormatError for input too short to hold even the fixed fields", () => {
Expand All @@ -102,6 +141,16 @@ describe("readOlePackage", () => {
});

describe("writeOlePackage", () => {
it("writes the header word as 0x0002 in little-endian order", () => {
const built = writeOlePackage({
label: "",
sourcePath: "",
tempPath: "",
fileBytes: new Uint8Array(0),
});
expect([...built.subarray(0, 2)]).toEqual([0x02, 0x00]);
});

it("round-trips through readOlePackage", () => {
const fileBytes = enc("the real embedded file");
const built = writeOlePackage({
Expand Down Expand Up @@ -133,6 +182,17 @@ describe("writeOlePackage", () => {
expect(parsed.fileBytes).toEqual(new Uint8Array(0));
});

it("accepts U+007F (DEL), the highest code point this field's ASCII check allows", () => {
expect(() =>
writeOlePackage({
label: "a\u007fb",
sourcePath: "",
tempPath: "",
fileBytes: new Uint8Array(0),
}),
).not.toThrow();
});

it("throws OlePackageWriteError when label contains a non-ASCII character", () => {
expect(() =>
writeOlePackage({
Expand All @@ -141,7 +201,9 @@ describe("writeOlePackage", () => {
tempPath: "",
fileBytes: new Uint8Array(0),
}),
).toThrow(OlePackageWriteError);
).toThrow(
"Package stream's label contains a character (U+00e9) outside ASCII; encoding it to an arbitrary windows-1252 byte would need a full codepage table this package does not carry",
);
});

it("throws OlePackageWriteError when sourcePath or tempPath contains a non-ASCII character", () => {
Expand All @@ -152,27 +214,35 @@ describe("writeOlePackage", () => {
tempPath: "",
fileBytes: new Uint8Array(0),
}),
).toThrow(OlePackageWriteError);
).toThrow("Package stream's source path contains a character");
expect(() =>
writeOlePackage({
label: "a",
sourcePath: "",
tempPath: "C:\\café\\a.docx",
fileBytes: new Uint8Array(0),
}),
).toThrow(OlePackageWriteError);
).toThrow("Package stream's temp path contains a character");
});

// A NUL byte is itself ASCII (U+0000, well under 0x7f), so the non-ASCII check above cannot catch it -- but this field's own encoding is null-terminated, so an embedded NUL would silently truncate the field and mis-frame every field written after it, exactly the round-trip guarantee this function's own doc comment states.
it("throws OlePackageWriteError when label contains an embedded NUL byte", () => {
expect(() =>
let caught: unknown;
try {
writeOlePackage({
label: "a\u0000b",
sourcePath: "",
tempPath: "",
fileBytes: new Uint8Array(0),
}),
).toThrow(OlePackageWriteError);
});
} catch (error) {
caught = error;
}
expect(caught).toBeInstanceOf(OlePackageWriteError);
expect((caught as Error).name).toBe("OlePackageWriteError");
expect((caught as Error).message).toBe(
"Package stream's label contains an embedded NUL byte, which this field's own null-terminated encoding cannot carry: it would silently truncate the field and mis-frame every field written after it",
);
});

it("throws OlePackageWriteError when sourcePath or tempPath contains an embedded NUL byte", () => {
Expand All @@ -183,14 +253,14 @@ describe("writeOlePackage", () => {
tempPath: "",
fileBytes: new Uint8Array(0),
}),
).toThrow(OlePackageWriteError);
).toThrow("Package stream's source path contains an embedded NUL byte");
expect(() =>
writeOlePackage({
label: "a",
sourcePath: "",
tempPath: "C:\\a\u0000b.docx",
fileBytes: new Uint8Array(0),
}),
).toThrow(OlePackageWriteError);
).toThrow("Package stream's temp path contains an embedded NUL byte");
});
});
18 changes: 9 additions & 9 deletions packages/archive-codec/src/cfb/ole-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,9 @@ function readZeroTerminated(
offset: number,
fieldName: string,
): { readonly value: string; readonly next: number } {
let end = offset;
while (end < bytes.length && bytes[end] !== 0) {
end++;
}
if (end >= bytes.length) {
// indexOf, not a hand-rolled scanning loop with its own bounds check: it already reports "not found" as a single -1 sentinel, so there is exactly one place (below) that decides whether the terminator was found, not two redundant bounds checks that could disagree.
const end = bytes.indexOf(0, offset);
if (end === -1) {
throw new OlePackageFormatError(
`Package stream ends inside its ${fieldName} string with no terminator`,
);
Expand Down Expand Up @@ -85,8 +83,10 @@ function asciiZeroTerminated(
fieldName: string,
): Uint8Array<ArrayBuffer> {
const bytes = new Uint8Array(value.length + 1); // +1 for the terminator, already zero from the Uint8Array's own zero-fill
for (let index = 0; index < value.length; index++) {
const code = value.charCodeAt(index);
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
// Walks value.split("") rather than a `for` loop bound by value.length: the allocation's own reserved terminator byte sits right after the last character, so a loop bound one iteration too long would write its extra byte there -- a genuinely equivalent mutant, since that byte is already zero and no test could ever observe the difference. split("") has no comparison bound to mismeasure in the first place.
value.split("").forEach((char, index) => {
const code = char.charCodeAt(0);
if (code === 0) {
throw new OlePackageWriteError(
`Package stream's ${fieldName} contains an embedded NUL byte, which this field's own null-terminated encoding cannot carry: it would silently truncate the field and mis-frame every field written after it`,
Expand All @@ -97,8 +97,8 @@ function asciiZeroTerminated(
`Package stream's ${fieldName} contains a character (U+${code.toString(16).padStart(4, "0")}) outside ASCII; encoding it to an arbitrary windows-1252 byte would need a full codepage table this package does not carry`,
);
}
bytes[index] = code;
}
view.setUint8(index, code);
});
return bytes;
}

Expand Down
Loading