@patrickdk77/office-defuse 0.0.0-stage → 0.1.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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +317 -2
  3. package/dist/biff.d.ts +62 -0
  4. package/dist/biff.js +95 -0
  5. package/dist/cfb.d.ts +58 -0
  6. package/dist/cfb.js +456 -0
  7. package/dist/context.d.ts +66 -0
  8. package/dist/context.js +121 -0
  9. package/dist/crc32.d.ts +8 -0
  10. package/dist/crc32.js +58 -0
  11. package/dist/engine.d.ts +5 -0
  12. package/dist/engine.js +200 -0
  13. package/dist/errors.d.ts +19 -0
  14. package/dist/errors.js +33 -0
  15. package/dist/fields.d.ts +29 -0
  16. package/dist/fields.js +97 -0
  17. package/dist/findings.d.ts +25 -0
  18. package/dist/findings.js +188 -0
  19. package/dist/formats.d.ts +10 -0
  20. package/dist/formats.js +46 -0
  21. package/dist/formulas.d.ts +22 -0
  22. package/dist/formulas.js +77 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.js +30 -0
  25. package/dist/index.mjs +3 -0
  26. package/dist/io.d.ts +23 -0
  27. package/dist/io.js +139 -0
  28. package/dist/links.d.ts +26 -0
  29. package/dist/links.js +145 -0
  30. package/dist/msg.d.ts +7 -0
  31. package/dist/msg.js +97 -0
  32. package/dist/objects.d.ts +42 -0
  33. package/dist/objects.js +134 -0
  34. package/dist/odf.d.ts +3 -0
  35. package/dist/odf.js +297 -0
  36. package/dist/offcrypto.d.ts +31 -0
  37. package/dist/offcrypto.js +408 -0
  38. package/dist/ole.d.ts +12 -0
  39. package/dist/ole.js +127 -0
  40. package/dist/oleobj.d.ts +74 -0
  41. package/dist/oleobj.js +198 -0
  42. package/dist/ooxml.d.ts +8 -0
  43. package/dist/ooxml.js +468 -0
  44. package/dist/package.d.ts +3 -0
  45. package/dist/package.js +21 -0
  46. package/dist/ppt.d.ts +8 -0
  47. package/dist/ppt.js +241 -0
  48. package/dist/rtf.d.ts +8 -0
  49. package/dist/rtf.js +539 -0
  50. package/dist/score.d.ts +17 -0
  51. package/dist/score.js +63 -0
  52. package/dist/types.d.ts +274 -0
  53. package/dist/types.js +152 -0
  54. package/dist/word.d.ts +9 -0
  55. package/dist/word.js +160 -0
  56. package/dist/xls.d.ts +27 -0
  57. package/dist/xls.js +174 -0
  58. package/dist/xlsb.d.ts +19 -0
  59. package/dist/xlsb.js +101 -0
  60. package/dist/xml.d.ts +256 -0
  61. package/dist/xml.js +1105 -0
  62. package/dist/zip-writer.d.ts +38 -0
  63. package/dist/zip-writer.js +182 -0
  64. package/dist/zip.d.ts +55 -0
  65. package/dist/zip.js +535 -0
  66. package/package.json +96 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 patrickdk77
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 CHANGED
@@ -1,3 +1,318 @@
1
- # Temporary Holding Version
1
+ # office-defuse
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ office-defuse reads Word, Excel, PowerPoint and Visio files, in their ZIP formats and in the compound file formats before them, OpenDocument packages, Outlook messages, Publisher and Project files, and RTF. It reports VBA, WordBasic and Excel 4.0 macros, LibreOffice Basic and scripts, DDE, fields and events that run code, templates and objects loaded from outside the file, embedded OLE objects, Equation Editor objects, attachments, ActiveX controls and unsafe links. It scores the risk from 0 to 100.
4
+
5
+ This version inspects and changes nothing.
6
+
7
+ It has no runtime dependencies and keeps memory bounded. It reads ZIP archives, compound files and XML strictly, and refuses anything two readers could take in different ways. RTF it reads the way Word does, quirks included.
8
+
9
+ ```
10
+ npm install @patrickdk77/office-defuse
11
+ ```
12
+
13
+ Node 18 or later.
14
+
15
+ ## Quick start
16
+
17
+ ```js
18
+ const { inspectOffice } = require('@patrickdk77/office-defuse');
19
+
20
+ const report = await inspectOffice(bytes);
21
+ report.status; // 'clean', 'strippable' or 'rejected'
22
+ report.risk; // 'NONE', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL' or 'UNKNOWN'
23
+ report.score; // 0 to 100, null when the file could not be inspected
24
+ report.format; // 'docx', 'xls', 'msg', 'rtf', ... or '' when not recognized
25
+ report.mimeType; // the MIME type of that format, or ''
26
+ report.findings; // [{ category, detail, description, action, location, data }]
27
+ ```
28
+
29
+ ES modules work too: `import { inspectOffice } from '@patrickdk77/office-defuse'`.
30
+
31
+ `strippable` means at least one finding has the action `strip`, content a defused copy would leave out. `rejected` means a finding has the action `reject`, or the run stopped before it read the whole file.
32
+
33
+ ### Files and S3
34
+
35
+ `inspectOfficeSource` reads through a source with `size()` and `read(offset, length)`, so a local file or an S3 object never has to sit in memory. `fileSource` and `bufferSource` are included. For S3, `read` can be a ranged GetObject.
36
+
37
+ ```js
38
+ const { inspectOfficeSource, fileSource } = require('@patrickdk77/office-defuse');
39
+
40
+ await inspectOfficeSource(fileSource('upload.docx'));
41
+
42
+ const source = {
43
+ size: async () => contentLength,
44
+ read: async (offset, length) => getObjectRange(bucket, key, offset, offset + length - 1),
45
+ };
46
+ await inspectOfficeSource(source);
47
+ ```
48
+
49
+ `inspectOfficeSource` calls the source's `close()`, if it has one, when the run ends. A `read` must return exactly the bytes asked for, or fewer only at the end of the source. Any other length fails the run with an error whose `code` is `EIO`. An error the source throws comes back unchanged, instead of a verdict on the file.
50
+
51
+ ## Formats
52
+
53
+ The format comes from the content, never from the file name.
54
+
55
+ | Family | ZIP formats | Compound file and RTF formats |
56
+ |---|---|---|
57
+ | Word | .docx .docm .dotx .dotm | .doc .dot from Word 6.0 to 2003, and .rtf |
58
+ | Excel | .xlsx .xlsm .xltx .xltm .xlam .xlsb | .xls .xlt .xla from Excel 5.0 to 2003 |
59
+ | PowerPoint | .pptx .pptm .potx .potm .ppsx .ppsm .ppam .sldx .sldm | .ppt from PowerPoint 97 to 2003 |
60
+ | Visio | .vsdx .vsdm .vstx .vstm .vssx .vssm | .vsd from Visio 2003 to 2010 |
61
+ | OpenDocument | .odt .ott .ods .ots .odp .otp .odg .otg .odf .odm .odb | |
62
+ | Outlook | | .msg .oft |
63
+ | Publisher | | .pub |
64
+ | Project | | .mpp |
65
+
66
+ `[Content_Types].xml` marks an Office Open XML package. The package relationship of type `officeDocument`, transitional or strict, or Visio's `document`, names the main part, and its content type gives the format. The `mimetype` entry marks an OpenDocument package, or the manifest's entry for `/` when there is no mimetype entry.
67
+
68
+ A ZIP with both markers is rejected as PACKAGE/AMBIGUOUS_FORMAT, since Office could read one and LibreOffice the other. So is an OOXML package with two main parts, and an ODF package whose mimetype and manifest name different formats. Anything else is PACKAGE/UNRECOGNIZED.
69
+
70
+ A .xlsb gets the package checks. Its sheets are binary records, and the formulas in them go unread. Its external link parts are binary too, and office-defuse reads their link records for DDE links. A .odb gets every ODF check, and the database under `database/` goes unread.
71
+
72
+ A file that starts with the compound file signature is a compound file, MS-CFB. The streams and storages at its root name the format: `WordDocument` for Word, `Workbook` or `Book` for Excel, `PowerPoint Document`, `VisioDocument`, `__properties_version1.0` for an Outlook message, a `Quill` storage or Publisher's class id beside a `Contents` stream for Publisher, and a `Props` stream beside a storage named by a number for Project. Word's FIB says whether a document is a template, and Excel's records whether a workbook is a template or an add-in. An Outlook template has a class id of its own. A PowerPoint 97 to 2003 template or show reports its format as .ppt, and a Visio stencil or template as .vsd.
73
+
74
+ A compound file with two of these is PACKAGE/AMBIGUOUS_FORMAT. One with none is PACKAGE/UNRECOGNIZED, and so is a `WordDocument` stream that holds no Word 6.0 to 2003 FIB. A PowerPoint 4.0 file, a bare OLE object and a `vbaProject.bin` on its own all land there.
75
+
76
+ An encrypted .docx, .xlsx or .pptx is a compound file holding `EncryptionInfo` and `EncryptedPackage`. See [Encrypted files](#encrypted-files).
77
+
78
+ A file that starts with `{\rt` is RTF, as Word takes it. A compound file or RTF file in which the ZIP reader also finds an archive is PACKAGE/AMBIGUOUS_FORMAT. A file with the compound file signature whose compound file does not read, but whose ZIP archive does, is that ZIP archive with LEADING_BYTES.
79
+
80
+ ## What it reports
81
+
82
+ In a ZIP package:
83
+
84
+ - VBA projects, Excel 4.0 macro sheets, and OpenDocument `Basic/`, `Scripts/` and `Dialogs/` folders. These go by content type, relationship type or folder name, and a folder counts when it holds a file that is not empty. office-defuse parses no OLE part of a package, such as `vbaProject.bin`.
85
+ - OpenDocument script event listeners that name a script URL or macro. Those that fire as the document opens or is created get a finding of their own. LibreOffice ignores a listener whose URL and macro name are both empty, and so does office-defuse. Visio ShapeSheet cells that call RUNADDON, RUNADDONWARGS, CALLTHIS, RUNMACRO, OPENFILE or DOOLEVERB.
86
+ - Links and actions that run macros: `vnd.sun.star.script:` and `macro:` targets, PowerPoint's `ppaction://macro`, Excel drawing shapes with a `macro` attribute, and form controls with an `x:FmlaMacro`. Ribbon callbacks such as `onLoad` and `onAction`. OpenDocument menu, toolbar and key settings under `Configurations2/`. LibreOffice writes an empty `Configurations2/accelerator/current.xml` into the documents it saves, and an empty file counts for nothing.
87
+ - Excel defined names that run a macro sheet: Auto_Open and Auto_Activate as MACRO/EVENT_ON_OPEN, Auto_Close and Auto_Deactivate as MACRO/EVENT. A name counts when it starts with one of these, as `Auto_Open2` does.
88
+ - PowerPoint and Impress actions that run a program or an object verb.
89
+ - DDE: Word `DDE` and `DDEAUTO` fields, Excel `ddeLink` parts and the .xlsb record for them, Excel formulas in `application|topic!item` form, OpenDocument DDE connections and `DDE()` formulas. No amount of white space before a field's name hides it, and a formula is checked whole however long it runs.
90
+ - Word fields that run a macro, `MACROBUTTON`, and fields whose type another field builds. That is the QUOTE trick that hides `DDEAUTO`.
91
+ - References that load from outside the file on their own: attached and OpenDocument templates, linked OLE objects and packages, images, frames, included files and linked sections, external workbooks, `meta:auto-reload`, and any other external relationship. A target on the machine itself is EXTERNAL/LOCAL_FILE. An empty OpenDocument reference is the document itself and names no file, so it goes unreported. A target counts as local only if its percent-decoded form is local too, because a CVE-2017-0199 sample writes its `script:` scheme as `%73cript:`.
92
+ - Hyperlinks, with pdf-defuse's checks of the target: the scheme, file and network paths, data URLs, credentials before the host, IP hosts, punycode and mixed-script hosts, and percent-encoded hosts. A link to a place inside the document goes unreported, and so does an empty link, which is the document itself.
93
+ - Embedded OLE objects and packages, OLE files nothing declares as an object, Java applets, plug-ins and ActiveX controls.
94
+ - Package structure: parts without a content type, parts no relationship reaches, files the ODF manifest does not list, an irregular ODF layout, and names a package gives twice.
95
+ - Document type declarations in XML parts, and references to external entities. See [Document types and entities](#document-types-and-entities).
96
+
97
+ The OOXML rules match elements by namespace URI and local name, compared ignoring case, so a prefix bound to the Word namespace reads as Word whatever it is called. The OpenDocument rules read `urn:oasis:names:tc:<any>:xmlns:<name>:1.<any>` as version 1.0 of that namespace, which is what LibreOffice does. Every branch of `mc:AlternateContent` gets read.
98
+
99
+ In a compound file, the first two rules run over every storage of the tree, and the others over the streams of the format:
100
+
101
+ - VBA projects: a `VBA` storage with a `dir` stream, or a `_VBA_PROJECT_CUR` or `Macros` storage with something in it.
102
+ - Embedded objects, by the streams an object's storage holds, under `ObjectPool` or anywhere else: OLE objects with their class id, ProgID and user type, the file a Packager object holds, Equation Editor 3.0 objects by class id or ProgID, ActiveX controls, and linked objects with the target their moniker names. An object's storage is part of the tree, so a macro or an object inside an object is found.
103
+ - Word: WordBasic macros in Word 6.0 and 95 files. The fields of the text, with the rules a .docx gets, and the attached template.
104
+ - Excel: Excel 4.0 macro sheets, by sheet type or by the sheet's own BOF record, and the Auto_ names above, built in or defined. External workbooks, with the paths Excel encodes written out, DDE and OLE links, hyperlinks, shapes and buttons that run a macro, and the `Ctls` stream that holds ActiveX controls.
105
+ - PowerPoint: actions that run a macro, a program or an object verb, hyperlinks, and linked sounds and movies. PowerPoint keeps the storages of objects, controls and the VBA project in its document stream, often compressed. office-defuse inflates them and gives them the rules above.
106
+ - Outlook: each attachment, as EMBEDDED/ATTACHMENT with its name, MIME type and size, and each attachment by reference with its path. An attached message's own attachments are read, 16 messages deep.
107
+ - Visio, Publisher and Project: the VBA projects and objects. Nothing in their own streams is read.
108
+
109
+ In RTF:
110
+
111
+ - Objects in `\objdata`. office-defuse decodes the OLE 1.0 object, the file a Packager object holds, and the storage of an OLE 2 object, which gets the compound file rules. An object linked with `\objlink` or `\objautlink`, or of the class OLE2Link that CVE-2017-0199 uses, is EXTERNAL/OBJECT. `\objocx` marks an ActiveX control. `\objupdate` is ACTION/OBJECT_UPDATE, since it starts the object's program as the document opens.
112
+ - Fields, with the rules a .docx gets, the `\*\template` destination, and shape hyperlinks.
113
+ - Text after the group that closes the document, as TRAILING_BYTES. White space and NULs there are what writers leave, and count for nothing.
114
+
115
+ ### Encrypted files
116
+
117
+ office-defuse decrypts an OOXML package encrypted with Standard or Agile encryption, MS-OFFCRYPTO, and an .xls workbook encrypted with RC4 or RC4 CryptoAPI. It tries the `password` option first, then VelvetSweatshop, the password Excel tries on its own, so such a workbook opens without asking. The decrypted file gets every check of its format. It is read in place. Agile encryption decrypts in 4,096-byte segments and Standard in 16-byte blocks, so only the parts office-defuse reads get decrypted.
118
+
119
+ | Finding | Action | Weight | When |
120
+ |---|---|---|---|
121
+ | ENCRYPTED/DEFAULT_PASSWORD | `strip` | 10 | VelvetSweatshop opened the file. Anyone can open it, so the encryption hides its content from a scanner and from no one else |
122
+ | ENCRYPTED/USER_PASSWORD | `strip` | 0 | The password the caller gave opened the file |
123
+ | ENCRYPTED/RC4_40, RC4_128, RC4_OTHER, AES_128, AES_192, AES_256 | `info` | 0 | The cipher and key size |
124
+ | ENCRYPTED/PASSWORD_REQUIRED | `reject` | 0 | No password at hand opens the file, or an Agile spin count is above 10,000,000. Every encrypted .doc and .ppt, which office-defuse does not decrypt |
125
+ | ENCRYPTED/UNSUPPORTED_ALGORITHM | `reject` | 0 | Excel's XOR obfuscation, or a cipher or hash outside the list below |
126
+
127
+ AES-128, AES-192 and AES-256 in CBC or ECB mode, SHA-1, SHA-256, SHA-384, SHA-512 and MD5 go to Node's crypto module, and no other algorithm name from a file reaches it. RC4 is office-defuse's own code, since OpenSSL 3 keeps RC4 in its legacy provider.
128
+
129
+ Pass the password in code, never on a command line, where other users can read it.
130
+
131
+ ## The containers
132
+
133
+ The ZIP reader starts from the end record and the central directory. It checks each local header against the central directory before it reads any content. It refuses these, and stops the run:
134
+
135
+ | Detail | Refused |
136
+ |---|---|
137
+ | NOT_ZIP | No end record in the last 65,557 bytes, or one whose comment runs past the end of the file |
138
+ | AMBIGUOUS_END | An end record whose comment holds a second end signature |
139
+ | MULTI_DISK | Any disk number other than 0 |
140
+ | DIRECTORY_MISMATCH | An entry count or directory size that disagrees with the central directory, or a central directory that does not end where the end record begins. Data prepended without fixing the offsets lands here |
141
+ | OUT_OF_RANGE | A central directory or local header outside the file or its entries, or a 64-bit value larger than a JavaScript number holds exactly |
142
+ | OVERLAP | Two entries that share bytes, or an entry that runs into the central directory |
143
+ | HEADER_MISMATCH | A local header that is missing, or whose name, method, encryption, data descriptor or UTF-8 flags, CRC or sizes differ from the central directory, and a data descriptor that differs |
144
+ | DUPLICATE_NAME | Two names equal after percent-decoding, Unicode NFC and lowercasing |
145
+ | UNSAFE_NAME | An empty name, a name over 1024 bytes, a leading `/`, a drive letter, a backslash, a control character, an empty, `.` or `..` segment, or a directory entry with content |
146
+ | NAME_ENCODING | A non-ASCII name without the UTF-8 flag, invalid UTF-8, or an Info-ZIP Unicode path field that names another entry |
147
+ | ENCRYPTED_ENTRY | Any of the encryption flags |
148
+ | UNSUPPORTED_METHOD | Any compression method but stored and deflate |
149
+ | ZIP64_MISMATCH | A field at its maximum with no ZIP64 value behind it, a ZIP64 extra field that holds other fields than its header defers to it, a local ZIP64 field that disagrees with its header, an end record that disagrees with the ZIP64 end record, or a locator that does not point at one |
150
+ | BAD_EXTRA_FIELD | An extra field block that does not split exactly into records, or a record id given twice |
151
+ | DATA_ERROR | Data that does not inflate to its declared size and CRC, deflate data that is corrupt, ends early or leaves compressed bytes unused, or a stored entry with two different sizes |
152
+
153
+ It reports three more without stopping. LEADING_BYTES is data before the first entry. HIDDEN_DATA is bytes between entries, or before the central directory, that no entry covers. TRAILING_BYTES is bytes after the end record that its comment does not cover. Each is `strip`.
154
+
155
+ The reader inflates every entry once, start to end, and checks it against its CRC and declared size. Inflation stops as soon as the output passes the declared size.
156
+
157
+ A writer that streams an entry sets the data descriptor flag and may leave the local header's CRC and sizes unset, as zero, or as ones in a local ZIP64 field. The data descriptor after the data then gives them, and they must match the central directory.
158
+
159
+ The XML tokenizer reads each part in pieces. It refuses these, and stops the run:
160
+
161
+ - XML/ENCODING: anything but UTF-8 or UTF-16, the two encodings OPC allows, an encoding declaration that disagrees with the bytes, and bytes that are not valid in their encoding.
162
+ - XML/MALFORMED: markup that breaks XML 1.0 or its namespaces. That covers an undeclared prefix, a duplicate attribute, a mismatched end tag, an element or text outside the root, a malformed name, reference or declaration, and characters XML 1.0 forbids. It also covers what XML 1.0 refuses about entities: a reference to an entity nothing declares, a reference to an external entity in an attribute value, entity text that refers to itself or leaves an element open, and a markup declaration outside the document type declaration.
163
+
164
+ References get expanded and line ends normalized as the XML specification says, so `DD&#69;AUTO` reads as `DDEAUTO`.
165
+
166
+ The `Basic/`, `Scripts/`, `Dialogs/` and `Configurations2/` folders of an OpenDocument package are reported by name, as a whole. The files in them are inflated and checked, not parsed. In an OOXML package, the parts read as XML are `[Content_Types].xml`, every `.rels` part, and every part whose content type ends in `+xml`, is `application/xml` or `text/xml`, or is the VML drawing type. In an OpenDocument package they are the other entries whose names end in `.xml`.
167
+
168
+ PowerPoint and Excel write VML drawings that are not well-formed XML. They put `<![if gte mso 9]>` and `<![endif]>` around content, and Excel 2007 leaves `<br>` unclosed in a button's text. office-defuse reads a part of the VML drawing type in that dialect. It skips the markers, so the content on both sides of them reaches the rules, and it takes `<br>` as an empty element. Apart from those two, it reads the part as strictly as any other.
169
+
170
+ ### Compound files
171
+
172
+ The compound file reader follows MS-CFB, and where MS-CFB leaves room, it accepts what olefile and LibreOffice accept. It reads the header, the FAT through the DIFAT, the directory, the mini FAT and the mini stream, and records which stream owns each sector, so no sector serves two. It follows a stream's chain only as far as the stream's size needs, as olefile and LibreOffice do, so a chain that runs on past that does no harm. It refuses these, and stops the run:
173
+
174
+ | Detail | Refused |
175
+ |---|---|
176
+ | BAD_HEADER | A file shorter than its header, a version and sector size other than 3 with 512 bytes or 4 with 4,096, mini sectors other than 64 bytes, a mini stream cutoff other than 4,096, a byte order other than little-endian, FAT or DIFAT sector counts that disagree with the sectors listed, a mini FAT too short for the mini stream, or an empty directory |
177
+ | OUT_OF_RANGE | A sector or a stream outside the file, a directory entry that names one past the directory, or a stream size larger than a JavaScript number holds exactly |
178
+ | OVERLAP | A sector or mini sector that two streams take, or a stream and the FAT, DIFAT, directory or mini FAT |
179
+ | CHAIN_LOOP | A FAT, DIFAT or mini FAT chain that comes back to a sector it took already |
180
+ | BROKEN_CHAIN | A chain that runs into a free or reserved sector before its stream ends, or a DIFAT chain that does not end after its last sector |
181
+ | DATA_ERROR | A chain that ends before its stream does |
182
+ | DIRECTORY_LOOP | A directory entry the tree reaches twice |
183
+ | BAD_ENTRY | An entry in the tree of an unknown or unused type, a root entry that is not the first, a name length or terminator that does not fit, a NUL in a name, or a stream with children |
184
+ | UNSAFE_NAME | A name holding `/`, `\`, `:` or `!`, which MS-CFB forbids |
185
+ | DUPLICATE_NAME | Two entries of one storage whose names are equal ignoring case |
186
+
187
+ It reports two more without stopping, each `strip`. HIDDEN_DATA counts the bytes that no stream covers and that are not zero: sectors no chain takes, the rest of each stream's last sector or mini sector, mini sectors no stream takes, directory entries the tree does not reach, and the padding of a version 4 header. Zeros are padding and count for nothing. TRAILING_BYTES is data past the last sector the FAT describes. HIDDEN_DATA is common in real files. LibreOffice, for one, leaves an old copy of a directory sector in a free sector when it saves.
188
+
189
+ `limits.entries` counts directory entries, `limits.decompressedBytes` bounds the size of one stream, and `limits.totalDecompressedBytes` the sizes of all streams together, the way they count ZIP entries. The reader holds the FAT, the mini FAT, the directory and the owner of each sector, and reads streams in ranges as the rules ask for them.
190
+
191
+ ### RTF
192
+
193
+ The RTF reader takes the file in 256 KiB pieces. It holds the destinations open at the point it has reached, the short text it collects, such as a class name or a template path, and the data of each object it decodes. It reads the way Word reads:
194
+
195
+ - A control word's name and its number each end after 250 characters.
196
+ - A destination that `\*` marks and that office-defuse does not know is skipped with its group, and an unknown control word is ignored. Every destination of RTF 1.9.1 is known, so a decoy group such as `{\listtable 4142}` inside an object's data adds no hex digits.
197
+ - In an object's data, anything but hex digits drops out, a digit left unpaired before `\'hh` is dropped, and `\bin` data goes in as its bytes.
198
+ - Inside an object's data, a group that `\*` marks and that opens with a control word that is no destination, such as `{\*\par574 0}`, still holds hex digits for rtfobj, and Word opens files that hide an object's data that way. When the data reads differently with and without those groups, both readings get the object rules, and the findings from the second have `reading` in their data.
199
+ - A file can end inside groups, and the reader closes them. Text after the group that closes the document is still read, as rtfobj reads it.
200
+
201
+ `limits.rtfDepth`, 256 by default, bounds group nesting and the destinations open at once. Past it the run stops with LIMIT/RTF_DEPTH. One object's decoded data counts against `limits.decompressedBytes`, and all of them together against `limits.totalDecompressedBytes`.
202
+
203
+ ### Document types and entities
204
+
205
+ office-defuse never loads an external DTD and never fetches an external entity. A document type declaration does not stop the run.
206
+
207
+ | Finding | Action | Weight | What it is |
208
+ |---|---|---|---|
209
+ | XML/DOCTYPE | `info` | 0 | A declaration with a name and maybe an external identifier, and no internal subset. OpenOffice.org 2.x and StarOffice 8 wrote one into their manifests, and OpenOffice.org 2.x and 3.x into their formula objects |
210
+ | XML/INTERNAL_SUBSET | `strip` | 20 | A declaration with an internal subset, the declarations between square brackets, which can declare entities and attribute defaults |
211
+ | XML/EXTERNAL_ENTITY | `strip` | 40 | A reference to an external entity in element text |
212
+
213
+ The tokenizer expands internal entities where the part refers to them, in element text and in attribute values. The rules see the expanded text, so a relationship target, a hyperlink or a field code built from entities gets checked as its real value. Markup in entity text becomes elements.
214
+
215
+ A reference to an external entity in text stands for content office-defuse cannot read. Findings show it as `&name;`, and a rule that needs the value treats it as unknown. A hyperlink target holding one is LINK/RELATIVE, which is what pdf-defuse makes of a URI it cannot resolve. A target that loads from outside never counts as a local file. A Word field whose name holds one is FIELD/COMPUTED, and an Excel formula that holds one is DDE/FORMULA, since the unknown text could make it a DDE call. XML 1.0 forbids a reference to an external entity in an attribute value, and xmllint and expat refuse one, so office-defuse refuses it as XML/MALFORMED.
216
+
217
+ The internal subset is read the way XML 1.0 has a processor read it when that processor reads internal parameter entities and no external ones. The first declaration of a name binds. A parameter entity reference that is not read, because the entity is external or undeclared, makes the tokenizer skip the entity and attribute-list declarations after it, unless the XML declaration says `standalone="yes"`. Conditional sections and parameter entity references inside a declaration are refused. libxml2 reads further. It keeps reading declarations after a parameter entity it does not load, and it takes a parameter entity reference inside a declaration that came from another parameter entity's text.
218
+
219
+ An element that leaves out an attribute gets the default its attribute-list declaration gives, namespace declarations included. An attribute declared with a type other than CDATA has its spaces collapsed. expat does both. libxml2 does both for namespace declarations, and applies other defaults only with XML_PARSE_DTDATTR.
220
+
221
+ Three limits bound what a document type can cost: entity references nested 16 deep, 100,000 expansions, and 16 Mi characters of expanded text. The last two count over the whole run, and each attribute default added to an element counts as an expansion. Past one of them the run stops with LIMIT/XML_ENTITY_DEPTH, LIMIT/XML_ENTITY_EXPANSIONS or LIMIT/XML_ENTITY_SIZE. The billion laughs stops at the expansion limit.
222
+
223
+ ### When a run stops
224
+
225
+ A run that stops rejects the file with a null score and the UNKNOWN level, whatever `actionOverrides` say about the finding that stopped it. The container refusals, a limit, an unrecognized or ambiguous format, an encrypted file that no password at hand opens or whose algorithm office-defuse does not decrypt, and an unexpected error, PACKAGE/UNREADABLE, all stop a run. The findings made before the stop stay in the report.
226
+
227
+ ## Findings
228
+
229
+ Every finding has a `category` and a `detail`, plain strings such as `MACRO` and `VBA_PROJECT`, and a `description` that is safe to show the uploader. A description that quotes a value from the file cuts it to 60 characters and shows control and format characters as `?`. `data` keeps a value from the file up to 200 characters, or 2,000 for a target, and `location` names the part, the relationship, the folder, the stream or storage, or the RTF object's offset.
230
+
231
+ ```js
232
+ { category: 'EXTERNAL', detail: 'TEMPLATE', description: 'The document loads its template from https://templates.example.net/report.dotm', action: 'strip', location: 'word/_rels/settings.xml.rels, rIdTpl', data: { target: '...', type: '...' } }
233
+ ```
234
+
235
+ Past 200 findings of one kind, one more finding with the location `additional occurrences` counts the rest in `data.count`.
236
+
237
+ `action` is `reject`, `strip` or `info`. Each rule decides its own action, and `info` only reports. The enums are exported as `OfficeCategory` and `OfficeDetail`, and `allFindingSpecs()` lists every pair with its default action, description and weight. `actionOverrides` changes the action by category, or by category and detail:
238
+
239
+ ```js
240
+ await inspectOffice(bytes, { actionOverrides: [{ category: 'MACRO', action: 'reject' }] });
241
+ ```
242
+
243
+ ## Scoring
244
+
245
+ The method is pdf-defuse's. Each kind of finding has a weight: a program run by a click is 80, VBA is 70, a safe link is 0. The highest weight counts in full, every other kind adds a quarter of its weight, and repeats of one kind count once.
246
+
247
+ Three combinations add more. Code here means macros or DDE. Code or a run-program action together with a template, object or include from outside adds 20. Code with an embedded object or ActiveX control adds 15. Code with leading, hidden or trailing container data adds 10. The total is capped at 100.
248
+
249
+ The bands are NONE at 0, LOW from 1, MEDIUM from 25, HIGH from 50 and CRITICAL from 80. `scoreWeights` and `scoreBands` change them. A finding of a kind office-defuse does not make, such as one from another package, scores at its `weight`, or 0 without one.
250
+
251
+ ## Options
252
+
253
+ | Option | Default | Purpose |
254
+ |---|---|---|
255
+ | `limits.fileSize` | none | Bytes in the upload, checked before any read |
256
+ | `limits.entries` | 10,000 | Entries in the central directory, or in a compound file's directory |
257
+ | `limits.decompressedBytes` | 512 MiB | The declared size of any one entry or stream, one RTF object's decoded data, or one compressed PowerPoint storage |
258
+ | `limits.totalDecompressedBytes` | 2 GiB | The declared sizes of all entries, or of all streams, together, and all RTF objects' decoded data |
259
+ | `limits.xmlDepth` | 256 | Element nesting in one XML part |
260
+ | `limits.xmlTokenSize` | 16 Mi | Characters in one tag, comment, processing instruction or CDATA section. A document type declaration counts as one. Text goes through in pieces and has no limit |
261
+ | `limits.xmlEntityDepth` | 16 | Entity references nested inside entity text |
262
+ | `limits.xmlEntityExpansions` | 100,000 | Entity references expanded, and attribute defaults added, in the whole run |
263
+ | `limits.xmlEntitySize` | 16 Mi | Characters of entity text and attribute defaults expanded in the whole run |
264
+ | `limits.rtfDepth` | 256 | Group nesting in an RTF file, and destinations open at once |
265
+ | `limits.timeMs` | none | The whole run |
266
+ | `password` | none | Tried first on an encrypted OOXML package or .xls workbook, before VelvetSweatshop |
267
+ | `actionOverrides` | none | Change the action for a category, or for a category and detail |
268
+ | `scoreWeights`, `scoreBands` | built in | Change the scoring |
269
+
270
+ The nine limits with a default bound the memory or work any input can cost, so they apply unless you set them. `Infinity` turns one off. A limit that is reached stops the run with a LIMIT finding.
271
+
272
+ Besides one XML token, the stack of open elements and the declarations of the part's internal subset, office-defuse holds the central directory's table of entries, which parts each part's relationships reach, and, while it reads a relationships part, the Ids that part has given so far, about 100 bytes each, so it can find one given twice. Content types, manifest paths and object references that name no entry of the archive are not kept. Everything else streams.
273
+
274
+ ## The defuse family
275
+
276
+ office-defuse is one of a family of packages that each handle one kind of file, beside [pdf-defuse](https://github.com/patrickdk77/pdf-defuse). It declares the family's shared types with the same shapes as pdf-defuse: `DefuseFinding`, `ByteSource`, `ByteSink`, `PluginContext`, `ContainedFile`, `ContainedFileResult` and `ContainedFilePlugin`. It does not depend on pdf-defuse, and TypeScript matches the types by shape.
277
+
278
+ A finding's `category` and `detail` are plain strings, and `weight`, when set, says how much it adds to a score. The time limit's error carries `code: 'DEFUSE_LIMIT'` and `limit: 'time'`, the code every package of the family passes up the tree of files. `inspectOffice` turns it into LIMIT/TIME.
279
+
280
+ ## What it does not check
281
+
282
+ - The OLE parts of a ZIP package, such as `vbaProject.bin`, the objects inside `oleObject*.bin`, and ActiveX binaries.
283
+ - VBA source. A VBA project is reported whole, and its modules go unread.
284
+ - Encrypted .doc and .ppt files, and Excel's XOR obfuscation. They are rejected.
285
+ - Formulas in .xls and .xlsb sheets. A DDE link that a formula uses is found in the workbook's link records.
286
+ - Visio .vsd ShapeSheet cells, and anything in Publisher and Project files but VBA projects and objects.
287
+ - An Outlook message's body, its RTF body included, and what an attached file holds.
288
+ - The piece table of a Word 6.0 or 95 document. Its text is read in the order it is stored between the FIB's bounds, as msodde reads it, so a field that fast save split into pieces stored apart may not read whole.
289
+ - Images, fonts and other media, and the inside of an embedded Office package such as a chart's workbook.
290
+ - Excel functions other than DDE that reach outside, such as WEBSERVICE, RTD and IMAGE. Data connections in `connections.xml`.
291
+ - OpenDocument form submit targets, Word's hyperlink base, and whether a link's visible text names a different site than its target.
292
+ - Flat OpenDocument files such as `.fodt`, Flat OPC, and XPS. Inspect rejects them as not a ZIP archive or not a recognized package.
293
+ - Bugs in an application's parsers that ordinary content can trigger.
294
+
295
+ ## Development
296
+
297
+ Development needs Node 22.12 or later. mocha 12 requires it, and the CommonJS test build loads chai 6, an ES module, with `require()`, which Node 22 supports only from 22.12. The pre-commit hook needs Node 22.22.1 or later for lint-staged. The tests also run `python3` and `unzip` to read the ZIP writer's output. The published package has no runtime dependencies and runs on Node 18 or later.
298
+
299
+ ```sh
300
+ npm install --ignore-scripts
301
+ npm run build
302
+ npm test
303
+ npm run coverage
304
+ npm run lint
305
+ npm run format
306
+ ```
307
+
308
+ `npm run build` compiles `src` into `dist`. `npm test` runs the lint, builds the package and the tests, then runs mocha, so a lint error fails the tests. `npm run coverage` runs the tests under c8 and prints the line and branch coverage of each source file. `npm run lint` runs `biome check` over the whole tree and changes nothing. `npm run format` rewrites files in Biome's format.
309
+
310
+ `npx husky` activates the pre-commit hook. A plain `npm install` does the same through the `prepare` script. The hook runs Biome on the staged files, applying its safe fixes, then runs `npm audit`. The commit stops if either one fails.
311
+
312
+ The fixtures under `test/fixtures` are committed. `python3 -I scripts/make-fixtures.py` rebuilds them, and needs LibreOffice's `soffice`, Python 3 and Info-ZIP's `zip`. It converts flat OpenDocument sources with LibreOffice, which also saves .doc, .xls, .ppt and .rtf versions of some of them. It adds macros, fields, relationships, objects and controls with Python's `zipfile`, and builds the hostile archives byte by byte. Compound files, RTF, Outlook messages and encrypted files come from its own compound file writer, AES and RC4. Two runs write the same bytes: the script sets `SAL_RAND_REPEATABLE` for LibreOffice, and fixes the timestamps and random ids LibreOffice writes.
313
+
314
+ `test/fixtures/cases` holds one small file for each behavior a run over a corpus of real Office files turned up, written from scratch by `python3 -I scripts/make-fixtures.py cases`, which needs only Python. No corpus file, file name or path is in the repository, macro and OLE payloads are inert markers, and every address uses a reserved name. `manifest.json` gives each file's issue, what office-defuse reports for it, and what the build before the fix it documents reported. A test checks every entry, and that the manifest and the folder list the same files.
315
+
316
+ ## License
317
+
318
+ MIT
package/dist/biff.d.ts ADDED
@@ -0,0 +1,62 @@
1
+ import type { CfbEntry, CompoundFile } from './cfb';
2
+ /** Record types this reader acts on (MS-XLS 2.3). */
3
+ export declare const R: {
4
+ bof: number;
5
+ eof: number;
6
+ filePass: number;
7
+ continue: number;
8
+ lbl: number;
9
+ externName: number;
10
+ boundSheet: number;
11
+ template: number;
12
+ addIn: number;
13
+ supBook: number;
14
+ externSheet: number;
15
+ obj: number;
16
+ hlink: number;
17
+ };
18
+ export interface BiffRecord {
19
+ type: number;
20
+ /** Where the record header starts in the stream. */
21
+ offset: number;
22
+ body: Uint8Array;
23
+ }
24
+ /** A sheet as BoundSheet8 (or BIFF5's BOUNDSHEET) lists it. */
25
+ export interface Sheet {
26
+ position: number;
27
+ /** 0 visible, 1 hidden, 2 very hidden. */
28
+ state: number;
29
+ /** 0 worksheet or dialog, 1 macro sheet, 2 chart, 6 VBA module. */
30
+ type: number;
31
+ name: string;
32
+ }
33
+ /**
34
+ * Reads the records of a workbook stream one at a time, from `start` to the end of the stream. A record's body may
35
+ * run past the stream's end only by being cut short, which stops the reading as Excel stops.
36
+ */
37
+ export declare class RecordReader {
38
+ readonly stream: CfbEntry;
39
+ private readonly deadline?;
40
+ private readonly reader;
41
+ private pos;
42
+ constructor(cfb: CompoundFile, stream: CfbEntry, start: number, deadline?: number | undefined);
43
+ next(): Promise<BiffRecord | undefined>;
44
+ }
45
+ /** BIFF8 writes strings with a flag byte: 8-bit characters, or 16-bit ones when its low bit is set. */
46
+ export declare function xlString(b: Uint8Array, at: number, chars: number): {
47
+ text: string;
48
+ next: number;
49
+ };
50
+ /** A BoundSheet record, in BIFF8 or BIFF5. */
51
+ export declare function parseBoundSheet(body: Uint8Array, biff8: boolean): Sheet;
52
+ /** A defined name, in BIFF8 or BIFF5, with a built-in name spelled out. */
53
+ export declare function parseLbl(body: Uint8Array, biff8: boolean): {
54
+ name: string;
55
+ builtin: boolean;
56
+ hidden: boolean;
57
+ };
58
+ /** The event a defined name runs a macro on, when Excel runs it on one: any name that starts with Auto_Open and so on. */
59
+ export declare function autoName(name: string): {
60
+ event: string;
61
+ onOpen: boolean;
62
+ } | undefined;
package/dist/biff.js ADDED
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RecordReader = exports.R = void 0;
4
+ exports.xlString = xlString;
5
+ exports.parseBoundSheet = parseBoundSheet;
6
+ exports.parseLbl = parseLbl;
7
+ exports.autoName = autoName;
8
+ const errors_1 = require("./errors");
9
+ const io_1 = require("./io");
10
+ const oleobj_1 = require("./oleobj");
11
+ /** Record types this reader acts on (MS-XLS 2.3). */
12
+ exports.R = {
13
+ bof: 0x0809,
14
+ eof: 0x000a,
15
+ filePass: 0x002f,
16
+ continue: 0x003c,
17
+ lbl: 0x0018,
18
+ externName: 0x0023,
19
+ boundSheet: 0x0085,
20
+ template: 0x0060,
21
+ addIn: 0x0087,
22
+ supBook: 0x01ae,
23
+ externSheet: 0x0017,
24
+ obj: 0x005d,
25
+ hlink: 0x01b8,
26
+ };
27
+ /**
28
+ * Reads the records of a workbook stream one at a time, from `start` to the end of the stream. A record's body may
29
+ * run past the stream's end only by being cut short, which stops the reading as Excel stops.
30
+ */
31
+ class RecordReader {
32
+ stream;
33
+ deadline;
34
+ reader;
35
+ pos;
36
+ constructor(cfb, stream, start, deadline) {
37
+ this.stream = stream;
38
+ this.deadline = deadline;
39
+ this.reader = new io_1.Reader(cfb.source(stream), stream.size, 16384, 8);
40
+ this.pos = start;
41
+ }
42
+ async next() {
43
+ if (this.pos + 4 > this.stream.size)
44
+ return undefined;
45
+ const h = await this.reader.read(this.pos, 4);
46
+ const type = h[0] | (h[1] << 8);
47
+ const length = h[2] | (h[3] << 8);
48
+ if (this.pos + 4 + length > this.stream.size)
49
+ return undefined;
50
+ const offset = this.pos;
51
+ const body = await this.reader.read(this.pos + 4, length);
52
+ this.pos += 4 + length;
53
+ if ((offset & 0xffff) < 4)
54
+ (0, errors_1.checkTime)(this.deadline);
55
+ return { type, offset, body };
56
+ }
57
+ }
58
+ exports.RecordReader = RecordReader;
59
+ /** BIFF8 writes strings with a flag byte: 8-bit characters, or 16-bit ones when its low bit is set. */
60
+ function xlString(b, at, chars) {
61
+ if (at >= b.length)
62
+ return { text: '', next: at };
63
+ const wide = (b[at] & 1) === 1;
64
+ const start = at + 1;
65
+ const end = Math.min(b.length, start + chars * (wide ? 2 : 1));
66
+ return { text: wide ? (0, oleobj_1.utf16)(b.subarray(start, end)) : (0, oleobj_1.latin1)(b.subarray(start, end)), next: end };
67
+ }
68
+ /** A BoundSheet record, in BIFF8 or BIFF5. */
69
+ function parseBoundSheet(body, biff8) {
70
+ const position = (body[0] | (body[1] << 8) | (body[2] << 16) | (body[3] << 24)) >>> 0;
71
+ const state = body[4] & 3;
72
+ const type = body[5];
73
+ const cch = body[6] ?? 0;
74
+ const name = biff8 ? xlString(body, 7, cch).text : (0, oleobj_1.latin1)(body.subarray(7, 7 + cch));
75
+ return { position, state, type, name };
76
+ }
77
+ /** Built-in names Excel runs as macros (MS-XLS 2.5.112): Auto_Open and Auto_Activate as the workbook opens, Auto_Close and Auto_Deactivate as it closes. */
78
+ const BUILTIN_NAMES = { 1: 'Auto_Open', 2: 'Auto_Close', 10: 'Auto_Activate', 11: 'Auto_Deactivate' };
79
+ /** A defined name, in BIFF8 or BIFF5, with a built-in name spelled out. */
80
+ function parseLbl(body, biff8) {
81
+ const flags = body[0] | (body[1] << 8);
82
+ const cch = body[3] ?? 0;
83
+ const builtin = (flags & 0x20) !== 0;
84
+ const name = biff8 ? xlString(body, 14, cch).text : (0, oleobj_1.latin1)(body.subarray(14, 14 + cch));
85
+ const shown = builtin && name.length > 0 ? (BUILTIN_NAMES[name.charCodeAt(0)] ?? `built-in name ${name.charCodeAt(0)}`) : name;
86
+ return { name: shown, builtin, hidden: (flags & 1) !== 0 };
87
+ }
88
+ /** The event a defined name runs a macro on, when Excel runs it on one: any name that starts with Auto_Open and so on. */
89
+ function autoName(name) {
90
+ const m = /^(?:_xlnm\.)?(auto_open|auto_close|auto_activate|auto_deactivate)/i.exec(name);
91
+ if (!m)
92
+ return undefined;
93
+ const event = m[1].toLowerCase();
94
+ return { event: `${event[0].toUpperCase()}${event.slice(1, 5)}${event[5].toUpperCase()}${event.slice(6)}`, onOpen: event === 'auto_open' || event === 'auto_activate' };
95
+ }
package/dist/cfb.d.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { type ByteSource, OfficeDetail as D } from './types';
2
+ /** The first eight bytes of every compound file. */
3
+ export declare const CFB_SIGNATURE: Uint8Array<ArrayBuffer>;
4
+ export interface CfbLimits {
5
+ entries: number;
6
+ decompressedBytes: number;
7
+ totalDecompressedBytes: number;
8
+ }
9
+ export interface CfbEntry {
10
+ /** The entry's position in the directory. */
11
+ id: number;
12
+ name: string;
13
+ kind: 'root' | 'storage' | 'stream';
14
+ /** The class id, as Windows writes a GUID, in upper case. */
15
+ clsid: string;
16
+ size: number;
17
+ parent?: CfbEntry;
18
+ children: CfbEntry[];
19
+ }
20
+ /** Something the reader saw that it does not refuse. Each is a strip finding, since a rewrite leaves the bytes out. */
21
+ export interface CfbNote {
22
+ detail: D.HiddenData | D.TrailingBytes;
23
+ bytes: number;
24
+ }
25
+ /** A GUID as Windows shows it: the first three fields little-endian, upper case, without braces. */
26
+ export declare function guid(b: Uint8Array, at?: number): string;
27
+ /** An entry name with control and format characters written as \xNN, so a location can show it. */
28
+ export declare const showName: (name: string) => string;
29
+ /** The entry's path from the root, its names joined by "/", which no name may hold. */
30
+ export declare function entryPath(e: CfbEntry): string;
31
+ /**
32
+ * A compound file (MS-CFB), read from its header, its allocation tables and its directory. Every chain is followed as
33
+ * far as its stream needs and every sector has one owner, so two readers cannot read the file differently. Reading a
34
+ * stream maps its offsets through its chain; nothing is held but the tables.
35
+ */
36
+ export declare class CompoundFile {
37
+ private readonly reader;
38
+ readonly sectorSize: number;
39
+ readonly root: CfbEntry;
40
+ /** Every entry the directory tree reaches, the root first, each storage before its children. */
41
+ readonly entries: CfbEntry[];
42
+ readonly notes: CfbNote[];
43
+ private readonly miniStream;
44
+ private readonly deadline;
45
+ private readonly streamChains;
46
+ private constructor();
47
+ /** The child of a storage with this name, compared ignoring case. */
48
+ child(storage: CfbEntry, name: string): CfbEntry | undefined;
49
+ /** Reads a compound file. The caller has seen its signature. */
50
+ static open(source: ByteSource, limits: CfbLimits, deadline?: number): Promise<CompoundFile>;
51
+ /** Bytes of the mini stream, by their offset in it. */
52
+ private readMini;
53
+ private readChain;
54
+ /** A random-access source over a stream's content. */
55
+ source(e: CfbEntry): ByteSource;
56
+ /** A stream's whole content. Callers read only streams whose size they bounded. */
57
+ bytes(e: CfbEntry): Promise<Uint8Array>;
58
+ }