@shbernal/ts-xlsx 1.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 (223) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +230 -0
  3. package/dist/core/address.d.ts +49 -0
  4. package/dist/core/address.js +125 -0
  5. package/dist/core/autofilter.d.ts +55 -0
  6. package/dist/core/autofilter.js +48 -0
  7. package/dist/core/cell.d.ts +132 -0
  8. package/dist/core/cell.js +249 -0
  9. package/dist/core/color-resolution.d.ts +58 -0
  10. package/dist/core/color-resolution.js +170 -0
  11. package/dist/core/column.d.ts +87 -0
  12. package/dist/core/column.js +167 -0
  13. package/dist/core/comment-thread.d.ts +116 -0
  14. package/dist/core/comment-thread.js +62 -0
  15. package/dist/core/conditional-formatting-overlay.d.ts +15 -0
  16. package/dist/core/conditional-formatting-overlay.js +24 -0
  17. package/dist/core/conditional-formatting.d.ts +70 -0
  18. package/dist/core/conditional-formatting.js +35 -0
  19. package/dist/core/containers.d.ts +2 -0
  20. package/dist/core/containers.js +25 -0
  21. package/dist/core/data-validation-overlay.d.ts +28 -0
  22. package/dist/core/data-validation-overlay.js +56 -0
  23. package/dist/core/data-validation.d.ts +38 -0
  24. package/dist/core/data-validation.js +13 -0
  25. package/dist/core/date.d.ts +25 -0
  26. package/dist/core/date.js +58 -0
  27. package/dist/core/formula.d.ts +49 -0
  28. package/dist/core/formula.js +319 -0
  29. package/dist/core/grid-edits.d.ts +22 -0
  30. package/dist/core/grid-edits.js +206 -0
  31. package/dist/core/image.d.ts +76 -0
  32. package/dist/core/image.js +65 -0
  33. package/dist/core/internal.d.ts +11 -0
  34. package/dist/core/internal.js +29 -0
  35. package/dist/core/merge.d.ts +13 -0
  36. package/dist/core/merge.js +26 -0
  37. package/dist/core/modern-functions.d.ts +1 -0
  38. package/dist/core/modern-functions.js +165 -0
  39. package/dist/core/page-setup.d.ts +94 -0
  40. package/dist/core/page-setup.js +6 -0
  41. package/dist/core/pivot-table.d.ts +113 -0
  42. package/dist/core/pivot-table.js +249 -0
  43. package/dist/core/preserved.d.ts +58 -0
  44. package/dist/core/preserved.js +5 -0
  45. package/dist/core/protection.d.ts +67 -0
  46. package/dist/core/protection.js +68 -0
  47. package/dist/core/range.d.ts +112 -0
  48. package/dist/core/range.js +283 -0
  49. package/dist/core/row.d.ts +73 -0
  50. package/dist/core/row.js +135 -0
  51. package/dist/core/style.d.ts +246 -0
  52. package/dist/core/style.js +125 -0
  53. package/dist/core/table-style.d.ts +67 -0
  54. package/dist/core/table-style.js +90 -0
  55. package/dist/core/table.d.ts +194 -0
  56. package/dist/core/table.js +291 -0
  57. package/dist/core/theme.d.ts +90 -0
  58. package/dist/core/theme.js +272 -0
  59. package/dist/core/value.d.ts +115 -0
  60. package/dist/core/value.js +144 -0
  61. package/dist/core/workbook-protection.d.ts +27 -0
  62. package/dist/core/workbook-protection.js +25 -0
  63. package/dist/core/workbook.d.ts +528 -0
  64. package/dist/core/workbook.js +732 -0
  65. package/dist/core/worksheet-model.d.ts +34 -0
  66. package/dist/core/worksheet-model.js +100 -0
  67. package/dist/core/worksheet.d.ts +647 -0
  68. package/dist/core/worksheet.js +1025 -0
  69. package/dist/customui/errors.d.ts +15 -0
  70. package/dist/customui/errors.js +15 -0
  71. package/dist/customui/index.d.ts +2 -0
  72. package/dist/customui/index.js +7 -0
  73. package/dist/customui/ribbon.d.ts +86 -0
  74. package/dist/customui/ribbon.js +180 -0
  75. package/dist/entries/core.d.ts +23 -0
  76. package/dist/entries/core.js +20 -0
  77. package/dist/entries/csv.d.ts +2 -0
  78. package/dist/entries/csv.js +6 -0
  79. package/dist/entries/customui.d.ts +1 -0
  80. package/dist/entries/customui.js +3 -0
  81. package/dist/entries/errors.d.ts +7 -0
  82. package/dist/entries/errors.js +20 -0
  83. package/dist/entries/vba.d.ts +2 -0
  84. package/dist/entries/vba.js +7 -0
  85. package/dist/entries/xlsb.d.ts +1 -0
  86. package/dist/entries/xlsb.js +7 -0
  87. package/dist/entries/xlsx.d.ts +5 -0
  88. package/dist/entries/xlsx.js +18 -0
  89. package/dist/errors.d.ts +65 -0
  90. package/dist/errors.js +66 -0
  91. package/dist/index.d.ts +7 -0
  92. package/dist/index.js +19 -0
  93. package/dist/io/csv/read.d.ts +15 -0
  94. package/dist/io/csv/read.js +113 -0
  95. package/dist/io/csv/write.d.ts +28 -0
  96. package/dist/io/csv/write.js +124 -0
  97. package/dist/io/opc/errors.d.ts +46 -0
  98. package/dist/io/opc/errors.js +49 -0
  99. package/dist/io/opc/inflate.d.ts +15 -0
  100. package/dist/io/opc/inflate.js +91 -0
  101. package/dist/io/opc/namespaces.d.ts +18 -0
  102. package/dist/io/opc/namespaces.js +18 -0
  103. package/dist/io/opc/part-paths.d.ts +4 -0
  104. package/dist/io/opc/part-paths.js +35 -0
  105. package/dist/io/opc/read-opc.d.ts +24 -0
  106. package/dist/io/opc/read-opc.js +153 -0
  107. package/dist/io/opc/read-options.d.ts +10 -0
  108. package/dist/io/opc/read-options.js +7 -0
  109. package/dist/io/opc/rels.d.ts +15 -0
  110. package/dist/io/opc/rels.js +28 -0
  111. package/dist/io/opc/sniff-format.d.ts +27 -0
  112. package/dist/io/opc/sniff-format.js +92 -0
  113. package/dist/io/style/xf-style.d.ts +47 -0
  114. package/dist/io/style/xf-style.js +95 -0
  115. package/dist/io/xlsb/errors.d.ts +15 -0
  116. package/dist/io/xlsb/errors.js +15 -0
  117. package/dist/io/xlsb/formula.d.ts +48 -0
  118. package/dist/io/xlsb/formula.js +398 -0
  119. package/dist/io/xlsb/primitives.d.ts +86 -0
  120. package/dist/io/xlsb/primitives.js +230 -0
  121. package/dist/io/xlsb/ptg-functions.d.ts +16 -0
  122. package/dist/io/xlsb/ptg-functions.js +659 -0
  123. package/dist/io/xlsb/read-shared-strings.d.ts +2 -0
  124. package/dist/io/xlsb/read-shared-strings.js +22 -0
  125. package/dist/io/xlsb/read-styles.d.ts +4 -0
  126. package/dist/io/xlsb/read-styles.js +386 -0
  127. package/dist/io/xlsb/read-worksheet.d.ts +9 -0
  128. package/dist/io/xlsb/read-worksheet.js +301 -0
  129. package/dist/io/xlsb/read.d.ts +20 -0
  130. package/dist/io/xlsb/read.js +199 -0
  131. package/dist/io/xlsb/record-stream.d.ts +14 -0
  132. package/dist/io/xlsb/record-stream.js +56 -0
  133. package/dist/io/xlsb/record-types.d.ts +55 -0
  134. package/dist/io/xlsb/record-types.js +73 -0
  135. package/dist/io/xlsx/cell-accumulator.d.ts +25 -0
  136. package/dist/io/xlsx/cell-accumulator.js +172 -0
  137. package/dist/io/xlsx/cell-value.d.ts +32 -0
  138. package/dist/io/xlsx/cell-value.js +85 -0
  139. package/dist/io/xlsx/comments.d.ts +74 -0
  140. package/dist/io/xlsx/comments.js +243 -0
  141. package/dist/io/xlsx/conditional-formatting.d.ts +32 -0
  142. package/dist/io/xlsx/conditional-formatting.js +464 -0
  143. package/dist/io/xlsx/data-validation.d.ts +20 -0
  144. package/dist/io/xlsx/data-validation.js +258 -0
  145. package/dist/io/xlsx/edit-vba.d.ts +23 -0
  146. package/dist/io/xlsx/edit-vba.js +139 -0
  147. package/dist/io/xlsx/errors.d.ts +17 -0
  148. package/dist/io/xlsx/errors.js +17 -0
  149. package/dist/io/xlsx/hyperlinks.d.ts +42 -0
  150. package/dist/io/xlsx/hyperlinks.js +139 -0
  151. package/dist/io/xlsx/images.d.ts +34 -0
  152. package/dist/io/xlsx/images.js +218 -0
  153. package/dist/io/xlsx/namespaces.d.ts +55 -0
  154. package/dist/io/xlsx/namespaces.js +55 -0
  155. package/dist/io/xlsx/package-plan.d.ts +103 -0
  156. package/dist/io/xlsx/package-plan.js +168 -0
  157. package/dist/io/xlsx/pivot-read.d.ts +5 -0
  158. package/dist/io/xlsx/pivot-read.js +134 -0
  159. package/dist/io/xlsx/pivot.d.ts +12 -0
  160. package/dist/io/xlsx/pivot.js +141 -0
  161. package/dist/io/xlsx/read-rows.d.ts +85 -0
  162. package/dist/io/xlsx/read-rows.js +286 -0
  163. package/dist/io/xlsx/read-styles.d.ts +8 -0
  164. package/dist/io/xlsx/read-styles.js +551 -0
  165. package/dist/io/xlsx/read-worksheet.d.ts +4 -0
  166. package/dist/io/xlsx/read-worksheet.js +528 -0
  167. package/dist/io/xlsx/read.d.ts +32 -0
  168. package/dist/io/xlsx/read.js +680 -0
  169. package/dist/io/xlsx/relationships.d.ts +33 -0
  170. package/dist/io/xlsx/relationships.js +47 -0
  171. package/dist/io/xlsx/rich-runs.d.ts +13 -0
  172. package/dist/io/xlsx/rich-runs.js +59 -0
  173. package/dist/io/xlsx/rich-text.d.ts +7 -0
  174. package/dist/io/xlsx/rich-text.js +23 -0
  175. package/dist/io/xlsx/shared-formulas.d.ts +6 -0
  176. package/dist/io/xlsx/shared-formulas.js +52 -0
  177. package/dist/io/xlsx/shared-strings-read.d.ts +2 -0
  178. package/dist/io/xlsx/shared-strings-read.js +70 -0
  179. package/dist/io/xlsx/shared-strings.d.ts +17 -0
  180. package/dist/io/xlsx/shared-strings.js +50 -0
  181. package/dist/io/xlsx/sheet-properties.d.ts +13 -0
  182. package/dist/io/xlsx/sheet-properties.js +211 -0
  183. package/dist/io/xlsx/styles.d.ts +138 -0
  184. package/dist/io/xlsx/styles.js +821 -0
  185. package/dist/io/xlsx/tables.d.ts +9 -0
  186. package/dist/io/xlsx/tables.js +208 -0
  187. package/dist/io/xlsx/threaded-comments.d.ts +102 -0
  188. package/dist/io/xlsx/threaded-comments.js +308 -0
  189. package/dist/io/xlsx/workbook-xml.d.ts +12 -0
  190. package/dist/io/xlsx/workbook-xml.js +353 -0
  191. package/dist/io/xlsx/worksheet-xml.d.ts +86 -0
  192. package/dist/io/xlsx/worksheet-xml.js +643 -0
  193. package/dist/io/xlsx/write-stream.d.ts +171 -0
  194. package/dist/io/xlsx/write-stream.js +399 -0
  195. package/dist/io/xlsx/write.d.ts +81 -0
  196. package/dist/io/xlsx/write.js +396 -0
  197. package/dist/io/xlsx/x14-ext.d.ts +9 -0
  198. package/dist/io/xlsx/x14-ext.js +12 -0
  199. package/dist/vba/cfb-writer.d.ts +19 -0
  200. package/dist/vba/cfb-writer.js +277 -0
  201. package/dist/vba/cfb.d.ts +16 -0
  202. package/dist/vba/cfb.js +324 -0
  203. package/dist/vba/codepage.d.ts +18 -0
  204. package/dist/vba/codepage.js +87 -0
  205. package/dist/vba/errors.d.ts +24 -0
  206. package/dist/vba/errors.js +24 -0
  207. package/dist/vba/index.d.ts +5 -0
  208. package/dist/vba/index.js +15 -0
  209. package/dist/vba/ms-ovba.d.ts +12 -0
  210. package/dist/vba/ms-ovba.js +176 -0
  211. package/dist/vba/project-editor.d.ts +62 -0
  212. package/dist/vba/project-editor.js +377 -0
  213. package/dist/vba/project.d.ts +45 -0
  214. package/dist/vba/project.js +160 -0
  215. package/dist/vba/vba-encoding.d.ts +14 -0
  216. package/dist/vba/vba-encoding.js +39 -0
  217. package/dist/xml/errors.d.ts +15 -0
  218. package/dist/xml/errors.js +15 -0
  219. package/dist/xml/xml-read.d.ts +107 -0
  220. package/dist/xml/xml-read.js +283 -0
  221. package/dist/xml/xml.d.ts +41 -0
  222. package/dist/xml/xml.js +86 -0
  223. package/package.json +117 -0
@@ -0,0 +1,377 @@
1
+ // Structural edits to an existing `vbaProject.bin` that do NOT touch any module's compiled p-code:
2
+ // remove a standard module, or add a registered library reference. Each is a surgical splice — parse the
3
+ // original container, rebuild its whole storage/stream tree, and change only the `dir` records (and, for
4
+ // a removal, the `PROJECT`/`PROJECTwm` text) the edit demands. Every module stream, `_VBA_PROJECT`, and
5
+ // every untouched record rides through byte-for-byte; preservation is by *not touching* them.
6
+ //
7
+ // Authoring or editing module SOURCE is deliberately NOT here. Excel does not recompile from source on
8
+ // open — a module runs the p-code it ships, and only a real Excel can produce genuinely source-matched
9
+ // p-code — so source authoring/editing lives in the offline `tools/vba-compiler` (VBIDE), not in this
10
+ // pure-TS path (ADR 0019). These splices are safe precisely because they leave every module's p-code
11
+ // exactly as its own compiler wrote it.
12
+ import { CompoundFile } from "./cfb.js";
13
+ import { writeCompoundFile } from "./cfb-writer.js";
14
+ import { decoderForCodePage, encoderForCodePage } from "./codepage.js";
15
+ import { VbaAuthorError, VbaParseError } from "./errors.js";
16
+ import { compressContainer, decompressContainer } from "./ms-ovba.js";
17
+ import { parseVbaProject } from "./project.js";
18
+ import { push, u16, u32, utf16le, validateVbaName } from "./vba-encoding.js";
19
+ const DIR_STREAM = 'dir';
20
+ const PROJECT_STREAM = 'PROJECT';
21
+ const PROJECTWM_STREAM = 'PROJECTwm';
22
+ const VBA_STORAGE = 'VBA';
23
+ // `dir`-record ids the remove-module splice reads to delimit a module's record block ([MS-OVBA] 2.3.4.2);
24
+ // every other record is preserved verbatim. MODULES_COUNT is decremented as a block is removed.
25
+ const REC_MODULE_NAME = 0x0019;
26
+ const REC_MODULE_STREAMNAME = 0x001a;
27
+ const REC_MODULE_TERMINATOR = 0x002b;
28
+ const REC_PROJECT_VERSION = 0x0009; // its uncounted 2-byte VersionMinor trails the counted payload
29
+ const REC_MODULES_COUNT = 0x000f;
30
+ // `dir`-record ids the add-reference splice builds ([MS-OVBA] 2.3.4.2.2). REFERENCENAME's Unicode half
31
+ // is a *literal* 0x003E marker, not a nested record id — but it is laid out as its own Id+Size+data TLV,
32
+ // so a generic walk (and this splice) sees REFERENCENAME as two chained records, exactly like
33
+ // MODULE_NAME/MODULE_NAME_UNICODE. Verified against a real Excel-authored dir stream (2026-07-23).
34
+ const REC_REFERENCE_NAME = 0x0016;
35
+ const REC_REFERENCE_NAME_UNICODE = 0x003e;
36
+ const REC_REFERENCE_REGISTERED = 0x000d;
37
+ /**
38
+ * Remove a standard module from an existing `vbaProject.bin`, returning new bytes that carry every
39
+ * remaining module, reference, and host-info record unchanged. It drops the module's `VBA/<name>`
40
+ * stream, its MODULE record block in `dir` (decrementing `MODULES_COUNT`), and its `Module=`/`Class=` +
41
+ * workspace lines in `PROJECT`/`PROJECTwm`.
42
+ *
43
+ * Only `procedural` and `class` modules can be removed this way — removing a `document` module (e.g.
44
+ * `ThisWorkbook`) or a `designer` module (a UserForm) would leave the host referencing code that no
45
+ * longer exists, since their names are tied to a worksheet/workbook `codeName` or a designer storage
46
+ * this project-level primitive has no visibility into. Editing such a module's code-behind is a job for
47
+ * the offline `tools/vba-compiler` (in-place mode), which drives the real host.
48
+ *
49
+ * @throws {VbaParseError} if `bin` is not a parseable VBA project (validated before any edit).
50
+ * @throws {VbaAuthorError} if `name` is not in the project, or names a `document`/`designer` module.
51
+ */
52
+ export function removeVbaModule(bin, name) {
53
+ // Parse fail-closed first: validates the container and resolves the module's kind/stream name, so
54
+ // nothing is mutated on a bad input or an unsupported module kind.
55
+ const project = parseVbaProject(bin);
56
+ const nameKey = name.toUpperCase(); // VBA names are case-insensitive
57
+ const module = project.modules.find((m) => m.name.toUpperCase() === nameKey);
58
+ if (!module)
59
+ throw new VbaAuthorError(`module '${name}' is not in the VBA project`);
60
+ if (module.kind !== 'procedural' && module.kind !== 'class') {
61
+ throw new VbaAuthorError(`cannot remove module '${name}': its kind '${module.kind}' is tied to host linkage this ` +
62
+ 'primitive cannot verify');
63
+ }
64
+ const cfb = new CompoundFile(bin);
65
+ const dirCompressed = cfb.readStream(DIR_STREAM);
66
+ if (!dirCompressed)
67
+ throw new VbaParseError("VBA project has no 'dir' stream");
68
+ const patchedDir = removeModuleDirRecord(decompressContainer(dirCompressed), module.streamName, project.codePage);
69
+ // Leave _VBA_PROJECT untouched. Resetting it to an "unmatchable version" cookie does NOT force Excel
70
+ // to recompile from source (Excel runs the p-code as-is); on a project that carries real p-code the
71
+ // reset actively crashes the VBA load (verified 2026-07-24, ADR 0019). The surviving modules keep
72
+ // their own compiled p-code; the `dir` stream — authoritative for the module list — no longer names
73
+ // the removed module, which is what makes the removal take.
74
+ const replacements = new Map([[DIR_STREAM, compressContainer(patchedDir)]]);
75
+ const decoder = decoderForCodePage(project.codePage);
76
+ const encode = encoderForCodePage(project.codePage);
77
+ const projectText = cfb.readStream(PROJECT_STREAM);
78
+ if (projectText) {
79
+ replacements.set(PROJECT_STREAM, encode(removeProjectStreamLines(decoder.decode(projectText), module.name, module.kind)));
80
+ }
81
+ const projectwm = cfb.readStream(PROJECTWM_STREAM);
82
+ if (projectwm) {
83
+ replacements.set(PROJECTWM_STREAM, removeProjectwmRecord(projectwm, project.modules.length, module.name, decoder));
84
+ }
85
+ const applied = new Set();
86
+ const withReplacements = replaceStreams(cfb.tree(), replacements, applied);
87
+ if (!applied.has(DIR_STREAM))
88
+ throw new VbaParseError("VBA project 'dir' stream is not in the container tree");
89
+ const removed = new Set();
90
+ const newTree = removeFromStorage(withReplacements, VBA_STORAGE, module.streamName, removed);
91
+ if (!removed.has(VBA_STORAGE)) {
92
+ throw new VbaParseError(`module stream '${module.streamName}' is not in the '${VBA_STORAGE}' storage`);
93
+ }
94
+ return writeCompoundFile(newTree);
95
+ }
96
+ // LibidMajorVersion/LibidMinorVersion ([MS-OVBA] 2.1.1.8): 1*4HEXDIG, so at most 0xFFFF.
97
+ const MAX_LIBID_VERSION = 0xffff;
98
+ // LibidLcid: 1*8HEXDIG, so at most 0xFFFFFFFF (practically always 0 — locale-neutral).
99
+ const MAX_LIBID_LCID = 0xffffffff;
100
+ // LibidRegName: *255(%x01-FF) — at most 255 bytes, never NUL.
101
+ const MAX_DISPLAY_NAME_CHARS = 255;
102
+ const GUID_PATTERN = /^\{?([0-9A-Fa-f]{8})-([0-9A-Fa-f]{4})-([0-9A-Fa-f]{4})-([0-9A-Fa-f]{4})-([0-9A-Fa-f]{12})\}?$/;
103
+ // Validate every field fail-closed and assemble the Libid string ([MS-OVBA] 2.1.1.8 LibidReference ABNF:
104
+ // `*\G{GUID}#Major.Minor#LCID#Path#RegName`, hex digit strings with no `0x` prefix) — confirmed
105
+ // byte-for-byte against a real Excel-authored reference (2026-07-23):
106
+ // `*\G{420B2830-E718-11CF-893D-00A0C9054228}#1.0#0#C:\Windows\System32\scrrun.dll#Microsoft Scripting Runtime`.
107
+ function normalizeReference(ref) {
108
+ validateVbaName(ref.name, 'reference');
109
+ const guidMatch = GUID_PATTERN.exec(ref.guid.trim());
110
+ if (!guidMatch)
111
+ throw new VbaAuthorError(`invalid reference GUID '${ref.guid}'`);
112
+ const guid = `{${guidMatch.slice(1, 6).join('-').toUpperCase()}}`;
113
+ for (const [field, value] of [
114
+ ['majorVersion', ref.majorVersion],
115
+ ['minorVersion', ref.minorVersion],
116
+ ]) {
117
+ if (!Number.isInteger(value) || value < 0 || value > MAX_LIBID_VERSION) {
118
+ throw new VbaAuthorError(`reference ${field} must be an integer in [0, 0xFFFF], got ${value}`);
119
+ }
120
+ }
121
+ const lcid = ref.lcid ?? 0;
122
+ if (!Number.isInteger(lcid) || lcid < 0 || lcid > MAX_LIBID_LCID) {
123
+ throw new VbaAuthorError(`reference lcid must be an integer in [0, 0xFFFFFFFF], got ${lcid}`);
124
+ }
125
+ if (ref.path.length === 0 || ref.path.includes('\0') || ref.path.includes('#')) {
126
+ throw new VbaAuthorError(`invalid reference path '${ref.path}' (must be non-empty and contain no NUL or '#')`);
127
+ }
128
+ const displayName = ref.displayName ?? ref.name;
129
+ if (displayName.length === 0 ||
130
+ displayName.length > MAX_DISPLAY_NAME_CHARS ||
131
+ displayName.includes('\0')) {
132
+ throw new VbaAuthorError(`invalid reference display name '${displayName}'`);
133
+ }
134
+ const libid = `*\\G${guid}#${ref.majorVersion.toString(16).toUpperCase()}.` +
135
+ `${ref.minorVersion.toString(16).toUpperCase()}#${lcid.toString(16).toUpperCase()}` +
136
+ `#${ref.path}#${displayName}`;
137
+ return { name: ref.name, libid };
138
+ }
139
+ /**
140
+ * Add a registered (COM type-library) reference to an existing `vbaProject.bin`, returning new bytes
141
+ * that carry every existing module, reference, and host-info record unchanged. It grows the project's
142
+ * `dir` stream by one `REFERENCENAME` + `REFERENCEREGISTERED` record pair, positioned immediately before
143
+ * `MODULES_COUNT` (references have no count field of their own — `MODULES_COUNT` simply marks where the
144
+ * reference array ends). It needs no change to `PROJECT`/`PROJECTwm`: a real Excel-authored `PROJECT`
145
+ * stream carries no `Reference=` line at all — references live only in `dir` (confirmed against a genuine
146
+ * Excel-authored project).
147
+ *
148
+ * @throws {VbaParseError} if `bin` is not a parseable VBA project (validated before any edit).
149
+ * @throws {VbaAuthorError} if any field of `ref` is invalid (see {@link VbaLibraryReference}), or the
150
+ * assembled reference text has a character the project's code page cannot represent.
151
+ */
152
+ export function addVbaReference(bin, ref) {
153
+ const normalized = normalizeReference(ref);
154
+ // Parse fail-closed first: validates the container before any mutation.
155
+ const project = parseVbaProject(bin);
156
+ const encode = encoderForCodePage(project.codePage);
157
+ const cfb = new CompoundFile(bin);
158
+ const dirCompressed = cfb.readStream(DIR_STREAM);
159
+ if (!dirCompressed)
160
+ throw new VbaParseError("VBA project has no 'dir' stream");
161
+ const records = buildReferenceDirRecords(normalized, encode);
162
+ const patchedDir = insertReferenceDirRecords(decompressContainer(dirCompressed), records);
163
+ // Leave _VBA_PROJECT untouched — see the note in removeVbaModule. The new reference is unused by the
164
+ // existing modules' p-code, so they load and run unchanged; only the `dir` reference array grows.
165
+ const replacements = new Map([[DIR_STREAM, compressContainer(patchedDir)]]);
166
+ const applied = new Set();
167
+ const newTree = replaceStreams(cfb.tree(), replacements, applied);
168
+ if (!applied.has(DIR_STREAM))
169
+ throw new VbaParseError("VBA project 'dir' stream is not in the container tree");
170
+ return writeCompoundFile(newTree);
171
+ }
172
+ // Build the REFERENCENAME + REFERENCEREGISTERED record bytes ([MS-OVBA] 2.3.4.2.2.2 / .2.2.5) for one
173
+ // reference. REFERENCENAME's MBCS/Unicode name pair mirrors MODULE_NAME/MODULE_NAME_UNICODE's shape;
174
+ // REFERENCEREGISTERED is one record carrying SizeOfLibid + Libid + two zero Reserved fields.
175
+ function buildReferenceDirRecords(ref, encode) {
176
+ const r = [];
177
+ const nameBytes = [...encode(ref.name)];
178
+ push(r, REC_REFERENCE_NAME, nameBytes);
179
+ push(r, REC_REFERENCE_NAME_UNICODE, utf16le(ref.name));
180
+ const libidBytes = [...encode(ref.libid)];
181
+ push(r, REC_REFERENCE_REGISTERED, [
182
+ ...u32(libidBytes.length),
183
+ ...libidBytes,
184
+ ...u32(0),
185
+ ...u16(0),
186
+ ]);
187
+ return r;
188
+ }
189
+ // Insert new reference dir records right before MODULES_COUNT (0x000f) — the reference array has no
190
+ // explicit count field; MODULES_COUNT is simply the next record once the last reference ends (confirmed
191
+ // against a real Excel-authored dir stream). Every other record — other references, all modules — rides
192
+ // through unchanged.
193
+ function insertReferenceDirRecords(dir, records) {
194
+ let insertAt = -1;
195
+ let pos = 0;
196
+ while (pos + 6 <= dir.length) {
197
+ const recordStart = pos;
198
+ const id = readU16(dir, pos);
199
+ const size = readU32(dir, pos + 2);
200
+ const dataStart = pos + 6;
201
+ if (dataStart + size > dir.length) {
202
+ throw new VbaParseError(`dir record 0x${id.toString(16)} overruns while adding a reference`);
203
+ }
204
+ pos = dataStart + size;
205
+ if (id === REC_PROJECT_VERSION)
206
+ pos += 2; // uncounted VersionMinor (u16)
207
+ if (id === REC_MODULES_COUNT) {
208
+ insertAt = recordStart;
209
+ break;
210
+ }
211
+ }
212
+ if (insertAt < 0)
213
+ throw new VbaParseError('dir stream is missing MODULES_COUNT');
214
+ const rec = Uint8Array.from(records);
215
+ const out = new Uint8Array(dir.length + rec.length);
216
+ out.set(dir.subarray(0, insertAt), 0);
217
+ out.set(rec, insertAt);
218
+ out.set(dir.subarray(insertAt), insertAt + rec.length);
219
+ return out;
220
+ }
221
+ // Remove one module's MODULE record block from a decompressed `dir` stream, and decrement MODULES_COUNT.
222
+ // A block runs from its MODULE_NAME record (which always opens the block — mirrors buildModuleDirRecord's
223
+ // emission order) through its own MODULE_TERMINATOR, identified by matching MODULE_STREAMNAME against
224
+ // `streamName`. Every other record — PROJECTREFERENCES, other modules, project-level fields — is carried
225
+ // through untouched.
226
+ function removeModuleDirRecord(dir, streamName, codePage) {
227
+ const decoder = decoderForCodePage(codePage);
228
+ let countAt = -1;
229
+ let blockStart = -1;
230
+ let removeStart = -1;
231
+ let removeEnd = -1;
232
+ let currentStream;
233
+ let pos = 0;
234
+ while (pos + 6 <= dir.length) {
235
+ const recordStart = pos;
236
+ const id = readU16(dir, pos);
237
+ const size = readU32(dir, pos + 2);
238
+ const dataStart = pos + 6;
239
+ if (dataStart + size > dir.length) {
240
+ throw new VbaParseError(`dir record 0x${id.toString(16)} overruns while removing a module`);
241
+ }
242
+ pos = dataStart + size;
243
+ if (id === REC_PROJECT_VERSION)
244
+ pos += 2; // uncounted VersionMinor (u16)
245
+ if (id === REC_MODULES_COUNT) {
246
+ if (size < 2)
247
+ throw new VbaParseError('PROJECTMODULES MODULES_COUNT record is malformed');
248
+ countAt = dataStart;
249
+ }
250
+ else if (id === REC_MODULE_NAME) {
251
+ blockStart = recordStart;
252
+ }
253
+ else if (id === REC_MODULE_STREAMNAME) {
254
+ currentStream = decoder.decode(dir.subarray(dataStart, dataStart + size));
255
+ }
256
+ else if (id === REC_MODULE_TERMINATOR) {
257
+ if (currentStream === streamName) {
258
+ removeStart = blockStart;
259
+ removeEnd = pos;
260
+ }
261
+ currentStream = undefined;
262
+ blockStart = -1;
263
+ }
264
+ }
265
+ if (countAt < 0)
266
+ throw new VbaParseError('dir stream is missing MODULES_COUNT');
267
+ if (removeStart < 0 || removeEnd < 0) {
268
+ throw new VbaParseError(`module stream '${streamName}' not found in the dir stream`);
269
+ }
270
+ // MODULES_COUNT always precedes every module block, so countAt is unaffected by removing bytes after it.
271
+ const out = new Uint8Array(dir.length - (removeEnd - removeStart));
272
+ out.set(dir.subarray(0, removeStart), 0);
273
+ out.set(dir.subarray(removeEnd), removeStart);
274
+ const newCount = readU16(out, countAt) - 1;
275
+ out[countAt] = newCount & 0xff;
276
+ out[countAt + 1] = (newCount >> 8) & 0xff;
277
+ return out;
278
+ }
279
+ // Remove a module's declaration line (`Module=`/`Class=`) and its workspace line from the `PROJECT` text
280
+ // stream — the inverse of insertProjectStreamLines. Every other line is left exactly as it was.
281
+ function removeProjectStreamLines(text, name, kind) {
282
+ const eol = text.includes('\r\n') ? '\r\n' : '\n';
283
+ const lines = text.split(/\r\n|\r|\n/);
284
+ const declLine = `${kind === 'procedural' ? 'Module' : 'Class'}=${name}`;
285
+ const declIndex = lines.indexOf(declLine);
286
+ if (declIndex >= 0)
287
+ lines.splice(declIndex, 1);
288
+ const wsIndex = lines.findIndex((l) => l.trim() === '[Workspace]');
289
+ if (wsIndex >= 0) {
290
+ for (let i = wsIndex + 1; i < lines.length; i++) {
291
+ const l = lines[i];
292
+ if (l.trim() === '' || l.startsWith('['))
293
+ break;
294
+ if (l.startsWith(`${name}=`)) {
295
+ lines.splice(i, 1);
296
+ break;
297
+ }
298
+ }
299
+ }
300
+ return lines.join(eol);
301
+ }
302
+ // Remove a module's (MBCS name, UTF-16 name) pair from the binary PROJECTwm stream — the inverse of
303
+ // insertProjectwmRecord. `existingModuleCount` (from the already fail-closed-parsed project, before
304
+ // removal) bounds the walk to the module records, so it never mistakes the terminator for a record.
305
+ function removeProjectwmRecord(wm, existingModuleCount, name, decoder) {
306
+ let pos = 0;
307
+ let removeStart = -1;
308
+ let removeEnd = -1;
309
+ for (let i = 0; i < existingModuleCount; i++) {
310
+ const recordStart = pos;
311
+ const mbcsEnd = wm.indexOf(0x00, pos);
312
+ if (mbcsEnd < 0)
313
+ throw new VbaParseError('PROJECTwm record is missing its MBCS name terminator');
314
+ const mbcsName = decoder.decode(wm.subarray(pos, mbcsEnd));
315
+ pos = mbcsEnd + 1;
316
+ let utf16End = pos;
317
+ while (utf16End + 1 < wm.length && (wm[utf16End] !== 0 || wm[utf16End + 1] !== 0))
318
+ utf16End += 2;
319
+ if (utf16End + 1 >= wm.length) {
320
+ throw new VbaParseError('PROJECTwm record is missing its Unicode name terminator');
321
+ }
322
+ pos = utf16End + 2;
323
+ if (mbcsName === name) {
324
+ removeStart = recordStart;
325
+ removeEnd = pos;
326
+ }
327
+ }
328
+ if (removeStart < 0 || removeEnd < 0) {
329
+ throw new VbaParseError(`module '${name}' not found in the PROJECTwm stream`);
330
+ }
331
+ const out = new Uint8Array(wm.length - (removeEnd - removeStart));
332
+ out.set(wm.subarray(0, removeStart), 0);
333
+ out.set(wm.subarray(removeEnd), removeStart);
334
+ return out;
335
+ }
336
+ // Remove the first direct child stream named `streamName` from the first storage named `storageName`
337
+ // found in the tree (depth-first), marking `storageName` in `removed` once done. The inverse of
338
+ // insertIntoStorage.
339
+ function removeFromStorage(nodes, storageName, streamName, removed) {
340
+ return nodes.map((n) => {
341
+ if ('data' in n)
342
+ return n;
343
+ const children = removeFromStorage(n.children, storageName, streamName, removed);
344
+ if (n.name === storageName && !removed.has(storageName)) {
345
+ const filtered = children.filter((c) => !('data' in c && c.name === streamName));
346
+ if (filtered.length !== children.length)
347
+ removed.add(storageName);
348
+ return { name: n.name, children: filtered };
349
+ }
350
+ return { name: n.name, children };
351
+ });
352
+ }
353
+ // Rebuild the node tree, swapping any stream whose name has a replacement. Non-stream nodes (storages)
354
+ // recurse; everything without a replacement is carried through byte-for-byte.
355
+ function replaceStreams(nodes, replacements, applied) {
356
+ return nodes.map((node) => {
357
+ if ('data' in node) {
358
+ const data = replacements.get(node.name);
359
+ if (data !== undefined) {
360
+ applied.add(node.name);
361
+ return { name: node.name, data };
362
+ }
363
+ return node;
364
+ }
365
+ return { name: node.name, children: replaceStreams(node.children, replacements, applied) };
366
+ });
367
+ }
368
+ function readU16(buf, at) {
369
+ return buf[at] | (buf[at + 1] << 8);
370
+ }
371
+ function readU32(buf, at) {
372
+ return ((buf[at] |
373
+ (buf[at + 1] << 8) |
374
+ (buf[at + 2] << 16) |
375
+ (buf[at + 3] << 24)) >>>
376
+ 0);
377
+ }
@@ -0,0 +1,45 @@
1
+ /** How a module participates in the project — the classification the VBA editor shows. */
2
+ export type VbaModuleKind = 'procedural' | 'document' | 'class' | 'designer';
3
+ export interface VbaModule {
4
+ /** The module's code name as seen in the VBA editor, e.g. `ThisWorkbook`, `JsonConverter`. */
5
+ readonly name: string;
6
+ /** The CFB stream the module's bytes live in — usually equal to {@link name}. */
7
+ readonly streamName: string;
8
+ /** Procedural (`.bas`), document code-behind, class module, or designer (UserForm). */
9
+ readonly kind: VbaModuleKind;
10
+ /** The decompressed VBA source (p-code and PerformanceCache are not included). */
11
+ readonly source: string;
12
+ }
13
+ export interface VbaProject {
14
+ /** The project code page (`PROJECTCODEPAGE`) used to decode module names and source. */
15
+ readonly codePage: number;
16
+ /** The project's modules, in declaration order. */
17
+ readonly modules: readonly VbaModule[];
18
+ }
19
+ export declare const VBA_PROJECT_REL_TYPE = "http://schemas.microsoft.com/office/2006/relationships/vbaProject";
20
+ export declare const VBA_PROJECT_CONTENT_TYPE = "application/vnd.ms-office.vbaProject";
21
+ export declare const VBA_PROJECT_PART_PATH = "xl/vbaProject.bin";
22
+ /**
23
+ * Which generation of VBA project signature a part is — Office emits up to three sibling signature
24
+ * parts off `vbaProject.bin`'s own rels over the same project bytes ([MS-OFFMACRO2]): the original
25
+ * `legacy` signature, the `agile` (V2) successor, and the `v3` scheme that closes a tampering hole
26
+ * the earlier two left open (KB5000676). All three can coexist in one package.
27
+ */
28
+ export type VbaProjectSignatureKind = 'legacy' | 'agile' | 'v3';
29
+ /** One digital signature over a workbook's VBA project — its generation and its raw signature bytes. */
30
+ export interface VbaProjectSignature {
31
+ readonly kind: VbaProjectSignatureKind;
32
+ /**
33
+ * The raw signature part bytes (a PKCS#7/CMS blob), passed through verbatim — this library does not
34
+ * parse or cryptographically verify them. Their presence means "a signature is attached," never
35
+ * "this signature is valid."
36
+ */
37
+ readonly bytes: Uint8Array;
38
+ }
39
+ /**
40
+ * The VBA-signature generation a relationship Type names, or `undefined` if it is not a signature
41
+ * relationship. Keys off the Type's final path segment, so it is independent of the year the URI
42
+ * carries (`.../office/2006/...` vs `.../2014/...` vs `.../2020/...`).
43
+ */
44
+ export declare function vbaProjectSignatureKind(relType: string): VbaProjectSignatureKind | undefined;
45
+ export declare function parseVbaProject(bin: Uint8Array): VbaProject;
@@ -0,0 +1,160 @@
1
+ // Turn a raw `vbaProject.bin` into readable module source.
2
+ //
3
+ // Pipeline ([MS-OVBA] 2.3.4.2): the CFB container holds a `VBA/dir` stream (itself MS-OVBA compressed)
4
+ // that lists each module's stream name, its code page, and the byte offset in that module's stream
5
+ // where the compressed source text begins — past the p-code / PerformanceCache. We decompress `dir`,
6
+ // read those facts, then decompress each module stream from its text offset and decode with the
7
+ // project code page. The p-code is version-specific and deliberately not exposed; a reader wants source.
8
+ import { CompoundFile } from "./cfb.js";
9
+ import { decoderForCodePage } from "./codepage.js";
10
+ import { VbaParseError } from "./errors.js";
11
+ import { decompressContainer } from "./ms-ovba.js";
12
+ // The OPC packaging identity of a workbook's VBA project — the relationship Type URI Excel wires it
13
+ // under, the content type its `.bin` part declares, and the canonical package path. These are the
14
+ // facts an authoring path needs to attach a macro blob so the writer emits a valid macro-enabled
15
+ // package; the reader matches the same relationship type by suffix (`isPreservedWorkbookRelType`).
16
+ export const VBA_PROJECT_REL_TYPE = 'http://schemas.microsoft.com/office/2006/relationships/vbaProject';
17
+ export const VBA_PROJECT_CONTENT_TYPE = 'application/vnd.ms-office.vbaProject';
18
+ export const VBA_PROJECT_PART_PATH = 'xl/vbaProject.bin';
19
+ // A signature part is matched by the FINAL segment of its relationship Type, not the whole URI: the
20
+ // segment (`vbaProjectSignature{,Agile,V3}`) names the generation, while the year the URI carries
21
+ // (2006 / 2014 / 2020) is incidental and varies. The closure walk already carries any such part
22
+ // through verbatim regardless of type, so recognising the generation token is all detection needs —
23
+ // no exact URI to pin, and a future scheme this map does not know stays preserved but unreported.
24
+ const SIGNATURE_KIND_BY_REL_SEGMENT = {
25
+ vbaProjectSignature: 'legacy',
26
+ vbaProjectSignatureAgile: 'agile',
27
+ vbaProjectSignatureV3: 'v3',
28
+ };
29
+ /**
30
+ * The VBA-signature generation a relationship Type names, or `undefined` if it is not a signature
31
+ * relationship. Keys off the Type's final path segment, so it is independent of the year the URI
32
+ * carries (`.../office/2006/...` vs `.../2014/...` vs `.../2020/...`).
33
+ */
34
+ export function vbaProjectSignatureKind(relType) {
35
+ return SIGNATURE_KIND_BY_REL_SEGMENT[relType.slice(relType.lastIndexOf('/') + 1)];
36
+ }
37
+ // `dir`-stream record ids we consume ([MS-OVBA] 2.3.4.2). Every other record is skipped by the uniform
38
+ // TLV walk; its Size field already accounts for its payload, so skipping is just advancing past it.
39
+ const REC_PROJECT_CODEPAGE = 0x0003;
40
+ const REC_MODULE_NAME = 0x0019;
41
+ const REC_MODULE_STREAMNAME = 0x001a;
42
+ const REC_MODULE_TYPE_PROCEDURAL = 0x0021;
43
+ const REC_MODULE_TYPE_DOCUMENT = 0x0022;
44
+ const REC_MODULE_OFFSET = 0x0031;
45
+ const REC_MODULE_TERMINATOR = 0x002b;
46
+ // PROJECTVERSION carries VersionMajor (u32) + VersionMinor (u16) after its fixed Size=4 field, but Size
47
+ // only accounts for the 4-byte major. The extra 2-byte minor is uncounted, so a uniform TLV walk
48
+ // misaligns here by 2 bytes unless it is skipped explicitly.
49
+ const REC_PROJECT_VERSION = 0x0009;
50
+ export function parseVbaProject(bin) {
51
+ const cfb = new CompoundFile(bin);
52
+ const dirCompressed = cfb.readStream('dir');
53
+ if (!dirCompressed)
54
+ throw new VbaParseError("VBA project has no 'dir' stream");
55
+ const dir = decompressContainer(dirCompressed);
56
+ let codePage = 1252; // Western-European default until PROJECTCODEPAGE says otherwise.
57
+ const rawModules = [];
58
+ let pending = {};
59
+ // The dir stream is a flat sequence of TLV records: Id(u16) Size(u32) data[Size].
60
+ let pos = 0;
61
+ while (pos + 6 <= dir.length) {
62
+ const id = readU16(dir, pos);
63
+ const size = readU32(dir, pos + 2);
64
+ const dataStart = pos + 6;
65
+ if (dataStart + size > dir.length)
66
+ throw new VbaParseError(`dir record 0x${id.toString(16)} overruns stream`);
67
+ pos = dataStart + size;
68
+ if (id === REC_PROJECT_VERSION)
69
+ pos += 2; // uncounted VersionMinor (u16)
70
+ switch (id) {
71
+ case REC_PROJECT_CODEPAGE:
72
+ if (size >= 2)
73
+ codePage = readU16(dir, dataStart);
74
+ break;
75
+ case REC_MODULE_NAME:
76
+ pending.name = dir.subarray(dataStart, dataStart + size);
77
+ break;
78
+ case REC_MODULE_STREAMNAME:
79
+ pending.streamName = dir.subarray(dataStart, dataStart + size);
80
+ break;
81
+ case REC_MODULE_TYPE_PROCEDURAL:
82
+ pending.documentType = false;
83
+ break;
84
+ case REC_MODULE_TYPE_DOCUMENT:
85
+ pending.documentType = true;
86
+ break;
87
+ case REC_MODULE_OFFSET:
88
+ if (size >= 4)
89
+ pending.offset = readU32(dir, dataStart);
90
+ break;
91
+ case REC_MODULE_TERMINATOR:
92
+ if (pending.streamName !== undefined && pending.offset !== undefined)
93
+ rawModules.push(pending);
94
+ pending = {};
95
+ break;
96
+ default:
97
+ break;
98
+ }
99
+ }
100
+ const decoder = decoderForCodePage(codePage);
101
+ const kindByName = readProjectStreamKinds(cfb, decoder);
102
+ const modules = rawModules.map((m) => {
103
+ const streamName = decoder.decode(m.streamName);
104
+ const name = m.name !== undefined ? decoder.decode(m.name) : streamName;
105
+ const kind = kindByName.get(name) ?? (m.documentType ? 'document' : 'procedural');
106
+ return {
107
+ name,
108
+ streamName,
109
+ kind,
110
+ source: readModuleSource(cfb, streamName, m.offset, decoder),
111
+ };
112
+ });
113
+ return { codePage, modules };
114
+ }
115
+ function readModuleSource(cfb, streamName, textOffset, decoder) {
116
+ const stream = cfb.readStream(streamName);
117
+ if (!stream)
118
+ throw new VbaParseError(`module stream '${streamName}' not found in container`);
119
+ return decoder.decode(decompressContainer(stream, textOffset));
120
+ }
121
+ // The `PROJECT` stream (uncounted, plain MBCS text) declares each module's exact kind by keyword:
122
+ // `Document=`, `Module=` (procedural .bas), `Class=`, `BaseClass=` (designer/UserForm). The `dir`
123
+ // stream's MODULETYPE only distinguishes procedural from non-procedural, so PROJECT refines it. Absent
124
+ // or unparsable PROJECT just falls back to that coarser MODULETYPE classification.
125
+ function readProjectStreamKinds(cfb, decoder) {
126
+ const kinds = new Map();
127
+ const stream = cfb.readStream('PROJECT');
128
+ if (!stream)
129
+ return kinds;
130
+ const text = decoder.decode(stream);
131
+ const keyword = {
132
+ Document: 'document',
133
+ Module: 'procedural',
134
+ Class: 'class',
135
+ BaseClass: 'designer',
136
+ };
137
+ for (const line of text.split(/\r\n|\r|\n/)) {
138
+ const eq = line.indexOf('=');
139
+ if (eq < 0)
140
+ continue;
141
+ const kind = keyword[line.slice(0, eq)];
142
+ if (kind === undefined)
143
+ continue;
144
+ // Value is `Name` or `Name/&H00000000` (document modules carry a cookie); take the name.
145
+ const name = (line.slice(eq + 1).split('/')[0] ?? '').trim();
146
+ if (name)
147
+ kinds.set(name, kind);
148
+ }
149
+ return kinds;
150
+ }
151
+ function readU16(buf, at) {
152
+ return buf[at] | (buf[at + 1] << 8);
153
+ }
154
+ function readU32(buf, at) {
155
+ return ((buf[at] |
156
+ (buf[at + 1] << 8) |
157
+ (buf[at + 2] << 16) |
158
+ (buf[at + 3] << 24)) >>>
159
+ 0);
160
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Validate a module, project, or reference name against the shared VBA identifier contract: a valid
3
+ * identifier, at most 31 characters (the CFB stream-name limit, which doubles as VBA's own module-name
4
+ * limit). Used by {@link project-editor.ts | project-editor}'s structural edits.
5
+ *
6
+ * @throws {VbaAuthorError} if `name` is not a valid VBA identifier or exceeds 31 characters.
7
+ */
8
+ export declare function validateVbaName(name: string, what: 'project' | 'module' | 'reference'): void;
9
+ /** Append one `dir`-stream TLV record (Id, Size, data) to `out`. */
10
+ export declare function push(out: number[], id: number, data: number[]): void;
11
+ export declare function u16(n: number): number[];
12
+ export declare function u32(n: number): number[];
13
+ /** A name as NUL-free UTF-16LE code units — the encoding [MS-OVBA] uses for every "Unicode" name field. */
14
+ export declare function utf16le(s: string): number[];
@@ -0,0 +1,39 @@
1
+ // Shared low-level encoders for the VBA `dir` stream and name validation — the primitives the structural
2
+ // splices in `project-editor.ts` build on. ([MS-OVBA] 2.3.4.2 record TLVs, and the VBA identifier rules.)
3
+ //
4
+ // There is no from-scratch `vbaProject.bin` synthesizer here. Excel does not recompile VBA from source
5
+ // on open — a module runs the compiled p-code it ships — so authoring/editing module SOURCE is done by
6
+ // the offline `tools/vba-compiler` (VBIDE), which produces genuinely compiled p-code. This module holds
7
+ // only what the pure-TS structural edits (remove module, add reference) still need (ADR 0019).
8
+ import { VbaAuthorError } from "./errors.js";
9
+ const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_]*$/;
10
+ const MAX_NAME_CHARS = 31; // the CFB stream-name limit, and VBA's own module-name limit
11
+ /**
12
+ * Validate a module, project, or reference name against the shared VBA identifier contract: a valid
13
+ * identifier, at most 31 characters (the CFB stream-name limit, which doubles as VBA's own module-name
14
+ * limit). Used by {@link project-editor.ts | project-editor}'s structural edits.
15
+ *
16
+ * @throws {VbaAuthorError} if `name` is not a valid VBA identifier or exceeds 31 characters.
17
+ */
18
+ export function validateVbaName(name, what) {
19
+ if (!IDENTIFIER.test(name) || name.length > MAX_NAME_CHARS) {
20
+ throw new VbaAuthorError(`invalid ${what} name '${name}' (must be a VBA identifier ≤ 31 chars)`);
21
+ }
22
+ }
23
+ /** Append one `dir`-stream TLV record (Id, Size, data) to `out`. */
24
+ export function push(out, id, data) {
25
+ out.push(...u16(id), ...u32(data.length), ...data);
26
+ }
27
+ export function u16(n) {
28
+ return [n & 0xff, (n >> 8) & 0xff];
29
+ }
30
+ export function u32(n) {
31
+ return [n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff];
32
+ }
33
+ /** A name as NUL-free UTF-16LE code units — the encoding [MS-OVBA] uses for every "Unicode" name field. */
34
+ export function utf16le(s) {
35
+ const out = [];
36
+ for (let i = 0; i < s.length; i++)
37
+ out.push(...u16(s.charCodeAt(i)));
38
+ return out;
39
+ }
@@ -0,0 +1,15 @@
1
+ import { XlsxError } from '../errors.ts';
2
+ /**
3
+ * Thrown when XML text cannot be scanned into parse events — an unterminated tag, comment, CDATA
4
+ * section, markup declaration or processing instruction.
5
+ *
6
+ * This is a *typed* failure rather than the native `SyntaxError` it reads like, because the text
7
+ * being parsed is almost always a part lifted out of an untrusted `.xlsx`. A caller wrapping
8
+ * `readXlsx` needs to tell "the file I was handed is corrupt" from "something in my own code threw a
9
+ * `SyntaxError`", and a native error gives them no way to. The message still names the construct that
10
+ * did not terminate; only the type changed.
11
+ */
12
+ export declare class XmlParseError extends XlsxError {
13
+ readonly name = "XmlParseError";
14
+ readonly code = "malformed-input";
15
+ }