doc-codec 2.4.3 → 2.6.0

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.
Files changed (90) hide show
  1. package/README.md +53 -20
  2. package/dist/chp-B6AOh85P.d.ts +18 -0
  3. package/dist/chp-BpCqpb7V.d.cts +18 -0
  4. package/dist/detect.cjs +3 -0
  5. package/dist/detect.d.cts +3 -1
  6. package/dist/detect.d.ts +3 -1
  7. package/dist/detect.js +3 -1
  8. package/dist/encryption.cjs +44 -10
  9. package/dist/encryption.d.cts +2 -2
  10. package/dist/encryption.d.ts +2 -2
  11. package/dist/encryption.js +45 -11
  12. package/dist/fib/fib.cjs +14 -0
  13. package/dist/fib/fib.d.cts +1 -1
  14. package/dist/fib/fib.d.ts +1 -1
  15. package/dist/fib/fib.js +14 -0
  16. package/dist/fib/offsets.cjs +14 -0
  17. package/dist/fib/offsets.d.cts +14 -0
  18. package/dist/fib/offsets.d.ts +14 -0
  19. package/dist/fib/offsets.js +14 -0
  20. package/dist/{fib-DWFta19r.d.cts → fib-6BFIx153.d.cts} +14 -0
  21. package/dist/{fib-DWFta19r.d.ts → fib-6BFIx153.d.ts} +14 -0
  22. package/dist/fkp-I-laf-8y.d.cts +33 -0
  23. package/dist/fkp-I-laf-8y.d.ts +33 -0
  24. package/dist/headers-footers.cjs +34 -0
  25. package/dist/headers-footers.d.cts +16 -0
  26. package/dist/headers-footers.d.ts +16 -0
  27. package/dist/headers-footers.js +33 -0
  28. package/dist/index.cjs +20 -9
  29. package/dist/index.d.cts +16 -12
  30. package/dist/index.d.ts +16 -12
  31. package/dist/index.js +15 -11
  32. package/dist/list/numbering-write.d.cts +1 -1
  33. package/dist/list/numbering-write.d.ts +1 -1
  34. package/dist/list/numbering.d.cts +23 -1
  35. package/dist/list/numbering.d.ts +23 -1
  36. package/dist/notes.cjs +26 -0
  37. package/dist/notes.d.cts +20 -0
  38. package/dist/notes.d.ts +20 -0
  39. package/dist/notes.js +25 -0
  40. package/dist/{pap-BT1p0uzu.d.cts → pap-BQ2E17K9.d.cts} +5 -3
  41. package/dist/{pap-DOFLMWcG.d.ts → pap-iWFIpCdo.d.ts} +5 -3
  42. package/dist/paragraphs-DQUp8_HB.d.cts +31 -0
  43. package/dist/paragraphs-s2lJ6w2I.d.ts +31 -0
  44. package/dist/pictures.cjs +93 -0
  45. package/dist/pictures.d.cts +6 -0
  46. package/dist/pictures.d.ts +6 -0
  47. package/dist/pictures.js +92 -0
  48. package/dist/piece-table-CA18mhjv.d.cts +28 -0
  49. package/dist/piece-table-CA18mhjv.d.ts +28 -0
  50. package/dist/prop/chp.d.cts +1 -17
  51. package/dist/prop/chp.d.ts +1 -17
  52. package/dist/prop/fkp.d.cts +1 -32
  53. package/dist/prop/fkp.d.ts +1 -32
  54. package/dist/prop/pap.cjs +4 -2
  55. package/dist/prop/pap.d.cts +1 -1
  56. package/dist/prop/pap.d.ts +1 -1
  57. package/dist/prop/pap.js +4 -2
  58. package/dist/prop/sep.cjs +20 -10
  59. package/dist/prop/sep.d.cts +8 -4
  60. package/dist/prop/sep.d.ts +8 -4
  61. package/dist/prop/sep.js +20 -10
  62. package/dist/read.cjs +48 -153
  63. package/dist/read.d.cts +13 -15
  64. package/dist/read.d.ts +13 -15
  65. package/dist/read.js +51 -156
  66. package/dist/stsh-Bj0YSEOp.d.ts +40 -0
  67. package/dist/stsh-CK59x6XG.d.cts +40 -0
  68. package/dist/style/stsh.d.cts +1 -39
  69. package/dist/style/stsh.d.ts +1 -39
  70. package/dist/subdocument.cjs +20 -0
  71. package/dist/subdocument.d.cts +8 -0
  72. package/dist/subdocument.d.ts +8 -0
  73. package/dist/subdocument.js +18 -0
  74. package/dist/table/read.cjs +31 -17
  75. package/dist/table/read.d.cts +1 -1
  76. package/dist/table/read.d.ts +1 -1
  77. package/dist/table/read.js +32 -18
  78. package/dist/text/characters.d.cts +1 -1
  79. package/dist/text/characters.d.ts +1 -1
  80. package/dist/text/paragraphs.cjs +189 -0
  81. package/dist/text/paragraphs.d.cts +2 -0
  82. package/dist/text/paragraphs.d.ts +2 -0
  83. package/dist/text/paragraphs.js +187 -0
  84. package/dist/text/piece-table.d.cts +1 -27
  85. package/dist/text/piece-table.d.ts +1 -27
  86. package/dist/write.cjs +3 -3
  87. package/dist/write.js +3 -3
  88. package/package.json +2 -2
  89. package/dist/numbering-DSpm9w6y.d.cts +0 -24
  90. package/dist/numbering-DayjnHXY.d.ts +0 -24
package/README.md CHANGED
@@ -17,14 +17,17 @@ Built and shipped, on the read side:
17
17
  - **Text reconstruction** — `readTextRange` turns a range of character positions into real characters through [MS-DOC] 2.4.1's own Retrieving Text algorithm, applying the specification's byte-to-code-point mapping for compressed pieces, and returns each character's byte offset alongside it.
18
18
  - **Character and paragraph formatting** — the `PlcBteChpx`/`PlcBtePapx` bin tables and the `ChpxFkp`/`PapxFkp` pages behind them, the `Sprm`/`Prl` operand-sizing rules, and the subset of the character- and paragraph-property tables listed under [What is converted](#what-is-converted), now including `sprmCRgFtc0`'s font-table lookup (see [The font table](#the-font-table)).
19
19
  - **The style sheet** — `parseStsh` reads each style's index, name, kind, parent, and (for a paragraph or character style) its own `grLPUpxSw` formatting sets; `resolveStyleFormatting` walks the `istdBase` inheritance chain and folds each style's own paragraph/character properties, most-specific style winning, beneath a paragraph's or run's own direct exceptions ([ExaDev/documents.js#1005](https://github.com/ExaDev/documents.js/issues/1005)). `headingLevelFromIstd` applies `sprmPIstd`'s own rule that an `istd` of 1 through 9 states an outline level.
20
- - **Tables** — `table/read.ts`'s `assembleBlocks` folds a contiguous run of table-depth-1 paragraphs into a real `ContentTable`: cell boundaries at each cell-mark (`0x07`) character, a cell holding more than one paragraph where only its last ends in a cell mark, and each row's own trailing mark (`sprmPFTtp`) resolved through `table/tap.ts`'s `applyTableSprms` for its TAP — column boundaries and every physical cell's own horizontal/vertical merge state, from `sprmTDefTable`'s `TDefTableOperand` (and a `sprmTMerge` range or `sprmTVertMerge` per-cell flag where a real producer states a merge that way instead — see [Tables](#tables) below for why both are read). A table nested inside a table cell (table depth greater than 1, detected via `sprmPItap`/`sprmPFInnerTableCell`/`sprmPFInnerTtp`) is refused with `DocUnsupportedError` rather than mis-read; a row whose own TAP this reader cannot resolve at all — no direct `sprmTDefTable` anywhere in its grpprl, or a cell-mark count that disagrees with it — degrades the whole run back to flat paragraphs instead, since that is a legal producer choice this reader does not yet follow rather than corruption (see [Tables](#tables)).
20
+ - **Tables, including nested tables** — `table/read.ts`'s `walkBlocksAtDepth`/`assembleBlocks` fold a contiguous run of table-depth paragraphs into a real `ContentTable`, at whatever nesting depth the file states: cell boundaries at each cell-mark (`0x07`) character at depth 1, or a paragraph mark (`0x000D`) carrying `sprmPFInnerTableCell` one level deeper ([MS-DOC] 2.4.3's own depth-1-vs-deeper distinction), a cell holding more than one paragraph where only its last ends in a cell mark, and each row's own trailing mark (`sprmPFTtp`/`sprmPFInnerTtp`) resolved through `table/tap.ts`'s `applyTableSprms` for its TAP — column boundaries and every physical cell's own horizontal/vertical merge state, from `sprmTDefTable`'s `TDefTableOperand` (and a `sprmTMerge` range or `sprmTVertMerge` per-cell flag where a real producer states a merge that way instead — see [Tables](#tables) below for why both are read). A table nested inside a table cell (detected via `sprmPItap`/`sprmPFInnerTableCell`/`sprmPFInnerTtp`) resolves to a real nested `ContentTable`, to whatever depth the file actually states, rather than being refused. A row whose own TAP this reader cannot resolve at all — no direct `sprmTDefTable` anywhere in its grpprl, or a cell-mark count that disagrees with it — degrades the whole run back to flat paragraphs instead, since that is a legal producer choice this reader does not yet follow rather than corruption (see [Tables](#tables)).
21
21
  - **Table cell decoration** — `ContentTableCell.background` and `.borders`, from every encoding [MS-DOC] states them in: each cell's own four `Brc80` borders inside `TC80`, the exact-colour `sprmTSetBrc` layer a real producer writes beside them, the row-level shading arrays (`sprmTDefTableShd`/`2nd`/`3rd`, their `Raw` counterparts, the Word 97-era `sprmTDefTableShd80`, and `sprmTSetShd`/`sprmTSetShdOdd`), and, cascaded onto whichever cells and sides those leave unstated, the row/table-level `sprmTTableBorders`/`sprmTTableBorders80` border set and `sprmTSetShdTable`'s own per-row background. See [Cell decoration](#cell-decoration).
22
22
  - **`readDocContent`** — the whole chain, producing a `'wordprocessing'` `ContentDocument` of paragraphs, runs and tables.
23
23
  - **`isDocBytes`** — distinguishes a `.doc` from the `.xls`, `.ppt` and OLE embeddings that share its container, by looking for a `WordDocument` stream carrying `FibBase.wIdent`.
24
24
  - **Document metadata** — `title`/`subject`/`author`/`keywords`/`createdIso`/`modifiedIso` read from a `"\x05SummaryInformation"` stream when one is present (see [Metadata](#metadata)); `comments` and `lastPrintedIso` remain unread, since `LayoutMetadata` has no field for either.
25
- - **RC4-encrypted documents, decrypted for real (ExaDev/documents.js#1113)** — `readDocContent`/`readDocStreams` take an optional `password`, verified against the Table stream's own `EncryptionHeader` before anything is trusted (see [Encryption](#encryption)). XOR obfuscation and RC4 CryptoAPI stay refused, tracked on [ExaDev/documents.js#922](https://github.com/ExaDev/documents.js/issues/922).
25
+ - **RC4-encrypted and XOR-obfuscated documents, decrypted for real (ExaDev/documents.js#1113, #922)** — `readDocContent`/`readDocStreams` take an optional `password`, verified against the Table stream's own `EncryptionHeader` for RC4, or against `FibBase.lKey` directly for XOR obfuscation (see [Encryption](#encryption)). RC4 CryptoAPI stays refused.
26
26
  - **Numbering definitions** — `readDocContent`'s own `numbering` field: every list's glyph/format, level-text template, and start-at value, resolved from `PlfLst`/`PlfLfo` and keyed by the same `listId` a paragraph's `ContentParagraph.list.numId` already carries. See [Numbering definitions](#numbering-definitions), which also covers `writeDocContent`'s own inverse.
27
- - **The one section's own page size and margins** — `prop/sep.ts`'s `readSectionProperties` resolves `PlcfSed`/`Sepx` (`sprmSXaPage`/`sprmSYaPage`/`sprmSDxaLeft`/`sprmSDxaRight`/`sprmSDyaTop`/`sprmSDyaBottom`) into `ContentSection.pageSize`/`.margins`, falling back field by field to Word's own new-document default (US Letter, one-inch margins) for a document carrying no `PlcfSed` at all, or for any one sprm it leaves unstated. This reader only ever resolves the section spanning the whole main document the "Section boundaries" row below states what "one section" still means.
27
+ - **Every section's own page size and margins** — `prop/sep.ts`'s `readAllSectionProperties` resolves every `Sed` `PlcfSed` carries, each into its own `ContentSection.pageSize`/`.margins` (`sprmSXaPage`/`sprmSYaPage`/`sprmSDxaLeft`/`sprmSDxaRight`/`sprmSDyaTop`/`sprmSDyaBottom`), falling back field by field to Word's own new-document default (US Letter, one-inch margins) for a document carrying no `PlcfSed` at all, or for any one sprm a section leaves unstated. `read.ts`'s `splitIntoSections` groups the main document's own paragraphs by which section each falls in, per [MS-DOC] 2.8.26's own "each CP specifies the beginning of a range of text ... that constitutes a section" a genuinely multi-section `.doc` now reads as multiple `ContentSection`s, each with its own real page geometry, rather than the whole document collapsing onto the first section's.
28
+ - **Footnotes, endnotes, and comments, as plain text** — `notes.ts`'s `readNoteBodies` resolves `PlcffndTxt`/`PlcfendTxt`/`PlcfandTxt` (each a CP-only plex dividing its own subdocument into stories, [MS-DOC]'s own "each CP ... specifies the beginning of a story ... the story ends immediately prior to the next CP") into `DocContent.footnotes`/`.endnotes`/`.comments`: one `{id, text}`/`{id, text}` entry per story, `id` a synthetic one-based ordinal, matching the same simplification `ooxml.js`'s own `DocxDocument.footnotes`/`.endnotes`/`.comments` already make for the identical constructs (plain text, no paragraph/run structure). A story's own trailing guard paragraph mark ("not considered part of the story contents", [MS-DOC]'s own words) is excluded from its text; a genuinely empty story reads as `""`.
29
+ - **Headers and footers, as real block flow** — `headers-footers.ts`'s `readHeaderFooterStories` resolves `Plcfhdd` (the identical CP-only-plex shape as the note-text plexes above) into `DocContent.headerFooterStories`: one entry per non-empty story, each carrying which section it belongs to, which of the six fixed slots it is (`evenHeader`/`oddHeader`/`evenFooter`/`oddFooter`/`firstHeader`/`firstFooter`, [MS-DOC]'s own fixed per-section story order), and a real `ContentBlock[]` — paragraphs and tables, read through the identical pipeline the main document uses, rather than plain text. An empty story ([MS-DOC]'s own "the beginning CP has the same value as the next CP") is left out of the array entirely, since it means "reuse the previous section's header/footer of this kind" rather than a genuinely blank one. The six footnote/endnote-separator stories Plcfhdd carries ahead of every section's own six are not read: neither this package's schema nor `ooxml.js`'s `DocxDocument` has anywhere to put a separator story.
30
+ - **Inline pictures** — `pictures.ts`'s `readInlinePicture` resolves a picture character (`U+0001`, `sprmCFSpec` applied) through its own Chpx's `sprmCPicLocation` operand: an offset into the container's `"Data"` stream where a `PICF` ([MS-DOC] 68-byte type/size/border header) is followed, for every producer this reader has seen, by a real `OfficeArtInlineSpContainer` ([MS-ODRAW] 2.2.15) regardless of `PICF.mfpf.mm`'s own value. The container's own `shape` (an `OfficeArtSpContainer`) is skipped whole by its own record header's `recLen`, and the first blip record in `rgfb` immediately after it is decoded when it is `OfficeArtBlipJPEG`/`OfficeArtBlipPNG` — the two raster formats `document-schema.js`'s `ContentImageBlock` can hold losslessly — into a real inline image, sized from `PICMID`'s own `dxaGoal`/`dyaGoal` scaled by `mx`/`my`. A paragraph containing a picture anchor is split around it into a real `ContentBlock[]` (`text/paragraphs.ts`'s `buildParagraphBlocks`), since an inline image is block-level in `document-schema.js`'s own model rather than a run property — mirroring how `ooxml.js`'s own docx reader splits a paragraph around a mid-run page break, paragraph-level formatting carried unchanged onto every split piece. See [Images](#images) for what this does not read.
28
31
 
29
32
  Built and shipped, on the write side — see [Writing](#writing) for the full scope statement:
30
33
 
@@ -33,19 +36,19 @@ Built and shipped, on the write side — see [Writing](#writing) for the full sc
33
36
 
34
37
  **Not built, and not approximated, on either side.** Each of these is a genuine layer of [MS-DOC] that this package does not implement; none is silently faked, and a document using one reads (or fails to write) as though it did not:
35
38
 
36
- | Absent | Consequence |
37
- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
38
- | **Nested tables** | A table inside a table cell (table depth greater than 1) is a genuinely different [MS-DOC] structure — `sprmPFInnerTableCell`/`sprmPFInnerTtp` mark its cell/row ends rather than `sprmPFInTable`/`sprmPFTtp`, and `sprmPItap`/`sprmPDtap` state the depth. Neither side of this package descends into one: `readDocContent` refuses with `DocUnsupportedError` the moment it detects a table depth greater than 1, and `writeDocContent` refuses a `ContentTable` block found inside a table cell's own blocks the same way. See [Tables](#tables) for the full scope of what a depth-1 table does resolve. |
39
- | **Images and drawn objects** | The anchor characters (`U+0001`, `U+0008`) are dropped rather than emitted as control characters. No picture data is read. `writeDocContent` refuses an image block. |
40
- | **A style's own formatting on write** | A style's own property sets (`STD.grLPUpxSw`) are resolved and folded on read ([ExaDev/documents.js#1005](https://github.com/ExaDev/documents.js/issues/1005)): a paragraph's formatting is its style's own grpprlPapx/grpprlChpx (walked up the full `istdBase` inheritance chain, most-specific style winning) plus its own direct exceptions on top, and a run's own referenced character style (`sprmCIstd`) folds in the same way. `writeDocContent` mints a real STSH entry for every distinct `styleId`/`headingLevel` a document's paragraphs use ([ExaDev/documents.js#1059](https://github.com/ExaDev/documents.js/issues/1059)), so style IDENTITY round-trips -- but every minted entry carries an empty `grLPUpxSw` (no formatting of its own), since doc-codec's flat `ContentDocument` has no style-vs-direct-formatting split to draw a real one from: every property this writer emits is already, unconditionally, a direct exception. A style's own inherited formatting therefore never round-trips through a write -- only its name and istd position do. |
41
- | **Table and numbering style formatting** | `STD.grLPUpxSw` is resolved only for paragraph and character styles (`stk` 1/2, see the row above) -- a table style's `StkTableGRLPUPX` (its own `TAPX` plus `CNFC`-conditional per-region formatting, [MS-DOC] 2.4.6.6's own table-specific steps 6-9) and a numbering style's `StkListGRLPUPX` are neither read nor written. A table applying a named table style, or a paragraph using a numbering style rather than direct list formatting, reports none of the formatting either would supply. |
42
- | **Subdocuments** | Only the main document (character positions 0 to `ccpText`) is converted. Footnotes, endnotes, headers, footers, comments and text boxes are not, in either direction. |
43
- | **Section boundaries** | Section boundaries themselves are not read — the whole document is always treated as one section, spanning the entire main document — so a multi-section `.doc`'s later sections' own page size and margins are never consulted, only the first's (see the read-side page-size/margins bullet above for what is now read of that one section). `writeDocContent` refuses a `ContentDocument` with more than one section, rather than silently merging their content into what would read back as one. |
44
- | **Extended and user-defined document properties** | `title`/`subject`/`author`/`keywords`/`createdIso`/`modifiedIso` are read from and written to a `"\x05SummaryInformation"` stream when present (see [Metadata](#metadata)); the sibling `"\x05DocumentSummaryInformation"` stream (company, manager, and custom user-defined properties) is not read or written at all. |
45
- | **Encryption** | RC4-encrypted documents ([MS-DOC] 2.2.6.2, ExaDev/documents.js#1113) are decrypted given a password -- `readDocContent`/`readDocStreams` take an optional `password`, verified against the Table stream's own EncryptionHeader before anything is trusted (see [Encryption](#encryption) below). XOR obfuscation and RC4 CryptoAPI (a different, newer EncryptionHeader shape) still refuse with a `DocUnsupportedError` rather than being read as plaintext or misread as the wrong scheme. `writeDocContent` never encrypts. |
46
- | **`sprmPHugePapx` / `sprmPTableProps`** | Paragraph properties stored indirectly in the Data stream are not followed, so such a paragraph reads with fewer properties than it states. [MS-DOC] 2.4.3's own Overview of Tables text names `sprmPTableProps` as a real, legal alternative to `sprmTDefTable` some applications process — but a real producer's row mark is not shown to prefer it: a genuine LibreOffice-authored `.doc` table's own row mark states its TAP through the identical direct `sprmTDefTable` this package's reader and writer already use (confirmed by parsing a LibreOffice 26.2.5.2-authored table's raw `PapxFkp` bytes; see [ExaDev/documents.js#892](https://github.com/ExaDev/documents.js/issues/892)), matching 2.4.3's own compatibility guidance ("An application SHOULD use sprmTDefTable to define table cells for applications that do not process sprmPTableProps"). `writeDocContent` never writes an indirect Papx. |
47
- | **Hyperlinks and fields** | `ContentRun.hyperlink`, footnote/comment/annotation references, and every other field or anchor character are read as plain text or dropped (see [What is converted](#what-is-converted)) and are not written. |
48
- | **Every FIB field beyond what this package's own reader needs** | `writeDocContent` populates only the fc/lcb pairs its own reader consults (the style sheet, the two property bin tables, the Clx, the font table). Roughly 140 other `FibRgFcLcb97` pairs — `SttbfAssoc`, `Dop`, the printer-driver structures among them — are left zero, which is the format's own "undefined, MUST be ignored" contract for most of them, but not a certification that every third-party [MS-DOC] reader accepts the result; see `fib/write.ts`'s own note. |
39
+ | Absent | Consequence |
40
+ | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
41
+ | **Nested tables, on write** | `readDocContent` recurses into a table nested inside a table cell to whatever depth the file states (see the read-side Tables bullet above and [Tables](#tables)). `writeDocContent` still refuses a `ContentTable` block found inside a table cell's own blocks: it has no depth-aware sgc-1/sgc-5 grpprl encoding for anything past depth 1, and writing one incorrectly would silently corrupt the nested table's own structure rather than fail loudly. |
42
+ | **Floating/anchored drawn objects, text boxes, and every picture format beyond JPEG/PNG** | An inline picture (`U+0001`) resolves to a real image when its own blip is `OfficeArtBlipJPEG`/`OfficeArtBlipPNG` (see the read-side Inline pictures bullet above and [Images](#images)). A floating or anchored drawn object (`U+0008`, `PlcfSpa`) is a genuinely different structure — an arbitrary `OfficeArtSpContainer` shape (which may or may not be a picture at all), reached through a document-level drawing tree (`OfficeArtDgContainer`) rather than a single inline blip — and is not resolved at all; its anchor character is dropped. A text box rides the identical drawing layer (`PlcfTxbxTxt`, itself anchored via a drawn object), so it is likewise not read. A blip format other than JPEG/PNG (a WMF/EMF/PICT metafile, a raw DIB, TIFF) is a structure this package's own image schema (`png`/`jpeg`/`svg`/`gif` only) has nowhere to hold without re-encoding pixels this package has no image codec to perform, so `readInlinePicture` returns undefined for one rather than mis-decoding it, and the anchor drops exactly as an unresolved floating object's does. `writeDocContent` refuses an image block outright, on either side of this line. |
43
+ | **A style's own formatting on write** | A style's own property sets (`STD.grLPUpxSw`) are resolved and folded on read ([ExaDev/documents.js#1005](https://github.com/ExaDev/documents.js/issues/1005)): a paragraph's formatting is its style's own grpprlPapx/grpprlChpx (walked up the full `istdBase` inheritance chain, most-specific style winning) plus its own direct exceptions on top, and a run's own referenced character style (`sprmCIstd`) folds in the same way. `writeDocContent` mints a real STSH entry for every distinct `styleId`/`headingLevel` a document's paragraphs use ([ExaDev/documents.js#1059](https://github.com/ExaDev/documents.js/issues/1059)), so style IDENTITY round-trips -- but every minted entry carries an empty `grLPUpxSw` (no formatting of its own), since doc-codec's flat `ContentDocument` has no style-vs-direct-formatting split to draw a real one from: every property this writer emits is already, unconditionally, a direct exception. A style's own inherited formatting therefore never round-trips through a write -- only its name and istd position do. |
44
+ | **Table and numbering style formatting** | `STD.grLPUpxSw` is resolved only for paragraph and character styles (`stk` 1/2, see the row above) -- a table style's `StkTableGRLPUPX` (its own `TAPX` plus `CNFC`-conditional per-region formatting, [MS-DOC] 2.4.6.6's own table-specific steps 6-9) and a numbering style's `StkListGRLPUPX` are neither read nor written. A table applying a named table style, or a paragraph using a numbering style rather than direct list formatting, reports none of the formatting either would supply. |
45
+ | **Subdocuments, on write; text boxes, on either side** | Footnotes, endnotes, comments, and headers/footers are now read (see the three read-side bullets above); none is written — `writeDocContent` still only ever produces a main document with `ccpFtn`/`ccpHdd`/`ccpAtn`/`ccpEdn` all zero, and a `ContentDocument` carries nowhere on its own `ContentSection.blocks` to state a header/footer's own content distinctly from the body's, which a real inverse would need. Text boxes are not read or written in either direction: `PlcfTxbxTxt` divides its own subdocument into stories the identical way the note-text plexes above do, but a text box's own anchor rides the drawn-object mechanism ([Images](#images)'s own scope note), which this package does not resolve. |
46
+ | **Multiple sections, on write** | `readDocContent` resolves every section `PlcfSed` states, each with its own page size and margins (see the read-side bullet above). `writeDocContent` still refuses a `ContentDocument` with more than one section, rather than silently merging their content into what would read back as one — writing a real multi-section `PlcfSed`/`Sepx` array is unimplemented on the write side. |
47
+ | **Extended and user-defined document properties** | `title`/`subject`/`author`/`keywords`/`createdIso`/`modifiedIso` are read from and written to a `"\x05SummaryInformation"` stream when present (see [Metadata](#metadata)); the sibling `"\x05DocumentSummaryInformation"` stream (company, manager, and custom user-defined properties) is not read or written at all. |
48
+ | **Encryption** | RC4-encrypted ([MS-DOC] 2.2.6.2, ExaDev/documents.js#1113) and XOR-obfuscated (2.2.6.1, #922) documents are both decrypted given a password -- `readDocContent`/`readDocStreams` take an optional `password`, verified against the Table stream's own EncryptionHeader for RC4, or against `FibBase.lKey` directly for XOR obfuscation (see [Encryption](#encryption) below). RC4 CryptoAPI (a different, newer EncryptionHeader shape) still refuses with a `DocUnsupportedError` rather than being read as plaintext or misread as the wrong scheme. `writeDocContent` never encrypts. |
49
+ | **`sprmPHugePapx` / `sprmPTableProps`** | Paragraph properties stored indirectly in the Data stream are not followed, so such a paragraph reads with fewer properties than it states. [MS-DOC] 2.4.3's own Overview of Tables text names `sprmPTableProps` as a real, legal alternative to `sprmTDefTable` some applications process — but a real producer's row mark is not shown to prefer it: a genuine LibreOffice-authored `.doc` table's own row mark states its TAP through the identical direct `sprmTDefTable` this package's reader and writer already use (confirmed by parsing a LibreOffice 26.2.5.2-authored table's raw `PapxFkp` bytes; see [ExaDev/documents.js#892](https://github.com/ExaDev/documents.js/issues/892)), matching 2.4.3's own compatibility guidance ("An application SHOULD use sprmTDefTable to define table cells for applications that do not process sprmPTableProps"). `writeDocContent` never writes an indirect Papx. |
50
+ | **Hyperlinks and fields** | `ContentRun.hyperlink`, footnote/comment/annotation references, and every other field or anchor character are read as plain text or dropped (see [What is converted](#what-is-converted)) and are not written. |
51
+ | **Every FIB field beyond what this package's own reader needs** | `writeDocContent` populates only the fc/lcb pairs its own reader consults (the style sheet, the two property bin tables, the Clx, the font table). Roughly 140 other `FibRgFcLcb97` pairs — `SttbfAssoc`, `Dop`, the printer-driver structures among them — are left zero, which is the format's own "undefined, MUST be ignored" contract for most of them, but not a certification that every third-party [MS-DOC] reader accepts the result; see `fib/write.ts`'s own note. |
49
52
 
50
53
  One construct is refused rather than mis-read: a `sprmPChgTabs` whose `cb` is the `255` sentinel encodes its own length as a formula over tab-stop counts this package does not parse, and its length is needed to find the next `Prl`. Rather than guess and silently mis-read every property after it, `operandSize` throws.
51
54
 
@@ -86,7 +89,7 @@ Fields are handled structurally: everything between a field-begin (`U+0013`) and
86
89
 
87
90
  ## Tables
88
91
 
89
- A table in [MS-DOC] is not a separate container: it is a run of ordinary paragraphs marked `sprmPFInTable`, with cell boundaries at literal `0x07` cell-mark characters in the text stream and each row closed by its own row-ending mark — a cell mark additionally carrying `sprmPFTtp` — per [MS-DOC] 2.4.3's own Overview of Tables. `src/table/` implements exactly this model, at table depth 1 only; a table nested inside a table cell is refused rather than mis-read (see the "Nested tables" row in the scope table above).
92
+ A table in [MS-DOC] is not a separate container: it is a run of ordinary paragraphs marked `sprmPFInTable`, with cell boundaries at literal `0x07` cell-mark characters in the text stream and each row closed by its own row-ending mark — a cell mark additionally carrying `sprmPFTtp` — per [MS-DOC] 2.4.3's own Overview of Tables. `src/table/` implements exactly this model on the read side at every depth a document states: a table nested inside a table cell (table depth greater than 1) uses a genuinely different pair of marks — a paragraph mark (`0x000D`) carrying `sprmPFInnerTableCell` for a cell boundary, `sprmPFInnerTtp` for a row's own terminating mark, in place of the cell-mark character depth 1 uses — and `table/read.ts`'s `walkBlocksAtDepth`/`tryAssembleTable` are mutually recursive over that depth, so a cell's own content resolves to a nested `ContentTable` wherever the file states one, to whatever depth it actually nests. The write side stays depth-1 only: `writeDocContent` still refuses a `ContentTable` block found inside a table cell's own blocks (see the "Nested tables, on write" row in the scope table above).
90
93
 
91
94
  **Reading** (`table/read.ts`'s `assembleBlocks`, called from `read.ts`). It walks the flat paragraph sequence `read.ts` already produces, grouping every contiguous run of `inTable` paragraphs into a `ContentTable`: consecutive paragraphs up to and including the one terminated by an ordinary cell mark become one cell's own `blocks` (a cell may hold more than one paragraph — only its last ends in a cell mark, per 2.4.3's own "the last paragraph in a table cell is terminated by a cell mark"), and the row's own trailing mark resolves the row's whole TAP through `table/tap.ts`'s `applyTableSprms`: column boundaries and every physical cell's own merge state, read directly from `sprmTDefTable`'s `TDefTableOperand` — its `rgdxaCenter` array and its `rgTc80` array of per-column `TC80` records ([MS-DOC] 2.9.313, whose own `tcgrf` field is 2.9.317) — folded with a `sprmTMerge` range or `sprmTVertMerge` per-cell flag on top where a real producer states a merge incrementally instead, genuinely regardless of which order the two appear in within the grpprl (`table/tap.ts`'s own note). Column layout is never assumed shared across a table's own rows: [MS-DOC] 2.6.3 permits each row to declare its own independent `rgdxaCenter` ("There is no requirement that each row of a table have the same number of cells"), and a real, independent [MS-DOC] implementation (LibreOffice 26.2.5.2) was confirmed to rely on exactly this for a horizontal merge — its own merged row simply has fewer, wider physical cells, with no `TCGRF.horzMerge`/`sprmTMerge` signal at all (see the third-party verification paragraph below). `table/read.ts` reconstructs the table's shared column grid as the union of every row's own `rgdxaCenter` boundary values, then expresses each physical cell's own `colSpan` as however many of that shared grid's segments its own boundaries cover. That union is taken within one point rather than by exact integer equality, because [MS-DOC] states those boundaries per row and defines no quantum coarser than the twip itself for them, so two rows meaning the identical grid may legally disagree by a twip or two — and an exact union turns that drift into a phantom hairline column plus a spurious `colSpan` on the cells of every row either side of it (two rows one twip apart across a 2338-twip boundary read back as `columnWidthsPt` `[116.9, 0.05, 144.95, 220]` instead of `[116.9, 145, 220]`; [ExaDev/documents.js#898](https://github.com/ExaDev/documents.js/issues/898)). The default tolerance is `TWIPS_PER_POINT` itself, not a picked number: `columnWidthsPt` states the reconstructed grid in points, so a segment narrower than one point sits below the smallest unit that grid can distinguish at all. It is also the fuzz a real, independent implementation applies to an analogous reconstruct-one-shared-grid-from-N-per-row-arrays problem — LibreOffice's table model is per-row too (`SwTableLine` → `SwTableBox`, each box carrying its own width), and `sw/source/filter/inc/wrtswtbl.hxx` answers it, on its own ODF export (the point at which it projects that per-row model onto one shared grid, `sw/source/filter/xml/xmltble.cxx`'s `SwXMLTableColumn_Impl`), with `#define COLFUZZY 20` twips, `SwWriteTableCol::operator==` treating two column positions as equal when they differ by at most that. Its changeover was confirmed empirically and exactly, not assumed: patching a single `int16` inside a real LibreOffice-authored table's second row and round-tripping it through that implementation's own `.doc` import followed by its ODF export gives three columns and no covered cell for a drift of 1 through 20 twips, and four columns with a real `table:covered-table-cell` from 21 (the `.doc` import side alone preserves the drifted boundary byte-for-byte — the fuzz is applied on export, not import). Per-row drift is not hypothetical even without Word or LibreOffice's own export step in the picture: `WW8TabDesc::CalcDefaults` widens any imported cell narrower than that same implementation's own minimum cell width (`MINLAY`, 23 twips in `sw/inc/swtypes.hxx`) by mutating boundaries per row during `.doc` import itself, so a document that has been through that import is one real mechanism by which per-row drift reaches a `.doc` at all.
92
95
 
@@ -158,6 +161,34 @@ A paragraph's own `list.numId`/`list.level` (`sprmPIlfo`/`sprmPIlvl`, unchanged
158
161
 
159
162
  **The write side is verified by this package's own round trip, not yet against a third-party implementation.** `write.test.ts`'s own `describe("writeDocContent numbering")` block covers a multi-level list (`format`/`text` per level surviving through `readDocContent`), a bulleted list (its glyph, not a numbered placeholder), separate `ilfo` minting for separate `numId`s, a document with no lists at all (no `PlfLst`/`PlfLfo` written), and a list membership inside a table cell -- every case reading back through this package's own reader exactly as written. `list/numbering-write.test.ts` covers `buildNumberingTables` one layer down, independently of `gatherListUsage`: the `LSTF`/`LFO` byte shape a hand-built `NumberingDefinitions` produces, the empty-definitions-map case returning `undefined`, the numerically-colliding-key rejection described above, and that `gatherListUsage`'s own output can never trigger that rejection. Unlike the read side's LibreOffice cross-check above, no independent [MS-DOC] implementation has yet opened a `writeDocContent` numbering sample: that verification is the next thing worth doing here, mirroring how [Tables](#tables) and [Cell decoration](#cell-decoration) were each verified once a real producer's own file was available to compare against.
160
163
 
164
+ ## Sections
165
+
166
+ [MS-DOC] 2.8.26's `PlcfSed` divides the main document's own text into sections the identical way `Plcfhdd`/`PlcffndTxt`/`PlcfandTxt`/`PlcfendTxt` (below) divide their own subdocuments into stories: an array of CPs, each naming where the next section's text begins, "the range of text ends immediately prior to the next CP." `prop/sep.ts`'s `readAllSectionProperties` resolves every `Sed` this way, in document order, each into its own page width/height and four margins via `applySectionSprms` (`sprmSXaPage`/`sprmSYaPage`/`sprmSDxaLeft`/`sprmSDxaRight`/`sprmSDyaTop`/`sprmSDyaBottom`); `read.ts`'s `splitIntoSections` then walks the main document's own flat paragraph sequence once, advancing to the next section exactly when a paragraph's own end CP matches the next section's start CP — the paragraph carrying the end-of-section character (`0x000C`) itself, per [MS-DOC]'s own "there MUST also be an end-of-section character... as the final character in the text range of all but the last section." A file with no `PlcfSed` at all (`lcbPlcfSed` 0) reads as a single section falling back, field by field, to Word's own new-document default (US Letter, one-inch margins) — the identical fallback a `PlcfSed` that leaves one sprm unstated already used before multi-section support existed.
167
+
168
+ Only page size and the four margins are resolved, matching what `document-schema.js`'s own `ContentSection` (`pageSize` + `margins`) has anywhere to hold; every other section sprm (columns, headers/footers distance, vertical justification, page borders, line numbering, and the rest of the roughly seventy [MS-DOC] 2.6.4 names) is absent rather than present-and-ignored, the identical documented-gap convention the paragraph- and character-property tables already use. `writeDocContent` still only ever writes one section (see [Writing](#writing)); a real multi-section write is unimplemented.
169
+
170
+ ## Subdocuments
171
+
172
+ The main document is only the first of several logical text ranges [MS-DOC] 2.4.1 concatenates into one CP space, in this fixed order: the main document (`0` to `ccpText`), the footnote document (`ccpFtn` characters), the header document (`ccpHdd`), the comment/annotation document (`ccpAtn`), the endnote document (`ccpEdn`), and the textbox documents (`ccpTxbx`/`ccpHdrTxbx`, not read at all — see below). `subdocument.ts`'s `readSubdocumentStories` is the one function every subdocument this package reads goes through: it reads a subdocument's own CP range via the identical `readTextRange`/`readParagraphs` pipeline the main document uses, then splits the resulting flat paragraph sequence by a boundary plex's own CPs (`text/paragraphs.ts`'s `splitEntriesByBoundaries`) into one group per story. `PlcffndTxt`/`PlcfandTxt`/`PlcfendTxt`/`Plcfhdd` all share the identical shape and the identical two conventions this one function handles once: "the story is considered empty" when two consecutive CPs are equal (skipped past, never assigned any entry — a run of several empty stories in a row is handled correctly, not just a single one), and a trailing CP the structure's own page states is "undefined and MUST be ignored" (the last group `readSubdocumentStories` produces is always dropped). A non-empty story's own final paragraph — a bare paragraph mark with no content of its own, the guard "not considered part of the story contents" — is likewise dropped from what a caller actually sees.
173
+
174
+ **Footnotes, endnotes, and comments** (`notes.ts`) resolve to `DocContent.footnotes`/`.endnotes`/`.comments`: one `{id, text}` entry per story, `id` a synthetic one-based ordinal in document order (this reader does not resolve `PlcffndRef`/`PlcfandRef`/`PlcfendRef`'s own reference data — the auto-numbering/custom-symbol state a real footnote mark carries — only the order its own text plex already states), `text` the story's own paragraphs joined by `\n`. This is a deliberate simplification matching `ooxml.js`'s own `DocxDocument.footnotes`/`.endnotes`/`.comments`, which carry the identical flat-text shape for the identical constructs rather than real block flow.
175
+
176
+ **Headers and footers** (`headers-footers.ts`) resolve to `DocContent.headerFooterStories`, real block flow instead: `Plcfhdd`'s own fixed layout is six footnote/endnote-separator stories (not read — neither this package's schema nor `ooxml.js`'s `DocxDocument` has anywhere to put one), then six per section in a fixed order (`evenHeader`, `oddHeader`, `evenFooter`, `oddFooter`, `firstHeader`, `firstFooter`), so each resolved story carries its own `section` index, `slot`, and a genuine `ContentBlock[]` read through the same `assembleBlocks` pipeline the main document uses — paragraphs and tables both. An empty story means "the header/footer of this kind from the previous section is reused" ([MS-DOC]'s own words) rather than a deliberately blank one, so it is left out of the array entirely instead of appearing as a hollow `{blocks: []}` entry.
177
+
178
+ **Not read at all: text boxes.** `PlcfTxbxTxt`/`PlcfHdrtxbxTxt` divide their own subdocument into stories the identical way, but a text box's own anchor is a drawn object (`U+0008`, `PlcfSpa`) — the same drawing-layer mechanism [Images](#images) below does not resolve — so there is no way to reach a text box's own story from the main document's text at all without that layer, and this package does not attempt it.
179
+
180
+ Nothing on this subdocument side is written: `writeDocContent` produces a main document only, with `ccpFtn`/`ccpHdd`/`ccpAtn`/`ccpEdn` all zero (`fib/write.ts`'s own note).
181
+
182
+ ## Images
183
+
184
+ An inline picture is a character with Unicode value `U+0001` ("A picture location that is used in conjunction with sprmCPicLocation") carrying `sprmCFSpec` and `sprmCPicLocation` — a signed 32-bit offset into the container's own `"Data"` stream, a stream with "no predefined structure... referenced from the FIB or from other parts of the file," genuinely optional like `"\x05SummaryInformation"`. `pictures.ts`'s `readInlinePicture` reads what sits at that offset: a 68-byte `PICF` (type/size/border information — `cbHeader`, `mfpf.mm`, `PICF_Shape`, `PICMID`'s own `dxaGoal`/`dyaGoal`/`mx`/`my` and four `Brc80` borders this package does not read), an optional `cchPicName`/`stPicName` pair when `mfpf.mm` is `MM_SHAPEFILE`, then `picture`: a real `OfficeArtInlineSpContainer` ([MS-ODRAW] 2.2.15) — every producer this reader has seen writes this container regardless of `mfpf.mm`'s own value, so there is no simpler, non-OfficeArt path even for the plainest bitmap. The container's own `shape` (an `OfficeArtSpContainer`, [MS-ODRAW] 2.2.14) is skipped whole by its own 8-byte record header's `recLen` — this reader has no need for a shape's own fill/line/position properties — and the first entry of `rgfb` immediately after it, an `OfficeArtBlip` record, is read directly: its own `recInstance` states how many 16-byte MD4 `rgbUid` fields precede the one-byte internal-resource `tag` and the raw file bytes (one for `OfficeArtBlipJPEG`'s RGB/CMYK-1-UID instances and `OfficeArtBlipPNG`'s 1-UID instance, two for each format's own 2-UID sibling), so `blipDataStart`/`blipDataLength` fall out directly from the header once the format is known. `widthPt`/`heightPt` come from `PICMID`'s own `dxaGoal`/`dyaGoal` (the picture's initial size in twips) scaled by `mx`/`my` (each "the ratio, measured in tenths of a percent, between the final display width/height and the initial picture width/height") and converted to points.
185
+
186
+ **Only `OfficeArtBlipJPEG` (`0xF01D`) and `OfficeArtBlipPNG` (`0xF01E`) are decoded** — the two raster formats `document-schema.js`'s `ContentImageBlockSchema` can hold losslessly (`format: "png" | "jpeg" | "svg" | "gif"`). Every other blip kind `readInlinePicture` returns `undefined` for rather than mis-decoding: a metafile (`OfficeArtBlipEMF`/`WMF`/`PICT`) carries a further `OfficeArtMetafileHeader` and, for EMF/WMF, DEFLATE-compressed payload bytes this package has no decompressor for; a raw DIB (`OfficeArtBlipDIB`) has no `ContentImageBlock` format token to hold it under at all without an image codec to re-encode its pixels as PNG, which this package does not have; `OfficeArtBlipTIFF` is likewise a format this schema names no token for. A paragraph whose only content is such an anchor still reads as a genuine, empty `ContentParagraph` — the identical degrade an unresolved anchor already produced before this feature existed — rather than losing the paragraph entirely.
187
+
188
+ **A paragraph is split around a resolved (or attempted) picture, never left as a run.** An inline image is block-level in `document-schema.js`'s own model, so `text/paragraphs.ts`'s `buildParagraphBlocks` splits the paragraph's own text at each `U+0001` anchor into however many `ContentBlock`s the anchors demand — a leading paragraph, the image, a trailing paragraph, and so on for a paragraph carrying more than one — every split paragraph piece keeping the whole paragraph's own alignment/spacing/list attributes unchanged, mirroring how `ooxml.js`'s own docx reader already splits a paragraph around a mid-run page break. A paragraph containing nothing but one unresolved picture anchor (an unsupported blip format, or no `"Data"` stream at all) collapses to the identical single empty `ContentParagraph` a plain anchor-only paragraph always produced; a paragraph that resolves to at least one real block (text, an image) never gains a synthetic empty wrapper alongside it.
189
+
190
+ **Not read: floating/anchored drawn objects and text boxes.** A floating picture is a different anchor entirely — `U+0008` ("A drawn object. See plcfSpa"), referenced by a `PlcfSpa` (`SPA` records, 26 bytes each, one `spid` per anchor) that names a shape inside a document-level `OfficeArtDgContainer` drawing tree rather than a single inline blip a `Chpx` sprm points at directly. That shape need not even be a picture — an arbitrary `OfficeArtSpContainer` (autoshape, connector, text box) — so resolving one properly means walking the whole drawing tree, not decoding one more blip kind; this package does not attempt it, and the anchor character drops exactly as any other unresolved anchor does. `writeDocContent` refuses an image block outright, on either side of this line.
191
+
161
192
  ## Metadata
162
193
 
163
194
  A `.doc`'s title, author, and dates do not live in any [MS-DOC] structure at all — they live in a `"\x05SummaryInformation"` stream, a genuinely different format ([MS-OLEPS] Property Set Streams) that happens to sit beside `WordDocument`/`1Table` in the same [MS-CFB] compound file. `readDocContent` reads that stream when present (`archive-codec`'s `readSummaryInformation`, since the property-set format itself is zero document-format knowledge, exactly as the [MS-CFB] container it sits inside is) and maps it onto `document-schema.js`'s `LayoutMetadata` (`archive-codec`'s own `summaryInformationToLayoutMetadata` — the mapping is format-agnostic, so it lives there rather than being copied in this package, alongside `xls-codec`'s and `ppt-codec`'s identical need for it); `writeDocContent` does the inverse (`src/metadata.ts`'s `layoutMetadataToSummaryInformation`, which validates `createdIso`/`modifiedIso` as real dates and throws a `DocFormatError` naming the offending field before delegating to `archive-codec`'s own mapping — see [Writing](#writing)), including a `"\x05SummaryInformation"` stream in its `writeCompoundFile` call only when the input's metadata actually carries something that stream can hold — an input whose metadata is `{}`, or carries only fields the mapping below has no destination for, produces no stream at all, matching what an absent-metadata read already returns.
@@ -173,7 +204,7 @@ Only the fixed SummaryInformation property set is read or written — the siblin
173
204
 
174
205
  ## Encryption
175
206
 
176
- A `.doc` protected with a password to open uses one of three schemes [MS-DOC] 2.2.6 names, selected by `FibBase`'s own `fEncrypted`/`fObfuscated` flags: RC4 encryption ([MS-DOC] 2.2.6.2, this reader), XOR obfuscation (2.2.6.1), or RC4 CryptoAPI (2.2.6.3, a different, newer `EncryptionHeader` shape). `readDocContent`/`readDocStreams` take an optional `password`, ignored for an unencrypted document; a missing or incorrect password against an RC4-encrypted one throws `DocUnsupportedError` rather than returning a partial or garbled document, and so does an XOR-obfuscated or RC4-CryptoAPI-encrypted one regardless of password, since neither is implemented.
207
+ A `.doc` protected with a password to open uses one of three schemes [MS-DOC] 2.2.6 names, selected by `FibBase`'s own `fEncrypted`/`fObfuscated` flags: RC4 encryption ([MS-DOC] 2.2.6.2, this reader), XOR obfuscation (2.2.6.1, this reader), or RC4 CryptoAPI (2.2.6.3, a different, newer `EncryptionHeader` shape). `readDocContent`/`readDocStreams` take an optional `password`, ignored for an unencrypted document; a missing or incorrect password against an encrypted or obfuscated one throws `DocUnsupportedError` rather than returning a partial or garbled document, and so does an RC4-CryptoAPI-encrypted one regardless of password, since that scheme alone is not implemented.
177
208
 
178
209
  ```ts
179
210
  import { readDocContent } from "doc-codec";
@@ -181,12 +212,14 @@ import { readDocContent } from "doc-codec";
181
212
  const document = readDocContent(bytes, "correct horse battery staple");
182
213
  ```
183
214
 
184
- [MS-DOC] 2.2.6.2's own `EncryptionHeader` _is_ [MS-OFFCRYPTO] 2.3.6.1's RC4 encryption header — byte-identical to what `xls-codec`'s own `FilePass` record carries (ExaDev/documents.js#1108), so `src/encryption.ts` needs no new cryptography, only `archive-codec`'s shared `deriveOfficeRc4BaseHash`/`decryptOfficeRc4` primitives and the doc-specific container layout, which differs from BIFF8's in three real ways rather than being a copy of xls-codec's own scheme:
215
+ [MS-DOC] 2.2.6.2's own `EncryptionHeader` _is_ [MS-OFFCRYPTO] 2.3.6.1's RC4 encryption header — byte-identical to what `xls-codec`'s own `FilePass` record carries (ExaDev/documents.js#1108), so `src/encryption.ts` needs no new cryptography for RC4, only `archive-codec`'s shared `deriveOfficeRc4BaseHash`/`decryptOfficeRc4` primitives and the doc-specific container layout, which differs from BIFF8's in three real ways rather than being a copy of xls-codec's own scheme:
185
216
 
186
217
  - **Location.** The `EncryptionHeader` sits unencrypted at the very start of the Table stream (`0Table`/`1Table`, whichever `FibBase.fWhichTblStm` selects), its own byte length given by `FibBase.lKey` — unlike `FilePass`, an inline record within the Workbook stream.
187
218
  - **Re-keying interval.** 512 bytes, not `xls-codec`'s 1024 (`archive-codec`'s `OFFICE_RC4_DOC_BLOCK_SIZE`) — a real [MS-DOC]-specific value, confirmed against Apache POI's `BinaryRC4Decryptor` independently of `xls-codec`'s own `Biff8DecryptingStream`.
188
219
  - **Per-stream block-zero origin.** WordDocument and Table are each encrypted independently, each with its own block-number counter starting at zero at that stream's own byte 0 — unlike `xls-codec`'s single continuous Workbook-stream offset. WordDocument's own unencrypted prefix is a fixed 68 bytes ([MS-DOC] 2.2.6.1/2.2.6.2's own stated literal); Table's is `FibBase.lKey`, since the `EncryptionHeader` occupying it has to be readable before any key can be derived at all. The Data stream is also encrypted in full per the spec, but this reader does not read the Data stream at all today, so decrypting it is out of scope until something needs to.
189
220
 
221
+ XOR obfuscation (Method 2) needs `archive-codec`'s own `createXorObfuscationKey`/`createXorObfuscationPasswordVerifier`/`createXorObfuscationArray`/`decryptXorObfuscationMethod2` (see [`archive-codec`'s own XOR obfuscation section](../archive-codec/README.md#xor-obfuscation-xlsdoc)) — genuinely simpler than RC4's own container layout, in one real way: `FibBase.lKey` carries the 32-bit password verifier itself here (its high 16 bits `createXorObfuscationKey`'s own output, its low 16 bits `createXorObfuscationPasswordVerifier`'s), not a Table-stream `EncryptionHeader` byte length, so the Table stream needs no unencrypted prefix at all and is obfuscated in full from its own byte 0 — confirmed against LibreOffice's own WW8 import (`ww8par.cxx`'s `DecryptXOR`, called with no prior seek for the Table stream, unlike WordDocument's own explicit 68-byte copy-then-decrypt). WordDocument's own 68-byte unencrypted prefix is unchanged from the RC4 case.
222
+
190
223
  `writeDocContent` never encrypts.
191
224
 
192
225
  ## Writing
@@ -293,7 +326,7 @@ const written = writeDocContent({
293
326
  });
294
327
  ```
295
328
 
296
- `readDocContent` throws a `DocFormatError` when the bytes do not conform to [MS-DOC], and a `DocUnsupportedError` when they conform but use a feature this package deliberately refuses rather than approximates (XOR obfuscation or RC4 CryptoAPI encryption, an RC4-encrypted document given no password or the wrong one, or the `sprmPChgTabs` sentinel above). `writeDocContent` throws a `DocUnsupportedError` for a document, section count, or block kind outside its own scope (see [Writing](#writing)) and a `DocFormatError` for a value that would need a property out of a sprm's own operand range (a font size or indent too large to fit its 2-byte operand, for instance).
329
+ `readDocContent` throws a `DocFormatError` when the bytes do not conform to [MS-DOC], and a `DocUnsupportedError` when they conform but use a feature this package deliberately refuses rather than approximates (RC4 CryptoAPI encryption, an encrypted or obfuscated document given no password or the wrong one, or the `sprmPChgTabs` sentinel above). `writeDocContent` throws a `DocUnsupportedError` for a document, section count, or block kind outside its own scope (see [Writing](#writing)) and a `DocFormatError` for a value that would need a property out of a sprm's own operand range (a font size or indent too large to fit its 2-byte operand, for instance).
297
330
 
298
331
  ## Worker-isomorphic
299
332
 
@@ -0,0 +1,18 @@
1
+ import { t as Prl } from "./sprm-DEyvdOo0.js";
2
+ import { Color } from "document-schema.js";
3
+ //#region src/prop/chp.d.ts
4
+ interface CharacterProperties {
5
+ bold?: boolean;
6
+ italic?: boolean;
7
+ underline?: boolean;
8
+ strike?: boolean;
9
+ sizePt?: number;
10
+ color?: Color;
11
+ fontFamily?: string;
12
+ /** The istd of a character style applied by sprmCIstd, carried so a caller can resolve the style's own name. */
13
+ istd?: number;
14
+ }
15
+ declare function applyCharacterSprms(prls: readonly Prl[], into: CharacterProperties, fonts?: readonly string[]): CharacterProperties;
16
+ declare function characterIstdFromGrpprl(prls: readonly Prl[]): number | undefined;
17
+ //#endregion
18
+ export { applyCharacterSprms as n, characterIstdFromGrpprl as r, CharacterProperties as t };
@@ -0,0 +1,18 @@
1
+ import { t as Prl } from "./sprm-DEyvdOo0.cjs";
2
+ import { Color } from "document-schema.js";
3
+ //#region src/prop/chp.d.ts
4
+ interface CharacterProperties {
5
+ bold?: boolean;
6
+ italic?: boolean;
7
+ underline?: boolean;
8
+ strike?: boolean;
9
+ sizePt?: number;
10
+ color?: Color;
11
+ fontFamily?: string;
12
+ /** The istd of a character style applied by sprmCIstd, carried so a caller can resolve the style's own name. */
13
+ istd?: number;
14
+ }
15
+ declare function applyCharacterSprms(prls: readonly Prl[], into: CharacterProperties, fonts?: readonly string[]): CharacterProperties;
16
+ declare function characterIstdFromGrpprl(prls: readonly Prl[]): number | undefined;
17
+ //#endregion
18
+ export { applyCharacterSprms as n, characterIstdFromGrpprl as r, CharacterProperties as t };
package/dist/detect.cjs CHANGED
@@ -7,6 +7,8 @@ let archive_codec = require("archive-codec");
7
7
  const WORD_DOCUMENT_STREAM = "WordDocument";
8
8
  /** The [MS-OLEPS] Property Set Stream a .doc's title/author/dates live in when present -- a genuinely optional stream, unlike WordDocument, since a valid Word Binary File need not carry document properties at all. */
9
9
  const SUMMARY_INFORMATION_STREAM = "SummaryInformation";
10
+ /** The stream sprmCPicLocation's operand addresses, [MS-DOC] "Data Stream": "has no predefined structure. It contains data that is referenced from the FIB or from other parts of the file. This stream need not be present if there are no references to it." -- genuinely optional, like SUMMARY_INFORMATION_STREAM, since a document with no pictures need not carry one. */
11
+ const DATA_STREAM = "Data";
10
12
  function isDocBytes(bytes) {
11
13
  if (!(0, archive_codec.isCompoundFile)(bytes)) return false;
12
14
  let streams;
@@ -20,6 +22,7 @@ function isDocBytes(bytes) {
20
22
  return require_bytes.readUint16LE(wordDocument.bytes, 0) === require_fib_offsets.FIB_W_IDENT;
21
23
  }
22
24
  //#endregion
25
+ exports.DATA_STREAM = DATA_STREAM;
23
26
  exports.SUMMARY_INFORMATION_STREAM = SUMMARY_INFORMATION_STREAM;
24
27
  exports.WORD_DOCUMENT_STREAM = WORD_DOCUMENT_STREAM;
25
28
  exports.isDocBytes = isDocBytes;
package/dist/detect.d.cts CHANGED
@@ -3,6 +3,8 @@
3
3
  declare const WORD_DOCUMENT_STREAM = "WordDocument";
4
4
  /** The [MS-OLEPS] Property Set Stream a .doc's title/author/dates live in when present -- a genuinely optional stream, unlike WordDocument, since a valid Word Binary File need not carry document properties at all. */
5
5
  declare const SUMMARY_INFORMATION_STREAM = "SummaryInformation";
6
+ /** The stream sprmCPicLocation's operand addresses, [MS-DOC] "Data Stream": "has no predefined structure. It contains data that is referenced from the FIB or from other parts of the file. This stream need not be present if there are no references to it." -- genuinely optional, like SUMMARY_INFORMATION_STREAM, since a document with no pictures need not carry one. */
7
+ declare const DATA_STREAM = "Data";
6
8
  declare function isDocBytes(bytes: Uint8Array<ArrayBuffer>): boolean;
7
9
  //#endregion
8
- export { SUMMARY_INFORMATION_STREAM, WORD_DOCUMENT_STREAM, isDocBytes };
10
+ export { DATA_STREAM, SUMMARY_INFORMATION_STREAM, WORD_DOCUMENT_STREAM, isDocBytes };
package/dist/detect.d.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  declare const WORD_DOCUMENT_STREAM = "WordDocument";
4
4
  /** The [MS-OLEPS] Property Set Stream a .doc's title/author/dates live in when present -- a genuinely optional stream, unlike WordDocument, since a valid Word Binary File need not carry document properties at all. */
5
5
  declare const SUMMARY_INFORMATION_STREAM = "SummaryInformation";
6
+ /** The stream sprmCPicLocation's operand addresses, [MS-DOC] "Data Stream": "has no predefined structure. It contains data that is referenced from the FIB or from other parts of the file. This stream need not be present if there are no references to it." -- genuinely optional, like SUMMARY_INFORMATION_STREAM, since a document with no pictures need not carry one. */
7
+ declare const DATA_STREAM = "Data";
6
8
  declare function isDocBytes(bytes: Uint8Array<ArrayBuffer>): boolean;
7
9
  //#endregion
8
- export { SUMMARY_INFORMATION_STREAM, WORD_DOCUMENT_STREAM, isDocBytes };
10
+ export { DATA_STREAM, SUMMARY_INFORMATION_STREAM, WORD_DOCUMENT_STREAM, isDocBytes };
package/dist/detect.js CHANGED
@@ -6,6 +6,8 @@ import { isCompoundFile, readCompoundFile } from "archive-codec";
6
6
  const WORD_DOCUMENT_STREAM = "WordDocument";
7
7
  /** The [MS-OLEPS] Property Set Stream a .doc's title/author/dates live in when present -- a genuinely optional stream, unlike WordDocument, since a valid Word Binary File need not carry document properties at all. */
8
8
  const SUMMARY_INFORMATION_STREAM = "SummaryInformation";
9
+ /** The stream sprmCPicLocation's operand addresses, [MS-DOC] "Data Stream": "has no predefined structure. It contains data that is referenced from the FIB or from other parts of the file. This stream need not be present if there are no references to it." -- genuinely optional, like SUMMARY_INFORMATION_STREAM, since a document with no pictures need not carry one. */
10
+ const DATA_STREAM = "Data";
9
11
  function isDocBytes(bytes) {
10
12
  if (!isCompoundFile(bytes)) return false;
11
13
  let streams;
@@ -19,4 +21,4 @@ function isDocBytes(bytes) {
19
21
  return readUint16LE(wordDocument.bytes, 0) === FIB_W_IDENT;
20
22
  }
21
23
  //#endregion
22
- export { SUMMARY_INFORMATION_STREAM, WORD_DOCUMENT_STREAM, isDocBytes };
24
+ export { DATA_STREAM, SUMMARY_INFORMATION_STREAM, WORD_DOCUMENT_STREAM, isDocBytes };
@@ -44,27 +44,61 @@ function verifyPassword(baseHash, header) {
44
44
  if (!(computedHash.length === decryptedVerifierHash.length && computedHash.every((byte, index) => byte === decryptedVerifierHash[index]))) throw new require_errors.DocUnsupportedError("incorrect password for RC4-encrypted document");
45
45
  }
46
46
  /** Decrypts everything after `prefixLength` bytes of `stream`, leaving the prefix itself untouched -- WORD_DOCUMENT_UNENCRYPTED_PREFIX for WordDocument, FibBase.lKey for Table, each stream's own block-number counter starting fresh at its own byte 0 (this file's own top comment, point 3). */
47
- function decryptStream(baseHash, stream, prefixLength) {
47
+ function decryptStreamRc4(baseHash, stream, prefixLength) {
48
48
  const decrypted = new Uint8Array(stream.length);
49
49
  decrypted.set(stream.subarray(0, prefixLength), 0);
50
50
  decrypted.set((0, archive_codec.decryptOfficeRc4)(baseHash, prefixLength, stream.subarray(prefixLength), archive_codec.OFFICE_RC4_DOC_BLOCK_SIZE), prefixLength);
51
51
  return decrypted;
52
52
  }
53
- /**
54
- * Decrypts an RC4-encrypted (fEncrypted=1, fObfuscated=0) document's WordDocument and Table streams given the password, verifying it first against the Table stream's own EncryptionHeader.
55
- *
56
- * Throws `DocUnsupportedError` for a missing password, an incorrect one, or an encryption scheme this module does not implement (RC4 CryptoAPI) -- there is no partial or best-effort result to return in any of those cases.
57
- */
58
- function decryptDocStreams(wordDocument, table, password) {
59
- if (password === void 0) throw new require_errors.DocUnsupportedError("this document is RC4-encrypted ([MS-DOC] 2.2.6.2); call readDocContent with a password to decrypt it");
53
+ /** Decrypts an RC4-encrypted (fEncrypted=1, fObfuscated=0) document's WordDocument and Table streams given the password, verifying it first against the Table stream's own EncryptionHeader. */
54
+ function decryptDocStreamsRc4(wordDocument, table, password) {
60
55
  const header = readRc4Header(table);
61
56
  const baseHash = (0, archive_codec.deriveOfficeRc4BaseHash)(password, header.salt);
62
57
  verifyPassword(baseHash, header);
63
58
  const lKey = require_bytes.readUint32LE(wordDocument, 14);
64
59
  return {
65
- wordDocument: decryptStream(baseHash, wordDocument, WORD_DOCUMENT_UNENCRYPTED_PREFIX),
66
- table: decryptStream(baseHash, table, lKey)
60
+ wordDocument: decryptStreamRc4(baseHash, wordDocument, WORD_DOCUMENT_UNENCRYPTED_PREFIX),
61
+ table: decryptStreamRc4(baseHash, table, lKey)
67
62
  };
68
63
  }
64
+ /** Decrypts everything after `prefixLength` bytes of `stream` against Method 2's own transform, leaving the prefix itself untouched -- `initialIndex` is `prefixLength % 16`, the XorArrayIndex the decrypted span's own first byte starts at (confirmed against LibreOffice's own `ww8par.cxx` `DecryptXOR`, whose `InitCipher(); Skip(nSt)` is exactly this: reset to 0, then advance by the skipped prefix's own length mod 16). */
65
+ function decryptStreamXor(array, stream, prefixLength) {
66
+ const decrypted = new Uint8Array(stream.length);
67
+ decrypted.set(stream.subarray(0, prefixLength), 0);
68
+ decrypted.set((0, archive_codec.decryptXorObfuscationMethod2)(array, stream.subarray(prefixLength), prefixLength % archive_codec.XOR_OBFUSCATION_ARRAY_LENGTH), prefixLength);
69
+ return decrypted;
70
+ }
71
+ /**
72
+ * Decrypts an XOR-obfuscated (fEncrypted=1, fObfuscated=1) document's WordDocument and Table streams given the password, verifying it first against FibBase's own lKey field -- not a Table-stream EncryptionHeader the way RC4 needs, see this file's own top comment for why. The Table stream carries no unencrypted prefix under this scheme, unlike RC4's own FibBase.lKey-byte EncryptionHeader; Data (also obfuscated per [MS-DOC], from its own byte 0) is out of scope, matching decryptDocStreamsRc4 and this package's own read.ts, which does not read the Data stream at all.
73
+ */
74
+ function decryptDocStreamsXor(wordDocument, table, password) {
75
+ const lKey = require_bytes.readUint32LE(wordDocument, 14);
76
+ const headerKey = lKey >>> 16 & 65535;
77
+ const headerVerifier = lKey & 65535;
78
+ let computedKey;
79
+ let computedVerifier;
80
+ try {
81
+ computedKey = (0, archive_codec.createXorObfuscationKey)(password);
82
+ computedVerifier = (0, archive_codec.createXorObfuscationPasswordVerifier)(password);
83
+ } catch (error) {
84
+ if (error instanceof RangeError) throw new require_errors.DocUnsupportedError("incorrect password for XOR-obfuscated document");
85
+ throw error;
86
+ }
87
+ if (computedKey !== headerKey || computedVerifier !== headerVerifier) throw new require_errors.DocUnsupportedError("incorrect password for XOR-obfuscated document");
88
+ const array = (0, archive_codec.createXorObfuscationArray)(password, archive_codec.XOR_OBFUSCATION_ROTATE_DISTANCE_METHOD2);
89
+ return {
90
+ wordDocument: decryptStreamXor(array, wordDocument, WORD_DOCUMENT_UNENCRYPTED_PREFIX),
91
+ table: decryptStreamXor(array, table, 0)
92
+ };
93
+ }
94
+ /**
95
+ * Decrypts an encrypted document's WordDocument and Table streams given the password, dispatching on `fObfuscated` between [MS-DOC] 2.2.6.2's RC4 encryption header and 2.2.6.1's XOR obfuscation (Method 2).
96
+ *
97
+ * Throws `DocUnsupportedError` for a missing password, an incorrect one, or an encryption scheme this module does not implement (RC4 CryptoAPI) -- there is no partial or best-effort result to return in any of those cases.
98
+ */
99
+ function decryptDocStreams(wordDocument, table, password, fObfuscated) {
100
+ if (password === void 0) throw new require_errors.DocUnsupportedError(`this document is ${fObfuscated ? "XOR-obfuscated ([MS-DOC] 2.2.6.1)" : "RC4-encrypted ([MS-DOC] 2.2.6.2)"}; call readDocContent with a password to decrypt it`);
101
+ return fObfuscated ? decryptDocStreamsXor(wordDocument, table, password) : decryptDocStreamsRc4(wordDocument, table, password);
102
+ }
69
103
  //#endregion
70
104
  exports.decryptDocStreams = decryptDocStreams;
@@ -4,10 +4,10 @@ interface DecryptedDocStreams {
4
4
  readonly table: Uint8Array<ArrayBuffer>;
5
5
  }
6
6
  /**
7
- * Decrypts an RC4-encrypted (fEncrypted=1, fObfuscated=0) document's WordDocument and Table streams given the password, verifying it first against the Table stream's own EncryptionHeader.
7
+ * Decrypts an encrypted document's WordDocument and Table streams given the password, dispatching on `fObfuscated` between [MS-DOC] 2.2.6.2's RC4 encryption header and 2.2.6.1's XOR obfuscation (Method 2).
8
8
  *
9
9
  * Throws `DocUnsupportedError` for a missing password, an incorrect one, or an encryption scheme this module does not implement (RC4 CryptoAPI) -- there is no partial or best-effort result to return in any of those cases.
10
10
  */
11
- declare function decryptDocStreams(wordDocument: Uint8Array<ArrayBuffer>, table: Uint8Array<ArrayBuffer>, password: string | undefined): DecryptedDocStreams;
11
+ declare function decryptDocStreams(wordDocument: Uint8Array<ArrayBuffer>, table: Uint8Array<ArrayBuffer>, password: string | undefined, fObfuscated: boolean): DecryptedDocStreams;
12
12
  //#endregion
13
13
  export { DecryptedDocStreams, decryptDocStreams };
@@ -4,10 +4,10 @@ interface DecryptedDocStreams {
4
4
  readonly table: Uint8Array<ArrayBuffer>;
5
5
  }
6
6
  /**
7
- * Decrypts an RC4-encrypted (fEncrypted=1, fObfuscated=0) document's WordDocument and Table streams given the password, verifying it first against the Table stream's own EncryptionHeader.
7
+ * Decrypts an encrypted document's WordDocument and Table streams given the password, dispatching on `fObfuscated` between [MS-DOC] 2.2.6.2's RC4 encryption header and 2.2.6.1's XOR obfuscation (Method 2).
8
8
  *
9
9
  * Throws `DocUnsupportedError` for a missing password, an incorrect one, or an encryption scheme this module does not implement (RC4 CryptoAPI) -- there is no partial or best-effort result to return in any of those cases.
10
10
  */
11
- declare function decryptDocStreams(wordDocument: Uint8Array<ArrayBuffer>, table: Uint8Array<ArrayBuffer>, password: string | undefined): DecryptedDocStreams;
11
+ declare function decryptDocStreams(wordDocument: Uint8Array<ArrayBuffer>, table: Uint8Array<ArrayBuffer>, password: string | undefined, fObfuscated: boolean): DecryptedDocStreams;
12
12
  //#endregion
13
13
  export { DecryptedDocStreams, decryptDocStreams };
@@ -1,7 +1,7 @@
1
1
  import { DocFormatError, DocUnsupportedError } from "./errors.js";
2
2
  import { readUint16LE, readUint32LE } from "./bytes.js";
3
3
  import "./fib/offsets.js";
4
- import { OFFICE_RC4_DOC_BLOCK_SIZE, OFFICE_RC4_VERIFIER_LENGTH, decryptOfficeRc4, deriveOfficeRc4BaseHash, md5 } from "archive-codec";
4
+ import { OFFICE_RC4_DOC_BLOCK_SIZE, OFFICE_RC4_VERIFIER_LENGTH, XOR_OBFUSCATION_ARRAY_LENGTH, XOR_OBFUSCATION_ROTATE_DISTANCE_METHOD2, createXorObfuscationArray, createXorObfuscationKey, createXorObfuscationPasswordVerifier, decryptOfficeRc4, decryptXorObfuscationMethod2, deriveOfficeRc4BaseHash, md5 } from "archive-codec";
5
5
  //#region src/encryption.ts
6
6
  /** [MS-DOC] 2.2.6.2's own EncryptionHeader field layout, byte offsets within the Table stream's own first FibBase.lKey bytes: EncryptionVersionInfo (vMajor/vMinor, 2 bytes each) at 0, then Salt/EncryptedVerifier/EncryptedVerifierHash, each OFFICE_RC4_VERIFIER_LENGTH (16) bytes, back to back. */
7
7
  const HEADER_OFFSET = {
@@ -43,27 +43,61 @@ function verifyPassword(baseHash, header) {
43
43
  if (!(computedHash.length === decryptedVerifierHash.length && computedHash.every((byte, index) => byte === decryptedVerifierHash[index]))) throw new DocUnsupportedError("incorrect password for RC4-encrypted document");
44
44
  }
45
45
  /** Decrypts everything after `prefixLength` bytes of `stream`, leaving the prefix itself untouched -- WORD_DOCUMENT_UNENCRYPTED_PREFIX for WordDocument, FibBase.lKey for Table, each stream's own block-number counter starting fresh at its own byte 0 (this file's own top comment, point 3). */
46
- function decryptStream(baseHash, stream, prefixLength) {
46
+ function decryptStreamRc4(baseHash, stream, prefixLength) {
47
47
  const decrypted = new Uint8Array(stream.length);
48
48
  decrypted.set(stream.subarray(0, prefixLength), 0);
49
49
  decrypted.set(decryptOfficeRc4(baseHash, prefixLength, stream.subarray(prefixLength), OFFICE_RC4_DOC_BLOCK_SIZE), prefixLength);
50
50
  return decrypted;
51
51
  }
52
- /**
53
- * Decrypts an RC4-encrypted (fEncrypted=1, fObfuscated=0) document's WordDocument and Table streams given the password, verifying it first against the Table stream's own EncryptionHeader.
54
- *
55
- * Throws `DocUnsupportedError` for a missing password, an incorrect one, or an encryption scheme this module does not implement (RC4 CryptoAPI) -- there is no partial or best-effort result to return in any of those cases.
56
- */
57
- function decryptDocStreams(wordDocument, table, password) {
58
- if (password === void 0) throw new DocUnsupportedError("this document is RC4-encrypted ([MS-DOC] 2.2.6.2); call readDocContent with a password to decrypt it");
52
+ /** Decrypts an RC4-encrypted (fEncrypted=1, fObfuscated=0) document's WordDocument and Table streams given the password, verifying it first against the Table stream's own EncryptionHeader. */
53
+ function decryptDocStreamsRc4(wordDocument, table, password) {
59
54
  const header = readRc4Header(table);
60
55
  const baseHash = deriveOfficeRc4BaseHash(password, header.salt);
61
56
  verifyPassword(baseHash, header);
62
57
  const lKey = readUint32LE(wordDocument, 14);
63
58
  return {
64
- wordDocument: decryptStream(baseHash, wordDocument, WORD_DOCUMENT_UNENCRYPTED_PREFIX),
65
- table: decryptStream(baseHash, table, lKey)
59
+ wordDocument: decryptStreamRc4(baseHash, wordDocument, WORD_DOCUMENT_UNENCRYPTED_PREFIX),
60
+ table: decryptStreamRc4(baseHash, table, lKey)
66
61
  };
67
62
  }
63
+ /** Decrypts everything after `prefixLength` bytes of `stream` against Method 2's own transform, leaving the prefix itself untouched -- `initialIndex` is `prefixLength % 16`, the XorArrayIndex the decrypted span's own first byte starts at (confirmed against LibreOffice's own `ww8par.cxx` `DecryptXOR`, whose `InitCipher(); Skip(nSt)` is exactly this: reset to 0, then advance by the skipped prefix's own length mod 16). */
64
+ function decryptStreamXor(array, stream, prefixLength) {
65
+ const decrypted = new Uint8Array(stream.length);
66
+ decrypted.set(stream.subarray(0, prefixLength), 0);
67
+ decrypted.set(decryptXorObfuscationMethod2(array, stream.subarray(prefixLength), prefixLength % XOR_OBFUSCATION_ARRAY_LENGTH), prefixLength);
68
+ return decrypted;
69
+ }
70
+ /**
71
+ * Decrypts an XOR-obfuscated (fEncrypted=1, fObfuscated=1) document's WordDocument and Table streams given the password, verifying it first against FibBase's own lKey field -- not a Table-stream EncryptionHeader the way RC4 needs, see this file's own top comment for why. The Table stream carries no unencrypted prefix under this scheme, unlike RC4's own FibBase.lKey-byte EncryptionHeader; Data (also obfuscated per [MS-DOC], from its own byte 0) is out of scope, matching decryptDocStreamsRc4 and this package's own read.ts, which does not read the Data stream at all.
72
+ */
73
+ function decryptDocStreamsXor(wordDocument, table, password) {
74
+ const lKey = readUint32LE(wordDocument, 14);
75
+ const headerKey = lKey >>> 16 & 65535;
76
+ const headerVerifier = lKey & 65535;
77
+ let computedKey;
78
+ let computedVerifier;
79
+ try {
80
+ computedKey = createXorObfuscationKey(password);
81
+ computedVerifier = createXorObfuscationPasswordVerifier(password);
82
+ } catch (error) {
83
+ if (error instanceof RangeError) throw new DocUnsupportedError("incorrect password for XOR-obfuscated document");
84
+ throw error;
85
+ }
86
+ if (computedKey !== headerKey || computedVerifier !== headerVerifier) throw new DocUnsupportedError("incorrect password for XOR-obfuscated document");
87
+ const array = createXorObfuscationArray(password, XOR_OBFUSCATION_ROTATE_DISTANCE_METHOD2);
88
+ return {
89
+ wordDocument: decryptStreamXor(array, wordDocument, WORD_DOCUMENT_UNENCRYPTED_PREFIX),
90
+ table: decryptStreamXor(array, table, 0)
91
+ };
92
+ }
93
+ /**
94
+ * Decrypts an encrypted document's WordDocument and Table streams given the password, dispatching on `fObfuscated` between [MS-DOC] 2.2.6.2's RC4 encryption header and 2.2.6.1's XOR obfuscation (Method 2).
95
+ *
96
+ * Throws `DocUnsupportedError` for a missing password, an incorrect one, or an encryption scheme this module does not implement (RC4 CryptoAPI) -- there is no partial or best-effort result to return in any of those cases.
97
+ */
98
+ function decryptDocStreams(wordDocument, table, password, fObfuscated) {
99
+ if (password === void 0) throw new DocUnsupportedError(`this document is ${fObfuscated ? "XOR-obfuscated ([MS-DOC] 2.2.6.1)" : "RC4-encrypted ([MS-DOC] 2.2.6.2)"}; call readDocContent with a password to decrypt it`);
100
+ return fObfuscated ? decryptDocStreamsXor(wordDocument, table, password) : decryptDocStreamsRc4(wordDocument, table, password);
101
+ }
68
102
  //#endregion
69
103
  export { decryptDocStreams };
package/dist/fib/fib.cjs CHANGED
@@ -49,6 +49,20 @@ function parseFib(wordDocument) {
49
49
  lcbClx: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.lcbClx),
50
50
  fcPlcfSed: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.fcPlcfSed),
51
51
  lcbPlcfSed: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.lcbPlcfSed),
52
+ fcPlcffndRef: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.fcPlcffndRef),
53
+ lcbPlcffndRef: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.lcbPlcffndRef),
54
+ fcPlcffndTxt: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.fcPlcffndTxt),
55
+ lcbPlcffndTxt: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.lcbPlcffndTxt),
56
+ fcPlcfandRef: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.fcPlcfandRef),
57
+ lcbPlcfandRef: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.lcbPlcfandRef),
58
+ fcPlcfandTxt: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.fcPlcfandTxt),
59
+ lcbPlcfandTxt: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.lcbPlcfandTxt),
60
+ fcPlcfendRef: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.fcPlcfendRef),
61
+ lcbPlcfendRef: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.lcbPlcfendRef),
62
+ fcPlcfendTxt: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.fcPlcfendTxt),
63
+ lcbPlcfendTxt: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.lcbPlcfendTxt),
64
+ fcPlcfHdd: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.fcPlcfHdd),
65
+ lcbPlcfHdd: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.lcbPlcfHdd),
52
66
  fcSttbfFfn: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.fcSttbfFfn),
53
67
  lcbSttbfFfn: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.lcbSttbfFfn),
54
68
  fcPlfLst: fcLcb(require_fib_offsets.FC_LCB_VALUE_INDEX.fcPlfLst),
@@ -1,2 +1,2 @@
1
- import { i as tableStreamName, n as parseFib, r as peekFibBaseFlags, t as Fib } from "../fib-DWFta19r.cjs";
1
+ import { i as tableStreamName, n as parseFib, r as peekFibBaseFlags, t as Fib } from "../fib-6BFIx153.cjs";
2
2
  export { Fib, parseFib, peekFibBaseFlags, tableStreamName };
package/dist/fib/fib.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { i as tableStreamName, n as parseFib, r as peekFibBaseFlags, t as Fib } from "../fib-DWFta19r.js";
1
+ import { i as tableStreamName, n as parseFib, r as peekFibBaseFlags, t as Fib } from "../fib-6BFIx153.js";
2
2
  export { Fib, parseFib, peekFibBaseFlags, tableStreamName };
package/dist/fib/fib.js CHANGED
@@ -48,6 +48,20 @@ function parseFib(wordDocument) {
48
48
  lcbClx: fcLcb(FC_LCB_VALUE_INDEX.lcbClx),
49
49
  fcPlcfSed: fcLcb(FC_LCB_VALUE_INDEX.fcPlcfSed),
50
50
  lcbPlcfSed: fcLcb(FC_LCB_VALUE_INDEX.lcbPlcfSed),
51
+ fcPlcffndRef: fcLcb(FC_LCB_VALUE_INDEX.fcPlcffndRef),
52
+ lcbPlcffndRef: fcLcb(FC_LCB_VALUE_INDEX.lcbPlcffndRef),
53
+ fcPlcffndTxt: fcLcb(FC_LCB_VALUE_INDEX.fcPlcffndTxt),
54
+ lcbPlcffndTxt: fcLcb(FC_LCB_VALUE_INDEX.lcbPlcffndTxt),
55
+ fcPlcfandRef: fcLcb(FC_LCB_VALUE_INDEX.fcPlcfandRef),
56
+ lcbPlcfandRef: fcLcb(FC_LCB_VALUE_INDEX.lcbPlcfandRef),
57
+ fcPlcfandTxt: fcLcb(FC_LCB_VALUE_INDEX.fcPlcfandTxt),
58
+ lcbPlcfandTxt: fcLcb(FC_LCB_VALUE_INDEX.lcbPlcfandTxt),
59
+ fcPlcfendRef: fcLcb(FC_LCB_VALUE_INDEX.fcPlcfendRef),
60
+ lcbPlcfendRef: fcLcb(FC_LCB_VALUE_INDEX.lcbPlcfendRef),
61
+ fcPlcfendTxt: fcLcb(FC_LCB_VALUE_INDEX.fcPlcfendTxt),
62
+ lcbPlcfendTxt: fcLcb(FC_LCB_VALUE_INDEX.lcbPlcfendTxt),
63
+ fcPlcfHdd: fcLcb(FC_LCB_VALUE_INDEX.fcPlcfHdd),
64
+ lcbPlcfHdd: fcLcb(FC_LCB_VALUE_INDEX.lcbPlcfHdd),
51
65
  fcSttbfFfn: fcLcb(FC_LCB_VALUE_INDEX.fcSttbfFfn),
52
66
  lcbSttbfFfn: fcLcb(FC_LCB_VALUE_INDEX.lcbSttbfFfn),
53
67
  fcPlfLst: fcLcb(FC_LCB_VALUE_INDEX.fcPlfLst),