@shbernal/ts-xlsx 3.0.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 (213) 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 +4 -3
  7. package/dist/core/autofilter.js +31 -30
  8. package/dist/core/axis-handle.d.ts +2 -0
  9. package/dist/core/axis-handle.js +5 -2
  10. package/dist/core/cell.d.ts +4 -3
  11. package/dist/core/cell.js +4 -6
  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 +1 -0
  16. package/dist/core/column.js +3 -0
  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 +9 -1
  21. package/dist/core/conditional-formatting.js +34 -18
  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 +6 -0
  27. package/dist/core/data-validation.js +15 -4
  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 +13 -9
  39. package/dist/core/image.js +7 -5
  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.d.ts +5 -4
  44. package/dist/core/merge.js +18 -19
  45. package/dist/core/pivot-table.js +63 -45
  46. package/dist/core/protection.js +2 -11
  47. package/dist/core/range.js +34 -30
  48. package/dist/core/row-input.js +2 -2
  49. package/dist/core/row.d.ts +1 -0
  50. package/dist/core/row.js +3 -0
  51. package/dist/core/style.d.ts +85 -70
  52. package/dist/core/style.js +92 -28
  53. package/dist/core/table-style.d.ts +1 -1
  54. package/dist/core/table-style.js +4 -4
  55. package/dist/core/table.d.ts +27 -9
  56. package/dist/core/table.js +51 -34
  57. package/dist/core/theme.js +6 -5
  58. package/dist/core/value.d.ts +9 -0
  59. package/dist/core/value.js +36 -34
  60. package/dist/core/workbook-media.d.ts +34 -0
  61. package/dist/core/workbook-media.js +44 -0
  62. package/dist/core/workbook-styles.d.ts +67 -1
  63. package/dist/core/workbook-vba.js +3 -2
  64. package/dist/core/workbook.d.ts +23 -2
  65. package/dist/core/workbook.js +24 -40
  66. package/dist/core/worksheet-comments.d.ts +2 -1
  67. package/dist/core/worksheet-comments.js +20 -13
  68. package/dist/core/worksheet-merges.d.ts +44 -0
  69. package/dist/core/worksheet-merges.js +59 -0
  70. package/dist/core/worksheet-model.d.ts +9 -1
  71. package/dist/core/worksheet-model.js +24 -9
  72. package/dist/core/worksheet.d.ts +55 -6
  73. package/dist/core/worksheet.js +36 -44
  74. package/dist/customui/ribbon.js +2 -1
  75. package/dist/entries/core.d.ts +18 -13
  76. package/dist/entries/core.js +12 -6
  77. package/dist/entries/xlsx.d.ts +1 -1
  78. package/dist/errors.d.ts +64 -0
  79. package/dist/errors.js +22 -0
  80. package/dist/hex.d.ts +8 -0
  81. package/dist/hex.js +3 -0
  82. package/dist/io/csv/delimiter.d.ts +9 -0
  83. package/dist/io/csv/delimiter.js +6 -0
  84. package/dist/io/csv/read.js +11 -4
  85. package/dist/io/csv/write.d.ts +9 -2
  86. package/dist/io/csv/write.js +9 -38
  87. package/dist/io/opc/inflate.js +14 -0
  88. package/dist/io/opc/namespaces.d.ts +16 -0
  89. package/dist/io/opc/namespaces.js +4 -0
  90. package/dist/io/opc/part-paths.d.ts +9 -0
  91. package/dist/io/opc/part-paths.js +1 -1
  92. package/dist/io/opc/read-opc.d.ts +54 -14
  93. package/dist/io/opc/read-opc.js +42 -20
  94. package/dist/io/opc/read-options.d.ts +5 -1
  95. package/dist/io/opc/sniff-format.d.ts +3 -3
  96. package/dist/io/opc/sniff-format.js +2 -2
  97. package/dist/io/style/xf-style.d.ts +23 -1
  98. package/dist/io/style/xf-style.js +8 -0
  99. package/dist/io/xlsb/formula.d.ts +2 -2
  100. package/dist/io/xlsb/formula.js +49 -34
  101. package/dist/io/xlsb/primitives.js +8 -14
  102. package/dist/io/xlsb/ptg-functions.d.ts +19 -10
  103. package/dist/io/xlsb/ptg-functions.js +385 -596
  104. package/dist/io/xlsb/read-styles.js +45 -81
  105. package/dist/io/xlsb/read-worksheet.d.ts +20 -1
  106. package/dist/io/xlsb/read-worksheet.js +73 -89
  107. package/dist/io/xlsb/read.d.ts +6 -3
  108. package/dist/io/xlsb/read.js +44 -23
  109. package/dist/io/xlsb/record-stream.d.ts +30 -0
  110. package/dist/io/xlsb/record-stream.js +20 -0
  111. package/dist/io/xlsb/record-types.d.ts +1 -5
  112. package/dist/io/xlsb/record-types.js +1 -5
  113. package/dist/io/xlsx/cell-accumulator.d.ts +8 -1
  114. package/dist/io/xlsx/cell-accumulator.js +32 -9
  115. package/dist/io/xlsx/cell-style-resolution.d.ts +25 -0
  116. package/dist/io/xlsx/cell-style-resolution.js +29 -0
  117. package/dist/io/xlsx/cell-value.d.ts +5 -3
  118. package/dist/io/xlsx/cell-value.js +13 -15
  119. package/dist/io/xlsx/color-xml.js +3 -2
  120. package/dist/io/xlsx/column-budget.d.ts +26 -0
  121. package/dist/io/xlsx/column-budget.js +22 -0
  122. package/dist/io/xlsx/comments.d.ts +6 -2
  123. package/dist/io/xlsx/comments.js +36 -32
  124. package/dist/io/xlsx/conditional-formatting.d.ts +6 -3
  125. package/dist/io/xlsx/conditional-formatting.js +30 -26
  126. package/dist/io/xlsx/data-validation.js +29 -27
  127. package/dist/io/xlsx/edit-vba.d.ts +7 -2
  128. package/dist/io/xlsx/edit-vba.js +14 -21
  129. package/dist/io/xlsx/font-xml.d.ts +7 -0
  130. package/dist/io/xlsx/font-xml.js +104 -0
  131. package/dist/io/xlsx/hyperlinks.d.ts +37 -5
  132. package/dist/io/xlsx/hyperlinks.js +22 -13
  133. package/dist/io/xlsx/images.js +11 -13
  134. package/dist/io/xlsx/namespaces.d.ts +16 -0
  135. package/dist/io/xlsx/namespaces.js +7 -0
  136. package/dist/io/xlsx/package-plan.d.ts +14 -2
  137. package/dist/io/xlsx/package-plan.js +39 -11
  138. package/dist/io/xlsx/pivot.js +8 -7
  139. package/dist/io/xlsx/read-parts.d.ts +59 -0
  140. package/dist/io/xlsx/read-parts.js +252 -0
  141. package/dist/io/xlsx/read-pivot.js +2 -1
  142. package/dist/io/xlsx/read-repair.d.ts +26 -0
  143. package/dist/io/xlsx/read-repair.js +39 -0
  144. package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +6 -3
  145. package/dist/io/xlsx/read-rows.d.ts +18 -5
  146. package/dist/io/xlsx/read-rows.js +45 -31
  147. package/dist/io/xlsx/read-shared-strings.js +3 -2
  148. package/dist/io/xlsx/read-styles.d.ts +1 -6
  149. package/dist/io/xlsx/read-styles.js +6 -71
  150. package/dist/io/xlsx/read-workbook-xml.d.ts +36 -0
  151. package/dist/io/xlsx/read-workbook-xml.js +170 -0
  152. package/dist/io/xlsx/read-worksheet.d.ts +2 -1
  153. package/dist/io/xlsx/read-worksheet.js +35 -164
  154. package/dist/io/xlsx/read.d.ts +5 -12
  155. package/dist/io/xlsx/read.js +73 -406
  156. package/dist/io/xlsx/rich-text.js +1 -1
  157. package/dist/io/xlsx/row-position.d.ts +18 -0
  158. package/dist/io/xlsx/row-position.js +11 -0
  159. package/dist/io/xlsx/row-xml.d.ts +128 -0
  160. package/dist/io/xlsx/row-xml.js +203 -0
  161. package/dist/io/xlsx/shared-formulas.js +2 -2
  162. package/dist/io/xlsx/sheet-properties.d.ts +10 -1
  163. package/dist/io/xlsx/sheet-properties.js +145 -9
  164. package/dist/io/xlsx/style-elements.d.ts +43 -0
  165. package/dist/io/xlsx/style-elements.js +156 -0
  166. package/dist/io/xlsx/styles.d.ts +12 -3
  167. package/dist/io/xlsx/styles.js +5 -207
  168. package/dist/io/xlsx/tables.js +10 -14
  169. package/dist/io/xlsx/theme-xml.d.ts +26 -4
  170. package/dist/io/xlsx/theme-xml.js +107 -69
  171. package/dist/io/xlsx/threaded-comments.d.ts +8 -10
  172. package/dist/io/xlsx/threaded-comments.js +2 -1
  173. package/dist/io/xlsx/workbook-xml.d.ts +54 -4
  174. package/dist/io/xlsx/workbook-xml.js +60 -34
  175. package/dist/io/xlsx/worksheet-xml.d.ts +23 -70
  176. package/dist/io/xlsx/worksheet-xml.js +48 -229
  177. package/dist/io/xlsx/write-stream.d.ts +65 -16
  178. package/dist/io/xlsx/write-stream.js +50 -20
  179. package/dist/io/xlsx/write.d.ts +1 -2
  180. package/dist/io/xlsx/write.js +114 -74
  181. package/dist/rel-type.d.ts +16 -0
  182. package/dist/rel-type.js +6 -0
  183. package/dist/token-set.d.ts +13 -0
  184. package/dist/token-set.js +4 -1
  185. package/dist/vba/bytes.d.ts +14 -5
  186. package/dist/vba/bytes.js +16 -6
  187. package/dist/vba/cfb-format.d.ts +8 -0
  188. package/dist/vba/cfb-format.js +1 -0
  189. package/dist/vba/cfb-writer.d.ts +9 -0
  190. package/dist/vba/cfb-writer.js +12 -9
  191. package/dist/vba/cfb.d.ts +13 -1
  192. package/dist/vba/cfb.js +73 -51
  193. package/dist/vba/codepage.js +2 -1
  194. package/dist/vba/errors.d.ts +7 -0
  195. package/dist/vba/ms-ovba.js +51 -16
  196. package/dist/vba/project-editor.js +35 -40
  197. package/dist/vba/project.d.ts +11 -1
  198. package/dist/vba/project.js +27 -8
  199. package/dist/vba/vba-encoding.js +4 -5
  200. package/dist/xml/xml-attrs.d.ts +37 -0
  201. package/dist/xml/xml-attrs.js +30 -0
  202. package/dist/xml/xml-chars.d.ts +40 -0
  203. package/dist/xml/xml-chars.js +12 -0
  204. package/dist/xml/xml-namespaces.d.ts +31 -0
  205. package/dist/xml/xml-namespaces.js +50 -0
  206. package/dist/xml/xml-read.d.ts +72 -10
  207. package/dist/xml/xml-read.js +94 -18
  208. package/dist/xml/xml-scan.d.ts +15 -37
  209. package/dist/xml/xml-scan.js +12 -40
  210. package/dist/xml/xml.d.ts +28 -27
  211. package/dist/xml/xml.js +24 -24
  212. package/package.json +12 -7
  213. /package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +0 -0
@@ -8,5 +8,6 @@ export const TYPE_EMPTY = 0;
8
8
  export const TYPE_STORAGE = 1;
9
9
  export const TYPE_STREAM = 2;
10
10
  export const TYPE_ROOT = 5;
11
+ export const MINI_STREAM_CUTOFF = 4096;
11
12
  export const DIR_ENTRY_SIZE = 128;
12
13
  export const MAX_NAME_CHARS = 31;
@@ -8,6 +8,15 @@ export interface CfbStorage {
8
8
  readonly children: readonly CfbNode[];
9
9
  }
10
10
  export type CfbNode = CfbStream | CfbStorage;
11
+ /**
12
+ * Which arm of {@link CfbNode} a node is.
13
+ *
14
+ * Exported beside the union rather than kept private, because the discriminant is `'data' in node`
15
+ * and that is the sort of test callers re-spell inline: `project-editor.ts` had it three times, once
16
+ * negated. The name says what the test means, and the pair says that a node is one or the other.
17
+ */
18
+ export declare function isStream(node: CfbNode): node is CfbStream;
19
+ export declare function isStorage(node: CfbNode): node is CfbStorage;
11
20
  /**
12
21
  * Encode a hierarchy of storages and streams into a v3 compound file. The Root Entry is synthesized
13
22
  * automatically; `root` is its top-level children. Every stream becomes a directory entry reachable both
@@ -1,15 +1,18 @@
1
- import { DIFSECT, DIR_ENTRY_SIZE, ENDOFCHAIN, FATSECT, FREESECT, MAX_NAME_CHARS, NOSTREAM, TYPE_ROOT, TYPE_STORAGE, TYPE_STREAM, } from './cfb-format.js';
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';
2
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
+ }
3
10
  const SECTOR = 512;
4
11
  const MINI_SECTOR = 64;
5
- const MINI_CUTOFF = 4096;
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
15
  const COLOR_BLACK = 1;
10
- function isStream(node) {
11
- return 'data' in node;
12
- }
13
16
  export function writeCompoundFile(root) {
14
17
  const entries = [];
15
18
  const addEntry = (name, type, startSector) => {
@@ -38,7 +41,7 @@ export function writeCompoundFile(root) {
38
41
  siblings.push(entry);
39
42
  if (node.data.length === 0) {
40
43
  }
41
- else if (node.data.length >= MINI_CUTOFF) {
44
+ else if (node.data.length >= MINI_STREAM_CUTOFF) {
42
45
  bigStreams.push({ entry, data: node.data, sectors: Math.ceil(node.data.length / SECTOR) });
43
46
  }
44
47
  else {
@@ -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,8 +1,10 @@
1
+ import { quoted } from '../errors.js';
1
2
  import { concat, decodeUtf16le, readU16, readU32 } from './bytes.js';
2
- import { DIR_ENTRY_SIZE, MAX_REGULAR_SECTOR, NOSTREAM, TYPE_EMPTY, TYPE_ROOT, TYPE_STORAGE, TYPE_STREAM, } from './cfb-format.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';
3
4
  import { VbaParseError } from './errors.js';
4
5
  const CFB_SIGNATURE_LO = 0xe011cfd0;
5
6
  const CFB_SIGNATURE_HI = 0xe11ab1a1;
7
+ const MAX_TREE_DEPTH = 256;
6
8
  export class CompoundFile {
7
9
  #buf;
8
10
  #sectorSize;
@@ -33,6 +35,9 @@ export class CompoundFile {
33
35
  const numFatSectors = readU32(this.#buf, 44);
34
36
  const firstDirSector = readU32(this.#buf, 48);
35
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
+ }
36
41
  const firstMiniFatSector = readU32(this.#buf, 60);
37
42
  const firstDifatSector = readU32(this.#buf, 68);
38
43
  const numDifatSectors = readU32(this.#buf, 72);
@@ -63,11 +68,15 @@ export class CompoundFile {
63
68
  const root = this.#dir[rootIdx];
64
69
  return this.#buildSiblings(root.child, new Set([rootIdx]));
65
70
  }
66
- #buildSiblings(firstChild, seen) {
71
+ #buildSiblings(firstChild, seen, depth = 0) {
72
+ if (depth > MAX_TREE_DEPTH)
73
+ throw new VbaParseError('directory tree nests too deep');
67
74
  const nodes = [];
68
- const walk = (idx) => {
75
+ const walk = (idx, siblingDepth) => {
69
76
  if (idx >= MAX_REGULAR_SECTOR)
70
77
  return;
78
+ if (siblingDepth > MAX_TREE_DEPTH)
79
+ throw new VbaParseError('directory tree nests too deep');
71
80
  if (idx >= this.#dir.length)
72
81
  throw new VbaParseError('directory sibling index out of range');
73
82
  if (seen.has(idx))
@@ -76,16 +85,16 @@ export class CompoundFile {
76
85
  const e = this.#dir[idx];
77
86
  if (e.type === TYPE_EMPTY)
78
87
  throw new VbaParseError('directory tree links an empty entry');
79
- walk(e.left);
88
+ walk(e.left, siblingDepth + 1);
80
89
  if (e.type === TYPE_STORAGE) {
81
- 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) });
82
91
  }
83
92
  else {
84
93
  nodes.push({ name: e.name, data: this.#readEntryData(e) });
85
94
  }
86
- walk(e.right);
95
+ walk(e.right, siblingDepth + 1);
87
96
  };
88
- walk(firstChild);
97
+ walk(firstChild, depth);
89
98
  return nodes;
90
99
  }
91
100
  #readEntryData(entry) {
@@ -94,8 +103,9 @@ export class CompoundFile {
94
103
  return this.#readViaMiniFat(entry.startSector, entry.size);
95
104
  }
96
105
  #readDifat(numFatSectors, firstDifat, numDifat) {
106
+ const wanted = Math.min(numFatSectors, this.#maxSector);
97
107
  const ids = [];
98
- for (let i = 0; i < 109 && ids.length < numFatSectors; i++) {
108
+ for (let i = 0; i < 109 && ids.length < wanted; i++) {
99
109
  const v = readU32(this.#buf, 76 + i * 4);
100
110
  if (v >= MAX_REGULAR_SECTOR)
101
111
  break;
@@ -104,12 +114,12 @@ export class CompoundFile {
104
114
  const seen = new Set();
105
115
  let sector = firstDifat;
106
116
  const perSector = this.#sectorSize / 4 - 1;
107
- 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++) {
108
118
  if (seen.has(sector))
109
119
  throw new VbaParseError('cycle in DIFAT sector chain');
110
120
  seen.add(sector);
111
121
  const base = this.#dataSectorOffset(sector);
112
- for (let i = 0; i < perSector; i++) {
122
+ for (let i = 0; i < perSector && ids.length < wanted; i++) {
113
123
  const v = readU32(this.#buf, base + i * 4);
114
124
  if (v < MAX_REGULAR_SECTOR)
115
125
  ids.push(v);
@@ -123,23 +133,28 @@ export class CompoundFile {
123
133
  const perSector = this.#sectorSize / 4;
124
134
  for (const sid of fatSectorIds) {
125
135
  const base = this.#dataSectorOffset(sid);
126
- for (let i = 0; i < perSector; i++)
136
+ for (let i = 0; i < perSector && fat.length < this.#maxSector; i++) {
127
137
  fat.push(readU32(this.#buf, base + i * 4));
138
+ }
139
+ if (fat.length >= this.#maxSector)
140
+ break;
128
141
  }
129
142
  return fat;
130
143
  }
131
144
  #readChainValues(firstSector) {
132
145
  const values = [];
133
146
  const perSector = this.#sectorSize / 4;
147
+ const cap = this.#maxSector * (this.#sectorSize / this.#miniSectorSize);
134
148
  const seen = new Set();
135
149
  let sector = firstSector;
136
- while (sector < MAX_REGULAR_SECTOR) {
150
+ while (sector < MAX_REGULAR_SECTOR && values.length < cap) {
137
151
  if (seen.has(sector))
138
152
  throw new VbaParseError('cycle in mini-FAT sector chain');
139
153
  seen.add(sector);
140
154
  const base = this.#dataSectorOffset(sector);
141
- for (let i = 0; i < perSector; i++)
155
+ for (let i = 0; i < perSector && values.length < cap; i++) {
142
156
  values.push(readU32(this.#buf, base + i * 4));
157
+ }
143
158
  sector = this.#nextInFat(sector);
144
159
  }
145
160
  return values;
@@ -177,63 +192,70 @@ export class CompoundFile {
177
192
  const name = decodeUtf16le(raw.subarray(off, off + Math.max(0, nameLen - 2)));
178
193
  const startSector = readU32(raw, off + 116);
179
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
+ }
180
198
  entries.push({ name, type, startSector, size, left, right, child });
181
199
  }
182
200
  return entries;
183
201
  }
184
202
  #readChainFull(startSector) {
185
- const chunks = [];
186
- const seen = new Set();
187
- let sector = startSector;
188
- while (sector < MAX_REGULAR_SECTOR) {
189
- if (seen.has(sector))
190
- throw new VbaParseError('cycle in FAT sector chain');
191
- seen.add(sector);
192
- const base = this.#dataSectorOffset(sector);
193
- chunks.push(this.#buf.subarray(base, base + this.#sectorSize));
194
- sector = this.#nextInFat(sector);
195
- }
196
- return concat(chunks);
203
+ return this.#walkChain(startSector, undefined, this.#fatChain('FAT sector chain'));
197
204
  }
198
205
  #readViaFat(startSector, size) {
199
- const chunks = [];
200
- const seen = new Set();
201
- let sector = startSector;
202
- let remaining = size;
203
- while (sector < MAX_REGULAR_SECTOR && remaining > 0) {
204
- if (seen.has(sector))
205
- throw new VbaParseError('cycle in stream FAT chain');
206
- seen.add(sector);
207
- const base = this.#dataSectorOffset(sector);
208
- const take = Math.min(this.#sectorSize, remaining);
209
- chunks.push(this.#buf.subarray(base, base + take));
210
- remaining -= take;
211
- sector = this.#nextInFat(sector);
212
- }
213
- return concat(chunks);
206
+ return this.#walkChain(startSector, size, this.#fatChain('stream FAT chain'));
214
207
  }
215
208
  #readViaMiniFat(startSector, size) {
209
+ return this.#walkChain(startSector, size, this.#miniFatChain());
210
+ }
211
+ #walkChain(startSector, size, chain) {
216
212
  const chunks = [];
217
213
  const seen = new Set();
218
214
  let sector = startSector;
219
- let remaining = size;
215
+ let remaining = size ?? Infinity;
220
216
  while (sector < MAX_REGULAR_SECTOR && remaining > 0) {
221
217
  if (seen.has(sector))
222
- throw new VbaParseError('cycle in stream mini-FAT chain');
218
+ throw new VbaParseError(`cycle in ${chain.what}`);
223
219
  seen.add(sector);
224
- const base = sector * this.#miniSectorSize;
225
- const take = Math.min(this.#miniSectorSize, remaining);
226
- if (base + take > this.#miniStream.length) {
227
- throw new VbaParseError('mini-stream sector runs past end of the mini stream');
228
- }
229
- chunks.push(this.#miniStream.subarray(base, base + take));
220
+ const take = Math.min(chain.sectorSize, remaining);
221
+ chunks.push(chain.sliceAt(sector, take));
230
222
  remaining -= take;
231
- if (sector >= this.#miniFat.length)
232
- throw new VbaParseError('mini-FAT index out of range');
233
- 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`);
234
227
  }
235
228
  return concat(chunks);
236
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
+ }
237
259
  #nextInFat(sector) {
238
260
  if (sector >= this.#fat.length)
239
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 = new DecompressedBytes(maxOutput);
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);
@@ -55,16 +58,19 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
55
58
  for (let i = 0; i < length; i++)
56
59
  out.push(out.at(src + i));
57
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`);
63
+ }
58
64
  }
59
65
  pos = chunkEnd;
60
66
  }
61
67
  return out.bytes();
62
68
  }
63
- class DecompressedBytes {
69
+ class ByteSink {
64
70
  #buf;
65
71
  #length = 0;
66
72
  #limit;
67
- constructor(limit) {
73
+ constructor(limit = Infinity) {
68
74
  this.#limit = limit;
69
75
  this.#buf = new Uint8Array(Math.min(MAX_CHUNK_DECOMPRESSED, limit));
70
76
  }
@@ -74,6 +80,9 @@ class DecompressedBytes {
74
80
  at(index) {
75
81
  return this.#buf[index];
76
82
  }
83
+ set(index, byte) {
84
+ this.#buf[index] = byte;
85
+ }
77
86
  push(byte) {
78
87
  if (this.#length >= this.#limit) {
79
88
  throw new VbaParseError(`decompressed output exceeds the ${this.#limit}-byte ceiling (possible bomb)`);
@@ -85,26 +94,40 @@ class DecompressedBytes {
85
94
  }
86
95
  this.#buf[this.#length++] = byte;
87
96
  }
97
+ pushAll(bytes) {
98
+ for (const byte of bytes)
99
+ this.push(byte);
100
+ }
88
101
  bytes() {
89
102
  return this.#buf.slice(0, this.#length);
90
103
  }
91
104
  }
92
105
  export function compressContainer(data) {
93
- const out = [0x01];
106
+ const out = new ByteSink();
107
+ out.push(0x01);
94
108
  for (let start = 0; start < data.length; start += MAX_CHUNK_DECOMPRESSED) {
95
109
  const chunk = data.subarray(start, Math.min(start + MAX_CHUNK_DECOMPRESSED, data.length));
96
110
  const tokens = compressChunk(chunk);
97
111
  const compressed = tokens.length < chunk.length;
98
112
  const body = compressed ? tokens : chunk;
99
113
  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);
114
+ out.push(header & 0xff);
115
+ out.push((header >> 8) & 0xff);
116
+ out.pushAll(body);
103
117
  }
104
- return Uint8Array.from(out);
118
+ return out.bytes();
105
119
  }
106
120
  function compressChunk(chunk) {
107
- 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
+ };
108
131
  let pos = 0;
109
132
  while (pos < chunk.length) {
110
133
  const flagIndex = tokens.length;
@@ -116,37 +139,49 @@ function compressChunk(chunk) {
116
139
  const windowStart = Math.max(0, pos - (1 << bitCount));
117
140
  let bestLength = 0;
118
141
  let bestOffset = 0;
119
- 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++) {
120
144
  let len = 0;
121
145
  while (len < maxLength &&
122
146
  pos + len < chunk.length &&
123
- chunk[cand + len] === chunk[pos + len]) {
147
+ chunk[candidate + len] === chunk[pos + len]) {
124
148
  len++;
125
149
  }
126
150
  if (len > bestLength) {
127
151
  bestLength = len;
128
- bestOffset = pos - cand;
152
+ bestOffset = pos - candidate;
129
153
  if (bestLength === maxLength)
130
154
  break;
131
155
  }
156
+ candidate = prev[candidate];
132
157
  }
133
158
  if (bestLength >= 3) {
134
159
  const token = ((bestOffset - 1) << (16 - bitCount)) | (bestLength - 3);
135
- tokens.push(token & 0xff, (token >> 8) & 0xff);
160
+ tokens.push(token & 0xff);
161
+ tokens.push((token >> 8) & 0xff);
136
162
  flags |= 1 << bit;
163
+ for (let i = 0; i < bestLength; i++)
164
+ remember(pos + i);
137
165
  pos += bestLength;
138
166
  }
139
167
  else {
140
168
  tokens.push(chunk[pos]);
169
+ remember(pos);
141
170
  pos++;
142
171
  }
143
172
  }
144
- tokens[flagIndex] = flags;
173
+ tokens.set(flagIndex, flags);
145
174
  }
146
- return tokens;
175
+ return tokens.bytes();
176
+ }
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);
147
182
  }
148
183
  function copyTokenHelp(decompressedSoFar) {
149
- 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));
150
185
  const lengthMask = 0xffff >> bitCount;
151
186
  return { lengthMask, bitCount };
152
187
  }