wpd-codec 0.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/dist/bytes/view.cjs +23 -0
- package/dist/bytes/view.d.cts +7 -0
- package/dist/bytes/view.d.ts +7 -0
- package/dist/bytes/view.js +19 -0
- package/dist/codec.cjs +23 -0
- package/dist/codec.d.cts +8 -0
- package/dist/codec.d.ts +8 -0
- package/dist/codec.js +21 -0
- package/dist/container/container.cjs +53 -0
- package/dist/container/container.d.cts +16 -0
- package/dist/container/container.d.ts +16 -0
- package/dist/container/container.js +50 -0
- package/dist/container/header.cjs +49 -0
- package/dist/container/header.d.cts +16 -0
- package/dist/container/header.d.ts +16 -0
- package/dist/container/header.js +45 -0
- package/dist/container/prefix.cjs +66 -0
- package/dist/container/prefix.d.cts +18 -0
- package/dist/container/prefix.d.ts +18 -0
- package/dist/container/prefix.js +61 -0
- package/dist/diagnostics-Cp0HKphg.d.cts +15 -0
- package/dist/diagnostics-Cp0HKphg.d.ts +15 -0
- package/dist/diagnostics.cjs +12 -0
- package/dist/diagnostics.d.cts +2 -0
- package/dist/diagnostics.d.ts +2 -0
- package/dist/diagnostics.js +10 -0
- package/dist/errors.cjs +31 -0
- package/dist/errors.d.cts +15 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +27 -0
- package/dist/format.cjs +7 -0
- package/dist/format.d.cts +5 -0
- package/dist/format.d.ts +5 -0
- package/dist/format.js +5 -0
- package/dist/index.cjs +58 -0
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/read.cjs +296 -0
- package/dist/read.d.cts +10 -0
- package/dist/read.d.ts +10 -0
- package/dist/read.js +294 -0
- package/dist/stream/attributes.cjs +46 -0
- package/dist/stream/attributes.d.cts +37 -0
- package/dist/stream/attributes.d.ts +37 -0
- package/dist/stream/attributes.js +41 -0
- package/dist/stream/characters.cjs +205 -0
- package/dist/stream/characters.d.cts +12 -0
- package/dist/stream/characters.d.ts +12 -0
- package/dist/stream/characters.js +199 -0
- package/dist/stream/eol.cjs +52 -0
- package/dist/stream/eol.d.cts +10 -0
- package/dist/stream/eol.d.ts +10 -0
- package/dist/stream/eol.js +46 -0
- package/dist/stream/tokenise.cjs +125 -0
- package/dist/stream/tokenise.d.cts +30 -0
- package/dist/stream/tokenise.d.ts +30 -0
- package/dist/stream/tokenise.js +121 -0
- package/package.json +96 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joseph Mearman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# wpd-codec
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ExaDev/documents.js/tree/main/packages/wpd-codec) [](https://www.npmjs.com/package/wpd-codec) [](https://www.npmjs.com/package/wpd-codec) [](https://github.com/ExaDev/documents.js/actions)
|
|
4
|
+
|
|
5
|
+
> Hand-written, read-only WordPerfect 6.x-X6 (`.wpd`) reading into `document-schema.js`'s `ContentDocument`, from Corel's own published File Format SDK — part of the [documents.js family](../../README.md). Worker-isomorphic: the same code runs under Node and inside a Cloudflare Workers isolate.
|
|
6
|
+
|
|
7
|
+
**Status: under active development.** The read path below is tested against the specification's own worked examples and against hand-built fixtures derived from its field tables, but not yet against a corpus of real WordPerfect documents. See [Remaining scope](#remaining-scope) for what is deliberately not handled yet, and [What is not yet proven](#what-is-not-yet-proven) for the honest limits of the current evidence.
|
|
8
|
+
|
|
9
|
+
Created for [documents.js#819](https://github.com/ExaDev/documents.js/issues/819). The premise that made the issue worth acting on is that WordPerfect is not a reverse-engineered format: Corel shipped a File Format SDK as a supported developer product, and one specification covers the entire modern lineage — its own document-structure page states outright that "Files created in WordPerfect 6.x, through X6 are structured the same", so 1993 through 2012 is one format, not a family of them. There is also no JavaScript or TypeScript reader for it at all: [libwpd](https://libwpd.sourceforge.net/) is LGPL C++, WP_Reader is C#, and the SDK's own surviving mirror ships an Ada implementation.
|
|
10
|
+
|
|
11
|
+
## Sources
|
|
12
|
+
|
|
13
|
+
Everything this package does is derived from the vendor's own documentation, and every non-obvious decision in the source cites the page it comes from.
|
|
14
|
+
|
|
15
|
+
| Source | What it gives |
|
|
16
|
+
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
17
|
+
| [WordPerfect File Format SDK help](https://github.com/OneWingedShark/WordPerfect/tree/master/doc/SDK_Help/FileFormats) | The specification itself, mirrored in full: document structure, the prefix packet catalogue, single-byte characters and functions, every variable-length function group, the fixed-length functions, and table formulas. |
|
|
18
|
+
| [WPFF Document Structure](https://github.com/OneWingedShark/WordPerfect/blob/master/doc/SDK_Help/FileFormats/WPFF_DocumentStructure.htm) | The file header, the index and packet data areas, the function-code stream's shape, the units glossary, and a complete annotated hex dump of a conforming generic prefix. |
|
|
19
|
+
| [WPFF Single-Byte Characters and Functions](https://github.com/OneWingedShark/WordPerfect/blob/master/doc/SDK_Help/FileFormats/WPFF_SingleByte.htm) | The character model and the eighty single-byte function codes. |
|
|
20
|
+
| [WPFF D0 EOL Functions](https://github.com/OneWingedShark/WordPerfect/blob/master/doc/SDK_Help/FileFormats/WPFF_D0-EOL.htm) | The End-of-Line group and, crucially, its "Conversion/Search mappings" column — the specification stating what a converting application should turn each break code into. |
|
|
21
|
+
| [WPFF Fixed-Length Multi-Byte Functions](https://github.com/OneWingedShark/WordPerfect/blob/master/doc/SDK_Help/FileFormats/WPFF_xFixedLength.htm) | Attribute On/Off, the Extended Character function, and the size of every fixed-length code. |
|
|
22
|
+
| [WPFF D3 Paragraph](https://github.com/OneWingedShark/WordPerfect/blob/master/doc/SDK_Help/FileFormats/WPFF_D3-Paragraph.htm) and [D4 Character](https://github.com/OneWingedShark/WordPerfect/blob/master/doc/SDK_Help/FileFormats/WPFF_D4-Character.htm) | Justification, font face and size changes, colour, and the rest of the paragraph- and character-oriented functions. |
|
|
23
|
+
| [WPFF prefix packet catalogue](https://github.com/OneWingedShark/WordPerfect/blob/master/doc/SDK_Help/FileFormats/WPFF_PrefixPkt0-32.htm) | The packet types, including the font typeface descriptor layout this package reads a run's font family out of. |
|
|
24
|
+
| [Corel's File Format SDK product page](https://web.archive.org/web/20120125025312/http://apps.corel.com/partners_developers/csp/wordperfect_fileformatsdk.htm) | The provenance: a supported Corel developer product documenting "the entire document format, document prefix and document codes". |
|
|
25
|
+
|
|
26
|
+
## Getting started
|
|
27
|
+
|
|
28
|
+
Requires Node.js `>=20` and pnpm `11.6.0`.
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pnpm install
|
|
32
|
+
pnpm build # tsdown -> dist/ (ESM + CJS + .d.ts, one file set per src module)
|
|
33
|
+
pnpm typecheck # tsc -p tsconfig.json && tsc -p tsconfig.node.json, plus attw --pack
|
|
34
|
+
pnpm lint # eslint . --fix --cache --max-warnings 0
|
|
35
|
+
pnpm test # vitest run --project unit
|
|
36
|
+
pnpm test:watch # vitest --project unit
|
|
37
|
+
pnpm test:workers # vitest run --config vitest.workers.config.ts, inside a real Cloudflare Workers (workerd) isolate
|
|
38
|
+
pnpm test:smoke # builds dist/, then loads the built ESM and CJS barrels and every advertised deep import
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
To run a single test file, pass its path to vitest directly, e.g. `pnpm exec vitest run src/stream/tokenise.test.ts`.
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { readWpdContent } from "wpd-codec";
|
|
47
|
+
|
|
48
|
+
// Both containers are accepted, decided by inspecting the bytes: a bare
|
|
49
|
+
// WordPerfect 6.x file, and a WP7-and-later OLE compound file whose
|
|
50
|
+
// PerfectOffice_MAIN stream holds the identical byte stream.
|
|
51
|
+
const document = readWpdContent(bytes);
|
|
52
|
+
document.sections[0].blocks; // paragraphs, page breaks
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`readWpd` is the same read one level up, returning the tree-form `DocumentTree` every other codec in the family also offers. `wpdContentCodec` states the read half as `document-schema.js`'s own `ContentCodec` port, so a consumer dispatching over formats treats WordPerfect uniformly with the rest.
|
|
56
|
+
|
|
57
|
+
Anything that would silently lose information is reported through an optional diagnostic sink rather than swallowed:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
readWpdContent(bytes, {
|
|
61
|
+
sink: (diagnostic) => {
|
|
62
|
+
diagnostic.code; // 'wpd/unmapped-character', 'wpd/table-flattened', ...
|
|
63
|
+
diagnostic.message;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Structural nonconformance is not a diagnostic — it throws. `WpdNotAWordPerfectFileError`, `WpdEncryptedDocumentError`, `WpdUnsupportedVersionError`, and the general `WpdFormatError` are all exported, and all extend the last.
|
|
69
|
+
|
|
70
|
+
## What it provides
|
|
71
|
+
|
|
72
|
+
Every module is importable by package-relative path as well as through the barrel — `tsdown` builds one dist file per src module (`root: 'src'`, the layout `archive-codec` and `ooxml.js` also ship), and `package.json`'s `./*` exports wildcard maps each subpath onto it. The smoke suite is the guard on that advertisement: it loads each module below from the built `dist/` in both module systems.
|
|
73
|
+
|
|
74
|
+
| Module | Exports |
|
|
75
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
76
|
+
| `read` | `readWpdContent` (bytes to `ContentDocument`), `readWpd` (bytes to `DocumentTree`), `ReadWpdOptions` |
|
|
77
|
+
| `codec` | `wpdContentCodec` (`ContentCodec`), `WpdBytesSchema` |
|
|
78
|
+
| `format` | `WPD_MEDIA_TYPE`, `WPD_FILE_EXTENSION` |
|
|
79
|
+
| `diagnostics` | `WpdDiagnostic`, `WpdDiagnosticSink`, `WpdDiagnosticCodes`, `NOOP_WPD_DIAGNOSTIC_SINK` |
|
|
80
|
+
| `errors` | `WpdFormatError` and its three subclasses |
|
|
81
|
+
| `container/container` | `openWpdDocument` (container, header, packets, and document-area bounds in one), `PERFECT_OFFICE_MAIN_STREAM`, `PERFECT_OFFICE_OBJECTS_STORAGE` |
|
|
82
|
+
| `container/header` | `readFileHeader`, `hasWordPerfectFileId`, `WPD_FILE_ID`, `WPD_PREFIX_HEADER_SIZE` |
|
|
83
|
+
| `container/prefix` | `readPrefixPackets`, `packetByPrefixId`, `readTypefaceName`, `WPD_INDEX_RECORD_SIZE`, `PACKET_TYPE_DESIRED_FONT_DESCRIPTOR` |
|
|
84
|
+
| `stream/tokenise` | `tokeniseDocumentArea` and the four token types |
|
|
85
|
+
| `stream/characters` | `decodeWpCharacter`, `decodeSingleByteCharacter`, `decodeWordString`, `UNMAPPED_CHARACTER` |
|
|
86
|
+
| `stream/eol` | `eolMappingForSubfunction`, `subfunctionForSingleByteEol`, `isSingleByteEol` |
|
|
87
|
+
| `stream/attributes` | `decodeAttributeByte`, `runAttributesFrom`, `WpdAttribute` |
|
|
88
|
+
|
|
89
|
+
The container and stream layers are public deliberately, not by accident: a consumer inspecting a WordPerfect file — a migration audit, a forensic tool, a reader for a construct this package does not yet lift into the shared schema — needs the parsed prefix and the raw function stream, not only the document they fold into.
|
|
90
|
+
|
|
91
|
+
## Architecture
|
|
92
|
+
|
|
93
|
+
A WordPerfect 6.x-X6 file is a **prefix** followed by a **document area**, optionally wrapped in an OLE compound file. Reading it is three layers, each in its own directory.
|
|
94
|
+
|
|
95
|
+
**The container** (`src/container/`) resolves the wrapper and the prefix. A 16-byte header gives the offset of the document area, the product/type/version bytes, an encryption word, and the offset of the index area; a 496-byte extended header follows it, of which only the file size is documented. The index area is a run of 14-byte records — the first is the index header, and each of the rest points at one **packet** in the packet data area. A packet holds data referenced many times but not part of the document's content: a font descriptor, a style definition, a comment's text. Functions in the document area name a packet by its **prefix ID**, which is its 1-based position among the index entries, not its packet type.
|
|
96
|
+
|
|
97
|
+
**The tokeniser** (`src/stream/tokenise.ts`) walks the document area. Bytes at or below 0x7F are characters; above it, four ranges of function codes — single-byte (0x80-0xCF), variable-length multi-byte (0xD0-0xEF, self-describing through a size field), fixed-length multi-byte (0xF0-0xFE, sized by a table), and 0xFF, which cannot appear at all. Every multi-byte function is bracketed by matching begin and end gates, and the variable form repeats its size before the end gate; this package verifies all three redundancies, because they are the format's own integrity check — a stream that has gone out of step fails at the very next function rather than decoding rubbish for the rest of the file.
|
|
98
|
+
|
|
99
|
+
**The fold** (`src/read.ts`) turns tokens into a `ContentDocument`. Characters accumulate into the current run, an attribute or font change closes that run and opens another, and an end-of-line function closes the paragraph. Nothing recurses and nothing looks ahead, which is what makes a hand-written reader for this format tractable at all.
|
|
100
|
+
|
|
101
|
+
### Two containers, one document
|
|
102
|
+
|
|
103
|
+
WordPerfect 6.x writes the byte stream straight to disk. From WP7 onwards it may be wrapped in an OLE compound file, with the document in a `PerfectOffice_MAIN` stream — but the SDK is explicit that the wrapper is optional even then ("When creating WordPerfect 7/8 documents you do not need to include the OLE Compound Document wrapper"), so the container is decided by inspecting the bytes, never by the file's extension or its version bytes. Both paths produce the identical document, which the test suite asserts directly.
|
|
104
|
+
|
|
105
|
+
The compound-file half is [`archive-codec`](../archive-codec/README.md)'s bounded [MS-CFB] reader rather than anything written here: sectors, FAT chains, and directory entries are container structure with no document-format knowledge in them, which is exactly that package's charter.
|
|
106
|
+
|
|
107
|
+
### Byte 0x20 is not a space
|
|
108
|
+
|
|
109
|
+
The one part of the character model that looks like a bug on first reading, so it is worth stating plainly. The SDK maps byte values 1 through 32 to thirty-two "Default Extended International Characters" — a shorthand for common accented letters — and byte values 33 through 127 to ASCII. Byte 0x20 is therefore the sharp s, not a space. A space is the single-byte Soft Space function 0x80, which the specification describes as "Equivalent of an ASCII 0x20", or the Hard Space function 0x81. Both statements appear twice in the SDK, and the design reason is plain from the function list: WordPerfect must distinguish a justifiable soft space from a hard one, so neither can be a plain text byte. `src/stream/characters.ts` owns the whole mapping in one place.
|
|
110
|
+
|
|
111
|
+
### Deliberately not depending on libwpd
|
|
112
|
+
|
|
113
|
+
The only mature reader for this format is [libwpd](https://libwpd.sourceforge.net/), which is LGPL C++ — so binding it would forfeit both this family's MIT licensing and its Worker portability in one step, and it could not run in a browser or a Workers isolate at all. Writing the parser by hand against the vendor's own specification is the same bet `markdown-codec` makes against micromark and `pdf-codec` makes against pdf-lib, and here it is not really a bet: the format is documented at byte level by the company that wrote it. An ESLint rule bans importing any libwpd binding by name rather than leaving the decision to memory.
|
|
114
|
+
|
|
115
|
+
## Scope
|
|
116
|
+
|
|
117
|
+
**Read-only, WordPerfect 6.0 through X6.** Two deliberate exclusions, both decided before any code was written:
|
|
118
|
+
|
|
119
|
+
- **No writer.** WordPerfect File Format is complete enough to write against, but a lossless round-trip through a function-code stream — keeping prefix packet indices, use counts, and the document's own well-formedness invariants consistent — is a much larger job than reading one, and a half-correct writer is worse than no writer given the lossless bar the rest of this family holds to.
|
|
120
|
+
- **No WordPerfect 4.2, 5.x, or Macintosh generations.** Those share the file ID but not the structure; they are separate formats with their own vendor documentation, not earlier drafts of this one. A 5.x file reaches `WpdUnsupportedVersionError` on its major version byte rather than being misparsed as a 6.x file.
|
|
121
|
+
|
|
122
|
+
### What is handled
|
|
123
|
+
|
|
124
|
+
- Both containers: a bare WordPerfect file, and an OLE compound file's `PerfectOffice_MAIN` stream.
|
|
125
|
+
- The file header, with encryption, product type, file type, and major version all checked rather than assumed.
|
|
126
|
+
- The index area and packet data area, with prefix IDs resolvable to packets.
|
|
127
|
+
- The full document-area token stream: characters, all four function-code ranges, prefix ID references, non-deletable data, and gate/size verification.
|
|
128
|
+
- The character model: ASCII, the thirty-two international shorthands, and the Extended Character function for character set 0 and the documented part of set 1.
|
|
129
|
+
- Paragraph structure from the End-of-Line group, in both its single-byte and multi-byte spellings, using the specification's own conversion table — hard returns become paragraphs, soft returns become spaces, hard end-of-page becomes a `pageBreak` block.
|
|
130
|
+
- Character attributes: bold, italics, underline (plain and double), and strikeout, including the specification's "ignore" bit for a nested duplicate.
|
|
131
|
+
- Font family, from the Desired Font Descriptor packet a Font Face Change names; font size, from a Font Size Change; character colour.
|
|
132
|
+
- Paragraph justification.
|
|
133
|
+
- The Start/End of Text to Skip pair, whose contents the formatter does not display and this reader drops.
|
|
134
|
+
|
|
135
|
+
### Remaining scope
|
|
136
|
+
|
|
137
|
+
Everything below is recognised by the tokeniser and skipped by the fold, so a document containing it still reads — losing that construct's own structure, never the surrounding text.
|
|
138
|
+
|
|
139
|
+
- **Tables.** Cell and row boundaries become paragraph breaks so a table's text survives in reading order; the grid, cell attributes, and the table's own formula language (`WPFF_TableFormulas`) are not reconstructed. Reported through `wpd/table-flattened`.
|
|
140
|
+
- **Boxes and graphics** (the 0xDF group): figures, text boxes, and equations, and the WPG graphics they carry.
|
|
141
|
+
- **Embedded OLE objects**, stored under the compound file's `PerfectOffice_OBJECTS` storage.
|
|
142
|
+
- **Headers, footers, footnotes, and endnotes** (the 0xD6 and 0xD7 groups).
|
|
143
|
+
- **Styles** (the 0xDD group) beyond the document's own Open Style: a run's directly-applied attributes are read, but a style packet's own definitions are not resolved onto the runs that reference them.
|
|
144
|
+
- **Lists and outline numbering** (the 0xD8-0xDC groups).
|
|
145
|
+
- **Merge codes** (the 0xDE group) and **cross-references** (0xD5).
|
|
146
|
+
- **Page geometry** (the 0xD1 group): the section's page size and margins are the WordPerfect default (US Letter, one inch), not what a document that overrides them states.
|
|
147
|
+
- **Document metadata**, which lives in prefix packets this reader does not yet interpret, so `metadata` is an empty envelope rather than fields invented from the file's structure.
|
|
148
|
+
- **Character sets 2 and above**, and the part of set 1 the mirrored SDK pages do not tabulate. An unmapped character renders as U+FFFD and is reported through `wpd/unmapped-character` rather than dropped.
|
|
149
|
+
- **Encrypted documents**, which throw: the specification states that nothing beyond the file header is intelligible without the password, so there is no partial read to offer.
|
|
150
|
+
|
|
151
|
+
### What is not yet proven
|
|
152
|
+
|
|
153
|
+
Stated plainly, because it is the difference between this package being correct and being consistent with its own tests:
|
|
154
|
+
|
|
155
|
+
- **No real-world corpus.** Every test here is built either from the specification's own worked examples — the annotated generic-prefix hex dump, the `can't` extended-character example, the `com<0x83>ment` soft-hyphen example — or from byte sequences assembled directly from its field tables. That is strong evidence for the container, prefix, tokeniser, and the specific constructs covered; it is not evidence about what real WordPerfect documents in the wild actually contain, particularly around deletable data and the constructs listed above.
|
|
156
|
+
- **The document-area's own file-size bound.** The header's file-size field is honoured only when self-consistent, because the SDK itself warns that a third-party writer failing to update it is a common real-world defect whose symptom is a document reading back blank.
|
|
157
|
+
|
|
158
|
+
## Conventions
|
|
159
|
+
|
|
160
|
+
- Worker-isomorphic (see the [family-wide convention](../../README.md#conventions)): runtime `src/` must not import `node:*`, a bare Node builtin, or use the `Buffer` global — enforced by a `no-restricted-imports`/`no-restricted-globals` ESLint rule and exercised in CI by running a test suite inside an actual `workerd` isolate (`pnpm test:workers`). Test files under `src/**/*.test.ts` and `src/test-support/` are exempt.
|
|
161
|
+
- Only `src/index.ts` may be named `index.*`, and it may contain only re-export statements.
|
|
162
|
+
- Every non-obvious constant, offset, and mapping in `src/` cites the SDK page it comes from. A number that cannot be traced to the specification does not belong in this package.
|
|
163
|
+
|
|
164
|
+
## Install
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
pnpm add wpd-codec
|
|
168
|
+
# or
|
|
169
|
+
npm install wpd-codec
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Release and publishing
|
|
173
|
+
|
|
174
|
+
Release, CI, and commit-message conventions are all workspace-wide, not package-local — see the [monorepo root README](../../README.md#releases) for the mechanism (topological per-package `semantic-release` via `@exadev/semantic-release-workspace`, OIDC trusted npm publishing, automatic sibling dependency-range rewriting).
|
|
175
|
+
|
|
176
|
+
## Contributing
|
|
177
|
+
|
|
178
|
+
Conventional Commits, enforced workspace-wide by commitlint through a root `commit-msg` hook. Work inside `packages/wpd-codec/`; see [CONTRIBUTING.md](../../CONTRIBUTING.md) for the shared git hooks and history conventions.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_errors = require("../errors.cjs");
|
|
3
|
+
//#region src/bytes/view.ts
|
|
4
|
+
function byteAt(bytes, offset) {
|
|
5
|
+
const value = bytes[offset];
|
|
6
|
+
if (value === void 0) throw new require_errors.WpdFormatError(`Byte read at offset ${offset} is past the end of a ${bytes.length}-byte file.`);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function uint16At(bytes, offset) {
|
|
10
|
+
return byteAt(bytes, offset) | byteAt(bytes, offset + 1) << 8;
|
|
11
|
+
}
|
|
12
|
+
function uint32At(bytes, offset) {
|
|
13
|
+
return byteAt(bytes, offset) + byteAt(bytes, offset + 1) * 256 + byteAt(bytes, offset + 2) * 65536 + byteAt(bytes, offset + 3) * 16777216;
|
|
14
|
+
}
|
|
15
|
+
function sliceAt(bytes, offset, length) {
|
|
16
|
+
if (offset < 0 || length < 0 || offset + length > bytes.length) throw new require_errors.WpdFormatError(`A ${length}-byte read at offset ${offset} does not fit inside a ${bytes.length}-byte file.`);
|
|
17
|
+
return bytes.subarray(offset, offset + length);
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
exports.byteAt = byteAt;
|
|
21
|
+
exports.sliceAt = sliceAt;
|
|
22
|
+
exports.uint16At = uint16At;
|
|
23
|
+
exports.uint32At = uint32At;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/bytes/view.d.ts
|
|
2
|
+
declare function byteAt(bytes: Uint8Array, offset: number): number;
|
|
3
|
+
declare function uint16At(bytes: Uint8Array, offset: number): number;
|
|
4
|
+
declare function uint32At(bytes: Uint8Array, offset: number): number;
|
|
5
|
+
declare function sliceAt(bytes: Uint8Array, offset: number, length: number): Uint8Array;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { byteAt, sliceAt, uint16At, uint32At };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/bytes/view.d.ts
|
|
2
|
+
declare function byteAt(bytes: Uint8Array, offset: number): number;
|
|
3
|
+
declare function uint16At(bytes: Uint8Array, offset: number): number;
|
|
4
|
+
declare function uint32At(bytes: Uint8Array, offset: number): number;
|
|
5
|
+
declare function sliceAt(bytes: Uint8Array, offset: number, length: number): Uint8Array;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { byteAt, sliceAt, uint16At, uint32At };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { WpdFormatError } from "../errors.js";
|
|
2
|
+
//#region src/bytes/view.ts
|
|
3
|
+
function byteAt(bytes, offset) {
|
|
4
|
+
const value = bytes[offset];
|
|
5
|
+
if (value === void 0) throw new WpdFormatError(`Byte read at offset ${offset} is past the end of a ${bytes.length}-byte file.`);
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function uint16At(bytes, offset) {
|
|
9
|
+
return byteAt(bytes, offset) | byteAt(bytes, offset + 1) << 8;
|
|
10
|
+
}
|
|
11
|
+
function uint32At(bytes, offset) {
|
|
12
|
+
return byteAt(bytes, offset) + byteAt(bytes, offset + 1) * 256 + byteAt(bytes, offset + 2) * 65536 + byteAt(bytes, offset + 3) * 16777216;
|
|
13
|
+
}
|
|
14
|
+
function sliceAt(bytes, offset, length) {
|
|
15
|
+
if (offset < 0 || length < 0 || offset + length > bytes.length) throw new WpdFormatError(`A ${length}-byte read at offset ${offset} does not fit inside a ${bytes.length}-byte file.`);
|
|
16
|
+
return bytes.subarray(offset, offset + length);
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { byteAt, sliceAt, uint16At, uint32At };
|
package/dist/codec.cjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_container_header = require("./container/header.cjs");
|
|
3
|
+
const require_read = require("./read.cjs");
|
|
4
|
+
let zod = require("zod");
|
|
5
|
+
//#region src/codec.ts
|
|
6
|
+
const COMPOUND_FILE_ID = [
|
|
7
|
+
208,
|
|
8
|
+
207,
|
|
9
|
+
17,
|
|
10
|
+
224,
|
|
11
|
+
161,
|
|
12
|
+
177,
|
|
13
|
+
26,
|
|
14
|
+
225
|
|
15
|
+
];
|
|
16
|
+
function hasWordPerfectOrCompoundHeader(bytes) {
|
|
17
|
+
return require_container_header.WPD_FILE_ID.every((byte, index) => bytes[index] === byte) || COMPOUND_FILE_ID.every((byte, index) => bytes[index] === byte);
|
|
18
|
+
}
|
|
19
|
+
const WpdBytesSchema = zod.z.instanceof(Uint8Array).refine(hasWordPerfectOrCompoundHeader, { message: "not a WordPerfect document (no FF 57 50 43 file ID, and no OLE compound file signature that could wrap one)" });
|
|
20
|
+
const wpdContentCodec = { read: (bytes, options) => require_read.readWpdContent(bytes, options) };
|
|
21
|
+
//#endregion
|
|
22
|
+
exports.WpdBytesSchema = WpdBytesSchema;
|
|
23
|
+
exports.wpdContentCodec = wpdContentCodec;
|
package/dist/codec.d.cts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ReadWpdOptions } from "./read.cjs";
|
|
2
|
+
import { ContentCodec } from "document-schema.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/codec.d.ts
|
|
5
|
+
declare const WpdBytesSchema: z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>;
|
|
6
|
+
declare const wpdContentCodec: ContentCodec<ReadWpdOptions>;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { WpdBytesSchema, wpdContentCodec };
|
package/dist/codec.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ReadWpdOptions } from "./read.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { ContentCodec } from "document-schema.js";
|
|
4
|
+
//#region src/codec.d.ts
|
|
5
|
+
declare const WpdBytesSchema: z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>;
|
|
6
|
+
declare const wpdContentCodec: ContentCodec<ReadWpdOptions>;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { WpdBytesSchema, wpdContentCodec };
|
package/dist/codec.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { WPD_FILE_ID } from "./container/header.js";
|
|
2
|
+
import { readWpdContent } from "./read.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/codec.ts
|
|
5
|
+
const COMPOUND_FILE_ID = [
|
|
6
|
+
208,
|
|
7
|
+
207,
|
|
8
|
+
17,
|
|
9
|
+
224,
|
|
10
|
+
161,
|
|
11
|
+
177,
|
|
12
|
+
26,
|
|
13
|
+
225
|
|
14
|
+
];
|
|
15
|
+
function hasWordPerfectOrCompoundHeader(bytes) {
|
|
16
|
+
return WPD_FILE_ID.every((byte, index) => bytes[index] === byte) || COMPOUND_FILE_ID.every((byte, index) => bytes[index] === byte);
|
|
17
|
+
}
|
|
18
|
+
const WpdBytesSchema = z.instanceof(Uint8Array).refine(hasWordPerfectOrCompoundHeader, { message: "not a WordPerfect document (no FF 57 50 43 file ID, and no OLE compound file signature that could wrap one)" });
|
|
19
|
+
const wpdContentCodec = { read: (bytes, options) => readWpdContent(bytes, options) };
|
|
20
|
+
//#endregion
|
|
21
|
+
export { WpdBytesSchema, wpdContentCodec };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_errors = require("../errors.cjs");
|
|
3
|
+
const require_container_header = require("./header.cjs");
|
|
4
|
+
const require_container_prefix = require("./prefix.cjs");
|
|
5
|
+
let archive_codec = require("archive-codec");
|
|
6
|
+
//#region src/container/container.ts
|
|
7
|
+
const PERFECT_OFFICE_MAIN_STREAM = "PerfectOffice_MAIN";
|
|
8
|
+
const PERFECT_OFFICE_OBJECTS_STORAGE = "PerfectOffice_OBJECTS";
|
|
9
|
+
function isArrayBufferBacked(bytes) {
|
|
10
|
+
return bytes.buffer instanceof ArrayBuffer;
|
|
11
|
+
}
|
|
12
|
+
function toArrayBufferBacked(bytes) {
|
|
13
|
+
if (isArrayBufferBacked(bytes)) return bytes;
|
|
14
|
+
const copy = new Uint8Array(bytes.length);
|
|
15
|
+
copy.set(bytes);
|
|
16
|
+
return copy;
|
|
17
|
+
}
|
|
18
|
+
function unwrapContainer(bytes) {
|
|
19
|
+
if (require_container_header.hasWordPerfectFileId(bytes)) return {
|
|
20
|
+
bytes,
|
|
21
|
+
compound: false
|
|
22
|
+
};
|
|
23
|
+
if ((0, archive_codec.isCompoundFile)(bytes)) {
|
|
24
|
+
const main = (0, archive_codec.readCompoundFile)(bytes).find((stream) => stream.path === PERFECT_OFFICE_MAIN_STREAM);
|
|
25
|
+
if (main === void 0) throw new require_errors.WpdNotAWordPerfectFileError(`This OLE compound file carries no ${PERFECT_OFFICE_MAIN_STREAM} stream, so it holds no WordPerfect document.`);
|
|
26
|
+
return {
|
|
27
|
+
bytes: main.bytes,
|
|
28
|
+
compound: true
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
throw new require_errors.WpdNotAWordPerfectFileError("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.");
|
|
32
|
+
}
|
|
33
|
+
function documentAreaEnd(bytes, header) {
|
|
34
|
+
const { fileSize, documentAreaOffset } = header;
|
|
35
|
+
if (fileSize > documentAreaOffset && fileSize <= bytes.length) return fileSize;
|
|
36
|
+
return bytes.length;
|
|
37
|
+
}
|
|
38
|
+
function openWpdDocument(input) {
|
|
39
|
+
const { bytes, compound } = unwrapContainer(toArrayBufferBacked(input));
|
|
40
|
+
const header = require_container_header.readFileHeader(bytes);
|
|
41
|
+
return {
|
|
42
|
+
header,
|
|
43
|
+
packets: require_container_prefix.readPrefixPackets(bytes, header),
|
|
44
|
+
bytes,
|
|
45
|
+
documentAreaOffset: header.documentAreaOffset,
|
|
46
|
+
documentAreaEnd: documentAreaEnd(bytes, header),
|
|
47
|
+
compound
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
exports.PERFECT_OFFICE_MAIN_STREAM = PERFECT_OFFICE_MAIN_STREAM;
|
|
52
|
+
exports.PERFECT_OFFICE_OBJECTS_STORAGE = PERFECT_OFFICE_OBJECTS_STORAGE;
|
|
53
|
+
exports.openWpdDocument = openWpdDocument;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { WpdFileHeader } from "./header.cjs";
|
|
2
|
+
import { WpdPrefixPacket } from "./prefix.cjs";
|
|
3
|
+
//#region src/container/container.d.ts
|
|
4
|
+
declare const PERFECT_OFFICE_MAIN_STREAM = "PerfectOffice_MAIN";
|
|
5
|
+
declare const PERFECT_OFFICE_OBJECTS_STORAGE = "PerfectOffice_OBJECTS";
|
|
6
|
+
interface WpdDocumentContainer {
|
|
7
|
+
readonly header: WpdFileHeader;
|
|
8
|
+
readonly packets: readonly WpdPrefixPacket[];
|
|
9
|
+
readonly bytes: Uint8Array<ArrayBuffer>;
|
|
10
|
+
readonly documentAreaOffset: number;
|
|
11
|
+
readonly documentAreaEnd: number;
|
|
12
|
+
readonly compound: boolean;
|
|
13
|
+
}
|
|
14
|
+
declare function openWpdDocument(input: Uint8Array): WpdDocumentContainer;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { PERFECT_OFFICE_MAIN_STREAM, PERFECT_OFFICE_OBJECTS_STORAGE, WpdDocumentContainer, openWpdDocument };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { WpdFileHeader } from "./header.js";
|
|
2
|
+
import { WpdPrefixPacket } from "./prefix.js";
|
|
3
|
+
//#region src/container/container.d.ts
|
|
4
|
+
declare const PERFECT_OFFICE_MAIN_STREAM = "PerfectOffice_MAIN";
|
|
5
|
+
declare const PERFECT_OFFICE_OBJECTS_STORAGE = "PerfectOffice_OBJECTS";
|
|
6
|
+
interface WpdDocumentContainer {
|
|
7
|
+
readonly header: WpdFileHeader;
|
|
8
|
+
readonly packets: readonly WpdPrefixPacket[];
|
|
9
|
+
readonly bytes: Uint8Array<ArrayBuffer>;
|
|
10
|
+
readonly documentAreaOffset: number;
|
|
11
|
+
readonly documentAreaEnd: number;
|
|
12
|
+
readonly compound: boolean;
|
|
13
|
+
}
|
|
14
|
+
declare function openWpdDocument(input: Uint8Array): WpdDocumentContainer;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { PERFECT_OFFICE_MAIN_STREAM, PERFECT_OFFICE_OBJECTS_STORAGE, WpdDocumentContainer, openWpdDocument };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { WpdNotAWordPerfectFileError } from "../errors.js";
|
|
2
|
+
import { hasWordPerfectFileId, readFileHeader } from "./header.js";
|
|
3
|
+
import { readPrefixPackets } from "./prefix.js";
|
|
4
|
+
import { isCompoundFile, readCompoundFile } from "archive-codec";
|
|
5
|
+
//#region src/container/container.ts
|
|
6
|
+
const PERFECT_OFFICE_MAIN_STREAM = "PerfectOffice_MAIN";
|
|
7
|
+
const PERFECT_OFFICE_OBJECTS_STORAGE = "PerfectOffice_OBJECTS";
|
|
8
|
+
function isArrayBufferBacked(bytes) {
|
|
9
|
+
return bytes.buffer instanceof ArrayBuffer;
|
|
10
|
+
}
|
|
11
|
+
function toArrayBufferBacked(bytes) {
|
|
12
|
+
if (isArrayBufferBacked(bytes)) return bytes;
|
|
13
|
+
const copy = new Uint8Array(bytes.length);
|
|
14
|
+
copy.set(bytes);
|
|
15
|
+
return copy;
|
|
16
|
+
}
|
|
17
|
+
function unwrapContainer(bytes) {
|
|
18
|
+
if (hasWordPerfectFileId(bytes)) return {
|
|
19
|
+
bytes,
|
|
20
|
+
compound: false
|
|
21
|
+
};
|
|
22
|
+
if (isCompoundFile(bytes)) {
|
|
23
|
+
const main = readCompoundFile(bytes).find((stream) => stream.path === PERFECT_OFFICE_MAIN_STREAM);
|
|
24
|
+
if (main === void 0) throw new WpdNotAWordPerfectFileError(`This OLE compound file carries no ${PERFECT_OFFICE_MAIN_STREAM} stream, so it holds no WordPerfect document.`);
|
|
25
|
+
return {
|
|
26
|
+
bytes: main.bytes,
|
|
27
|
+
compound: true
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
throw new WpdNotAWordPerfectFileError("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.");
|
|
31
|
+
}
|
|
32
|
+
function documentAreaEnd(bytes, header) {
|
|
33
|
+
const { fileSize, documentAreaOffset } = header;
|
|
34
|
+
if (fileSize > documentAreaOffset && fileSize <= bytes.length) return fileSize;
|
|
35
|
+
return bytes.length;
|
|
36
|
+
}
|
|
37
|
+
function openWpdDocument(input) {
|
|
38
|
+
const { bytes, compound } = unwrapContainer(toArrayBufferBacked(input));
|
|
39
|
+
const header = readFileHeader(bytes);
|
|
40
|
+
return {
|
|
41
|
+
header,
|
|
42
|
+
packets: readPrefixPackets(bytes, header),
|
|
43
|
+
bytes,
|
|
44
|
+
documentAreaOffset: header.documentAreaOffset,
|
|
45
|
+
documentAreaEnd: documentAreaEnd(bytes, header),
|
|
46
|
+
compound
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { PERFECT_OFFICE_MAIN_STREAM, PERFECT_OFFICE_OBJECTS_STORAGE, openWpdDocument };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_errors = require("../errors.cjs");
|
|
3
|
+
const require_bytes_view = require("../bytes/view.cjs");
|
|
4
|
+
//#region src/container/header.ts
|
|
5
|
+
const WPD_FILE_ID = [
|
|
6
|
+
255,
|
|
7
|
+
87,
|
|
8
|
+
80,
|
|
9
|
+
67
|
|
10
|
+
];
|
|
11
|
+
const WPD_PREFIX_HEADER_SIZE = 512;
|
|
12
|
+
const PRODUCT_TYPE_WORDPERFECT = 1;
|
|
13
|
+
const DOCUMENT_FILE_TYPES = [10, 36];
|
|
14
|
+
const MAJOR_VERSION_WP6_THROUGH_X6 = 2;
|
|
15
|
+
function hasWordPerfectFileId(bytes) {
|
|
16
|
+
if (bytes.length < WPD_FILE_ID.length) return false;
|
|
17
|
+
return WPD_FILE_ID.every((expected, index) => bytes[index] === expected);
|
|
18
|
+
}
|
|
19
|
+
function readFileHeader(bytes) {
|
|
20
|
+
if (!hasWordPerfectFileId(bytes)) {
|
|
21
|
+
const actual = Array.from(require_bytes_view.sliceAt(bytes, 0, Math.min(4, bytes.length))).map((byte) => byte.toString(16).padStart(2, "0")).join(" ");
|
|
22
|
+
throw new require_errors.WpdNotAWordPerfectFileError(`Expected the WordPerfect file ID FF 57 50 43 (-1,"WPC") at offset 0, found ${actual}.`);
|
|
23
|
+
}
|
|
24
|
+
const documentAreaOffset = require_bytes_view.uint32At(bytes, 4);
|
|
25
|
+
const productType = require_bytes_view.byteAt(bytes, 8);
|
|
26
|
+
const fileType = require_bytes_view.byteAt(bytes, 9);
|
|
27
|
+
const majorVersion = require_bytes_view.byteAt(bytes, 10);
|
|
28
|
+
const minorVersion = require_bytes_view.byteAt(bytes, 11);
|
|
29
|
+
const encryption = require_bytes_view.uint16At(bytes, 12);
|
|
30
|
+
const indexAreaOffset = require_bytes_view.uint16At(bytes, 14);
|
|
31
|
+
if (encryption !== 0) throw new require_errors.WpdEncryptedDocumentError(`This document is encrypted (encryption word ${encryption}); nothing beyond the file header is intelligible without the password, which this reader does not support.`);
|
|
32
|
+
if (productType !== PRODUCT_TYPE_WORDPERFECT) throw new require_errors.WpdUnsupportedVersionError(`Product type ${productType} is not WordPerfect (${PRODUCT_TYPE_WORDPERFECT}); this file was produced by a different Corel product.`);
|
|
33
|
+
if (!DOCUMENT_FILE_TYPES.includes(fileType)) throw new require_errors.WpdUnsupportedVersionError(`File type ${fileType} is not a WordPerfect document (expected ${DOCUMENT_FILE_TYPES.join(" or ")}).`);
|
|
34
|
+
if (majorVersion !== MAJOR_VERSION_WP6_THROUGH_X6) throw new require_errors.WpdUnsupportedVersionError(`Major version ${majorVersion} is outside the WordPerfect 6.x-X6 lineage (major version ${MAJOR_VERSION_WP6_THROUGH_X6}), the one generation this reader covers.`);
|
|
35
|
+
return {
|
|
36
|
+
documentAreaOffset,
|
|
37
|
+
productType,
|
|
38
|
+
fileType,
|
|
39
|
+
majorVersion,
|
|
40
|
+
minorVersion,
|
|
41
|
+
indexAreaOffset,
|
|
42
|
+
fileSize: require_bytes_view.uint32At(bytes, 20)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
exports.WPD_FILE_ID = WPD_FILE_ID;
|
|
47
|
+
exports.WPD_PREFIX_HEADER_SIZE = WPD_PREFIX_HEADER_SIZE;
|
|
48
|
+
exports.hasWordPerfectFileId = hasWordPerfectFileId;
|
|
49
|
+
exports.readFileHeader = readFileHeader;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/container/header.d.ts
|
|
2
|
+
declare const WPD_FILE_ID: readonly number[];
|
|
3
|
+
declare const WPD_PREFIX_HEADER_SIZE = 512;
|
|
4
|
+
interface WpdFileHeader {
|
|
5
|
+
readonly documentAreaOffset: number;
|
|
6
|
+
readonly productType: number;
|
|
7
|
+
readonly fileType: number;
|
|
8
|
+
readonly majorVersion: number;
|
|
9
|
+
readonly minorVersion: number;
|
|
10
|
+
readonly indexAreaOffset: number;
|
|
11
|
+
readonly fileSize: number;
|
|
12
|
+
}
|
|
13
|
+
declare function hasWordPerfectFileId(bytes: Uint8Array): boolean;
|
|
14
|
+
declare function readFileHeader(bytes: Uint8Array): WpdFileHeader;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { WPD_FILE_ID, WPD_PREFIX_HEADER_SIZE, WpdFileHeader, hasWordPerfectFileId, readFileHeader };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/container/header.d.ts
|
|
2
|
+
declare const WPD_FILE_ID: readonly number[];
|
|
3
|
+
declare const WPD_PREFIX_HEADER_SIZE = 512;
|
|
4
|
+
interface WpdFileHeader {
|
|
5
|
+
readonly documentAreaOffset: number;
|
|
6
|
+
readonly productType: number;
|
|
7
|
+
readonly fileType: number;
|
|
8
|
+
readonly majorVersion: number;
|
|
9
|
+
readonly minorVersion: number;
|
|
10
|
+
readonly indexAreaOffset: number;
|
|
11
|
+
readonly fileSize: number;
|
|
12
|
+
}
|
|
13
|
+
declare function hasWordPerfectFileId(bytes: Uint8Array): boolean;
|
|
14
|
+
declare function readFileHeader(bytes: Uint8Array): WpdFileHeader;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { WPD_FILE_ID, WPD_PREFIX_HEADER_SIZE, WpdFileHeader, hasWordPerfectFileId, readFileHeader };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { WpdEncryptedDocumentError, WpdNotAWordPerfectFileError, WpdUnsupportedVersionError } from "../errors.js";
|
|
2
|
+
import { byteAt, sliceAt, uint16At, uint32At } from "../bytes/view.js";
|
|
3
|
+
//#region src/container/header.ts
|
|
4
|
+
const WPD_FILE_ID = [
|
|
5
|
+
255,
|
|
6
|
+
87,
|
|
7
|
+
80,
|
|
8
|
+
67
|
|
9
|
+
];
|
|
10
|
+
const WPD_PREFIX_HEADER_SIZE = 512;
|
|
11
|
+
const PRODUCT_TYPE_WORDPERFECT = 1;
|
|
12
|
+
const DOCUMENT_FILE_TYPES = [10, 36];
|
|
13
|
+
const MAJOR_VERSION_WP6_THROUGH_X6 = 2;
|
|
14
|
+
function hasWordPerfectFileId(bytes) {
|
|
15
|
+
if (bytes.length < WPD_FILE_ID.length) return false;
|
|
16
|
+
return WPD_FILE_ID.every((expected, index) => bytes[index] === expected);
|
|
17
|
+
}
|
|
18
|
+
function readFileHeader(bytes) {
|
|
19
|
+
if (!hasWordPerfectFileId(bytes)) {
|
|
20
|
+
const actual = Array.from(sliceAt(bytes, 0, Math.min(4, bytes.length))).map((byte) => byte.toString(16).padStart(2, "0")).join(" ");
|
|
21
|
+
throw new WpdNotAWordPerfectFileError(`Expected the WordPerfect file ID FF 57 50 43 (-1,"WPC") at offset 0, found ${actual}.`);
|
|
22
|
+
}
|
|
23
|
+
const documentAreaOffset = uint32At(bytes, 4);
|
|
24
|
+
const productType = byteAt(bytes, 8);
|
|
25
|
+
const fileType = byteAt(bytes, 9);
|
|
26
|
+
const majorVersion = byteAt(bytes, 10);
|
|
27
|
+
const minorVersion = byteAt(bytes, 11);
|
|
28
|
+
const encryption = uint16At(bytes, 12);
|
|
29
|
+
const indexAreaOffset = uint16At(bytes, 14);
|
|
30
|
+
if (encryption !== 0) throw new WpdEncryptedDocumentError(`This document is encrypted (encryption word ${encryption}); nothing beyond the file header is intelligible without the password, which this reader does not support.`);
|
|
31
|
+
if (productType !== PRODUCT_TYPE_WORDPERFECT) throw new WpdUnsupportedVersionError(`Product type ${productType} is not WordPerfect (${PRODUCT_TYPE_WORDPERFECT}); this file was produced by a different Corel product.`);
|
|
32
|
+
if (!DOCUMENT_FILE_TYPES.includes(fileType)) throw new WpdUnsupportedVersionError(`File type ${fileType} is not a WordPerfect document (expected ${DOCUMENT_FILE_TYPES.join(" or ")}).`);
|
|
33
|
+
if (majorVersion !== MAJOR_VERSION_WP6_THROUGH_X6) throw new WpdUnsupportedVersionError(`Major version ${majorVersion} is outside the WordPerfect 6.x-X6 lineage (major version ${MAJOR_VERSION_WP6_THROUGH_X6}), the one generation this reader covers.`);
|
|
34
|
+
return {
|
|
35
|
+
documentAreaOffset,
|
|
36
|
+
productType,
|
|
37
|
+
fileType,
|
|
38
|
+
majorVersion,
|
|
39
|
+
minorVersion,
|
|
40
|
+
indexAreaOffset,
|
|
41
|
+
fileSize: uint32At(bytes, 20)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { WPD_FILE_ID, WPD_PREFIX_HEADER_SIZE, hasWordPerfectFileId, readFileHeader };
|