@shbernal/ts-xlsx 2.0.0 → 3.0.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 (147) hide show
  1. package/README.md +20 -10
  2. package/dist/bytes.d.ts +24 -0
  3. package/dist/bytes.js +34 -0
  4. package/dist/core/address.d.ts +26 -4
  5. package/dist/core/address.js +21 -9
  6. package/dist/core/autofilter.d.ts +12 -1
  7. package/dist/core/autofilter.js +35 -12
  8. package/dist/core/axis-handle.d.ts +8 -0
  9. package/dist/core/axis-handle.js +14 -0
  10. package/dist/core/cell.d.ts +24 -0
  11. package/dist/core/cell.js +9 -1
  12. package/dist/core/color-resolution.d.ts +1 -1
  13. package/dist/core/color-resolution.js +7 -8
  14. package/dist/core/column.d.ts +4 -1
  15. package/dist/core/column.js +31 -35
  16. package/dist/core/conditional-formatting-overlay.d.ts +5 -0
  17. package/dist/core/conditional-formatting-overlay.js +11 -0
  18. package/dist/core/conditional-formatting.d.ts +45 -5
  19. package/dist/core/conditional-formatting.js +74 -0
  20. package/dist/core/data-validation-overlay.d.ts +11 -0
  21. package/dist/core/data-validation-overlay.js +21 -2
  22. package/dist/core/data-validation.d.ts +9 -2
  23. package/dist/core/data-validation.js +26 -0
  24. package/dist/core/grid-edits.d.ts +16 -0
  25. package/dist/core/grid-edits.js +26 -8
  26. package/dist/core/grid-shift.d.ts +19 -0
  27. package/dist/core/grid-shift.js +8 -0
  28. package/dist/core/image.d.ts +2 -0
  29. package/dist/core/image.js +2 -0
  30. package/dist/core/merge-index.d.ts +31 -0
  31. package/dist/core/merge-index.js +60 -0
  32. package/dist/core/merge.d.ts +12 -11
  33. package/dist/core/merge.js +36 -10
  34. package/dist/core/page-setup.d.ts +86 -2
  35. package/dist/core/page-setup.js +34 -1
  36. package/dist/core/pivot-table.d.ts +2 -0
  37. package/dist/core/pivot-table.js +23 -18
  38. package/dist/core/protection.d.ts +3 -0
  39. package/dist/core/protection.js +20 -14
  40. package/dist/core/row-input.d.ts +7 -0
  41. package/dist/core/row-input.js +10 -8
  42. package/dist/core/row.d.ts +4 -1
  43. package/dist/core/row.js +19 -23
  44. package/dist/core/style.d.ts +78 -7
  45. package/dist/core/style.js +90 -80
  46. package/dist/core/table.d.ts +26 -12
  47. package/dist/core/table.js +67 -33
  48. package/dist/core/theme.d.ts +3 -1
  49. package/dist/core/theme.js +4 -0
  50. package/dist/core/used-extent.d.ts +44 -0
  51. package/dist/core/used-extent.js +102 -0
  52. package/dist/core/workbook.d.ts +2 -2
  53. package/dist/core/worksheet-comments.d.ts +8 -0
  54. package/dist/core/worksheet-comments.js +18 -0
  55. package/dist/core/worksheet.d.ts +31 -7
  56. package/dist/core/worksheet.js +72 -86
  57. package/dist/customui/ribbon.js +23 -24
  58. package/dist/entries/core.d.ts +3 -3
  59. package/dist/entries/csv.d.ts +1 -1
  60. package/dist/entries/node-unavailable.d.ts +15 -0
  61. package/dist/entries/node-unavailable.js +20 -0
  62. package/dist/entries/node.d.ts +1 -0
  63. package/dist/entries/node.js +1 -0
  64. package/dist/entries/xlsx.d.ts +0 -1
  65. package/dist/entries/xlsx.js +0 -1
  66. package/dist/io/csv/read.js +1 -1
  67. package/dist/io/csv/write.d.ts +12 -2
  68. package/dist/io/csv/write.js +17 -2
  69. package/dist/io/opc/inflate.js +3 -13
  70. package/dist/io/opc/part-paths.d.ts +1 -0
  71. package/dist/io/opc/part-paths.js +15 -0
  72. package/dist/io/opc/read-opc.d.ts +22 -3
  73. package/dist/io/opc/read-opc.js +11 -39
  74. package/dist/io/opc/rels.d.ts +12 -6
  75. package/dist/io/opc/rels.js +3 -5
  76. package/dist/io/style/xf-style.d.ts +33 -4
  77. package/dist/io/style/xf-style.js +12 -1
  78. package/dist/io/xlsb/read-styles.js +6 -3
  79. package/dist/io/xlsb/read.d.ts +1 -1
  80. package/dist/io/xlsb/read.js +5 -8
  81. package/dist/io/xlsx/cell-accumulator.d.ts +2 -4
  82. package/dist/io/xlsx/cell-accumulator.js +20 -58
  83. package/dist/io/xlsx/cell-value.d.ts +1 -1
  84. package/dist/io/xlsx/cell-value.js +7 -4
  85. package/dist/io/xlsx/color-xml.d.ts +1 -1
  86. package/dist/io/xlsx/color-xml.js +8 -7
  87. package/dist/io/xlsx/comments.js +6 -3
  88. package/dist/io/xlsx/conditional-formatting.d.ts +4 -3
  89. package/dist/io/xlsx/conditional-formatting.js +81 -65
  90. package/dist/io/xlsx/data-validation.d.ts +10 -8
  91. package/dist/io/xlsx/data-validation.js +44 -41
  92. package/dist/io/xlsx/edit-vba.js +4 -11
  93. package/dist/io/xlsx/hyperlinks.d.ts +4 -2
  94. package/dist/io/xlsx/hyperlinks.js +26 -31
  95. package/dist/io/xlsx/images.js +33 -35
  96. package/dist/io/xlsx/package-plan.d.ts +9 -1
  97. package/dist/io/xlsx/package-plan.js +29 -16
  98. package/dist/io/xlsx/part-names.d.ts +41 -0
  99. package/dist/io/xlsx/part-names.js +46 -0
  100. package/dist/io/xlsx/read-pivot.js +4 -9
  101. package/dist/io/xlsx/read-rows.js +12 -13
  102. package/dist/io/xlsx/read-shared-strings.js +9 -47
  103. package/dist/io/xlsx/read-styles.d.ts +1 -1
  104. package/dist/io/xlsx/read-styles.js +65 -43
  105. package/dist/io/xlsx/read-worksheet.d.ts +10 -1
  106. package/dist/io/xlsx/read-worksheet.js +48 -63
  107. package/dist/io/xlsx/read.d.ts +3 -4
  108. package/dist/io/xlsx/read.js +124 -146
  109. package/dist/io/xlsx/rich-runs.d.ts +41 -7
  110. package/dist/io/xlsx/rich-runs.js +95 -30
  111. package/dist/io/xlsx/sheet-properties.d.ts +1 -1
  112. package/dist/io/xlsx/sheet-properties.js +23 -26
  113. package/dist/io/xlsx/styles.d.ts +3 -15
  114. package/dist/io/xlsx/styles.js +83 -105
  115. package/dist/io/xlsx/tables.js +14 -13
  116. package/dist/io/xlsx/theme-xml.js +8 -5
  117. package/dist/io/xlsx/threaded-comments.js +20 -19
  118. package/dist/io/xlsx/workbook-xml.js +43 -36
  119. package/dist/io/xlsx/worksheet-xml.d.ts +1 -1
  120. package/dist/io/xlsx/worksheet-xml.js +47 -52
  121. package/dist/io/xlsx/write-stream.d.ts +14 -1
  122. package/dist/io/xlsx/write-stream.js +40 -33
  123. package/dist/io/xlsx/write.d.ts +5 -0
  124. package/dist/io/xlsx/write.js +155 -131
  125. package/dist/sha512.d.ts +2 -0
  126. package/dist/sha512.js +146 -0
  127. package/dist/token-set.d.ts +15 -0
  128. package/dist/token-set.js +4 -0
  129. package/dist/vba/bytes.d.ts +1 -2
  130. package/dist/vba/bytes.js +1 -12
  131. package/dist/vba/cfb-format.d.ts +24 -0
  132. package/dist/vba/cfb-format.js +12 -0
  133. package/dist/vba/cfb-writer.js +17 -17
  134. package/dist/vba/cfb.js +1 -7
  135. package/dist/vba/dir-records.d.ts +53 -0
  136. package/dist/vba/dir-records.js +28 -0
  137. package/dist/vba/ms-ovba.js +32 -11
  138. package/dist/vba/project-editor.js +5 -34
  139. package/dist/vba/project.js +15 -31
  140. package/dist/vba/vba-encoding.js +1 -1
  141. package/dist/xml/xml-read.d.ts +117 -72
  142. package/dist/xml/xml-read.js +123 -169
  143. package/dist/xml/xml-scan.d.ts +100 -0
  144. package/dist/xml/xml-scan.js +208 -0
  145. package/dist/xml/xml.d.ts +44 -2
  146. package/dist/xml/xml.js +17 -5
  147. package/package.json +22 -6
@@ -1,20 +1,11 @@
1
+ import { DIFSECT, DIR_ENTRY_SIZE, ENDOFCHAIN, FATSECT, FREESECT, MAX_NAME_CHARS, NOSTREAM, TYPE_ROOT, TYPE_STORAGE, TYPE_STREAM, } from './cfb-format.js';
1
2
  import { VbaAuthorError } from './errors.js';
2
3
  const SECTOR = 512;
3
4
  const MINI_SECTOR = 64;
4
5
  const MINI_CUTOFF = 4096;
5
- const DIR_ENTRY_SIZE = 128;
6
6
  const ENTRIES_PER_DIR_SECTOR = SECTOR / DIR_ENTRY_SIZE;
7
7
  const FAT_ENTRIES_PER_SECTOR = SECTOR / 4;
8
8
  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
9
  const COLOR_BLACK = 1;
19
10
  function isStream(node) {
20
11
  return 'data' in node;
@@ -36,7 +27,8 @@ export function writeCompoundFile(root) {
36
27
  return entry;
37
28
  };
38
29
  const rootEntry = addEntry('Root Entry', TYPE_ROOT, ENDOFCHAIN);
39
- const miniBytes = [];
30
+ const miniChunks = [];
31
+ let miniLength = 0;
40
32
  const miniFat = [];
41
33
  const bigStreams = [];
42
34
  const addNode = (node, siblings) => {
@@ -50,11 +42,14 @@ export function writeCompoundFile(root) {
50
42
  bigStreams.push({ entry, data: node.data, sectors: Math.ceil(node.data.length / SECTOR) });
51
43
  }
52
44
  else {
53
- const startMini = miniBytes.length / MINI_SECTOR;
45
+ const startMini = miniLength / MINI_SECTOR;
54
46
  const numMini = Math.ceil(node.data.length / MINI_SECTOR);
55
47
  for (let k = 0; k < numMini; k++)
56
48
  miniFat.push(k < numMini - 1 ? startMini + k + 1 : ENDOFCHAIN);
57
- miniBytes.push(...node.data, ...new Array(numMini * MINI_SECTOR - node.data.length).fill(0));
49
+ const padded = new Uint8Array(numMini * MINI_SECTOR);
50
+ padded.set(node.data);
51
+ miniChunks.push(padded);
52
+ miniLength += padded.length;
58
53
  entry.startSector = startMini;
59
54
  }
60
55
  }
@@ -74,7 +69,7 @@ export function writeCompoundFile(root) {
74
69
  linkChildren(rootEntry, rootKids);
75
70
  const dirSectors = Math.ceil(entries.length / ENTRIES_PER_DIR_SECTOR);
76
71
  const miniFatSectors = miniFat.length > 0 ? Math.ceil((miniFat.length * 4) / FAT_ENTRIES_PER_SECTOR) : 0;
77
- const miniStreamSectors = Math.ceil(miniBytes.length / SECTOR);
72
+ const miniStreamSectors = Math.ceil(miniLength / SECTOR);
78
73
  const baseSectors = dirSectors +
79
74
  miniFatSectors +
80
75
  miniStreamSectors +
@@ -112,7 +107,7 @@ export function writeCompoundFile(root) {
112
107
  cursor += difatSectors;
113
108
  const totalSectors = cursor;
114
109
  rootEntry.startSector = miniStreamStart;
115
- rootEntry.size = miniBytes.length;
110
+ rootEntry.size = miniLength;
116
111
  const fat = new Array(fatSectors * FAT_ENTRIES_PER_SECTOR).fill(FREESECT);
117
112
  const chainRegion = (start, count) => {
118
113
  for (let k = 0; k < count; k++)
@@ -146,8 +141,13 @@ export function writeCompoundFile(root) {
146
141
  for (let i = 0; i < miniFatSectors * FAT_ENTRIES_PER_SECTOR; i++) {
147
142
  dv.setUint32(at(miniFatStart) + i * 4, miniFat[i] ?? FREESECT, true);
148
143
  }
149
- if (miniBytes.length > 0)
150
- buf.set(Uint8Array.from(miniBytes), at(miniStreamStart));
144
+ if (miniLength > 0) {
145
+ let offset = at(miniStreamStart);
146
+ for (const chunk of miniChunks) {
147
+ buf.set(chunk, offset);
148
+ offset += chunk.length;
149
+ }
150
+ }
151
151
  for (const big of bigStreams)
152
152
  buf.set(big.data, at(big.entry.startSector));
153
153
  for (const [i, sector] of fat.entries())
package/dist/vba/cfb.js CHANGED
@@ -1,14 +1,8 @@
1
1
  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';
2
3
  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
4
  const CFB_SIGNATURE_LO = 0xe011cfd0;
10
5
  const CFB_SIGNATURE_HI = 0xe11ab1a1;
11
- const DIR_ENTRY_SIZE = 128;
12
6
  export class CompoundFile {
13
7
  #buf;
14
8
  #sectorSize;
@@ -0,0 +1,53 @@
1
+ /** PROJECTCODEPAGE: the code page every MBCS name in the project decodes through. */
2
+ export declare const REC_PROJECT_CODEPAGE = 3;
3
+ /** PROJECTVERSION: the record whose `Size` under-counts its payload by the trailing VersionMinor. */
4
+ export declare const REC_PROJECT_VERSION = 9;
5
+ /** REFERENCEREGISTERED: a reference to a registered type library, by libid. */
6
+ export declare const REC_REFERENCE_REGISTERED = 13;
7
+ /** PROJECTMODULES MODULES_COUNT: how many module blocks follow, and the first record after the
8
+ * reference array, which has no count of its own. */
9
+ export declare const REC_MODULES_COUNT = 15;
10
+ /** REFERENCENAME: a reference's MBCS name. */
11
+ export declare const REC_REFERENCE_NAME = 22;
12
+ /** MODULENAME: opens a module's record block. */
13
+ export declare const REC_MODULE_NAME = 25;
14
+ /** MODULESTREAMNAME: the storage name the module's p-code lives under. */
15
+ export declare const REC_MODULE_STREAMNAME = 26;
16
+ /** MODULETYPE for a standard module. */
17
+ export declare const REC_MODULE_TYPE_PROCEDURAL = 33;
18
+ /** MODULETYPE for a document module (`ThisWorkbook`, a sheet's code-behind). */
19
+ export declare const REC_MODULE_TYPE_DOCUMENT = 34;
20
+ /** MODULEENDOFBLOCK: closes a module's record block. */
21
+ export declare const REC_MODULE_TERMINATOR = 43;
22
+ /** MODULEOFFSET: where the module's source begins inside its stream. */
23
+ export declare const REC_MODULE_OFFSET = 49;
24
+ /**
25
+ * REFERENCENAME's Unicode half. A *literal* `0x003E` marker rather than a nested record id, but it is
26
+ * laid out as its own Id+Size+data TLV, so a generic walk sees REFERENCENAME as two chained records,
27
+ * exactly like MODULENAME/MODULENAME_UNICODE. Verified against a real Excel-authored dir stream
28
+ * (2026-07-23).
29
+ */
30
+ export declare const REC_REFERENCE_NAME_UNICODE = 62;
31
+ /** One record as the walk sees it. */
32
+ export interface DirRecord {
33
+ /** The record id; one of the `REC_*` constants above, or a record this library does not consume. */
34
+ readonly id: number;
35
+ /** Offset of the record's `Id` field: where a splice that drops the record cuts from. */
36
+ readonly recordStart: number;
37
+ /** Offset of the record's payload. */
38
+ readonly dataStart: number;
39
+ /** The `Size` field, which is the payload length for every record but PROJECTVERSION. */
40
+ readonly size: number;
41
+ /** Offset of the next record, PROJECTVERSION's uncounted VersionMinor already skipped. Never
42
+ * recompute this from `dataStart + size`: that is the misalignment this module exists to prevent. */
43
+ readonly end: number;
44
+ }
45
+ /**
46
+ * Walk a decompressed `dir` stream, yielding each record in order. Consumers may stop early.
47
+ *
48
+ * @param context the phrase a truncated record's error carries, naming what the caller was doing:
49
+ * the message reaches a user who has no idea what a TLV is but does know they were removing a
50
+ * module.
51
+ * @throws {VbaParseError} when a record's payload runs past the end of the stream.
52
+ */
53
+ export declare function dirRecords(dir: Uint8Array, context: string): Generator<DirRecord>;
@@ -0,0 +1,28 @@
1
+ import { readU16, readU32 } from './bytes.js';
2
+ import { VbaParseError } from './errors.js';
3
+ export const REC_PROJECT_CODEPAGE = 0x0003;
4
+ export const REC_PROJECT_VERSION = 0x0009;
5
+ export const REC_REFERENCE_REGISTERED = 0x000d;
6
+ export const REC_MODULES_COUNT = 0x000f;
7
+ export const REC_REFERENCE_NAME = 0x0016;
8
+ export const REC_MODULE_NAME = 0x0019;
9
+ export const REC_MODULE_STREAMNAME = 0x001a;
10
+ export const REC_MODULE_TYPE_PROCEDURAL = 0x0021;
11
+ export const REC_MODULE_TYPE_DOCUMENT = 0x0022;
12
+ export const REC_MODULE_TERMINATOR = 0x002b;
13
+ export const REC_MODULE_OFFSET = 0x0031;
14
+ export const REC_REFERENCE_NAME_UNICODE = 0x003e;
15
+ export function* dirRecords(dir, context) {
16
+ let pos = 0;
17
+ while (pos + 6 <= dir.length) {
18
+ const id = readU16(dir, pos);
19
+ const size = readU32(dir, pos + 2);
20
+ const dataStart = pos + 6;
21
+ if (dataStart + size > dir.length) {
22
+ throw new VbaParseError(`dir record 0x${id.toString(16)} ${context}`);
23
+ }
24
+ const end = dataStart + size + (id === REC_PROJECT_VERSION ? 2 : 0);
25
+ yield { id, recordStart: pos, dataStart, size, end };
26
+ pos = end;
27
+ }
28
+ }
@@ -11,7 +11,7 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
11
11
  if (buf[start] !== 0x01) {
12
12
  throw new VbaParseError(`compressed container must begin with a 0x01 signature byte, found 0x${(buf[start] ?? 0).toString(16)}`);
13
13
  }
14
- const out = [];
14
+ const out = new DecompressedBytes(maxOutput);
15
15
  let pos = start + 1;
16
16
  while (pos + 2 <= buf.length) {
17
17
  const header = readU16(buf, pos);
@@ -28,7 +28,6 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
28
28
  if (!compressed) {
29
29
  for (let i = pos; i < chunkEnd; i++)
30
30
  out.push(buf[i]);
31
- guardOutput(out.length, maxOutput);
32
31
  pos = chunkEnd;
33
32
  continue;
34
33
  }
@@ -39,7 +38,6 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
39
38
  const isCopy = (flagByte >> bit) & 1;
40
39
  if (!isCopy) {
41
40
  out.push(buf[pos++]);
42
- guardOutput(out.length, maxOutput);
43
41
  continue;
44
42
  }
45
43
  if (pos + 2 > chunkEnd) {
@@ -55,13 +53,41 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
55
53
  throw new VbaParseError('copy token references before the start of its chunk');
56
54
  }
57
55
  for (let i = 0; i < length; i++)
58
- out.push(out[src + i]);
59
- guardOutput(out.length, maxOutput);
56
+ out.push(out.at(src + i));
60
57
  }
61
58
  }
62
59
  pos = chunkEnd;
63
60
  }
64
- return Uint8Array.from(out);
61
+ return out.bytes();
62
+ }
63
+ class DecompressedBytes {
64
+ #buf;
65
+ #length = 0;
66
+ #limit;
67
+ constructor(limit) {
68
+ this.#limit = limit;
69
+ this.#buf = new Uint8Array(Math.min(MAX_CHUNK_DECOMPRESSED, limit));
70
+ }
71
+ get length() {
72
+ return this.#length;
73
+ }
74
+ at(index) {
75
+ return this.#buf[index];
76
+ }
77
+ push(byte) {
78
+ if (this.#length >= this.#limit) {
79
+ throw new VbaParseError(`decompressed output exceeds the ${this.#limit}-byte ceiling (possible bomb)`);
80
+ }
81
+ if (this.#length === this.#buf.length) {
82
+ const grown = new Uint8Array(Math.min(Math.max(this.#buf.length * 2, MAX_CHUNK_DECOMPRESSED), this.#limit));
83
+ grown.set(this.#buf);
84
+ this.#buf = grown;
85
+ }
86
+ this.#buf[this.#length++] = byte;
87
+ }
88
+ bytes() {
89
+ return this.#buf.slice(0, this.#length);
90
+ }
65
91
  }
66
92
  export function compressContainer(data) {
67
93
  const out = [0x01];
@@ -119,11 +145,6 @@ function compressChunk(chunk) {
119
145
  }
120
146
  return tokens;
121
147
  }
122
- function guardOutput(size, maxOutput) {
123
- if (size > maxOutput) {
124
- throw new VbaParseError(`decompressed output exceeds the ${maxOutput}-byte ceiling (possible bomb)`);
125
- }
126
- }
127
148
  function copyTokenHelp(decompressedSoFar) {
128
149
  const bitCount = Math.max(Math.ceil(Math.log2(Math.max(decompressedSoFar, 1))), 4);
129
150
  const lengthMask = 0xffff >> bitCount;
@@ -1,7 +1,8 @@
1
- import { readU16, readU32 } from './bytes.js';
1
+ import { readU16 } from './bytes.js';
2
2
  import { writeCompoundFile } from './cfb-writer.js';
3
3
  import { CompoundFile } from './cfb.js';
4
4
  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';
5
6
  import { VbaAuthorError, VbaParseError } from './errors.js';
6
7
  import { compressContainer, decompressContainer } from './ms-ovba.js';
7
8
  import { parseVbaProject } from './project.js';
@@ -10,14 +11,6 @@ const DIR_STREAM = 'dir';
10
11
  const PROJECT_STREAM = 'PROJECT';
11
12
  const PROJECTWM_STREAM = 'PROJECTwm';
12
13
  const VBA_STORAGE = 'VBA';
13
- const REC_MODULE_NAME = 0x0019;
14
- const REC_MODULE_STREAMNAME = 0x001a;
15
- const REC_MODULE_TERMINATOR = 0x002b;
16
- const REC_PROJECT_VERSION = 0x0009;
17
- const REC_MODULES_COUNT = 0x000f;
18
- const REC_REFERENCE_NAME = 0x0016;
19
- const REC_REFERENCE_NAME_UNICODE = 0x003e;
20
- const REC_REFERENCE_REGISTERED = 0x000d;
21
14
  export function removeVbaModule(bin, name) {
22
15
  const project = parseVbaProject(bin);
23
16
  const nameKey = name.toUpperCase();
@@ -124,18 +117,7 @@ function buildReferenceDirRecords(ref, encode) {
124
117
  }
125
118
  function insertReferenceDirRecords(dir, records) {
126
119
  let insertAt = -1;
127
- let pos = 0;
128
- while (pos + 6 <= dir.length) {
129
- const recordStart = pos;
130
- const id = readU16(dir, pos);
131
- const size = readU32(dir, pos + 2);
132
- const dataStart = pos + 6;
133
- if (dataStart + size > dir.length) {
134
- throw new VbaParseError(`dir record 0x${id.toString(16)} overruns while adding a reference`);
135
- }
136
- pos = dataStart + size;
137
- if (id === REC_PROJECT_VERSION)
138
- pos += 2;
120
+ for (const { id, recordStart } of dirRecords(dir, 'overruns while adding a reference')) {
139
121
  if (id === REC_MODULES_COUNT) {
140
122
  insertAt = recordStart;
141
123
  break;
@@ -157,18 +139,7 @@ function removeModuleDirRecord(dir, streamName, codePage) {
157
139
  let removeStart = -1;
158
140
  let removeEnd = -1;
159
141
  let currentStream;
160
- let pos = 0;
161
- while (pos + 6 <= dir.length) {
162
- const recordStart = pos;
163
- const id = readU16(dir, pos);
164
- const size = readU32(dir, pos + 2);
165
- const dataStart = pos + 6;
166
- if (dataStart + size > dir.length) {
167
- throw new VbaParseError(`dir record 0x${id.toString(16)} overruns while removing a module`);
168
- }
169
- pos = dataStart + size;
170
- if (id === REC_PROJECT_VERSION)
171
- pos += 2;
142
+ for (const { id, recordStart, dataStart, size, end } of dirRecords(dir, 'overruns while removing a module')) {
172
143
  if (id === REC_MODULES_COUNT) {
173
144
  if (size < 2)
174
145
  throw new VbaParseError('PROJECTMODULES MODULES_COUNT record is malformed');
@@ -183,7 +154,7 @@ function removeModuleDirRecord(dir, streamName, codePage) {
183
154
  else if (id === REC_MODULE_TERMINATOR) {
184
155
  if (currentStream === streamName) {
185
156
  removeStart = blockStart;
186
- removeEnd = pos;
157
+ removeEnd = end;
187
158
  }
188
159
  currentStream = undefined;
189
160
  blockStart = -1;
@@ -1,27 +1,20 @@
1
1
  import { readU16, readU32 } from './bytes.js';
2
2
  import { CompoundFile } from './cfb.js';
3
3
  import { decoderForCodePage } from './codepage.js';
4
+ import { dirRecords, REC_MODULE_NAME, REC_MODULE_OFFSET, REC_MODULE_STREAMNAME, REC_MODULE_TERMINATOR, REC_MODULE_TYPE_DOCUMENT, REC_MODULE_TYPE_PROCEDURAL, REC_PROJECT_CODEPAGE, } from './dir-records.js';
4
5
  import { VbaParseError } from './errors.js';
5
6
  import { decompressContainer } from './ms-ovba.js';
6
7
  export const VBA_PROJECT_REL_TYPE = 'http://schemas.microsoft.com/office/2006/relationships/vbaProject';
7
8
  export const VBA_PROJECT_CONTENT_TYPE = 'application/vnd.ms-office.vbaProject';
8
9
  export const VBA_PROJECT_PART_PATH = 'xl/vbaProject.bin';
9
- const SIGNATURE_KIND_BY_REL_SEGMENT = {
10
- vbaProjectSignature: 'legacy',
11
- vbaProjectSignatureAgile: 'agile',
12
- vbaProjectSignatureV3: 'v3',
13
- };
10
+ const SIGNATURE_KIND_BY_REL_SEGMENT = new Map([
11
+ ['vbaProjectSignature', 'legacy'],
12
+ ['vbaProjectSignatureAgile', 'agile'],
13
+ ['vbaProjectSignatureV3', 'v3'],
14
+ ]);
14
15
  export function vbaProjectSignatureKind(relType) {
15
- return SIGNATURE_KIND_BY_REL_SEGMENT[relType.slice(relType.lastIndexOf('/') + 1)];
16
+ return SIGNATURE_KIND_BY_REL_SEGMENT.get(relType.slice(relType.lastIndexOf('/') + 1));
16
17
  }
17
- const REC_PROJECT_CODEPAGE = 0x0003;
18
- const REC_MODULE_NAME = 0x0019;
19
- const REC_MODULE_STREAMNAME = 0x001a;
20
- const REC_MODULE_TYPE_PROCEDURAL = 0x0021;
21
- const REC_MODULE_TYPE_DOCUMENT = 0x0022;
22
- const REC_MODULE_OFFSET = 0x0031;
23
- const REC_MODULE_TERMINATOR = 0x002b;
24
- const REC_PROJECT_VERSION = 0x0009;
25
18
  export function parseVbaProject(bin) {
26
19
  const cfb = new CompoundFile(bin);
27
20
  const dirCompressed = cfb.readStream('dir');
@@ -31,16 +24,7 @@ export function parseVbaProject(bin) {
31
24
  let codePage = 1252;
32
25
  const rawModules = [];
33
26
  let pending = {};
34
- let pos = 0;
35
- while (pos + 6 <= dir.length) {
36
- const id = readU16(dir, pos);
37
- const size = readU32(dir, pos + 2);
38
- const dataStart = pos + 6;
39
- if (dataStart + size > dir.length)
40
- throw new VbaParseError(`dir record 0x${id.toString(16)} overruns stream`);
41
- pos = dataStart + size;
42
- if (id === REC_PROJECT_VERSION)
43
- pos += 2;
27
+ for (const { id, dataStart, size } of dirRecords(dir, 'overruns stream')) {
44
28
  switch (id) {
45
29
  case REC_PROJECT_CODEPAGE:
46
30
  if (size >= 2)
@@ -92,23 +76,23 @@ function readModuleSource(cfb, streamName, textOffset, decoder) {
92
76
  throw new VbaParseError(`module stream '${streamName}' not found in container`);
93
77
  return decoder.decode(decompressContainer(stream, textOffset));
94
78
  }
79
+ const MODULE_KIND_BY_PROJECT_KEYWORD = new Map([
80
+ ['Document', 'document'],
81
+ ['Module', 'procedural'],
82
+ ['Class', 'class'],
83
+ ['BaseClass', 'designer'],
84
+ ]);
95
85
  function readProjectStreamKinds(cfb, decoder) {
96
86
  const kinds = new Map();
97
87
  const stream = cfb.readStream('PROJECT');
98
88
  if (!stream)
99
89
  return kinds;
100
90
  const text = decoder.decode(stream);
101
- const keyword = {
102
- Document: 'document',
103
- Module: 'procedural',
104
- Class: 'class',
105
- BaseClass: 'designer',
106
- };
107
91
  for (const line of text.split(/\r\n|\r|\n/)) {
108
92
  const eq = line.indexOf('=');
109
93
  if (eq < 0)
110
94
  continue;
111
- const kind = keyword[line.slice(0, eq)];
95
+ const kind = MODULE_KIND_BY_PROJECT_KEYWORD.get(line.slice(0, eq));
112
96
  if (kind === undefined)
113
97
  continue;
114
98
  const name = (line.slice(eq + 1).split('/')[0] ?? '').trim();
@@ -1,6 +1,6 @@
1
+ import { MAX_NAME_CHARS } from './cfb-format.js';
1
2
  import { VbaAuthorError } from './errors.js';
2
3
  const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_]*$/;
3
- const MAX_NAME_CHARS = 31;
4
4
  export function validateVbaName(name, what) {
5
5
  if (!IDENTIFIER.test(name) || name.length > MAX_NAME_CHARS) {
6
6
  throw new VbaAuthorError(`invalid ${what} name '${name}' (must be a VBA identifier ≤ 31 chars)`);