@shbernal/ts-xlsx 3.0.0 → 3.2.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 (277) hide show
  1. package/README.md +34 -24
  2. package/dist/bytes.d.ts +10 -2
  3. package/dist/bytes.js +22 -0
  4. package/dist/core/address.d.ts +96 -2
  5. package/dist/core/address.js +87 -33
  6. package/dist/core/array-formula-ranges.d.ts +33 -0
  7. package/dist/core/array-formula-ranges.js +62 -0
  8. package/dist/core/autofilter.d.ts +4 -3
  9. package/dist/core/autofilter.js +31 -30
  10. package/dist/core/axis-handle.d.ts +2 -0
  11. package/dist/core/axis-handle.js +5 -2
  12. package/dist/core/cell.d.ts +4 -3
  13. package/dist/core/cell.js +4 -6
  14. package/dist/core/clone.d.ts +29 -0
  15. package/dist/core/clone.js +24 -0
  16. package/dist/core/color-resolution.js +4 -6
  17. package/dist/core/column.d.ts +1 -0
  18. package/dist/core/column.js +6 -2
  19. package/dist/core/comment-thread.d.ts +7 -3
  20. package/dist/core/comment-thread.js +2 -1
  21. package/dist/core/conditional-formatting-overlay.d.ts +11 -1
  22. package/dist/core/conditional-formatting-overlay.js +30 -3
  23. package/dist/core/conditional-formatting.d.ts +107 -11
  24. package/dist/core/conditional-formatting.js +76 -19
  25. package/dist/core/containers.d.ts +12 -0
  26. package/dist/core/containers.js +5 -0
  27. package/dist/core/data-validation-overlay.d.ts +7 -1
  28. package/dist/core/data-validation-overlay.js +19 -4
  29. package/dist/core/data-validation.d.ts +22 -0
  30. package/dist/core/data-validation.js +30 -4
  31. package/dist/core/date-format.d.ts +20 -0
  32. package/dist/core/date-format.js +170 -0
  33. package/dist/core/date.d.ts +43 -8
  34. package/dist/core/date.js +19 -3
  35. package/dist/core/formula-references.d.ts +26 -0
  36. package/dist/core/formula-references.js +173 -0
  37. package/dist/core/formula-scan.d.ts +2 -0
  38. package/dist/core/formula-scan.js +58 -0
  39. package/dist/core/formula.d.ts +76 -17
  40. package/dist/core/formula.js +87 -80
  41. package/dist/core/function-values.d.ts +4 -0
  42. package/dist/core/function-values.js +58 -0
  43. package/dist/core/future-functions.d.ts +3 -0
  44. package/dist/core/{modern-functions.js → future-functions.js} +119 -88
  45. package/dist/core/grid-edits.d.ts +30 -3
  46. package/dist/core/grid-edits.js +248 -81
  47. package/dist/core/grid-shift.d.ts +58 -9
  48. package/dist/core/grid-shift.js +26 -4
  49. package/dist/core/hyperlink.d.ts +39 -0
  50. package/dist/core/hyperlink.js +88 -0
  51. package/dist/core/image.d.ts +49 -14
  52. package/dist/core/image.js +16 -5
  53. package/dist/core/internal.d.ts +10 -2
  54. package/dist/core/limits.d.ts +25 -0
  55. package/dist/core/limits.js +4 -0
  56. package/dist/core/merge.d.ts +18 -4
  57. package/dist/core/merge.js +33 -20
  58. package/dist/core/page-setup.d.ts +26 -11
  59. package/dist/core/pivot-table.d.ts +49 -10
  60. package/dist/core/pivot-table.js +112 -60
  61. package/dist/core/protection.d.ts +66 -4
  62. package/dist/core/protection.js +6 -11
  63. package/dist/core/range.js +34 -30
  64. package/dist/core/row-input.js +2 -2
  65. package/dist/core/row.d.ts +1 -0
  66. package/dist/core/row.js +6 -2
  67. package/dist/core/structured-reference.d.ts +7 -0
  68. package/dist/core/structured-reference.js +7 -0
  69. package/dist/core/style.d.ts +85 -70
  70. package/dist/core/style.js +92 -28
  71. package/dist/core/table-style.d.ts +1 -1
  72. package/dist/core/table-style.js +4 -4
  73. package/dist/core/table.d.ts +64 -16
  74. package/dist/core/table.js +87 -40
  75. package/dist/core/theme.d.ts +2 -0
  76. package/dist/core/theme.js +9 -5
  77. package/dist/core/value.d.ts +60 -28
  78. package/dist/core/value.js +48 -43
  79. package/dist/core/workbook-media.d.ts +34 -0
  80. package/dist/core/workbook-media.js +44 -0
  81. package/dist/core/workbook-styles.d.ts +83 -1
  82. package/dist/core/workbook-styles.js +4 -0
  83. package/dist/core/workbook-theme.js +5 -3
  84. package/dist/core/workbook-vba.js +3 -2
  85. package/dist/core/workbook.d.ts +41 -3
  86. package/dist/core/workbook.js +46 -41
  87. package/dist/core/worksheet-comments.d.ts +2 -1
  88. package/dist/core/worksheet-comments.js +20 -13
  89. package/dist/core/worksheet-merges.d.ts +49 -0
  90. package/dist/core/worksheet-merges.js +65 -0
  91. package/dist/core/worksheet-model.d.ts +10 -2
  92. package/dist/core/worksheet-model.js +28 -9
  93. package/dist/core/worksheet-pictures.d.ts +3 -3
  94. package/dist/core/worksheet-pictures.js +7 -6
  95. package/dist/core/worksheet.d.ts +165 -27
  96. package/dist/core/worksheet.js +206 -89
  97. package/dist/customui/ribbon.js +2 -1
  98. package/dist/entries/core.d.ts +21 -14
  99. package/dist/entries/core.js +14 -7
  100. package/dist/entries/errors.d.ts +1 -0
  101. package/dist/entries/errors.js +1 -0
  102. package/dist/entries/xlsx.d.ts +1 -1
  103. package/dist/errors.d.ts +64 -0
  104. package/dist/errors.js +22 -0
  105. package/dist/hex.d.ts +8 -0
  106. package/dist/hex.js +3 -0
  107. package/dist/io/cell-metadata/metadata.d.ts +42 -0
  108. package/dist/io/cell-metadata/metadata.js +35 -0
  109. package/dist/io/cell-metadata/rich-values.d.ts +11 -0
  110. package/dist/io/cell-metadata/rich-values.js +80 -0
  111. package/dist/io/csv/delimiter.d.ts +21 -0
  112. package/dist/io/csv/delimiter.js +18 -0
  113. package/dist/io/csv/errors.d.ts +14 -0
  114. package/dist/io/csv/errors.js +5 -0
  115. package/dist/io/csv/read.d.ts +6 -1
  116. package/dist/io/csv/read.js +51 -22
  117. package/dist/io/csv/write.d.ts +19 -5
  118. package/dist/io/csv/write.js +24 -44
  119. package/dist/io/opc/inflate.js +14 -0
  120. package/dist/io/opc/namespaces.d.ts +16 -0
  121. package/dist/io/opc/namespaces.js +4 -0
  122. package/dist/io/opc/part-paths.d.ts +9 -0
  123. package/dist/io/opc/part-paths.js +5 -5
  124. package/dist/io/opc/read-opc.d.ts +58 -14
  125. package/dist/io/opc/read-opc.js +49 -21
  126. package/dist/io/opc/read-options.d.ts +5 -1
  127. package/dist/io/opc/sniff-format.d.ts +3 -3
  128. package/dist/io/opc/sniff-format.js +2 -2
  129. package/dist/io/opc/strict-relationships.d.ts +2 -0
  130. package/dist/io/opc/strict-relationships.js +12 -0
  131. package/dist/io/read-policy/column-budget.d.ts +26 -0
  132. package/dist/io/read-policy/column-budget.js +18 -0
  133. package/dist/io/read-policy/read-repair.d.ts +74 -0
  134. package/dist/io/read-policy/read-repair.js +119 -0
  135. package/dist/io/style/cell-style-resolution.d.ts +25 -0
  136. package/dist/io/style/cell-style-resolution.js +30 -0
  137. package/dist/io/style/xf-style.d.ts +23 -1
  138. package/dist/io/style/xf-style.js +8 -0
  139. package/dist/io/xlsb/formula.d.ts +2 -2
  140. package/dist/io/xlsb/formula.js +52 -35
  141. package/dist/io/xlsb/primitives.js +8 -14
  142. package/dist/io/xlsb/ptg-functions.d.ts +19 -10
  143. package/dist/io/xlsb/ptg-functions.js +495 -596
  144. package/dist/io/xlsb/read-metadata.d.ts +7 -0
  145. package/dist/io/xlsb/read-metadata.js +69 -0
  146. package/dist/io/xlsb/read-styles.js +45 -81
  147. package/dist/io/xlsb/read-worksheet.d.ts +26 -1
  148. package/dist/io/xlsb/read-worksheet.js +148 -106
  149. package/dist/io/xlsb/read.d.ts +6 -3
  150. package/dist/io/xlsb/read.js +70 -31
  151. package/dist/io/xlsb/record-stream.d.ts +30 -0
  152. package/dist/io/xlsb/record-stream.js +20 -0
  153. package/dist/io/xlsb/record-types.d.ts +14 -5
  154. package/dist/io/xlsb/record-types.js +14 -5
  155. package/dist/io/xlsb/sheet-protection.d.ts +20 -0
  156. package/dist/io/xlsb/sheet-protection.js +60 -0
  157. package/dist/io/xlsx/cell-accumulator.d.ts +24 -9
  158. package/dist/io/xlsx/cell-accumulator.js +74 -21
  159. package/dist/io/xlsx/cell-metadata.d.ts +39 -0
  160. package/dist/io/xlsx/cell-metadata.js +162 -0
  161. package/dist/io/xlsx/cell-value.d.ts +19 -5
  162. package/dist/io/xlsx/cell-value.js +45 -30
  163. package/dist/io/xlsx/color-xml.js +3 -2
  164. package/dist/io/xlsx/column-span.d.ts +11 -0
  165. package/dist/io/xlsx/column-span.js +9 -0
  166. package/dist/io/xlsx/comments.d.ts +9 -30
  167. package/dist/io/xlsx/comments.js +25 -83
  168. package/dist/io/xlsx/conditional-formatting.d.ts +43 -23
  169. package/dist/io/xlsx/conditional-formatting.js +437 -138
  170. package/dist/io/xlsx/data-validation.d.ts +12 -7
  171. package/dist/io/xlsx/data-validation.js +67 -53
  172. package/dist/io/xlsx/edit-vba.d.ts +7 -2
  173. package/dist/io/xlsx/edit-vba.js +52 -52
  174. package/dist/io/xlsx/font-xml.d.ts +7 -0
  175. package/dist/io/xlsx/font-xml.js +104 -0
  176. package/dist/io/xlsx/hyperlinks.d.ts +19 -21
  177. package/dist/io/xlsx/hyperlinks.js +12 -32
  178. package/dist/io/xlsx/images.d.ts +49 -16
  179. package/dist/io/xlsx/images.js +149 -50
  180. package/dist/io/xlsx/namespaces.d.ts +24 -0
  181. package/dist/io/xlsx/namespaces.js +11 -0
  182. package/dist/io/xlsx/package-plan.d.ts +60 -15
  183. package/dist/io/xlsx/package-plan.js +83 -18
  184. package/dist/io/xlsx/part-names.d.ts +6 -8
  185. package/dist/io/xlsx/part-names.js +3 -7
  186. package/dist/io/xlsx/pivot.d.ts +5 -5
  187. package/dist/io/xlsx/pivot.js +26 -24
  188. package/dist/io/xlsx/preserved-splices.d.ts +6 -0
  189. package/dist/io/xlsx/preserved-splices.js +74 -0
  190. package/dist/io/xlsx/read-comments.d.ts +29 -0
  191. package/dist/io/xlsx/read-comments.js +75 -0
  192. package/dist/io/xlsx/read-parts.d.ts +59 -0
  193. package/dist/io/xlsx/read-parts.js +259 -0
  194. package/dist/io/xlsx/read-pivot.js +11 -4
  195. package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +8 -5
  196. package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +24 -4
  197. package/dist/io/xlsx/read-rows.d.ts +35 -11
  198. package/dist/io/xlsx/read-rows.js +73 -49
  199. package/dist/io/xlsx/read-shared-strings.js +3 -2
  200. package/dist/io/xlsx/read-styles.d.ts +0 -30
  201. package/dist/io/xlsx/read-styles.js +45 -105
  202. package/dist/io/xlsx/read-workbook-xml.d.ts +42 -0
  203. package/dist/io/xlsx/read-workbook-xml.js +175 -0
  204. package/dist/io/xlsx/read-worksheet.d.ts +9 -3
  205. package/dist/io/xlsx/read-worksheet.js +70 -177
  206. package/dist/io/xlsx/read.d.ts +3 -14
  207. package/dist/io/xlsx/read.js +74 -411
  208. package/dist/io/xlsx/relationships.d.ts +3 -0
  209. package/dist/io/xlsx/relationships.js +4 -0
  210. package/dist/io/xlsx/rich-text.js +1 -1
  211. package/dist/io/xlsx/row-position.d.ts +20 -0
  212. package/dist/io/xlsx/row-position.js +11 -0
  213. package/dist/io/xlsx/row-xml.d.ts +139 -0
  214. package/dist/io/xlsx/row-xml.js +222 -0
  215. package/dist/io/xlsx/shared-formulas.js +2 -2
  216. package/dist/io/xlsx/sheet-properties.d.ts +10 -1
  217. package/dist/io/xlsx/sheet-properties.js +182 -20
  218. package/dist/io/xlsx/strict-parts.d.ts +7 -0
  219. package/dist/io/xlsx/strict-parts.js +171 -0
  220. package/dist/io/xlsx/style-elements.d.ts +41 -0
  221. package/dist/io/xlsx/style-elements.js +136 -0
  222. package/dist/io/xlsx/styles.d.ts +17 -3
  223. package/dist/io/xlsx/styles.js +13 -209
  224. package/dist/io/xlsx/tables.d.ts +10 -2
  225. package/dist/io/xlsx/tables.js +55 -52
  226. package/dist/io/xlsx/theme-xml.d.ts +32 -4
  227. package/dist/io/xlsx/theme-xml.js +106 -71
  228. package/dist/io/xlsx/threaded-comments.d.ts +14 -11
  229. package/dist/io/xlsx/threaded-comments.js +16 -8
  230. package/dist/io/xlsx/workbook-xml.d.ts +45 -6
  231. package/dist/io/xlsx/workbook-xml.js +62 -54
  232. package/dist/io/xlsx/worksheet-xml.d.ts +26 -70
  233. package/dist/io/xlsx/worksheet-xml.js +69 -282
  234. package/dist/io/xlsx/write-stream.d.ts +90 -19
  235. package/dist/io/xlsx/write-stream.js +80 -25
  236. package/dist/io/xlsx/write.d.ts +12 -6
  237. package/dist/io/xlsx/write.js +226 -123
  238. package/dist/rel-type.d.ts +25 -0
  239. package/dist/rel-type.js +9 -0
  240. package/dist/token-set.d.ts +13 -0
  241. package/dist/token-set.js +4 -1
  242. package/dist/vba/bytes.d.ts +16 -5
  243. package/dist/vba/bytes.js +27 -6
  244. package/dist/vba/cfb-format.d.ts +55 -0
  245. package/dist/vba/cfb-format.js +35 -0
  246. package/dist/vba/cfb-writer.d.ts +8 -0
  247. package/dist/vba/cfb-writer.js +40 -40
  248. package/dist/vba/cfb.d.ts +21 -4
  249. package/dist/vba/cfb.js +124 -90
  250. package/dist/vba/codepage.js +2 -1
  251. package/dist/vba/dir-records.d.ts +6 -0
  252. package/dist/vba/dir-records.js +10 -2
  253. package/dist/vba/errors.d.ts +11 -4
  254. package/dist/vba/index.d.ts +0 -2
  255. package/dist/vba/index.js +0 -2
  256. package/dist/vba/ms-ovba.d.ts +6 -2
  257. package/dist/vba/ms-ovba.js +66 -22
  258. package/dist/vba/project-editor.js +129 -119
  259. package/dist/vba/project.d.ts +15 -1
  260. package/dist/vba/project.js +38 -13
  261. package/dist/vba/vba-encoding.d.ts +4 -10
  262. package/dist/vba/vba-encoding.js +3 -17
  263. package/dist/xml/xml-attrs.d.ts +37 -0
  264. package/dist/xml/xml-attrs.js +30 -0
  265. package/dist/xml/xml-chars.d.ts +40 -0
  266. package/dist/xml/xml-chars.js +12 -0
  267. package/dist/xml/xml-namespaces.d.ts +31 -0
  268. package/dist/xml/xml-namespaces.js +50 -0
  269. package/dist/xml/xml-read.d.ts +101 -11
  270. package/dist/xml/xml-read.js +150 -18
  271. package/dist/xml/xml-scan.d.ts +25 -34
  272. package/dist/xml/xml-scan.js +69 -46
  273. package/dist/xml/xml.d.ts +59 -26
  274. package/dist/xml/xml.js +45 -23
  275. package/package.json +15 -9
  276. package/skills/ts-xlsx-upstream/SKILL.md +21 -13
  277. package/dist/core/modern-functions.d.ts +0 -1
@@ -51,3 +51,9 @@ export interface DirRecord {
51
51
  * @throws {VbaParseError} when a record's payload runs past the end of the stream.
52
52
  */
53
53
  export declare function dirRecords(dir: Uint8Array, context: string): Generator<DirRecord>;
54
+ /**
55
+ * One record's bytes, `Id(u16) Size(u32) data[Size]`: the grammar {@link dirRecords} walks, written.
56
+ * Built as bytes rather than spread into a `number[]`, which overflowed the call stack on a payload of a
57
+ * few hundred thousand bytes. Not for PROJECTVERSION, whose uncounted VersionMinor no edit here writes.
58
+ */
59
+ export declare function dirRecord(id: number, data: Uint8Array): Uint8Array;
@@ -1,4 +1,5 @@
1
- import { readU16, readU32 } from './bytes.js';
1
+ import { hex } from '../hex.js';
2
+ import { readU16, readU32, writeU16, writeU32 } from './bytes.js';
2
3
  import { VbaParseError } from './errors.js';
3
4
  export const REC_PROJECT_CODEPAGE = 0x0003;
4
5
  export const REC_PROJECT_VERSION = 0x0009;
@@ -19,10 +20,17 @@ export function* dirRecords(dir, context) {
19
20
  const size = readU32(dir, pos + 2);
20
21
  const dataStart = pos + 6;
21
22
  if (dataStart + size > dir.length) {
22
- throw new VbaParseError(`dir record 0x${id.toString(16)} ${context}`);
23
+ throw new VbaParseError(`dir record 0x${hex(id, 4)} ${context}`);
23
24
  }
24
25
  const end = dataStart + size + (id === REC_PROJECT_VERSION ? 2 : 0);
25
26
  yield { id, recordStart: pos, dataStart, size, end };
26
27
  pos = end;
27
28
  }
28
29
  }
30
+ export function dirRecord(id, data) {
31
+ const out = new Uint8Array(6 + data.length);
32
+ writeU16(out, 0, id);
33
+ writeU32(out, 2, data.length);
34
+ out.set(data, 6);
35
+ return out;
36
+ }
@@ -12,11 +12,18 @@ export declare class VbaParseError extends XlsxError {
12
12
  readonly code = "malformed-input";
13
13
  }
14
14
  /**
15
- * Thrown when authoring a VBA project (synthesizing a `vbaProject.bin` from module source) is asked to
16
- * produce something that cannot be encoded to a well-formed container: a stream name longer than the
17
- * [MS-CFB] 31-character limit, a duplicate stream name, or a project so large it would exceed the
18
- * writer's single-header DIFAT bound. This is a caller-side contract violation, distinct from
15
+ * Thrown when a structural edit of a VBA project (removing a module, adding a library reference) is
16
+ * asked to produce something that cannot be encoded to a well-formed container: a stream name longer
17
+ * than the [MS-CFB] 31-character limit, a duplicate stream name, or a project so large it would exceed
18
+ * the writer's single-header DIFAT bound. This is a caller-side contract violation, distinct from
19
19
  * {@link VbaParseError} (which reports a malformed blob *read* from an untrusted file).
20
+ *
21
+ * It shares `code: 'authoring'` with `AuthoringError` rather than being one, and it is the only
22
+ * subsystem in the tree that got its own authoring class. The reason is symmetry with
23
+ * {@link VbaParseError} and not the VBA subsystem's importance: a caller who catches the parse error
24
+ * to skip a macro project it cannot read is the same caller who catches the author error to skip one
25
+ * it cannot write, and the two halves of that pair have to be nameable the same way. A CSV or table
26
+ * authoring failure has no read-side twin to pair with, which is why those stay `AuthoringError`.
20
27
  */
21
28
  export declare class VbaAuthorError extends XlsxError {
22
29
  readonly name = "VbaAuthorError";
@@ -1,5 +1,3 @@
1
- export { type CfbNode, type CfbStorage, type CfbStream, writeCompoundFile } from './cfb-writer.ts';
2
1
  export { VbaAuthorError, VbaParseError } from './errors.ts';
3
- export { compressContainer, decompressContainer } from './ms-ovba.ts';
4
2
  export { parseVbaProject, VBA_PROJECT_CONTENT_TYPE, VBA_PROJECT_PART_PATH, VBA_PROJECT_REL_TYPE, type VbaModule, type VbaModuleKind, type VbaProject, type VbaProjectSignature, type VbaProjectSignatureKind, vbaProjectSignatureKind, } from './project.ts';
5
3
  export { addVbaReference, removeVbaModule, type VbaLibraryReference } from './project-editor.ts';
package/dist/vba/index.js CHANGED
@@ -1,5 +1,3 @@
1
- export { writeCompoundFile } from './cfb-writer.js';
2
1
  export { VbaAuthorError, VbaParseError } from './errors.js';
3
- export { compressContainer, decompressContainer } from './ms-ovba.js';
4
2
  export { parseVbaProject, VBA_PROJECT_CONTENT_TYPE, VBA_PROJECT_PART_PATH, VBA_PROJECT_REL_TYPE, vbaProjectSignatureKind, } from './project.js';
5
3
  export { addVbaReference, removeVbaModule } from './project-editor.js';
@@ -6,7 +6,11 @@ export declare function decompressContainer(buf: Uint8Array, start?: number, max
6
6
  /**
7
7
  * Compress `data` into an MS-OVBA CompressedContainer: the inverse of {@link decompressContainer}.
8
8
  * Every 4096-decompressed-byte window is emitted as a compressed chunk of literal and copy tokens, or
9
- * stored verbatim when compression would not shrink it (so the encoded chunk never exceeds the 12-bit
10
- * size field). The result re-expands to `data` byte-for-byte.
9
+ * stored raw when those tokens would not fit the 4096 bytes a chunk's data may hold.
10
+ *
11
+ * The result re-expands to `data` byte-for-byte, with one exception the format itself prescribes: a
12
+ * final window that is shorter than 4096 bytes and does not compress (only possible past about 3640
13
+ * bytes, where a literal's flag-bit overhead first overflows the chunk) is stored raw and padded with
14
+ * `0x00` to 4096, so it re-expands with those zeros after it ([MS-OVBA] 2.4.1.3.10).
11
15
  */
12
16
  export declare function compressContainer(data: Uint8Array): Uint8Array;
@@ -1,25 +1,30 @@
1
+ import { hex } from '../hex.js';
1
2
  import { readU16 } from './bytes.js';
2
3
  import { VbaParseError } from './errors.js';
3
4
  const MAX_CHUNK_DECOMPRESSED = 4096;
4
5
  const CHUNK_SIGNATURE = 0b011 << 12;
6
+ const CHUNK_SIGNATURE_MASK = 0b111 << 12;
5
7
  const CHUNK_COMPRESSED_FLAG = 0x8000;
8
+ const MAX_MATCH_CANDIDATES = 64;
9
+ const HASH_BITS = 12;
10
+ const NO_CANDIDATE = -1;
6
11
  const DEFAULT_MAX_OUTPUT = 64 * 1024 * 1024;
7
12
  export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTPUT) {
8
13
  if (start >= buf.length) {
9
14
  throw new VbaParseError(`compressed container starts past end of stream (${start} >= ${buf.length})`);
10
15
  }
11
16
  if (buf[start] !== 0x01) {
12
- throw new VbaParseError(`compressed container must begin with a 0x01 signature byte, found 0x${(buf[start] ?? 0).toString(16)}`);
17
+ throw new VbaParseError(`compressed container must begin with a 0x01 signature byte, found 0x${hex(buf[start] ?? 0, 2)}`);
13
18
  }
14
- const out = new DecompressedBytes(maxOutput);
19
+ const out = new ByteSink(maxOutput);
15
20
  let pos = start + 1;
16
21
  while (pos + 2 <= buf.length) {
17
22
  const header = readU16(buf, pos);
18
23
  pos += 2;
19
24
  const chunkDataSize = (header & 0x0fff) + 1;
20
- const compressed = (header & 0x8000) !== 0;
21
- if (((header >> 12) & 0x7) !== 0b011) {
22
- throw new VbaParseError(`chunk header has a bad 0b011 signature (0x${header.toString(16)})`);
25
+ const compressed = (header & CHUNK_COMPRESSED_FLAG) !== 0;
26
+ if ((header & CHUNK_SIGNATURE_MASK) !== CHUNK_SIGNATURE) {
27
+ throw new VbaParseError(`chunk header has a bad 0b011 signature (0x${hex(header, 4)})`);
23
28
  }
24
29
  const chunkEnd = pos + chunkDataSize;
25
30
  if (chunkEnd > buf.length) {
@@ -55,16 +60,19 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
55
60
  for (let i = 0; i < length; i++)
56
61
  out.push(out.at(src + i));
57
62
  }
63
+ if (out.length - chunkStart > MAX_CHUNK_DECOMPRESSED) {
64
+ throw new VbaParseError(`chunk decompressed to more than ${MAX_CHUNK_DECOMPRESSED} bytes, which [MS-OVBA] forbids`);
65
+ }
58
66
  }
59
67
  pos = chunkEnd;
60
68
  }
61
69
  return out.bytes();
62
70
  }
63
- class DecompressedBytes {
71
+ class ByteSink {
64
72
  #buf;
65
73
  #length = 0;
66
74
  #limit;
67
- constructor(limit) {
75
+ constructor(limit = Infinity) {
68
76
  this.#limit = limit;
69
77
  this.#buf = new Uint8Array(Math.min(MAX_CHUNK_DECOMPRESSED, limit));
70
78
  }
@@ -74,6 +82,9 @@ class DecompressedBytes {
74
82
  at(index) {
75
83
  return this.#buf[index];
76
84
  }
85
+ set(index, byte) {
86
+ this.#buf[index] = byte;
87
+ }
77
88
  push(byte) {
78
89
  if (this.#length >= this.#limit) {
79
90
  throw new VbaParseError(`decompressed output exceeds the ${this.#limit}-byte ceiling (possible bomb)`);
@@ -85,26 +96,47 @@ class DecompressedBytes {
85
96
  }
86
97
  this.#buf[this.#length++] = byte;
87
98
  }
99
+ pushAll(bytes) {
100
+ for (const byte of bytes)
101
+ this.push(byte);
102
+ }
88
103
  bytes() {
89
104
  return this.#buf.slice(0, this.#length);
90
105
  }
91
106
  }
92
107
  export function compressContainer(data) {
93
- const out = [0x01];
108
+ const out = new ByteSink();
109
+ out.push(0x01);
94
110
  for (let start = 0; start < data.length; start += MAX_CHUNK_DECOMPRESSED) {
95
111
  const chunk = data.subarray(start, Math.min(start + MAX_CHUNK_DECOMPRESSED, data.length));
96
112
  const tokens = compressChunk(chunk);
97
- const compressed = tokens.length < chunk.length;
98
- const body = compressed ? tokens : chunk;
113
+ const compressed = tokens.length <= MAX_CHUNK_DECOMPRESSED;
114
+ const body = compressed ? tokens : padToChunk(chunk);
99
115
  const header = (compressed ? CHUNK_COMPRESSED_FLAG : 0) | CHUNK_SIGNATURE | ((body.length - 1) & 0x0fff);
100
- out.push(header & 0xff, (header >> 8) & 0xff);
101
- for (const b of body)
102
- out.push(b);
116
+ out.push(header & 0xff);
117
+ out.push((header >> 8) & 0xff);
118
+ out.pushAll(body);
103
119
  }
104
- return Uint8Array.from(out);
120
+ return out.bytes();
121
+ }
122
+ function padToChunk(chunk) {
123
+ if (chunk.length === MAX_CHUNK_DECOMPRESSED)
124
+ return chunk;
125
+ const padded = new Uint8Array(MAX_CHUNK_DECOMPRESSED);
126
+ padded.set(chunk);
127
+ return padded;
105
128
  }
106
129
  function compressChunk(chunk) {
107
- const tokens = [];
130
+ const tokens = new ByteSink();
131
+ const heads = new Int32Array(1 << HASH_BITS).fill(NO_CANDIDATE);
132
+ const prev = new Int32Array(MAX_CHUNK_DECOMPRESSED).fill(NO_CANDIDATE);
133
+ const remember = (at) => {
134
+ if (at + 2 >= chunk.length)
135
+ return;
136
+ const bucket = hash3(chunk, at);
137
+ prev[at] = heads[bucket];
138
+ heads[bucket] = at;
139
+ };
108
140
  let pos = 0;
109
141
  while (pos < chunk.length) {
110
142
  const flagIndex = tokens.length;
@@ -116,37 +148,49 @@ function compressChunk(chunk) {
116
148
  const windowStart = Math.max(0, pos - (1 << bitCount));
117
149
  let bestLength = 0;
118
150
  let bestOffset = 0;
119
- for (let cand = pos - 1; cand >= windowStart; cand--) {
151
+ let candidate = pos + 2 < chunk.length ? heads[hash3(chunk, pos)] : NO_CANDIDATE;
152
+ for (let tried = 0; candidate >= windowStart && tried < MAX_MATCH_CANDIDATES; tried++) {
120
153
  let len = 0;
121
154
  while (len < maxLength &&
122
155
  pos + len < chunk.length &&
123
- chunk[cand + len] === chunk[pos + len]) {
156
+ chunk[candidate + len] === chunk[pos + len]) {
124
157
  len++;
125
158
  }
126
159
  if (len > bestLength) {
127
160
  bestLength = len;
128
- bestOffset = pos - cand;
161
+ bestOffset = pos - candidate;
129
162
  if (bestLength === maxLength)
130
163
  break;
131
164
  }
165
+ candidate = prev[candidate];
132
166
  }
133
167
  if (bestLength >= 3) {
134
168
  const token = ((bestOffset - 1) << (16 - bitCount)) | (bestLength - 3);
135
- tokens.push(token & 0xff, (token >> 8) & 0xff);
169
+ tokens.push(token & 0xff);
170
+ tokens.push((token >> 8) & 0xff);
136
171
  flags |= 1 << bit;
172
+ for (let i = 0; i < bestLength; i++)
173
+ remember(pos + i);
137
174
  pos += bestLength;
138
175
  }
139
176
  else {
140
177
  tokens.push(chunk[pos]);
178
+ remember(pos);
141
179
  pos++;
142
180
  }
143
181
  }
144
- tokens[flagIndex] = flags;
182
+ tokens.set(flagIndex, flags);
145
183
  }
146
- return tokens;
184
+ return tokens.bytes();
185
+ }
186
+ function hash3(chunk, pos) {
187
+ const first = chunk[pos];
188
+ const second = chunk[pos + 1];
189
+ const third = chunk[pos + 2];
190
+ return ((first << 8) ^ (second << 4) ^ third) & ((1 << HASH_BITS) - 1);
147
191
  }
148
192
  function copyTokenHelp(decompressedSoFar) {
149
- const bitCount = Math.max(Math.ceil(Math.log2(Math.max(decompressedSoFar, 1))), 4);
193
+ const bitCount = Math.min(Math.max(Math.ceil(Math.log2(Math.max(decompressedSoFar, 1))), 4), Math.log2(MAX_CHUNK_DECOMPRESSED));
150
194
  const lengthMask = 0xffff >> bitCount;
151
195
  return { lengthMask, bitCount };
152
196
  }
@@ -1,62 +1,63 @@
1
- import { readU16 } from './bytes.js';
2
- import { writeCompoundFile } from './cfb-writer.js';
1
+ import { utf16leBytes } from '../bytes.js';
2
+ import { InternalError, quoted } from '../errors.js';
3
+ import { hex } from '../hex.js';
4
+ import { concat, readU16, spliceBytes, writeU16, writeU32 } from './bytes.js';
5
+ import { sameEntryName } from './cfb-format.js';
6
+ import { isStream, writeCompoundFile } from './cfb-writer.js';
3
7
  import { CompoundFile } from './cfb.js';
4
8
  import { decoderForCodePage, encoderForCodePage } from './codepage.js';
5
- import { dirRecords, REC_MODULE_NAME, REC_MODULE_STREAMNAME, REC_MODULE_TERMINATOR, REC_MODULES_COUNT, REC_REFERENCE_NAME, REC_REFERENCE_NAME_UNICODE, REC_REFERENCE_REGISTERED, } from './dir-records.js';
9
+ import { dirRecord, dirRecords, REC_MODULE_NAME, REC_MODULE_STREAMNAME, REC_MODULE_TERMINATOR, REC_MODULES_COUNT, REC_REFERENCE_NAME, REC_REFERENCE_NAME_UNICODE, REC_REFERENCE_REGISTERED, } from './dir-records.js';
6
10
  import { VbaAuthorError, VbaParseError } from './errors.js';
7
11
  import { compressContainer, decompressContainer } from './ms-ovba.js';
8
- import { parseVbaProject } from './project.js';
9
- import { push, u16, u32, utf16le, validateVbaName } from './vba-encoding.js';
10
- const DIR_STREAM = 'dir';
11
- const PROJECT_STREAM = 'PROJECT';
12
- const PROJECTWM_STREAM = 'PROJECTwm';
13
- const VBA_STORAGE = 'VBA';
12
+ import { DIR_PATH, parseVbaProjectIn, PROJECT_PATH, PROJECTWM_PATH, VBA_STORAGE } from './project.js';
13
+ import { validateReferenceName } from './vba-encoding.js';
14
14
  export function removeVbaModule(bin, name) {
15
- const project = parseVbaProject(bin);
15
+ const cfb = new CompoundFile(bin);
16
+ const project = parseVbaProjectIn(cfb);
16
17
  const nameKey = name.toUpperCase();
17
18
  const module = project.modules.find((m) => m.name.toUpperCase() === nameKey);
18
19
  if (!module)
19
- throw new VbaAuthorError(`module '${name}' is not in the VBA project`);
20
+ throw new VbaAuthorError(`module ${quoted(name)} is not in the VBA project`);
20
21
  if (module.kind !== 'procedural' && module.kind !== 'class') {
21
- throw new VbaAuthorError(`cannot remove module '${name}': its kind '${module.kind}' is tied to host linkage this ` +
22
+ throw new VbaAuthorError(`cannot remove module ${quoted(name)}: its kind ${quoted(module.kind)} is tied to host linkage this ` +
22
23
  'primitive cannot verify');
23
24
  }
24
- const cfb = new CompoundFile(bin);
25
- const dirCompressed = cfb.readStream(DIR_STREAM);
25
+ const decoder = decoderForCodePage(project.codePage);
26
+ const dirCompressed = cfb.readStream(DIR_PATH);
26
27
  if (!dirCompressed)
27
28
  throw new VbaParseError("VBA project has no 'dir' stream");
28
- const patchedDir = removeModuleDirRecord(decompressContainer(dirCompressed), module.streamName, project.codePage);
29
- const replacements = new Map([[DIR_STREAM, compressContainer(patchedDir)]]);
30
- const decoder = decoderForCodePage(project.codePage);
31
- const encode = encoderForCodePage(project.codePage);
32
- const projectText = cfb.readStream(PROJECT_STREAM);
29
+ const patchedDir = removeModuleDirRecord(decompressContainer(dirCompressed), module.streamName, decoder);
30
+ const replacements = [{ path: DIR_PATH, data: compressContainer(patchedDir) }];
31
+ const projectText = cfb.readStream(PROJECT_PATH);
33
32
  if (projectText) {
34
- replacements.set(PROJECT_STREAM, encode(removeProjectStreamLines(decoder.decode(projectText), module.name, module.kind)));
33
+ replacements.push({
34
+ path: PROJECT_PATH,
35
+ data: removeProjectStreamLines(projectText, module.name, module.kind, decoder),
36
+ });
35
37
  }
36
- const projectwm = cfb.readStream(PROJECTWM_STREAM);
38
+ const projectwm = cfb.readStream(PROJECTWM_PATH);
37
39
  if (projectwm) {
38
- replacements.set(PROJECTWM_STREAM, removeProjectwmRecord(projectwm, project.modules.length, module.name, decoder));
39
- }
40
- const applied = new Set();
41
- const withReplacements = replaceStreams(cfb.tree(), replacements, applied);
42
- if (!applied.has(DIR_STREAM))
43
- throw new VbaParseError("VBA project 'dir' stream is not in the container tree");
44
- const removed = new Set();
45
- const newTree = removeFromStorage(withReplacements, VBA_STORAGE, module.streamName, removed);
46
- if (!removed.has(VBA_STORAGE)) {
47
- throw new VbaParseError(`module stream '${module.streamName}' is not in the '${VBA_STORAGE}' storage`);
40
+ replacements.push({
41
+ path: PROJECTWM_PATH,
42
+ data: removeProjectwmRecord(projectwm, project.modules.length, module.name, decoder),
43
+ });
48
44
  }
45
+ const newTree = withoutStream(replaceStreams(cfb.tree(), replacements), [
46
+ VBA_STORAGE,
47
+ module.streamName,
48
+ ]);
49
49
  return writeCompoundFile(newTree);
50
50
  }
51
51
  const MAX_LIBID_VERSION = 0xffff;
52
52
  const MAX_LIBID_LCID = 0xffffffff;
53
53
  const MAX_DISPLAY_NAME_CHARS = 255;
54
+ const MAX_LIBID_PATH_CHARS = 32_767;
54
55
  const GUID_PATTERN = /^\{?([0-9A-Fa-f]{8})-([0-9A-Fa-f]{4})-([0-9A-Fa-f]{4})-([0-9A-Fa-f]{4})-([0-9A-Fa-f]{12})\}?$/;
55
56
  function normalizeReference(ref) {
56
- validateVbaName(ref.name, 'reference');
57
+ validateReferenceName(ref.name);
57
58
  const guidMatch = GUID_PATTERN.exec(ref.guid.trim());
58
59
  if (!guidMatch)
59
- throw new VbaAuthorError(`invalid reference GUID '${ref.guid}'`);
60
+ throw new VbaAuthorError(`invalid reference GUID ${quoted(ref.guid)}`);
60
61
  const guid = `{${guidMatch.slice(1, 6).join('-').toUpperCase()}}`;
61
62
  for (const [field, value] of [
62
63
  ['majorVersion', ref.majorVersion],
@@ -70,50 +71,43 @@ function normalizeReference(ref) {
70
71
  if (!Number.isInteger(lcid) || lcid < 0 || lcid > MAX_LIBID_LCID) {
71
72
  throw new VbaAuthorError(`reference lcid must be an integer in [0, 0xFFFFFFFF], got ${lcid}`);
72
73
  }
74
+ if (ref.path.length > MAX_LIBID_PATH_CHARS) {
75
+ throw new VbaAuthorError(`reference path is ${ref.path.length} characters long, past the ${MAX_LIBID_PATH_CHARS} a Windows path can hold`);
76
+ }
73
77
  if (ref.path.length === 0 || ref.path.includes('\0') || ref.path.includes('#')) {
74
- throw new VbaAuthorError(`invalid reference path '${ref.path}' (must be non-empty and contain no NUL or '#')`);
78
+ throw new VbaAuthorError(`invalid reference path ${quoted(ref.path)} (must be non-empty and contain no NUL or '#')`);
75
79
  }
76
80
  const displayName = ref.displayName ?? ref.name;
77
81
  if (displayName.length === 0 ||
78
82
  displayName.length > MAX_DISPLAY_NAME_CHARS ||
79
83
  displayName.includes('\0')) {
80
- throw new VbaAuthorError(`invalid reference display name '${displayName}'`);
84
+ throw new VbaAuthorError(`invalid reference display name ${quoted(displayName)}`);
81
85
  }
82
- const libid = `*\\G${guid}#${ref.majorVersion.toString(16).toUpperCase()}.` +
83
- `${ref.minorVersion.toString(16).toUpperCase()}#${lcid.toString(16).toUpperCase()}` +
86
+ const libid = `*\\G${guid}#${hex(ref.majorVersion, 1)}.${hex(ref.minorVersion, 1)}#${hex(lcid, 1)}` +
84
87
  `#${ref.path}#${displayName}`;
85
88
  return { name: ref.name, libid };
86
89
  }
87
90
  export function addVbaReference(bin, ref) {
88
91
  const normalized = normalizeReference(ref);
89
- const project = parseVbaProject(bin);
90
- const encode = encoderForCodePage(project.codePage);
91
92
  const cfb = new CompoundFile(bin);
92
- const dirCompressed = cfb.readStream(DIR_STREAM);
93
+ const encode = encoderForCodePage(parseVbaProjectIn(cfb).codePage);
94
+ const dirCompressed = cfb.readStream(DIR_PATH);
93
95
  if (!dirCompressed)
94
96
  throw new VbaParseError("VBA project has no 'dir' stream");
95
97
  const records = buildReferenceDirRecords(normalized, encode);
96
98
  const patchedDir = insertReferenceDirRecords(decompressContainer(dirCompressed), records);
97
- const replacements = new Map([[DIR_STREAM, compressContainer(patchedDir)]]);
98
- const applied = new Set();
99
- const newTree = replaceStreams(cfb.tree(), replacements, applied);
100
- if (!applied.has(DIR_STREAM))
101
- throw new VbaParseError("VBA project 'dir' stream is not in the container tree");
102
- return writeCompoundFile(newTree);
99
+ return writeCompoundFile(replaceStreams(cfb.tree(), [{ path: DIR_PATH, data: compressContainer(patchedDir) }]));
103
100
  }
104
101
  function buildReferenceDirRecords(ref, encode) {
105
- const r = [];
106
- const nameBytes = [...encode(ref.name)];
107
- push(r, REC_REFERENCE_NAME, nameBytes);
108
- push(r, REC_REFERENCE_NAME_UNICODE, utf16le(ref.name));
109
- const libidBytes = [...encode(ref.libid)];
110
- push(r, REC_REFERENCE_REGISTERED, [
111
- ...u32(libidBytes.length),
112
- ...libidBytes,
113
- ...u32(0),
114
- ...u16(0),
102
+ const libid = encode(ref.libid);
103
+ const sizeOfLibid = new Uint8Array(4);
104
+ writeU32(sizeOfLibid, 0, libid.length);
105
+ const reserved = new Uint8Array(6);
106
+ return concat([
107
+ dirRecord(REC_REFERENCE_NAME, encode(ref.name)),
108
+ dirRecord(REC_REFERENCE_NAME_UNICODE, utf16leBytes(ref.name)),
109
+ dirRecord(REC_REFERENCE_REGISTERED, concat([sizeOfLibid, libid, reserved])),
115
110
  ]);
116
- return r;
117
111
  }
118
112
  function insertReferenceDirRecords(dir, records) {
119
113
  let insertAt = -1;
@@ -125,15 +119,9 @@ function insertReferenceDirRecords(dir, records) {
125
119
  }
126
120
  if (insertAt < 0)
127
121
  throw new VbaParseError('dir stream is missing MODULES_COUNT');
128
- const rec = Uint8Array.from(records);
129
- const out = new Uint8Array(dir.length + rec.length);
130
- out.set(dir.subarray(0, insertAt), 0);
131
- out.set(rec, insertAt);
132
- out.set(dir.subarray(insertAt), insertAt + rec.length);
133
- return out;
122
+ return spliceBytes(dir, insertAt, insertAt, records);
134
123
  }
135
- function removeModuleDirRecord(dir, streamName, codePage) {
136
- const decoder = decoderForCodePage(codePage);
124
+ function removeModuleDirRecord(dir, streamName, decoder) {
137
125
  let countAt = -1;
138
126
  let blockStart = -1;
139
127
  let removeStart = -1;
@@ -143,7 +131,8 @@ function removeModuleDirRecord(dir, streamName, codePage) {
143
131
  if (id === REC_MODULES_COUNT) {
144
132
  if (size < 2)
145
133
  throw new VbaParseError('PROJECTMODULES MODULES_COUNT record is malformed');
146
- countAt = dataStart;
134
+ if (countAt < 0)
135
+ countAt = dataStart;
147
136
  }
148
137
  else if (id === REC_MODULE_NAME) {
149
138
  blockStart = recordStart;
@@ -163,36 +152,49 @@ function removeModuleDirRecord(dir, streamName, codePage) {
163
152
  if (countAt < 0)
164
153
  throw new VbaParseError('dir stream is missing MODULES_COUNT');
165
154
  if (removeStart < 0 || removeEnd < 0) {
166
- throw new VbaParseError(`module stream '${streamName}' not found in the dir stream`);
167
- }
168
- const out = new Uint8Array(dir.length - (removeEnd - removeStart));
169
- out.set(dir.subarray(0, removeStart), 0);
170
- out.set(dir.subarray(removeEnd), removeStart);
171
- const newCount = readU16(out, countAt) - 1;
172
- out[countAt] = newCount & 0xff;
173
- out[countAt + 1] = (newCount >> 8) & 0xff;
155
+ throw new VbaParseError(`module stream ${quoted(streamName)} not found in the dir stream`);
156
+ }
157
+ if (countAt >= removeStart) {
158
+ throw new VbaParseError('dir stream declares MODULES_COUNT after a module block');
159
+ }
160
+ const count = readU16(dir, countAt);
161
+ if (count === 0) {
162
+ throw new VbaParseError('dir stream declares zero modules but carries a module block');
163
+ }
164
+ const out = spliceBytes(dir, removeStart, removeEnd);
165
+ writeU16(out, countAt, count - 1);
174
166
  return out;
175
167
  }
176
- function removeProjectStreamLines(text, name, kind) {
177
- const eol = text.includes('\r\n') ? '\r\n' : '\n';
178
- const lines = text.split(/\r\n|\r|\n/);
168
+ function removeProjectStreamLines(stream, name, kind, decoder) {
169
+ const lines = projectLines(stream, decoder);
179
170
  const declLine = `${kind === 'procedural' ? 'Module' : 'Class'}=${name}`;
180
- const declIndex = lines.indexOf(declLine);
181
- if (declIndex >= 0)
182
- lines.splice(declIndex, 1);
183
- const wsIndex = lines.findIndex((l) => l.trim() === '[Workspace]');
184
- if (wsIndex >= 0) {
185
- for (let i = wsIndex + 1; i < lines.length; i++) {
186
- const l = lines[i];
187
- if (l.trim() === '' || l.startsWith('['))
188
- break;
189
- if (l.startsWith(`${name}=`)) {
190
- lines.splice(i, 1);
191
- break;
192
- }
171
+ const removals = [];
172
+ const decl = lines.find((line) => line.text === declLine);
173
+ if (decl !== undefined)
174
+ removals.push(decl);
175
+ const wsIndex = lines.findIndex((line) => line.text.trim() === '[Workspace]');
176
+ for (const line of wsIndex < 0 ? [] : lines.slice(wsIndex + 1)) {
177
+ if (line.text.trim() === '' || line.text.startsWith('['))
178
+ break;
179
+ if (line.text.startsWith(`${name}=`)) {
180
+ removals.push(line);
181
+ break;
193
182
  }
194
183
  }
195
- return lines.join(eol);
184
+ return removals
185
+ .sort((a, b) => b.start - a.start)
186
+ .reduce((bytes, line) => spliceBytes(bytes, line.start, line.end), stream);
187
+ }
188
+ function projectLines(stream, decoder) {
189
+ const lines = [];
190
+ for (let start = 0; start < stream.length;) {
191
+ const lf = stream.indexOf(0x0a, start);
192
+ const end = lf < 0 ? stream.length : lf + 1;
193
+ const contentEnd = lf > start && stream[lf - 1] === 0x0d ? lf - 1 : lf < 0 ? stream.length : lf;
194
+ lines.push({ text: decoder.decode(stream.subarray(start, contentEnd)), start, end });
195
+ start = end;
196
+ }
197
+ return lines;
196
198
  }
197
199
  function removeProjectwmRecord(wm, existingModuleCount, name, decoder) {
198
200
  let pos = 0;
@@ -218,37 +220,45 @@ function removeProjectwmRecord(wm, existingModuleCount, name, decoder) {
218
220
  }
219
221
  }
220
222
  if (removeStart < 0 || removeEnd < 0) {
221
- throw new VbaParseError(`module '${name}' not found in the PROJECTwm stream`);
223
+ throw new VbaParseError(`module ${quoted(name)} not found in the PROJECTwm stream`);
222
224
  }
223
- const out = new Uint8Array(wm.length - (removeEnd - removeStart));
224
- out.set(wm.subarray(0, removeStart), 0);
225
- out.set(wm.subarray(removeEnd), removeStart);
226
- return out;
227
- }
228
- function removeFromStorage(nodes, storageName, streamName, removed) {
229
- return nodes.map((n) => {
230
- if ('data' in n)
231
- return n;
232
- const children = removeFromStorage(n.children, storageName, streamName, removed);
233
- if (n.name === storageName && !removed.has(storageName)) {
234
- const filtered = children.filter((c) => !('data' in c && c.name === streamName));
235
- if (filtered.length !== children.length)
236
- removed.add(storageName);
237
- return { name: n.name, children: filtered };
238
- }
239
- return { name: n.name, children };
240
- });
225
+ return spliceBytes(wm, removeStart, removeEnd);
241
226
  }
242
- function replaceStreams(nodes, replacements, applied) {
243
- return nodes.map((node) => {
244
- if ('data' in node) {
245
- const data = replacements.get(node.name);
246
- if (data !== undefined) {
247
- applied.add(node.name);
248
- return { name: node.name, data };
249
- }
227
+ function replaceStreams(nodes, replacements) {
228
+ let applied = 0;
229
+ const rebuild = (level, pending, depth) => level.map((node) => {
230
+ const here = pending.filter((replacement) => {
231
+ const segment = replacement.path[depth];
232
+ return segment !== undefined && sameEntryName(segment, node.name);
233
+ });
234
+ if (here.length === 0)
250
235
  return node;
236
+ if (isStream(node)) {
237
+ const replacement = here.find((candidate) => candidate.path.length === depth + 1);
238
+ if (replacement === undefined)
239
+ return node;
240
+ applied++;
241
+ return { name: node.name, data: replacement.data };
251
242
  }
252
- return { name: node.name, children: replaceStreams(node.children, replacements, applied) };
243
+ return { name: node.name, children: rebuild(node.children, here, depth + 1) };
253
244
  });
245
+ const rebuilt = rebuild(nodes, replacements, 0);
246
+ if (applied !== replacements.length) {
247
+ throw new InternalError('a VBA stream read by its path is missing from the container tree');
248
+ }
249
+ return rebuilt;
250
+ }
251
+ function withoutStream(nodes, path) {
252
+ const [name, ...rest] = path;
253
+ const index = nodes.findIndex((node) => name !== undefined &&
254
+ sameEntryName(node.name, name) &&
255
+ isStream(node) === (rest.length === 0));
256
+ const target = nodes[index];
257
+ if (target === undefined) {
258
+ throw new InternalError('a VBA module stream read by its path is missing from the container tree');
259
+ }
260
+ const kept = isStream(target)
261
+ ? []
262
+ : [{ name: target.name, children: withoutStream(target.children, rest) }];
263
+ return [...nodes.slice(0, index), ...kept, ...nodes.slice(index + 1)];
254
264
  }
@@ -1,3 +1,4 @@
1
+ import { CompoundFile } from './cfb.ts';
1
2
  /** How a module participates in the project: the classification the VBA editor shows. */
2
3
  export type VbaModuleKind = 'procedural' | 'document' | 'class' | 'designer';
3
4
  export interface VbaModule {
@@ -42,4 +43,17 @@ export interface VbaProjectSignature {
42
43
  * carries (`.../office/2006/...` vs `.../2014/...` vs `.../2020/...`).
43
44
  */
44
45
  export declare function vbaProjectSignatureKind(relType: string): VbaProjectSignatureKind | undefined;
45
- export declare function parseVbaProject(bin: Uint8Array): VbaProject;
46
+ export declare const VBA_STORAGE = "VBA";
47
+ export declare const DIR_PATH: readonly string[];
48
+ export declare const PROJECT_PATH: readonly string[];
49
+ export declare const PROJECTWM_PATH: readonly string[];
50
+ export declare function parseVbaProject(bin: Uint8Array, maxOutput?: number): VbaProject;
51
+ /**
52
+ * As {@link parseVbaProject}, over a container the caller has already opened.
53
+ *
54
+ * Both project editors parse fail-closed first and then need the container itself, and each was
55
+ * building a second `CompoundFile` over the same bytes: a second walk of the DIFAT, FAT, mini-FAT,
56
+ * directory and mini-stream, which is roughly double the cost and double the peak allocation of every
57
+ * edit. Opening it once and handing it in is the whole of the fix.
58
+ */
59
+ export declare function parseVbaProjectIn(cfb: CompoundFile, maxOutput?: number): VbaProject;