Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
64c071a
test(wpd-codec): cover previously untested leaf modules directly
Mearman Sep 11, 2026
653faea
test(wpd-codec): close mutation gaps in the container package
Mearman Sep 11, 2026
7d396f3
test(wpd-codec): close mutation gaps in stream/characters, ole, style…
Mearman Sep 11, 2026
0bb16d7
fix(wpd-codec): type compoundFileWithStream's return as ArrayBuffer-b…
Mearman Sep 11, 2026
5262f78
fix(wpd-codec): type genericHeaderBytes' return as ArrayBuffer-backed
Mearman Sep 11, 2026
7dad5d2
test(wpd-codec): close mutation gaps in stream/box
Mearman Sep 12, 2026
2745598
test(wpd-codec): cover formula.ts's untested token codes and image.ts…
Mearman Sep 12, 2026
d1e2adb
test(wpd-codec): fix mutants that survived because frame requires bot…
Mearman Sep 12, 2026
79474e7
test(wpd-codec): cover wpg/formula/image boundary and dispatch branches
Mearman Sep 12, 2026
387ef6d
test(wpd-codec): mark proven-equivalent boundary mutants in image/com…
Mearman Sep 12, 2026
e805d99
test(wpd-codec): mark wpg.ts's characterization-boundary ties as equi…
Mearman Sep 12, 2026
85a08ec
test(wpd-codec): cover read.ts's attribute, note, style, and furnitur…
Mearman Sep 12, 2026
160a9e1
fix(wpd-codec): drop an unused IIFE return field that broke the works…
Mearman Sep 12, 2026
8bc3d8f
refactor(wpd-codec): remove equivalent-mutant patterns from compound-…
Mearman Sep 12, 2026
3f2e68e
refactor(wpd-codec): remove equivalent-mutant boundary checks from im…
Mearman Sep 12, 2026
cfd58c7
refactor(wpd-codec): remove equivalent-mutant boundary checks from wp…
Mearman Sep 12, 2026
383f932
test(wpd-codec): cover the box-embedded WPG graphic lift, previously …
Mearman Sep 12, 2026
0aa4de2
refactor(wpd-codec): remove equivalent-mutant patterns from formula.ts
Mearman Sep 12, 2026
35bea6d
test(wpd-codec): confirm readWpd's tree section omits absent furnitur…
Mearman Sep 12, 2026
37bfb5c
style(wpd-codec): apply formatting to table.ts
Mearman Sep 12, 2026
97d9f9d
refactor(wpd-codec): drop box.ts's redundant position-override room g…
Mearman Sep 12, 2026
6ca9732
refactor(wpd-codec): rely on the read-time throw for style.ts's PID-c…
Mearman Sep 12, 2026
40c4541
refactor(wpd-codec): remove prefix.ts's redundant text-block and type…
Mearman Sep 12, 2026
9a5c878
refactor(wpd-codec): rely on the read-time throw for summary.ts's gro…
Mearman Sep 12, 2026
8998380
refactor(wpd-codec): compute documentAreaEnd's upper bound with Math.min
Mearman Sep 12, 2026
4985e8e
refactor(wpd-codec): extract passwordByteAt with its own dedicated test
Mearman Sep 12, 2026
4232dd2
refactor(wpd-codec): add UNBOUNDED_WORDS for ole.ts's unbounded word-…
Mearman Sep 12, 2026
f47d001
test(wpd-codec): prove nearestPercentType's out-of-range throw is real
Mearman Sep 12, 2026
a4aa926
refactor(wpd-codec): stop writing an unread name for compound-file.ts…
Mearman Sep 12, 2026
a8c4f9f
refactor(wpd-codec): make image.ts's signature scan throw on out-of-r…
Mearman Sep 12, 2026
aebfd56
refactor(wpd-codec): close wpg.ts's remaining mutation gaps
Mearman Sep 12, 2026
0c8d7ae
test(wpd-codec): close a first batch of read.ts mutation gaps
Mearman Sep 12, 2026
58b1659
fix(wpd-codec): strip a table cell formula's own length-word framing
Mearman Sep 12, 2026
5b5c8c7
test(wpd-codec): cover the beginning-of-file EOL mapping and five sin…
Mearman Sep 12, 2026
5139be0
test(wpd-codec): close read.ts style, table, page-geometry, and font …
Mearman Sep 12, 2026
c9d03e7
refactor(wpd-codec): simplify pendingListLevel's guard and add depth-…
Mearman Sep 12, 2026
a965be7
test(wpd-codec): close note-marker, merge-field, and plainTextOf gaps
Mearman Sep 12, 2026
ccd5f18
test(wpd-codec): close WPG box-embedding mutation gaps
Mearman Sep 12, 2026
73a62a2
test(wpd-codec): close box content-type and frame-resolution gaps
Mearman Sep 12, 2026
5a67af1
refactor(wpd-codec): make the character-decode fallback a real assertion
Mearman Sep 12, 2026
2d67d02
test(wpd-codec): close readWpd, summary, and table end-of-stream gaps
Mearman Sep 12, 2026
f0f8258
test(wpd-codec): close font-size, colour, and character-group gaps
Mearman Sep 12, 2026
c2631aa
refactor(wpd-codec): remove three redundant final-case returns in rea…
Mearman Sep 12, 2026
8e4f091
test(wpd-codec): prove the attribute-code gate against a live attribute
Mearman Sep 12, 2026
ffc6213
Merge branch 'main' into feat/100-percent-mutation-wpd-codec
Mearman Sep 12, 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
37 changes: 37 additions & 0 deletions packages/wpd-codec/src/bytes/base64.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import { bytesToBase64 } from "./base64";

// Direct unit coverage for the RFC 4648 base64 encoder, isolated from the image/OLE integration tests that only ever exercise it indirectly through a real embedded payload.
describe("bytesToBase64", () => {
it("encodes an empty buffer as an empty string", () => {
expect(bytesToBase64(new Uint8Array())).toBe("");
});

it("encodes a length divisible by three with no padding", () => {
// "Man" -> "TWFu", the canonical RFC 4648 example.
expect(bytesToBase64(new Uint8Array([0x4d, 0x61, 0x6e]))).toBe("TWFu");
});

it("encodes exactly one trailing byte with two padding characters", () => {
// "M" -> "TQ==".
expect(bytesToBase64(new Uint8Array([0x4d]))).toBe("TQ==");
});

it("encodes exactly two trailing bytes with one padding character", () => {
// "Ma" -> "TWE=".
expect(bytesToBase64(new Uint8Array([0x4d, 0x61]))).toBe("TWE=");
});

it("uses every character of the alphabet across its full input range", () => {
// 0x00 through 0xff, 256 bytes: exercises b0/b1/b2 across every 6-bit slice value at least once, so a truncated or wrong alphabet index cannot go unnoticed the way a single short input would.
const bytes = new Uint8Array(256);
for (let i = 0; i < bytes.length; i += 1) {
bytes[i] = i;
}
const encoded = bytesToBase64(bytes);
expect(encoded).toHaveLength(344);
// Cross-check against the platform's own base64 decoder rather than a second hand-rolled implementation.
const decoded = Uint8Array.from(atob(encoded), (c) => c.charCodeAt(0));
expect(decoded).toEqual(bytes);
});
});
78 changes: 78 additions & 0 deletions packages/wpd-codec/src/bytes/view.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, expect, it } from "vitest";
import { WpdFormatError } from "../errors";
import { byteAt, int16At, sliceAt, uint16At, uint32At } from "./view";

describe("byteAt", () => {
it("reads the byte at the given offset", () => {
expect(byteAt(new Uint8Array([0x12, 0x34]), 1)).toBe(0x34);
});

it("throws, naming the offset and file length, past the end of the buffer", () => {
expect(() => byteAt(new Uint8Array([0x12]), 1)).toThrow(WpdFormatError);
expect(() => byteAt(new Uint8Array([0x12]), 1)).toThrow(
"Byte read at offset 1 is past the end of a 1-byte file.",
);
});
});

describe("uint16At / uint32At", () => {
it("reads a little-endian 16-bit value", () => {
expect(uint16At(new Uint8Array([0x34, 0x12]), 0)).toBe(0x1234);
});

it("reads a little-endian 32-bit value without sign-extending a high bit", () => {
expect(uint32At(new Uint8Array([0x00, 0x00, 0x00, 0x80]), 0)).toBe(
0x80000000,
);
});
});

describe("int16At", () => {
it("reads the largest positive value, 0x7fff, without reinterpreting it", () => {
expect(int16At(new Uint8Array([0xff, 0x7f]), 0)).toBe(0x7fff);
});

it("reinterprets 0x8000, the smallest value whose sign bit is set, as negative", () => {
expect(int16At(new Uint8Array([0x00, 0x80]), 0)).toBe(-0x8000);
});

it("reinterprets 0xffff as -1", () => {
expect(int16At(new Uint8Array([0xff, 0xff]), 0)).toBe(-1);
});
});

describe("sliceAt", () => {
it("returns a view onto the same buffer, not a copy", () => {
const bytes = new Uint8Array([1, 2, 3, 4]);
const slice = sliceAt(bytes, 1, 2);
expect(slice).toEqual(new Uint8Array([2, 3]));
bytes[1] = 9;
expect(slice[0]).toBe(9);
});

it("accepts a slice that exactly reaches the end of the buffer", () => {
const bytes = new Uint8Array([1, 2, 3, 4]);
expect(sliceAt(bytes, 2, 2)).toEqual(new Uint8Array([3, 4]));
});

it("rejects a negative offset", () => {
const bytes = new Uint8Array([1, 2, 3]);
expect(() => sliceAt(bytes, -1, 1)).toThrow(WpdFormatError);
expect(() => sliceAt(bytes, -1, 1)).toThrow(
"A 1-byte read at offset -1 does not fit inside a 3-byte file.",
);
});

it("rejects a negative length", () => {
const bytes = new Uint8Array([1, 2, 3]);
expect(() => sliceAt(bytes, 0, -1)).toThrow(WpdFormatError);
});

it("rejects a length that runs one byte past the end of the buffer", () => {
const bytes = new Uint8Array([1, 2, 3]);
expect(() => sliceAt(bytes, 2, 2)).toThrow(WpdFormatError);
expect(() => sliceAt(bytes, 2, 2)).toThrow(
"A 2-byte read at offset 2 does not fit inside a 3-byte file.",
);
});
});
39 changes: 39 additions & 0 deletions packages/wpd-codec/src/codec.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { WpdBytesSchema } from "./codec";

// Direct coverage of hasWordPerfectOrCompoundHeader's two independent every()-over-a-magic-byte-array checks, neither of which any other test in this package exercises: read.test.ts and container.test.ts only ever build bytes that already carry a genuine WPD or compound file ID, and never a byte array that partially, but not fully, matches one.
describe("WpdBytesSchema", () => {
it("accepts bytes carrying the exact WPD file ID", () => {
const bytes = new Uint8Array([0xff, 0x57, 0x50, 0x43, 0, 0, 0, 0]);
expect(WpdBytesSchema.safeParse(bytes).success).toBe(true);
});

it("rejects bytes matching the WPD file ID's first byte but not its second", () => {
const bytes = new Uint8Array([0xff, 0x00, 0x50, 0x43, 0, 0, 0, 0]);
expect(WpdBytesSchema.safeParse(bytes).success).toBe(false);
});

it("accepts bytes carrying the exact OLE compound file signature", () => {
const bytes = new Uint8Array([
0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1,
]);
expect(WpdBytesSchema.safeParse(bytes).success).toBe(true);
});

it("rejects bytes matching the compound signature's first byte but not its second", () => {
const bytes = new Uint8Array([
0xd0, 0x00, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1,
]);
expect(WpdBytesSchema.safeParse(bytes).success).toBe(false);
});

it("rejects bytes matching neither signature, with a message naming both", () => {
const result = WpdBytesSchema.safeParse(new Uint8Array([1, 2, 3, 4]));
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0]?.message).toBe(
"not a WordPerfect document (no FF 57 50 43 file ID, and no OLE compound file signature that could wrap one)",
);
}
});
});
52 changes: 51 additions & 1 deletion packages/wpd-codec/src/container/container.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { writeCompoundFile } from "archive-codec";
import { describe, expect, it } from "vitest";
import { WpdNotAWordPerfectFileError } from "../errors";
import { compoundFileWithStream } from "../test-support/compound-file";
Expand All @@ -11,13 +12,28 @@ import { openWpdDocument, PERFECT_OFFICE_MAIN_STREAM } from "./container";
describe("openWpdDocument", () => {
// The WordPerfect 6.x spelling: the prefix and document area written straight to disk, with the file ID at offset 0.
it("opens a bare WordPerfect file", () => {
const container = openWpdDocument(genericHeaderBytes());
const input = genericHeaderBytes();
const container = openWpdDocument(input);
expect(container.compound).toBe(false);
expect(container.documentAreaOffset).toBe(
GENERIC_HEADER_DOCUMENT_AREA_OFFSET,
);
expect(container.documentAreaEnd).toBe(GENERIC_HEADER_SIZE);
expect(container.packets).toHaveLength(4);
// An ArrayBuffer-backed input is used as-is, not copied: the container's own bytes are the identical object, not merely an equal one.
expect(container.bytes).toBe(input);
});

// document-schema.js's ContentCodec port types a read as taking a plain Uint8Array, whose backing buffer may be a SharedArrayBuffer rather than a plain ArrayBuffer. That case takes the one path this package ever copies bytes on, and no other test constructs a SharedArrayBuffer-backed input at all.
it("copies a SharedArrayBuffer-backed input into a real ArrayBuffer rather than reading it in place", () => {
const source = genericHeaderBytes();
const shared = new Uint8Array(new SharedArrayBuffer(source.length));
shared.set(source);
const container = openWpdDocument(shared);
expect(container.compound).toBe(false);
expect(container.bytes).not.toBe(shared);
expect(container.bytes.buffer).toBeInstanceOf(ArrayBuffer);
expect(container.documentAreaEnd).toBe(GENERIC_HEADER_SIZE);
});

// The WP7-and-later spelling: the identical byte stream inside an OLE compound file's PerfectOffice_MAIN stream. Both must produce the same document, which is the point of deciding the container by inspecting bytes rather than by version.
Expand All @@ -37,18 +53,39 @@ describe("openWpdDocument", () => {
expect(container.documentAreaEnd).toBe(GENERIC_HEADER_SIZE);
});

// Only a stream nested under PerfectOffice_OBJECTS/ belongs in oleObjectStreams, keyed by the part of its path after that prefix -- a sibling top-level stream (here, a made-up SummaryInformation stream no test elsewhere carries alongside PerfectOffice_MAIN) must be excluded entirely, not merely mis-keyed.
it("collects only the streams nested under PerfectOffice_OBJECTS, keyed by their name within it", () => {
const objectBytes = new Uint8Array([1, 2, 3]);
const compound = writeCompoundFile([
{ path: PERFECT_OFFICE_MAIN_STREAM, bytes: genericHeaderBytes() },
{ path: "PerfectOffice_OBJECTS/OLE1", bytes: objectBytes },
{ path: "SummaryInformation", bytes: new Uint8Array([9, 9]) },
]);
const container = openWpdDocument(compound);
expect(container.oleObjectStreams.size).toBe(1);
expect(container.oleObjectStreams.get("OLE1")).toEqual(objectBytes);
});

it("rejects a compound file carrying no PerfectOffice_MAIN stream", () => {
const wrapped = compoundFileWithStream(
"WordDocument",
genericHeaderBytes(),
);
expect(() => openWpdDocument(wrapped)).toThrow(WpdNotAWordPerfectFileError);
expect(() => openWpdDocument(wrapped)).toThrow(
"This OLE compound file carries no PerfectOffice_MAIN stream, so it holds no WordPerfect document.",
);
});

it("rejects bytes that are neither a WordPerfect file nor a compound file", () => {
expect(() =>
openWpdDocument(Uint8Array.from([0x50, 0x4b, 0x03, 0x04])),
).toThrow(WpdNotAWordPerfectFileError);
expect(() =>
openWpdDocument(Uint8Array.from([0x50, 0x4b, 0x03, 0x04])),
).toThrow(
"These bytes are neither a WordPerfect file (which opens with the file ID FF 57 50 43) nor an OLE compound file that could contain one.",
);
});

// The SDK warns that a third-party writer forgetting to update {file size} after adding text is a common real-world defect, and that the symptom is a document that "will appear ... to be blank". Trusting a stale field over the bytes in hand is exactly how that happens, so a file size that stops at the document area's own start is disregarded.
Expand All @@ -67,4 +104,17 @@ describe("openWpdDocument", () => {
bytes[23] = 0xff;
expect(openWpdDocument(bytes).documentAreaEnd).toBe(bytes.length);
});

// The happy path documentAreaEnd exists for: a genuinely valid file size, strictly less than the buffer's own length (trailing bytes past the document's real end). Every other test either leaves the two equal or forces the fallback, so neither ever proves the field is actually honoured rather than the buffer's length being reported by coincidence.
it("honours a valid file size shorter than the buffer, rather than falling back to the buffer's own end", () => {
const source = genericHeaderBytes();
const padded = new Uint8Array(source.length + 10);
padded.set(source);
expect(openWpdDocument(padded).documentAreaEnd).toBe(GENERIC_HEADER_SIZE);
});

it("treats an empty-string password identically to no password on an unencrypted document", () => {
const container = openWpdDocument(genericHeaderBytes(), { password: "" });
expect(container.documentAreaEnd).toBe(GENERIC_HEADER_SIZE);
});
});
14 changes: 6 additions & 8 deletions packages/wpd-codec/src/container/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ function documentAreaEnd(
header: WpdFileHeader,
): number {
const { fileSize, documentAreaOffset } = header;
if (fileSize > documentAreaOffset && fileSize <= bytes.length) {
return fileSize;
// The upper bound is expressed through Math.min rather than a second comparison: a `fileSize <= bytes.length` guard would disagree with `<` only when fileSize === bytes.length exactly, and at that exact point both branches return the same number, so a bare comparator here would be an unkillable equivalent mutant no test could ever distinguish. Math.min carries the identical fallback (bytes.length whenever fileSize would run past it) without emitting a comparison whose boundary case has no observable effect.
if (fileSize > documentAreaOffset) {
return Math.min(fileSize, bytes.length);
}
return bytes.length;
}
Expand All @@ -115,13 +116,10 @@ export function openWpdDocument(
oleObjectStreams,
} = unwrapContainer(toArrayBufferBacked(input));
const header = readFileHeader(wrapped, options);
// An empty-string password means no password (the identical normalisation readFileHeader applies), so both gates below see one consistent value.
const suppliedPassword =
options.password === "" ? undefined : options.password;
// An encrypted document's index area, packet data, and document area are all beyond the fixed header and therefore all ciphertext; decrypting the whole buffer in one pass here means every downstream reader (the prefix walker, the tokeniser) parses plaintext with no encryption awareness of its own. A password supplied for an unencrypted document never reaches this branch -- the header word gates it -- and is harmlessly ignored, mirroring every other codec here.
// No separate empty-string normalisation is needed here: readFileHeader above already applies the identical "" -> no password rule and throws WpdEncryptedDocumentError before this line is reached for any encrypted document given an empty-string password, so by the time header.encryption !== 0 is true, options.password is already known to be a defined, non-empty string. An encrypted document's index area, packet data, and document area are all beyond the fixed header and therefore all ciphertext; decrypting the whole buffer in one pass here means every downstream reader (the prefix walker, the tokeniser) parses plaintext with no encryption awareness of its own. A password supplied for an unencrypted document never reaches this branch -- the header word gates it -- and is harmlessly ignored, mirroring every other codec here.
const bytes =
header.encryption !== 0 && suppliedPassword !== undefined
? decryptWpdDocument(wrapped, header, suppliedPassword)
header.encryption !== 0 && options.password !== undefined
? decryptWpdDocument(wrapped, header, options.password)
: wrapped;
const packets = readPrefixPackets(bytes, header);
return {
Expand Down
77 changes: 75 additions & 2 deletions packages/wpd-codec/src/container/encryption.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { byteAt } from "../bytes/view";
import { describe, expect, it } from "vitest";
import { buildWpdFile } from "../test-support/build-wpd";
import { WpdEncryptedDocumentError, WpdWrongPasswordError } from "../errors";
import { readWpdContent } from "../read";
import {
WpdEncryptedDocumentError,
WpdFormatError,
WpdWrongPasswordError,
} from "../errors";
import { readWpd, readWpdContent } from "../read";
import type { WpdFileHeader } from "./header";
import {
applyWpdStandardEncryption,
decryptWpdDocument,
encryptWpdDocumentForTests,
normaliseWpdPassword,
passwordByteAt,
wpdPasswordChecksum16,
} from "./encryption";

Expand All @@ -24,6 +31,24 @@ describe("normaliseWpdPassword", () => {
]);
expect(() => normaliseWpdPassword("passwörd日")).toThrow(/Latin-1/);
});

it("accepts U+00FF, the last code unit Latin-1 can encode", () => {
expect(normaliseWpdPassword(String.fromCharCode(0xff))).toEqual([0xff]);
});

it("refuses U+0100, one past the last code unit Latin-1 can encode", () => {
expect(() => normaliseWpdPassword(String.fromCharCode(0x100))).toThrow(
WpdFormatError,
);
});

it("states the offending code unit in uppercase hex, padded to four digits, and its position", () => {
expect(() =>
normaliseWpdPassword(`ok${String.fromCharCode(0xabc)}`),
).toThrow(
"A password with characters outside Latin-1 cannot be encoded into the byte-keyed WordPerfect cipher (code unit U+0ABC at position 2).",
);
});
});

describe("wpdPasswordChecksum16", () => {
Expand Down Expand Up @@ -90,6 +115,48 @@ describe("applyWpdStandardEncryption", () => {
applyWpdStandardEncryption(bytes, password, 512);
expect(Array.from(bytes)).toEqual(before);
});

it("refuses an empty password, which the cipher cannot key with", () => {
expect(() => applyWpdStandardEncryption(filled(520), [], 512)).toThrow(
"The WordPerfect cipher is keyed by the password's own bytes, so an empty password decrypts nothing.",
);
});
});

describe("passwordByteAt", () => {
// applyWpdStandardEncryption's own empty-password guard means no real caller ever reaches this with an empty array, but the function is a plain exported contract with its own behaviour to prove directly, the same way decryptWpdDocument is tested on its own terms above.
it("throws when the password it is asked to cycle through is empty", () => {
expect(() => passwordByteAt([], 0)).toThrow(
"The password normalised to no bytes, which the cipher cannot key with.",
);
});

it("wraps around the password's own length rather than reading past it", () => {
expect(passwordByteAt([0x41, 0x42, 0x43], 3)).toBe(0x41);
expect(passwordByteAt([0x41, 0x42, 0x43], 4)).toBe(0x42);
});
});

describe("decryptWpdDocument", () => {
// decryptWpdDocument's own doc comment says it is "called only for ... a non-empty password", but it is a plain exported function with its own contract, tested directly here rather than only through the container-level guarantee that happens to hold today.
//
// The header's own encryption word is deliberately 0 here (never a real encrypted document's actual value, but this function never inspects that invariant itself): an empty password's checksum is always 0 too, so any non-zero encryption word would already fail the checksum comparison the normal flow performs anyway, masking whether the dedicated empty-password guard ran at all. Only encryption === 0 lets the guard's absence actually be observed -- without it, the empty password would fall through to applyWpdStandardEncryption and throw a WpdFormatError there instead, not a WpdWrongPasswordError.
it("treats an empty password as a wrong password rather than an empty-cipher-key error", () => {
const bytes = buildWpdFile([0]);
const header: WpdFileHeader = {
documentAreaOffset: 0,
productType: 1,
fileType: 0x0a,
majorVersion: 2,
minorVersion: 1,
indexAreaOffset: 512,
fileSize: bytes.length,
encryption: 0,
};
expect(() => decryptWpdDocument(bytes, header, "")).toThrow(
WpdWrongPasswordError,
);
});
});

describe("reading an encrypted document", () => {
Expand All @@ -114,6 +181,12 @@ describe("reading an encrypted document", () => {
);
});

// readWpd threads its own password option through to the same openWpdDocument call readWpdContent uses -- proven separately, since readWpd builds its own tree-form read from scratch rather than delegating to readWpdContent.
it("reads the tree form of the same encrypted document with the password", () => {
const tree = readWpd(encrypted, { password: "sECret" });
expect(tree.kind).toBe("wordprocessing");
});

it("throws WpdEncryptedDocumentError without a password", () => {
expect(() => readWpdContent(encrypted)).toThrow(WpdEncryptedDocumentError);
});
Expand Down
Loading