@shbernal/ts-xlsx 2.1.0 → 3.1.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 (225) hide show
  1. package/README.md +4 -1
  2. package/dist/bytes.d.ts +7 -0
  3. package/dist/bytes.js +22 -0
  4. package/dist/core/address.d.ts +60 -2
  5. package/dist/core/address.js +40 -24
  6. package/dist/core/autofilter.d.ts +5 -4
  7. package/dist/core/autofilter.js +34 -35
  8. package/dist/core/axis-handle.d.ts +10 -0
  9. package/dist/core/axis-handle.js +17 -0
  10. package/dist/core/cell.d.ts +25 -0
  11. package/dist/core/cell.js +9 -3
  12. package/dist/core/clone.d.ts +29 -0
  13. package/dist/core/clone.js +24 -0
  14. package/dist/core/color-resolution.js +2 -4
  15. package/dist/core/column.d.ts +5 -1
  16. package/dist/core/column.js +34 -35
  17. package/dist/core/comment-thread.js +2 -1
  18. package/dist/core/conditional-formatting-overlay.d.ts +2 -1
  19. package/dist/core/conditional-formatting-overlay.js +2 -2
  20. package/dist/core/conditional-formatting.d.ts +14 -6
  21. package/dist/core/conditional-formatting.js +45 -43
  22. package/dist/core/containers.d.ts +12 -0
  23. package/dist/core/containers.js +5 -0
  24. package/dist/core/data-validation-overlay.d.ts +2 -1
  25. package/dist/core/data-validation-overlay.js +4 -4
  26. package/dist/core/data-validation.d.ts +9 -3
  27. package/dist/core/data-validation.js +21 -18
  28. package/dist/core/date-format.d.ts +20 -0
  29. package/dist/core/date-format.js +160 -0
  30. package/dist/core/date.d.ts +43 -8
  31. package/dist/core/date.js +18 -2
  32. package/dist/core/formula.d.ts +26 -0
  33. package/dist/core/formula.js +20 -4
  34. package/dist/core/grid-edits.d.ts +2 -3
  35. package/dist/core/grid-edits.js +69 -70
  36. package/dist/core/grid-shift.d.ts +58 -9
  37. package/dist/core/grid-shift.js +26 -4
  38. package/dist/core/image.d.ts +14 -10
  39. package/dist/core/image.js +9 -9
  40. package/dist/core/internal.d.ts +8 -2
  41. package/dist/core/limits.d.ts +23 -0
  42. package/dist/core/limits.js +4 -0
  43. package/dist/core/merge-index.d.ts +31 -0
  44. package/dist/core/merge-index.js +60 -0
  45. package/dist/core/merge.d.ts +5 -14
  46. package/dist/core/merge.js +18 -27
  47. package/dist/core/page-setup.d.ts +72 -2
  48. package/dist/core/page-setup.js +34 -8
  49. package/dist/core/pivot-table.d.ts +1 -1
  50. package/dist/core/pivot-table.js +67 -53
  51. package/dist/core/protection.js +2 -11
  52. package/dist/core/range.js +34 -30
  53. package/dist/core/row-input.js +2 -2
  54. package/dist/core/row.d.ts +5 -1
  55. package/dist/core/row.js +22 -23
  56. package/dist/core/style.d.ts +134 -75
  57. package/dist/core/style.js +112 -53
  58. package/dist/core/table-style.d.ts +1 -1
  59. package/dist/core/table-style.js +4 -4
  60. package/dist/core/table.d.ts +28 -10
  61. package/dist/core/table.js +54 -39
  62. package/dist/core/theme.js +6 -5
  63. package/dist/core/used-extent.d.ts +44 -0
  64. package/dist/core/used-extent.js +102 -0
  65. package/dist/core/value.d.ts +9 -0
  66. package/dist/core/value.js +36 -34
  67. package/dist/core/workbook-media.d.ts +34 -0
  68. package/dist/core/workbook-media.js +44 -0
  69. package/dist/core/workbook-styles.d.ts +67 -1
  70. package/dist/core/workbook-vba.js +3 -2
  71. package/dist/core/workbook.d.ts +23 -2
  72. package/dist/core/workbook.js +24 -40
  73. package/dist/core/worksheet-comments.d.ts +2 -1
  74. package/dist/core/worksheet-comments.js +20 -13
  75. package/dist/core/worksheet-merges.d.ts +44 -0
  76. package/dist/core/worksheet-merges.js +59 -0
  77. package/dist/core/worksheet-model.d.ts +9 -1
  78. package/dist/core/worksheet-model.js +24 -9
  79. package/dist/core/worksheet.d.ts +72 -12
  80. package/dist/core/worksheet.js +68 -70
  81. package/dist/customui/ribbon.js +6 -7
  82. package/dist/entries/core.d.ts +18 -13
  83. package/dist/entries/core.js +12 -6
  84. package/dist/entries/xlsx.d.ts +1 -1
  85. package/dist/errors.d.ts +64 -0
  86. package/dist/errors.js +22 -0
  87. package/dist/hex.d.ts +8 -0
  88. package/dist/hex.js +3 -0
  89. package/dist/io/csv/delimiter.d.ts +9 -0
  90. package/dist/io/csv/delimiter.js +6 -0
  91. package/dist/io/csv/read.js +11 -4
  92. package/dist/io/csv/write.d.ts +9 -2
  93. package/dist/io/csv/write.js +9 -38
  94. package/dist/io/opc/inflate.js +15 -1
  95. package/dist/io/opc/namespaces.d.ts +16 -0
  96. package/dist/io/opc/namespaces.js +4 -0
  97. package/dist/io/opc/part-paths.d.ts +10 -0
  98. package/dist/io/opc/part-paths.js +16 -1
  99. package/dist/io/opc/read-opc.d.ts +69 -10
  100. package/dist/io/opc/read-opc.js +49 -55
  101. package/dist/io/opc/read-options.d.ts +5 -1
  102. package/dist/io/opc/rels.d.ts +1 -6
  103. package/dist/io/opc/rels.js +1 -4
  104. package/dist/io/opc/sniff-format.d.ts +3 -3
  105. package/dist/io/opc/sniff-format.js +2 -2
  106. package/dist/io/style/xf-style.d.ts +48 -2
  107. package/dist/io/style/xf-style.js +14 -0
  108. package/dist/io/xlsb/formula.d.ts +2 -2
  109. package/dist/io/xlsb/formula.js +49 -34
  110. package/dist/io/xlsb/primitives.js +8 -14
  111. package/dist/io/xlsb/ptg-functions.d.ts +19 -10
  112. package/dist/io/xlsb/ptg-functions.js +385 -596
  113. package/dist/io/xlsb/read-styles.js +50 -83
  114. package/dist/io/xlsb/read-worksheet.d.ts +20 -1
  115. package/dist/io/xlsb/read-worksheet.js +73 -89
  116. package/dist/io/xlsb/read.d.ts +6 -3
  117. package/dist/io/xlsb/read.js +47 -29
  118. package/dist/io/xlsb/record-stream.d.ts +30 -0
  119. package/dist/io/xlsb/record-stream.js +20 -0
  120. package/dist/io/xlsb/record-types.d.ts +1 -5
  121. package/dist/io/xlsb/record-types.js +1 -5
  122. package/dist/io/xlsx/cell-accumulator.d.ts +9 -2
  123. package/dist/io/xlsx/cell-accumulator.js +32 -9
  124. package/dist/io/xlsx/cell-style-resolution.d.ts +25 -0
  125. package/dist/io/xlsx/cell-style-resolution.js +29 -0
  126. package/dist/io/xlsx/cell-value.d.ts +5 -3
  127. package/dist/io/xlsx/cell-value.js +13 -15
  128. package/dist/io/xlsx/color-xml.js +3 -2
  129. package/dist/io/xlsx/column-budget.d.ts +26 -0
  130. package/dist/io/xlsx/column-budget.js +22 -0
  131. package/dist/io/xlsx/comments.d.ts +6 -2
  132. package/dist/io/xlsx/comments.js +36 -31
  133. package/dist/io/xlsx/conditional-formatting.d.ts +9 -5
  134. package/dist/io/xlsx/conditional-formatting.js +51 -46
  135. package/dist/io/xlsx/data-validation.d.ts +9 -7
  136. package/dist/io/xlsx/data-validation.js +38 -35
  137. package/dist/io/xlsx/edit-vba.d.ts +7 -2
  138. package/dist/io/xlsx/edit-vba.js +15 -29
  139. package/dist/io/xlsx/font-xml.d.ts +7 -0
  140. package/dist/io/xlsx/font-xml.js +104 -0
  141. package/dist/io/xlsx/hyperlinks.d.ts +41 -7
  142. package/dist/io/xlsx/hyperlinks.js +40 -28
  143. package/dist/io/xlsx/images.js +24 -25
  144. package/dist/io/xlsx/namespaces.d.ts +16 -0
  145. package/dist/io/xlsx/namespaces.js +7 -0
  146. package/dist/io/xlsx/package-plan.d.ts +23 -3
  147. package/dist/io/xlsx/package-plan.js +66 -25
  148. package/dist/io/xlsx/part-names.d.ts +41 -0
  149. package/dist/io/xlsx/part-names.js +46 -0
  150. package/dist/io/xlsx/pivot.js +8 -7
  151. package/dist/io/xlsx/read-parts.d.ts +59 -0
  152. package/dist/io/xlsx/read-parts.js +252 -0
  153. package/dist/io/xlsx/read-pivot.js +3 -1
  154. package/dist/io/xlsx/read-repair.d.ts +26 -0
  155. package/dist/io/xlsx/read-repair.js +39 -0
  156. package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +1 -1
  157. package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +7 -3
  158. package/dist/io/xlsx/read-rows.d.ts +18 -5
  159. package/dist/io/xlsx/read-rows.js +50 -37
  160. package/dist/io/xlsx/read-shared-strings.js +5 -3
  161. package/dist/io/xlsx/read-styles.d.ts +1 -6
  162. package/dist/io/xlsx/read-styles.js +65 -109
  163. package/dist/io/xlsx/read-workbook-xml.d.ts +36 -0
  164. package/dist/io/xlsx/read-workbook-xml.js +170 -0
  165. package/dist/io/xlsx/read-worksheet.d.ts +11 -1
  166. package/dist/io/xlsx/read-worksheet.js +51 -193
  167. package/dist/io/xlsx/read.d.ts +5 -13
  168. package/dist/io/xlsx/read.js +111 -436
  169. package/dist/io/xlsx/rich-text.js +1 -1
  170. package/dist/io/xlsx/row-position.d.ts +18 -0
  171. package/dist/io/xlsx/row-position.js +11 -0
  172. package/dist/io/xlsx/row-xml.d.ts +128 -0
  173. package/dist/io/xlsx/row-xml.js +203 -0
  174. package/dist/io/xlsx/shared-formulas.js +2 -2
  175. package/dist/io/xlsx/sheet-properties.d.ts +10 -1
  176. package/dist/io/xlsx/sheet-properties.js +166 -37
  177. package/dist/io/xlsx/style-elements.d.ts +43 -0
  178. package/dist/io/xlsx/style-elements.js +156 -0
  179. package/dist/io/xlsx/styles.d.ts +12 -3
  180. package/dist/io/xlsx/styles.js +5 -204
  181. package/dist/io/xlsx/tables.js +11 -14
  182. package/dist/io/xlsx/theme-xml.d.ts +26 -4
  183. package/dist/io/xlsx/theme-xml.js +107 -69
  184. package/dist/io/xlsx/threaded-comments.d.ts +8 -10
  185. package/dist/io/xlsx/threaded-comments.js +3 -1
  186. package/dist/io/xlsx/workbook-xml.d.ts +54 -4
  187. package/dist/io/xlsx/workbook-xml.js +84 -57
  188. package/dist/io/xlsx/worksheet-xml.d.ts +23 -70
  189. package/dist/io/xlsx/worksheet-xml.js +59 -242
  190. package/dist/io/xlsx/write-stream.d.ts +71 -16
  191. package/dist/io/xlsx/write-stream.js +79 -41
  192. package/dist/io/xlsx/write.d.ts +6 -2
  193. package/dist/io/xlsx/write.js +217 -153
  194. package/dist/rel-type.d.ts +16 -0
  195. package/dist/rel-type.js +6 -0
  196. package/dist/token-set.d.ts +28 -0
  197. package/dist/token-set.js +7 -0
  198. package/dist/vba/bytes.d.ts +14 -5
  199. package/dist/vba/bytes.js +16 -6
  200. package/dist/vba/cfb-format.d.ts +32 -0
  201. package/dist/vba/cfb-format.js +13 -0
  202. package/dist/vba/cfb-writer.d.ts +9 -0
  203. package/dist/vba/cfb-writer.js +28 -25
  204. package/dist/vba/cfb.d.ts +13 -1
  205. package/dist/vba/cfb.js +73 -57
  206. package/dist/vba/codepage.js +2 -1
  207. package/dist/vba/errors.d.ts +7 -0
  208. package/dist/vba/ms-ovba.js +79 -23
  209. package/dist/vba/project-editor.js +35 -40
  210. package/dist/vba/project.d.ts +11 -1
  211. package/dist/vba/project.js +40 -21
  212. package/dist/vba/vba-encoding.js +5 -6
  213. package/dist/xml/xml-attrs.d.ts +37 -0
  214. package/dist/xml/xml-attrs.js +30 -0
  215. package/dist/xml/xml-chars.d.ts +40 -0
  216. package/dist/xml/xml-chars.js +12 -0
  217. package/dist/xml/xml-namespaces.d.ts +31 -0
  218. package/dist/xml/xml-namespaces.js +50 -0
  219. package/dist/xml/xml-read.d.ts +128 -81
  220. package/dist/xml/xml-read.js +161 -178
  221. package/dist/xml/xml-scan.d.ts +78 -0
  222. package/dist/xml/xml-scan.js +180 -0
  223. package/dist/xml/xml.d.ts +29 -28
  224. package/dist/xml/xml.js +24 -24
  225. package/package.json +13 -9
@@ -1,24 +1,18 @@
1
+ import { quoted } from '../errors.js';
2
+ import { DIFSECT, DIR_ENTRY_SIZE, ENDOFCHAIN, FATSECT, FREESECT, MAX_NAME_CHARS, MINI_STREAM_CUTOFF, NOSTREAM, TYPE_ROOT, TYPE_STORAGE, TYPE_STREAM, } from './cfb-format.js';
1
3
  import { VbaAuthorError } from './errors.js';
4
+ export function isStream(node) {
5
+ return 'data' in node;
6
+ }
7
+ export function isStorage(node) {
8
+ return !isStream(node);
9
+ }
2
10
  const SECTOR = 512;
3
11
  const MINI_SECTOR = 64;
4
- const MINI_CUTOFF = 4096;
5
- const DIR_ENTRY_SIZE = 128;
6
12
  const ENTRIES_PER_DIR_SECTOR = SECTOR / DIR_ENTRY_SIZE;
7
13
  const FAT_ENTRIES_PER_SECTOR = SECTOR / 4;
8
14
  const DIFAT_HEADER_SLOTS = 109;
9
- const MAX_NAME_CHARS = 31;
10
- const FREESECT = 0xffffffff;
11
- const ENDOFCHAIN = 0xfffffffe;
12
- const FATSECT = 0xfffffffd;
13
- const DIFSECT = 0xfffffffc;
14
- const NOSTREAM = 0xffffffff;
15
- const TYPE_STORAGE = 1;
16
- const TYPE_STREAM = 2;
17
- const TYPE_ROOT = 5;
18
15
  const COLOR_BLACK = 1;
19
- function isStream(node) {
20
- return 'data' in node;
21
- }
22
16
  export function writeCompoundFile(root) {
23
17
  const entries = [];
24
18
  const addEntry = (name, type, startSector) => {
@@ -36,7 +30,8 @@ export function writeCompoundFile(root) {
36
30
  return entry;
37
31
  };
38
32
  const rootEntry = addEntry('Root Entry', TYPE_ROOT, ENDOFCHAIN);
39
- const miniBytes = [];
33
+ const miniChunks = [];
34
+ let miniLength = 0;
40
35
  const miniFat = [];
41
36
  const bigStreams = [];
42
37
  const addNode = (node, siblings) => {
@@ -46,15 +41,18 @@ export function writeCompoundFile(root) {
46
41
  siblings.push(entry);
47
42
  if (node.data.length === 0) {
48
43
  }
49
- else if (node.data.length >= MINI_CUTOFF) {
44
+ else if (node.data.length >= MINI_STREAM_CUTOFF) {
50
45
  bigStreams.push({ entry, data: node.data, sectors: Math.ceil(node.data.length / SECTOR) });
51
46
  }
52
47
  else {
53
- const startMini = miniBytes.length / MINI_SECTOR;
48
+ const startMini = miniLength / MINI_SECTOR;
54
49
  const numMini = Math.ceil(node.data.length / MINI_SECTOR);
55
50
  for (let k = 0; k < numMini; k++)
56
51
  miniFat.push(k < numMini - 1 ? startMini + k + 1 : ENDOFCHAIN);
57
- miniBytes.push(...node.data, ...new Array(numMini * MINI_SECTOR - node.data.length).fill(0));
52
+ const padded = new Uint8Array(numMini * MINI_SECTOR);
53
+ padded.set(node.data);
54
+ miniChunks.push(padded);
55
+ miniLength += padded.length;
58
56
  entry.startSector = startMini;
59
57
  }
60
58
  }
@@ -74,7 +72,7 @@ export function writeCompoundFile(root) {
74
72
  linkChildren(rootEntry, rootKids);
75
73
  const dirSectors = Math.ceil(entries.length / ENTRIES_PER_DIR_SECTOR);
76
74
  const miniFatSectors = miniFat.length > 0 ? Math.ceil((miniFat.length * 4) / FAT_ENTRIES_PER_SECTOR) : 0;
77
- const miniStreamSectors = Math.ceil(miniBytes.length / SECTOR);
75
+ const miniStreamSectors = Math.ceil(miniLength / SECTOR);
78
76
  const baseSectors = dirSectors +
79
77
  miniFatSectors +
80
78
  miniStreamSectors +
@@ -112,7 +110,7 @@ export function writeCompoundFile(root) {
112
110
  cursor += difatSectors;
113
111
  const totalSectors = cursor;
114
112
  rootEntry.startSector = miniStreamStart;
115
- rootEntry.size = miniBytes.length;
113
+ rootEntry.size = miniLength;
116
114
  const fat = new Array(fatSectors * FAT_ENTRIES_PER_SECTOR).fill(FREESECT);
117
115
  const chainRegion = (start, count) => {
118
116
  for (let k = 0; k < count; k++)
@@ -146,8 +144,13 @@ export function writeCompoundFile(root) {
146
144
  for (let i = 0; i < miniFatSectors * FAT_ENTRIES_PER_SECTOR; i++) {
147
145
  dv.setUint32(at(miniFatStart) + i * 4, miniFat[i] ?? FREESECT, true);
148
146
  }
149
- if (miniBytes.length > 0)
150
- buf.set(Uint8Array.from(miniBytes), at(miniStreamStart));
147
+ if (miniLength > 0) {
148
+ let offset = at(miniStreamStart);
149
+ for (const chunk of miniChunks) {
150
+ buf.set(chunk, offset);
151
+ offset += chunk.length;
152
+ }
153
+ }
151
154
  for (const big of bigStreams)
152
155
  buf.set(big.data, at(big.entry.startSector));
153
156
  for (const [i, sector] of fat.entries())
@@ -168,10 +171,10 @@ function validateSiblingNames(storageName, siblings) {
168
171
  if (node.name.length === 0)
169
172
  throw new VbaAuthorError('entry name must not be empty');
170
173
  if (node.name.length > MAX_NAME_CHARS) {
171
- throw new VbaAuthorError(`entry name '${node.name}' exceeds the ${MAX_NAME_CHARS}-character CFB limit`);
174
+ throw new VbaAuthorError(`entry name ${quoted(node.name)} exceeds the ${MAX_NAME_CHARS}-character CFB limit`);
172
175
  }
173
176
  if (seen.has(node.name)) {
174
- throw new VbaAuthorError(`duplicate entry name '${node.name}' under storage '${storageName}'`);
177
+ throw new VbaAuthorError(`duplicate entry name ${quoted(node.name)} under storage ${quoted(storageName)}`);
175
178
  }
176
179
  seen.add(node.name);
177
180
  if (!isStream(node))
@@ -207,7 +210,7 @@ function writeHeader(dv, p) {
207
210
  dv.setUint16(32, 6, true);
208
211
  dv.setUint32(44, p.fatSectors, true);
209
212
  dv.setUint32(48, p.dirStart, true);
210
- dv.setUint32(56, MINI_CUTOFF, true);
213
+ dv.setUint32(56, MINI_STREAM_CUTOFF, true);
211
214
  dv.setUint32(60, p.miniFatStart, true);
212
215
  dv.setUint32(64, p.miniFatSectors, true);
213
216
  dv.setUint32(68, p.difatSectors > 0 ? p.difatStart : ENDOFCHAIN, true);
package/dist/vba/cfb.d.ts CHANGED
@@ -4,13 +4,25 @@ export declare class CompoundFile {
4
4
  constructor(buf: Uint8Array);
5
5
  /** List every stream/storage name in the directory (order as stored). */
6
6
  names(): string[];
7
- /** Read a stream's raw bytes by exact entry name, or `undefined` if absent. */
7
+ /**
8
+ * Read a stream's raw bytes by exact entry name, or `undefined` if absent.
9
+ *
10
+ * @returns bytes that may be a **view onto the caller's buffer** rather than a copy. A stream small
11
+ * enough to fit in one sector is assembled by `concat` from a single chunk, and `concat` hands a
12
+ * lone chunk straight back; anything larger is copied into a fresh buffer. So whether the result
13
+ * aliases the source depends on the stream's size, which is a rule no caller should have to know
14
+ * and none here relies on: nothing mutates what this returns. Stated because it is the sort of
15
+ * thing that is true until someone writes the first mutation, and then true only for large
16
+ * streams. Copy before mutating.
17
+ */
8
18
  readStream(name: string): Uint8Array | undefined;
9
19
  /**
10
20
  * Reconstruct the container's top-level children as the writer's node shape, recursing into every
11
21
  * storage, so a caller can swap one stream and re-emit the whole hierarchy with {@link writeCompoundFile}.
12
22
  * Walks the red-black sibling tree each storage navigates (not the linear directory scan), so any part
13
23
  * a host reaches is carried through. Cycle- and bounds-guarded like every other chain walk here.
24
+ *
25
+ * @returns nodes whose stream bytes carry {@link readStream}'s aliasing caveat.
14
26
  */
15
27
  tree(): CfbNode[];
16
28
  }
package/dist/vba/cfb.js CHANGED
@@ -1,14 +1,10 @@
1
+ import { quoted } from '../errors.js';
1
2
  import { concat, decodeUtf16le, readU16, readU32 } from './bytes.js';
3
+ import { DIR_ENTRY_SIZE, MAX_REGULAR_SECTOR, MINI_STREAM_CUTOFF, NOSTREAM, TYPE_EMPTY, TYPE_ROOT, TYPE_STORAGE, TYPE_STREAM, } from './cfb-format.js';
2
4
  import { VbaParseError } from './errors.js';
3
- const MAX_REGULAR_SECTOR = 0xfffffffa;
4
- const NOSTREAM = 0xffffffff;
5
- const TYPE_EMPTY = 0;
6
- const TYPE_STORAGE = 1;
7
- const TYPE_STREAM = 2;
8
- const TYPE_ROOT = 5;
9
5
  const CFB_SIGNATURE_LO = 0xe011cfd0;
10
6
  const CFB_SIGNATURE_HI = 0xe11ab1a1;
11
- const DIR_ENTRY_SIZE = 128;
7
+ const MAX_TREE_DEPTH = 256;
12
8
  export class CompoundFile {
13
9
  #buf;
14
10
  #sectorSize;
@@ -39,6 +35,9 @@ export class CompoundFile {
39
35
  const numFatSectors = readU32(this.#buf, 44);
40
36
  const firstDirSector = readU32(this.#buf, 48);
41
37
  this.#miniCutoff = readU32(this.#buf, 56);
38
+ if (this.#miniCutoff !== MINI_STREAM_CUTOFF) {
39
+ throw new VbaParseError(`unsupported mini-stream cutoff ${this.#miniCutoff}`);
40
+ }
42
41
  const firstMiniFatSector = readU32(this.#buf, 60);
43
42
  const firstDifatSector = readU32(this.#buf, 68);
44
43
  const numDifatSectors = readU32(this.#buf, 72);
@@ -69,11 +68,15 @@ export class CompoundFile {
69
68
  const root = this.#dir[rootIdx];
70
69
  return this.#buildSiblings(root.child, new Set([rootIdx]));
71
70
  }
72
- #buildSiblings(firstChild, seen) {
71
+ #buildSiblings(firstChild, seen, depth = 0) {
72
+ if (depth > MAX_TREE_DEPTH)
73
+ throw new VbaParseError('directory tree nests too deep');
73
74
  const nodes = [];
74
- const walk = (idx) => {
75
+ const walk = (idx, siblingDepth) => {
75
76
  if (idx >= MAX_REGULAR_SECTOR)
76
77
  return;
78
+ if (siblingDepth > MAX_TREE_DEPTH)
79
+ throw new VbaParseError('directory tree nests too deep');
77
80
  if (idx >= this.#dir.length)
78
81
  throw new VbaParseError('directory sibling index out of range');
79
82
  if (seen.has(idx))
@@ -82,16 +85,16 @@ export class CompoundFile {
82
85
  const e = this.#dir[idx];
83
86
  if (e.type === TYPE_EMPTY)
84
87
  throw new VbaParseError('directory tree links an empty entry');
85
- walk(e.left);
88
+ walk(e.left, siblingDepth + 1);
86
89
  if (e.type === TYPE_STORAGE) {
87
- nodes.push({ name: e.name, children: this.#buildSiblings(e.child, seen) });
90
+ nodes.push({ name: e.name, children: this.#buildSiblings(e.child, seen, depth + 1) });
88
91
  }
89
92
  else {
90
93
  nodes.push({ name: e.name, data: this.#readEntryData(e) });
91
94
  }
92
- walk(e.right);
95
+ walk(e.right, siblingDepth + 1);
93
96
  };
94
- walk(firstChild);
97
+ walk(firstChild, depth);
95
98
  return nodes;
96
99
  }
97
100
  #readEntryData(entry) {
@@ -100,8 +103,9 @@ export class CompoundFile {
100
103
  return this.#readViaMiniFat(entry.startSector, entry.size);
101
104
  }
102
105
  #readDifat(numFatSectors, firstDifat, numDifat) {
106
+ const wanted = Math.min(numFatSectors, this.#maxSector);
103
107
  const ids = [];
104
- for (let i = 0; i < 109 && ids.length < numFatSectors; i++) {
108
+ for (let i = 0; i < 109 && ids.length < wanted; i++) {
105
109
  const v = readU32(this.#buf, 76 + i * 4);
106
110
  if (v >= MAX_REGULAR_SECTOR)
107
111
  break;
@@ -110,12 +114,12 @@ export class CompoundFile {
110
114
  const seen = new Set();
111
115
  let sector = firstDifat;
112
116
  const perSector = this.#sectorSize / 4 - 1;
113
- for (let s = 0; s < numDifat && sector < MAX_REGULAR_SECTOR; s++) {
117
+ for (let s = 0; s < numDifat && sector < MAX_REGULAR_SECTOR && ids.length < wanted; s++) {
114
118
  if (seen.has(sector))
115
119
  throw new VbaParseError('cycle in DIFAT sector chain');
116
120
  seen.add(sector);
117
121
  const base = this.#dataSectorOffset(sector);
118
- for (let i = 0; i < perSector; i++) {
122
+ for (let i = 0; i < perSector && ids.length < wanted; i++) {
119
123
  const v = readU32(this.#buf, base + i * 4);
120
124
  if (v < MAX_REGULAR_SECTOR)
121
125
  ids.push(v);
@@ -129,23 +133,28 @@ export class CompoundFile {
129
133
  const perSector = this.#sectorSize / 4;
130
134
  for (const sid of fatSectorIds) {
131
135
  const base = this.#dataSectorOffset(sid);
132
- for (let i = 0; i < perSector; i++)
136
+ for (let i = 0; i < perSector && fat.length < this.#maxSector; i++) {
133
137
  fat.push(readU32(this.#buf, base + i * 4));
138
+ }
139
+ if (fat.length >= this.#maxSector)
140
+ break;
134
141
  }
135
142
  return fat;
136
143
  }
137
144
  #readChainValues(firstSector) {
138
145
  const values = [];
139
146
  const perSector = this.#sectorSize / 4;
147
+ const cap = this.#maxSector * (this.#sectorSize / this.#miniSectorSize);
140
148
  const seen = new Set();
141
149
  let sector = firstSector;
142
- while (sector < MAX_REGULAR_SECTOR) {
150
+ while (sector < MAX_REGULAR_SECTOR && values.length < cap) {
143
151
  if (seen.has(sector))
144
152
  throw new VbaParseError('cycle in mini-FAT sector chain');
145
153
  seen.add(sector);
146
154
  const base = this.#dataSectorOffset(sector);
147
- for (let i = 0; i < perSector; i++)
155
+ for (let i = 0; i < perSector && values.length < cap; i++) {
148
156
  values.push(readU32(this.#buf, base + i * 4));
157
+ }
149
158
  sector = this.#nextInFat(sector);
150
159
  }
151
160
  return values;
@@ -183,63 +192,70 @@ export class CompoundFile {
183
192
  const name = decodeUtf16le(raw.subarray(off, off + Math.max(0, nameLen - 2)));
184
193
  const startSector = readU32(raw, off + 116);
185
194
  const size = readU32(raw, off + 120);
195
+ if (readU32(raw, off + 124) !== 0) {
196
+ throw new VbaParseError(`directory entry ${quoted(name)} declares a stream larger than 4 GiB`);
197
+ }
186
198
  entries.push({ name, type, startSector, size, left, right, child });
187
199
  }
188
200
  return entries;
189
201
  }
190
202
  #readChainFull(startSector) {
191
- const chunks = [];
192
- const seen = new Set();
193
- let sector = startSector;
194
- while (sector < MAX_REGULAR_SECTOR) {
195
- if (seen.has(sector))
196
- throw new VbaParseError('cycle in FAT sector chain');
197
- seen.add(sector);
198
- const base = this.#dataSectorOffset(sector);
199
- chunks.push(this.#buf.subarray(base, base + this.#sectorSize));
200
- sector = this.#nextInFat(sector);
201
- }
202
- return concat(chunks);
203
+ return this.#walkChain(startSector, undefined, this.#fatChain('FAT sector chain'));
203
204
  }
204
205
  #readViaFat(startSector, size) {
205
- const chunks = [];
206
- const seen = new Set();
207
- let sector = startSector;
208
- let remaining = size;
209
- while (sector < MAX_REGULAR_SECTOR && remaining > 0) {
210
- if (seen.has(sector))
211
- throw new VbaParseError('cycle in stream FAT chain');
212
- seen.add(sector);
213
- const base = this.#dataSectorOffset(sector);
214
- const take = Math.min(this.#sectorSize, remaining);
215
- chunks.push(this.#buf.subarray(base, base + take));
216
- remaining -= take;
217
- sector = this.#nextInFat(sector);
218
- }
219
- return concat(chunks);
206
+ return this.#walkChain(startSector, size, this.#fatChain('stream FAT chain'));
220
207
  }
221
208
  #readViaMiniFat(startSector, size) {
209
+ return this.#walkChain(startSector, size, this.#miniFatChain());
210
+ }
211
+ #walkChain(startSector, size, chain) {
222
212
  const chunks = [];
223
213
  const seen = new Set();
224
214
  let sector = startSector;
225
- let remaining = size;
215
+ let remaining = size ?? Infinity;
226
216
  while (sector < MAX_REGULAR_SECTOR && remaining > 0) {
227
217
  if (seen.has(sector))
228
- throw new VbaParseError('cycle in stream mini-FAT chain');
218
+ throw new VbaParseError(`cycle in ${chain.what}`);
229
219
  seen.add(sector);
230
- const base = sector * this.#miniSectorSize;
231
- const take = Math.min(this.#miniSectorSize, remaining);
232
- if (base + take > this.#miniStream.length) {
233
- throw new VbaParseError('mini-stream sector runs past end of the mini stream');
234
- }
235
- chunks.push(this.#miniStream.subarray(base, base + take));
220
+ const take = Math.min(chain.sectorSize, remaining);
221
+ chunks.push(chain.sliceAt(sector, take));
236
222
  remaining -= take;
237
- if (sector >= this.#miniFat.length)
238
- throw new VbaParseError('mini-FAT index out of range');
239
- sector = this.#miniFat[sector];
223
+ sector = chain.next(sector);
224
+ }
225
+ if (size !== undefined && remaining > 0) {
226
+ throw new VbaParseError(`${chain.what} ends after ${size - remaining} bytes, but ${size} were declared`);
240
227
  }
241
228
  return concat(chunks);
242
229
  }
230
+ #fatChain(what) {
231
+ return {
232
+ what,
233
+ sectorSize: this.#sectorSize,
234
+ sliceAt: (sector, take) => {
235
+ const base = this.#dataSectorOffset(sector);
236
+ return this.#buf.subarray(base, base + take);
237
+ },
238
+ next: (sector) => this.#nextInFat(sector),
239
+ };
240
+ }
241
+ #miniFatChain() {
242
+ return {
243
+ what: 'stream mini-FAT chain',
244
+ sectorSize: this.#miniSectorSize,
245
+ sliceAt: (sector, take) => {
246
+ const base = sector * this.#miniSectorSize;
247
+ if (base + take > this.#miniStream.length) {
248
+ throw new VbaParseError('mini-stream sector runs past end of the mini stream');
249
+ }
250
+ return this.#miniStream.subarray(base, base + take);
251
+ },
252
+ next: (sector) => {
253
+ if (sector >= this.#miniFat.length)
254
+ throw new VbaParseError('mini-FAT index out of range');
255
+ return this.#miniFat[sector];
256
+ },
257
+ };
258
+ }
243
259
  #nextInFat(sector) {
244
260
  if (sector >= this.#fat.length)
245
261
  throw new VbaParseError('FAT index out of range');
@@ -1,3 +1,4 @@
1
+ import { hex } from '../hex.js';
1
2
  import { VbaAuthorError, VbaParseError } from './errors.js';
2
3
  const CODEPAGE_LABEL = new Map([
3
4
  [1250, 'windows-1250'],
@@ -56,7 +57,7 @@ export function encoderForCodePage(codePage) {
56
57
  const code = text.charCodeAt(i);
57
58
  const byte = charToByte.get(code);
58
59
  if (byte === undefined) {
59
- throw new VbaAuthorError(`character U+${code.toString(16).toUpperCase().padStart(4, '0')} is not representable in code page ${codePage}`);
60
+ throw new VbaAuthorError(`character U+${hex(code, 4)} is not representable in code page ${codePage}`);
60
61
  }
61
62
  out[i] = byte;
62
63
  }
@@ -17,6 +17,13 @@ export declare class VbaParseError extends XlsxError {
17
17
  * [MS-CFB] 31-character limit, a duplicate stream name, or a project so large it would exceed the
18
18
  * 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";
@@ -3,6 +3,9 @@ import { VbaParseError } from './errors.js';
3
3
  const MAX_CHUNK_DECOMPRESSED = 4096;
4
4
  const CHUNK_SIGNATURE = 0b011 << 12;
5
5
  const CHUNK_COMPRESSED_FLAG = 0x8000;
6
+ const MAX_MATCH_CANDIDATES = 64;
7
+ const HASH_BITS = 12;
8
+ const NO_CANDIDATE = -1;
6
9
  const DEFAULT_MAX_OUTPUT = 64 * 1024 * 1024;
7
10
  export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTPUT) {
8
11
  if (start >= buf.length) {
@@ -11,7 +14,7 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
11
14
  if (buf[start] !== 0x01) {
12
15
  throw new VbaParseError(`compressed container must begin with a 0x01 signature byte, found 0x${(buf[start] ?? 0).toString(16)}`);
13
16
  }
14
- const out = [];
17
+ const out = new ByteSink(maxOutput);
15
18
  let pos = start + 1;
16
19
  while (pos + 2 <= buf.length) {
17
20
  const header = readU16(buf, pos);
@@ -28,7 +31,6 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
28
31
  if (!compressed) {
29
32
  for (let i = pos; i < chunkEnd; i++)
30
33
  out.push(buf[i]);
31
- guardOutput(out.length, maxOutput);
32
34
  pos = chunkEnd;
33
35
  continue;
34
36
  }
@@ -39,7 +41,6 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
39
41
  const isCopy = (flagByte >> bit) & 1;
40
42
  if (!isCopy) {
41
43
  out.push(buf[pos++]);
42
- guardOutput(out.length, maxOutput);
43
44
  continue;
44
45
  }
45
46
  if (pos + 2 > chunkEnd) {
@@ -55,30 +56,78 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
55
56
  throw new VbaParseError('copy token references before the start of its chunk');
56
57
  }
57
58
  for (let i = 0; i < length; i++)
58
- out.push(out[src + i]);
59
- guardOutput(out.length, maxOutput);
59
+ out.push(out.at(src + i));
60
+ }
61
+ if (out.length - chunkStart > MAX_CHUNK_DECOMPRESSED) {
62
+ throw new VbaParseError(`chunk decompressed to more than ${MAX_CHUNK_DECOMPRESSED} bytes, which [MS-OVBA] forbids`);
60
63
  }
61
64
  }
62
65
  pos = chunkEnd;
63
66
  }
64
- return Uint8Array.from(out);
67
+ return out.bytes();
68
+ }
69
+ class ByteSink {
70
+ #buf;
71
+ #length = 0;
72
+ #limit;
73
+ constructor(limit = Infinity) {
74
+ this.#limit = limit;
75
+ this.#buf = new Uint8Array(Math.min(MAX_CHUNK_DECOMPRESSED, limit));
76
+ }
77
+ get length() {
78
+ return this.#length;
79
+ }
80
+ at(index) {
81
+ return this.#buf[index];
82
+ }
83
+ set(index, byte) {
84
+ this.#buf[index] = byte;
85
+ }
86
+ push(byte) {
87
+ if (this.#length >= this.#limit) {
88
+ throw new VbaParseError(`decompressed output exceeds the ${this.#limit}-byte ceiling (possible bomb)`);
89
+ }
90
+ if (this.#length === this.#buf.length) {
91
+ const grown = new Uint8Array(Math.min(Math.max(this.#buf.length * 2, MAX_CHUNK_DECOMPRESSED), this.#limit));
92
+ grown.set(this.#buf);
93
+ this.#buf = grown;
94
+ }
95
+ this.#buf[this.#length++] = byte;
96
+ }
97
+ pushAll(bytes) {
98
+ for (const byte of bytes)
99
+ this.push(byte);
100
+ }
101
+ bytes() {
102
+ return this.#buf.slice(0, this.#length);
103
+ }
65
104
  }
66
105
  export function compressContainer(data) {
67
- const out = [0x01];
106
+ const out = new ByteSink();
107
+ out.push(0x01);
68
108
  for (let start = 0; start < data.length; start += MAX_CHUNK_DECOMPRESSED) {
69
109
  const chunk = data.subarray(start, Math.min(start + MAX_CHUNK_DECOMPRESSED, data.length));
70
110
  const tokens = compressChunk(chunk);
71
111
  const compressed = tokens.length < chunk.length;
72
112
  const body = compressed ? tokens : chunk;
73
113
  const header = (compressed ? CHUNK_COMPRESSED_FLAG : 0) | CHUNK_SIGNATURE | ((body.length - 1) & 0x0fff);
74
- out.push(header & 0xff, (header >> 8) & 0xff);
75
- for (const b of body)
76
- out.push(b);
114
+ out.push(header & 0xff);
115
+ out.push((header >> 8) & 0xff);
116
+ out.pushAll(body);
77
117
  }
78
- return Uint8Array.from(out);
118
+ return out.bytes();
79
119
  }
80
120
  function compressChunk(chunk) {
81
- const tokens = [];
121
+ const tokens = new ByteSink();
122
+ const heads = new Int32Array(1 << HASH_BITS).fill(NO_CANDIDATE);
123
+ const prev = new Int32Array(MAX_CHUNK_DECOMPRESSED).fill(NO_CANDIDATE);
124
+ const remember = (at) => {
125
+ if (at + 2 >= chunk.length)
126
+ return;
127
+ const bucket = hash3(chunk, at);
128
+ prev[at] = heads[bucket];
129
+ heads[bucket] = at;
130
+ };
82
131
  let pos = 0;
83
132
  while (pos < chunk.length) {
84
133
  const flagIndex = tokens.length;
@@ -90,42 +139,49 @@ function compressChunk(chunk) {
90
139
  const windowStart = Math.max(0, pos - (1 << bitCount));
91
140
  let bestLength = 0;
92
141
  let bestOffset = 0;
93
- for (let cand = pos - 1; cand >= windowStart; cand--) {
142
+ let candidate = pos + 2 < chunk.length ? heads[hash3(chunk, pos)] : NO_CANDIDATE;
143
+ for (let tried = 0; candidate >= windowStart && tried < MAX_MATCH_CANDIDATES; tried++) {
94
144
  let len = 0;
95
145
  while (len < maxLength &&
96
146
  pos + len < chunk.length &&
97
- chunk[cand + len] === chunk[pos + len]) {
147
+ chunk[candidate + len] === chunk[pos + len]) {
98
148
  len++;
99
149
  }
100
150
  if (len > bestLength) {
101
151
  bestLength = len;
102
- bestOffset = pos - cand;
152
+ bestOffset = pos - candidate;
103
153
  if (bestLength === maxLength)
104
154
  break;
105
155
  }
156
+ candidate = prev[candidate];
106
157
  }
107
158
  if (bestLength >= 3) {
108
159
  const token = ((bestOffset - 1) << (16 - bitCount)) | (bestLength - 3);
109
- tokens.push(token & 0xff, (token >> 8) & 0xff);
160
+ tokens.push(token & 0xff);
161
+ tokens.push((token >> 8) & 0xff);
110
162
  flags |= 1 << bit;
163
+ for (let i = 0; i < bestLength; i++)
164
+ remember(pos + i);
111
165
  pos += bestLength;
112
166
  }
113
167
  else {
114
168
  tokens.push(chunk[pos]);
169
+ remember(pos);
115
170
  pos++;
116
171
  }
117
172
  }
118
- tokens[flagIndex] = flags;
173
+ tokens.set(flagIndex, flags);
119
174
  }
120
- return tokens;
175
+ return tokens.bytes();
121
176
  }
122
- function guardOutput(size, maxOutput) {
123
- if (size > maxOutput) {
124
- throw new VbaParseError(`decompressed output exceeds the ${maxOutput}-byte ceiling (possible bomb)`);
125
- }
177
+ function hash3(chunk, pos) {
178
+ const first = chunk[pos];
179
+ const second = chunk[pos + 1];
180
+ const third = chunk[pos + 2];
181
+ return ((first << 8) ^ (second << 4) ^ third) & ((1 << HASH_BITS) - 1);
126
182
  }
127
183
  function copyTokenHelp(decompressedSoFar) {
128
- const bitCount = Math.max(Math.ceil(Math.log2(Math.max(decompressedSoFar, 1))), 4);
184
+ const bitCount = Math.min(Math.max(Math.ceil(Math.log2(Math.max(decompressedSoFar, 1))), 4), Math.log2(MAX_CHUNK_DECOMPRESSED));
129
185
  const lengthMask = 0xffff >> bitCount;
130
186
  return { lengthMask, bitCount };
131
187
  }