clear-excel 1.0.2 → 1.0.4

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 (71) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +26 -6
  3. package/dist/api/sanitize-excel-file.d.ts +0 -1
  4. package/dist/api/sanitize-excel-file.js +176 -0
  5. package/dist/api/sanitize-excel-file.js.map +1 -0
  6. package/dist/api/sanitize-excel.d.ts +0 -1
  7. package/dist/api/sanitize-excel.js +25 -0
  8. package/dist/api/sanitize-excel.js.map +1 -0
  9. package/dist/biff/workbook.d.ts +0 -1
  10. package/dist/biff/workbook.js +169 -0
  11. package/dist/biff/workbook.js.map +1 -0
  12. package/dist/constants.d.ts +0 -1
  13. package/dist/constants.js +28 -0
  14. package/dist/constants.js.map +1 -0
  15. package/dist/errors.d.ts +0 -1
  16. package/dist/errors.js +38 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/formats.d.ts +0 -1
  19. package/dist/formats.js +16 -0
  20. package/dist/formats.js.map +1 -0
  21. package/dist/index.d.ts +0 -1
  22. package/dist/index.js +4 -2776
  23. package/dist/index.js.map +1 -1
  24. package/dist/ooxml/cleanup.d.ts +0 -1
  25. package/dist/ooxml/cleanup.js +30 -0
  26. package/dist/ooxml/cleanup.js.map +1 -0
  27. package/dist/ooxml/constants.d.ts +0 -1
  28. package/dist/ooxml/constants.js +19 -0
  29. package/dist/ooxml/constants.js.map +1 -0
  30. package/dist/ooxml/parts.d.ts +1 -1
  31. package/dist/ooxml/parts.js +94 -0
  32. package/dist/ooxml/parts.js.map +1 -0
  33. package/dist/ooxml/relationships.d.ts +0 -1
  34. package/dist/ooxml/relationships.js +132 -0
  35. package/dist/ooxml/relationships.js.map +1 -0
  36. package/dist/ooxml/styles.d.ts +0 -1
  37. package/dist/ooxml/styles.js +67 -0
  38. package/dist/ooxml/styles.js.map +1 -0
  39. package/dist/ooxml/workbook.d.ts +0 -1
  40. package/dist/ooxml/workbook.js +119 -0
  41. package/dist/ooxml/workbook.js.map +1 -0
  42. package/dist/ooxml/xml.d.ts +26 -2
  43. package/dist/ooxml/xml.js +304 -0
  44. package/dist/ooxml/xml.js.map +1 -0
  45. package/dist/options.d.ts +0 -1
  46. package/dist/options.js +80 -0
  47. package/dist/options.js.map +1 -0
  48. package/dist/types.d.ts +0 -1
  49. package/dist/types.js +2 -0
  50. package/dist/types.js.map +1 -0
  51. package/dist/zip/central-directory.d.ts +0 -1
  52. package/dist/zip/central-directory.js +131 -0
  53. package/dist/zip/central-directory.js.map +1 -0
  54. package/package.json +17 -25
  55. package/dist/api/sanitize-excel-file.d.ts.map +0 -1
  56. package/dist/api/sanitize-excel.d.ts.map +0 -1
  57. package/dist/biff/workbook.d.ts.map +0 -1
  58. package/dist/constants.d.ts.map +0 -1
  59. package/dist/errors.d.ts.map +0 -1
  60. package/dist/formats.d.ts.map +0 -1
  61. package/dist/index.d.ts.map +0 -1
  62. package/dist/ooxml/cleanup.d.ts.map +0 -1
  63. package/dist/ooxml/constants.d.ts.map +0 -1
  64. package/dist/ooxml/parts.d.ts.map +0 -1
  65. package/dist/ooxml/relationships.d.ts.map +0 -1
  66. package/dist/ooxml/styles.d.ts.map +0 -1
  67. package/dist/ooxml/workbook.d.ts.map +0 -1
  68. package/dist/ooxml/xml.d.ts.map +0 -1
  69. package/dist/options.d.ts.map +0 -1
  70. package/dist/types.d.ts.map +0 -1
  71. package/dist/zip/central-directory.d.ts.map +0 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,33 @@
3
3
  All notable changes to this project are documented in this file. The project
4
4
  follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [1.0.4] - 2026-10-09
7
+
8
+ ### Fixed
9
+
10
+ - Preserve existing output files when exclusive creation fails and remove only
11
+ partial files created by the current write.
12
+ - Reject source-file aliases through symbolic links and hard links, and preserve
13
+ existing output permissions during atomic overwrite.
14
+ - Preserve worksheet text, CDATA, XML comments, and unrelated elements during
15
+ namespace-aware drawing and comment cleanup.
16
+ - Resolve sheet relationship IDs by namespace URI and reject missing or invalid
17
+ relationships before pruning styles.
18
+ - Decode UTF-8 and UTF-16 XML consistently and update encoding declarations when
19
+ writing UTF-8 output.
20
+ - Enforce the actual aggregate uncompressed size of all retained ZIP parts,
21
+ including binary VBA projects with forged ZIP size metadata.
22
+
23
+ ### Changed
24
+
25
+ - Replace Vite with one NodeNext TypeScript build for modular ESM output and
26
+ declarations, eliminating the duplicate bundled `cfb` dependency.
27
+ - Clean build artifacts before compilation, embed sources in JavaScript maps,
28
+ and stop publishing declaration maps that reference unavailable source files.
29
+ - Move the transitive `uuid` override to pnpm workspace configuration.
30
+ - Verify the installed package with real `.xls`, `.xlsx`, and `.xlsm` workbooks
31
+ through both APIs, including retained values, formulas, merges, and macros.
32
+
6
33
  ## [1.0.0] - 2026-08-19
7
34
 
8
35
  ### Changed
@@ -21,5 +48,6 @@ follows [Semantic Versioning](https://semver.org/).
21
48
  - Initial release with buffer and file APIs for sanitizing `.xls`, `.xlsx`,
22
49
  and `.xlsm` workbooks.
23
50
 
51
+ [1.0.4]: https://www.npmjs.com/package/clear-excel/v/1.0.4
24
52
  [1.0.0]: https://www.npmjs.com/package/clear-excel/v/1.0.0
25
53
  [0.1.0]: https://www.npmjs.com/package/clear-excel/v/0.1.0
package/README.md CHANGED
@@ -98,6 +98,11 @@ await sanitizeExcelFile("report.xlsx", "report.cleaned.xlsx", {
98
98
  });
99
99
  ```
100
100
 
101
+ Paths that refer to the source through symbolic links or hard links are also
102
+ rejected. New outputs are created exclusively; a failed open leaves an existing
103
+ file intact. Overwrites use an atomic replacement and preserve the existing
104
+ output's file permissions.
105
+
101
106
  ## Options
102
107
 
103
108
  Both sanitizing functions accept these options:
@@ -116,7 +121,7 @@ The exported `DEFAULT_LIMITS` object contains the defaults:
116
121
  | -------------------------- | ---------------------------: |
117
122
  | `maxEntryCount` | 10,000 entries |
118
123
  | `maxUncompressedBytes` | 512 MiB |
119
- | `maxXmlBytes` | 512 MiB aggregate XML output |
124
+ | `maxXmlBytes` | 512 MiB aggregate XML reads |
120
125
  | `maxWorksheetXmlBytes` | 256 MiB per worksheet |
121
126
  | `maxStylesXmlBytes` | 64 MiB |
122
127
  | `maxMetadataXmlBytes` | 16 MiB per metadata part |
@@ -164,7 +169,7 @@ try {
164
169
  | `ERR_INVALID_XLSX` | The ZIP/OOXML package is malformed or unsupported. |
165
170
  | `ERR_LIMIT_EXCEEDED` | A configured ZIP or XML limit was exceeded. |
166
171
  | `ERR_WORKSHEET_COUNT` | Exactly one worksheet was required. |
167
- | `ERR_SAME_PATH` | Input and output resolve to the same path. |
172
+ | `ERR_SAME_PATH` | Input and output refer to the same file. |
168
173
  | `ERR_OUTPUT_EXISTS` | Output exists and `overwrite` is false. |
169
174
  | `ERR_FILE_READ` | The input could not be read. |
170
175
  | `ERR_FILE_WRITE` | The sanitized output could not be written. |
@@ -178,7 +183,16 @@ Before loading an OOXML archive, `clear-excel` validates the ZIP central
178
183
  directory, rejects ZIP64 and multi-disk archives, rejects unsafe entry paths,
179
184
  and checks declared entry counts and expansion sizes. Required XML parts are
180
185
  inflated lazily with both per-part and aggregate runtime budgets, including
181
- protection when ZIP size metadata has been forged.
186
+ protection when ZIP size metadata has been forged. Before generating the output,
187
+ all retained archive parts are streamed through one actual-byte
188
+ `maxUncompressedBytes` budget, including binary VBA projects and other retained
189
+ content.
190
+
191
+ Processed OOXML XML parts support UTF-8, UTF-16LE, and UTF-16BE. Rewritten parts
192
+ use UTF-8 with a matching XML declaration. XML cleanup resolves namespace URIs
193
+ independently of prefix names and preserves text, CDATA, XML comments, and
194
+ unrelated elements. Missing or invalid worksheet relationships are rejected
195
+ before style records can be removed.
182
196
 
183
197
  For `.xls`, the library validates CFB entry and byte limits, parses the BIFF
184
198
  record stream without evaluating formulas, removes drawing-layer records, and
@@ -200,9 +214,15 @@ pnpm run check
200
214
  ```
201
215
 
202
216
  `pnpm run check` verifies Oxfmt formatting, runs type-aware Oxlint and strict
203
- TypeScript checks, executes the tests, builds the ESM bundle and declarations,
204
- inspects `pnpm pack --dry-run`, and installs a temporary tarball to smoke-test
205
- both runtime imports and a TypeScript consumer.
217
+ TypeScript checks, executes the tests, builds the ESM modules and declarations,
218
+ inspects `pnpm pack --dry-run`, and installs a temporary tarball to verify both
219
+ APIs with `.xls`, `.xlsx`, and `.xlsm` workbooks and a NodeNext TypeScript
220
+ consumer.
221
+
222
+ The build cleans `dist` and runs `tsc` once in NodeNext mode. It emits separate
223
+ ESM modules, TypeScript declarations, and JavaScript source maps with embedded
224
+ sources. Runtime dependencies are installed normally; the package does not
225
+ bundle a second copy of them.
206
226
 
207
227
  Useful development commands:
208
228
 
@@ -2,4 +2,3 @@ import { isExcelFilename, isXlsxFilename } from "../formats.js";
2
2
  import type { SanitizeExcelFileOptions } from "../types.js";
3
3
  export { isExcelFilename, isXlsxFilename };
4
4
  export declare function sanitizeExcelFile(inputPath: string, outputPath: string, options?: SanitizeExcelFileOptions): Promise<void>;
5
- //# sourceMappingURL=sanitize-excel-file.d.ts.map
@@ -0,0 +1,176 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { open, readFile, realpath, rename, stat, unlink } from "node:fs/promises";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { hasCompoundFileSignature } from "../biff/workbook.js";
5
+ import { createError } from "../errors.js";
6
+ import { excelFileFormat, isExcelFilename, isXlsxFilename } from "../formats.js";
7
+ import { normalizeOptions } from "../options.js";
8
+ import { sanitizeExcel } from "./sanitize-excel.js";
9
+ export { isExcelFilename, isXlsxFilename };
10
+ export async function sanitizeExcelFile(inputPath, outputPath, options) {
11
+ const format = validateFilePaths(inputPath, outputPath);
12
+ const normalized = normalizeOptions(options, true);
13
+ const absoluteInputPath = resolve(inputPath);
14
+ const absoluteOutputPath = resolve(outputPath);
15
+ if (absoluteInputPath === absoluteOutputPath) {
16
+ throw createError("ERR_SAME_PATH", {
17
+ details: { inputPath: absoluteInputPath, outputPath: absoluteOutputPath },
18
+ });
19
+ }
20
+ const input = await readInputFile(absoluteInputPath);
21
+ await validateDistinctFiles(absoluteInputPath, absoluteOutputPath);
22
+ validateInputFormat(input, format);
23
+ const sanitized = await sanitizeExcel(input, {
24
+ requireSingleWorksheet: normalized.requireSingleWorksheet,
25
+ limits: normalized.limits,
26
+ });
27
+ if (normalized.overwrite) {
28
+ await replaceFileAtomically(absoluteOutputPath, sanitized);
29
+ }
30
+ else {
31
+ await writeNewFile(absoluteOutputPath, sanitized);
32
+ }
33
+ }
34
+ function validateFilePaths(inputPath, outputPath) {
35
+ if (typeof inputPath !== "string" ||
36
+ inputPath.length === 0 ||
37
+ typeof outputPath !== "string" ||
38
+ outputPath.length === 0) {
39
+ throw createError("ERR_INVALID_ARGUMENT", {
40
+ message: "inputPath and outputPath must be non-empty strings",
41
+ });
42
+ }
43
+ const inputFormat = excelFileFormat(inputPath);
44
+ const outputFormat = excelFileFormat(outputPath);
45
+ if (!inputFormat || !outputFormat || inputFormat !== outputFormat) {
46
+ throw createError("ERR_UNSUPPORTED_EXTENSION", {
47
+ details: { inputPath, outputPath, inputFormat, outputFormat },
48
+ });
49
+ }
50
+ return inputFormat;
51
+ }
52
+ function validateInputFormat(input, format) {
53
+ const isXls = hasCompoundFileSignature(input);
54
+ if ((format === "xls") !== isXls) {
55
+ throw createError(format === "xls" ? "ERR_INVALID_XLS" : "ERR_INVALID_XLSX", {
56
+ details: { format },
57
+ });
58
+ }
59
+ }
60
+ async function readInputFile(inputPath) {
61
+ try {
62
+ return await readFile(inputPath);
63
+ }
64
+ catch (error) {
65
+ throw createError("ERR_FILE_READ", {
66
+ cause: error,
67
+ details: { inputPath },
68
+ });
69
+ }
70
+ }
71
+ async function writeNewFile(outputPath, contents) {
72
+ try {
73
+ await writeExclusiveFile(outputPath, contents);
74
+ }
75
+ catch (error) {
76
+ if (hasErrorCode(error, "EEXIST")) {
77
+ throw createError("ERR_OUTPUT_EXISTS", {
78
+ cause: error,
79
+ details: { outputPath },
80
+ });
81
+ }
82
+ throw createError("ERR_FILE_WRITE", {
83
+ cause: error,
84
+ details: { outputPath },
85
+ });
86
+ }
87
+ }
88
+ async function replaceFileAtomically(outputPath, contents) {
89
+ const temporaryPath = `${outputPath}.${process.pid}.${randomUUID()}.tmp`;
90
+ let temporaryCreated = false;
91
+ try {
92
+ const existingOutput = await stat(outputPath).catch((error) => {
93
+ if (hasErrorCode(error, "ENOENT")) {
94
+ return undefined;
95
+ }
96
+ throw error;
97
+ });
98
+ const outputMode = existingOutput ? existingOutput.mode & 0o777 : undefined;
99
+ await writeExclusiveFile(temporaryPath, contents, outputMode);
100
+ temporaryCreated = true;
101
+ await rename(temporaryPath, outputPath);
102
+ }
103
+ catch (error) {
104
+ if (temporaryCreated) {
105
+ await removeIfPresent(temporaryPath);
106
+ }
107
+ throw createError("ERR_FILE_WRITE", {
108
+ cause: error,
109
+ details: { outputPath },
110
+ });
111
+ }
112
+ }
113
+ async function writeExclusiveFile(path, contents, mode) {
114
+ // A failed open does not establish ownership, so it must never trigger unlink.
115
+ const handle = await open(path, "wx", mode);
116
+ try {
117
+ await handle.writeFile(contents);
118
+ if (mode !== undefined) {
119
+ await handle.chmod(mode);
120
+ }
121
+ await handle.close();
122
+ }
123
+ catch (error) {
124
+ await handle.close().catch(() => undefined);
125
+ await removeIfPresent(path);
126
+ throw error;
127
+ }
128
+ }
129
+ async function validateDistinctFiles(inputPath, outputPath) {
130
+ let inputRealPath;
131
+ let inputStat;
132
+ try {
133
+ inputRealPath = await realpath(inputPath);
134
+ inputStat = await stat(inputPath);
135
+ }
136
+ catch (error) {
137
+ throw createError("ERR_FILE_READ", { cause: error, details: { inputPath } });
138
+ }
139
+ let sameFile;
140
+ try {
141
+ const outputStat = await stat(outputPath).catch((error) => {
142
+ if (hasErrorCode(error, "ENOENT")) {
143
+ return undefined;
144
+ }
145
+ throw error;
146
+ });
147
+ const outputRealPath = outputStat
148
+ ? await realpath(outputPath)
149
+ : await realpath(dirname(outputPath))
150
+ .then((parent) => join(parent, basename(outputPath)))
151
+ .catch((error) => {
152
+ if (hasErrorCode(error, "ENOENT")) {
153
+ return outputPath;
154
+ }
155
+ throw error;
156
+ });
157
+ sameFile =
158
+ inputRealPath === outputRealPath ||
159
+ (outputStat !== undefined &&
160
+ inputStat.dev === outputStat.dev &&
161
+ inputStat.ino === outputStat.ino);
162
+ }
163
+ catch (error) {
164
+ throw createError("ERR_FILE_WRITE", { cause: error, details: { outputPath } });
165
+ }
166
+ if (sameFile) {
167
+ throw createError("ERR_SAME_PATH", { details: { inputPath, outputPath } });
168
+ }
169
+ }
170
+ function hasErrorCode(error, expectedCode) {
171
+ return (typeof error === "object" && error !== null && "code" in error && error.code === expectedCode);
172
+ }
173
+ async function removeIfPresent(path) {
174
+ await unlink(path).catch(() => undefined);
175
+ }
176
+ //# sourceMappingURL=sanitize-excel-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-excel-file.js","sourceRoot":"","sources":["../../src/api/sanitize-excel-file.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { open, readFile, realpath, rename, stat, unlink } from \"node:fs/promises\";\nimport { basename, dirname, join, resolve } from \"node:path\";\n\nimport { hasCompoundFileSignature } from \"../biff/workbook.js\";\nimport { createError } from \"../errors.js\";\nimport { excelFileFormat, isExcelFilename, isXlsxFilename } from \"../formats.js\";\nimport type { ExcelFileFormat } from \"../formats.js\";\nimport { normalizeOptions } from \"../options.js\";\nimport type { SanitizeExcelFileOptions } from \"../types.js\";\nimport { sanitizeExcel } from \"./sanitize-excel.js\";\n\nexport { isExcelFilename, isXlsxFilename };\n\nexport async function sanitizeExcelFile(\n inputPath: string,\n outputPath: string,\n options?: SanitizeExcelFileOptions,\n): Promise<void> {\n const format = validateFilePaths(inputPath, outputPath);\n const normalized = normalizeOptions(options, true);\n const absoluteInputPath = resolve(inputPath);\n const absoluteOutputPath = resolve(outputPath);\n if (absoluteInputPath === absoluteOutputPath) {\n throw createError(\"ERR_SAME_PATH\", {\n details: { inputPath: absoluteInputPath, outputPath: absoluteOutputPath },\n });\n }\n\n const input = await readInputFile(absoluteInputPath);\n await validateDistinctFiles(absoluteInputPath, absoluteOutputPath);\n validateInputFormat(input, format);\n const sanitized = await sanitizeExcel(input, {\n requireSingleWorksheet: normalized.requireSingleWorksheet,\n limits: normalized.limits,\n });\n\n if (normalized.overwrite) {\n await replaceFileAtomically(absoluteOutputPath, sanitized);\n } else {\n await writeNewFile(absoluteOutputPath, sanitized);\n }\n}\n\nfunction validateFilePaths(inputPath: string, outputPath: string): ExcelFileFormat {\n if (\n typeof inputPath !== \"string\" ||\n inputPath.length === 0 ||\n typeof outputPath !== \"string\" ||\n outputPath.length === 0\n ) {\n throw createError(\"ERR_INVALID_ARGUMENT\", {\n message: \"inputPath and outputPath must be non-empty strings\",\n });\n }\n const inputFormat = excelFileFormat(inputPath);\n const outputFormat = excelFileFormat(outputPath);\n if (!inputFormat || !outputFormat || inputFormat !== outputFormat) {\n throw createError(\"ERR_UNSUPPORTED_EXTENSION\", {\n details: { inputPath, outputPath, inputFormat, outputFormat },\n });\n }\n return inputFormat;\n}\n\nfunction validateInputFormat(input: Buffer, format: ExcelFileFormat): void {\n const isXls = hasCompoundFileSignature(input);\n if ((format === \"xls\") !== isXls) {\n throw createError(format === \"xls\" ? \"ERR_INVALID_XLS\" : \"ERR_INVALID_XLSX\", {\n details: { format },\n });\n }\n}\n\nasync function readInputFile(inputPath: string): Promise<Buffer> {\n try {\n return await readFile(inputPath);\n } catch (error) {\n throw createError(\"ERR_FILE_READ\", {\n cause: error,\n details: { inputPath },\n });\n }\n}\n\nasync function writeNewFile(outputPath: string, contents: Buffer): Promise<void> {\n try {\n await writeExclusiveFile(outputPath, contents);\n } catch (error) {\n if (hasErrorCode(error, \"EEXIST\")) {\n throw createError(\"ERR_OUTPUT_EXISTS\", {\n cause: error,\n details: { outputPath },\n });\n }\n throw createError(\"ERR_FILE_WRITE\", {\n cause: error,\n details: { outputPath },\n });\n }\n}\n\nasync function replaceFileAtomically(outputPath: string, contents: Buffer): Promise<void> {\n const temporaryPath = `${outputPath}.${process.pid}.${randomUUID()}.tmp`;\n let temporaryCreated = false;\n try {\n const existingOutput = await stat(outputPath).catch((error: unknown) => {\n if (hasErrorCode(error, \"ENOENT\")) {\n return undefined;\n }\n throw error;\n });\n const outputMode = existingOutput ? existingOutput.mode & 0o777 : undefined;\n await writeExclusiveFile(temporaryPath, contents, outputMode);\n temporaryCreated = true;\n await rename(temporaryPath, outputPath);\n } catch (error) {\n if (temporaryCreated) {\n await removeIfPresent(temporaryPath);\n }\n throw createError(\"ERR_FILE_WRITE\", {\n cause: error,\n details: { outputPath },\n });\n }\n}\n\nasync function writeExclusiveFile(path: string, contents: Buffer, mode?: number): Promise<void> {\n // A failed open does not establish ownership, so it must never trigger unlink.\n const handle = await open(path, \"wx\", mode);\n try {\n await handle.writeFile(contents);\n if (mode !== undefined) {\n await handle.chmod(mode);\n }\n await handle.close();\n } catch (error) {\n await handle.close().catch(() => undefined);\n await removeIfPresent(path);\n throw error;\n }\n}\n\nasync function validateDistinctFiles(inputPath: string, outputPath: string): Promise<void> {\n let inputRealPath: string;\n let inputStat: Awaited<ReturnType<typeof stat>>;\n try {\n inputRealPath = await realpath(inputPath);\n inputStat = await stat(inputPath);\n } catch (error) {\n throw createError(\"ERR_FILE_READ\", { cause: error, details: { inputPath } });\n }\n\n let sameFile: boolean;\n try {\n const outputStat = await stat(outputPath).catch((error: unknown) => {\n if (hasErrorCode(error, \"ENOENT\")) {\n return undefined;\n }\n throw error;\n });\n const outputRealPath = outputStat\n ? await realpath(outputPath)\n : await realpath(dirname(outputPath))\n .then((parent) => join(parent, basename(outputPath)))\n .catch((error: unknown) => {\n if (hasErrorCode(error, \"ENOENT\")) {\n return outputPath;\n }\n throw error;\n });\n sameFile =\n inputRealPath === outputRealPath ||\n (outputStat !== undefined &&\n inputStat.dev === outputStat.dev &&\n inputStat.ino === outputStat.ino);\n } catch (error) {\n throw createError(\"ERR_FILE_WRITE\", { cause: error, details: { outputPath } });\n }\n\n if (sameFile) {\n throw createError(\"ERR_SAME_PATH\", { details: { inputPath, outputPath } });\n }\n}\n\nfunction hasErrorCode(error: unknown, expectedCode: string): boolean {\n return (\n typeof error === \"object\" && error !== null && \"code\" in error && error.code === expectedCode\n );\n}\n\nasync function removeIfPresent(path: string): Promise<void> {\n await unlink(path).catch(() => undefined);\n}\n"]}
@@ -1,3 +1,2 @@
1
1
  import type { SanitizeExcelOptions } from "../types.js";
2
2
  export declare function sanitizeExcel(buffer: Buffer, options?: SanitizeExcelOptions): Promise<Buffer>;
3
- //# sourceMappingURL=sanitize-excel.d.ts.map
@@ -0,0 +1,25 @@
1
+ import { hasCompoundFileSignature, sanitizeBiffWorkbook } from "../biff/workbook.js";
2
+ import { ExcelSanitizationError, createError } from "../errors.js";
3
+ import { sanitizeOoxmlWorkbook } from "../ooxml/workbook.js";
4
+ import { normalizeOptions } from "../options.js";
5
+ export async function sanitizeExcel(buffer, options) {
6
+ if (!Buffer.isBuffer(buffer)) {
7
+ throw createError("ERR_INVALID_ARGUMENT", {
8
+ message: "buffer must be a Node.js Buffer",
9
+ });
10
+ }
11
+ const normalized = normalizeOptions(options);
12
+ const isBiff = hasCompoundFileSignature(buffer);
13
+ try {
14
+ return isBiff
15
+ ? await sanitizeBiffWorkbook(buffer, normalized)
16
+ : await sanitizeOoxmlWorkbook(buffer, normalized);
17
+ }
18
+ catch (error) {
19
+ if (error instanceof ExcelSanitizationError) {
20
+ throw error;
21
+ }
22
+ throw createError(isBiff ? "ERR_INVALID_XLS" : "ERR_INVALID_XLSX", { cause: error });
23
+ }
24
+ }
25
+ //# sourceMappingURL=sanitize-excel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-excel.js","sourceRoot":"","sources":["../../src/api/sanitize-excel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGjD,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAAc,EACd,OAA8B;IAE9B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7B,MAAM,WAAW,CAAC,sBAAsB,EAAE;YACxC,OAAO,EAAE,iCAAiC;SAC3C,CAAC,CAAC;IACL,CAAC;IAED,MAAM,UAAU,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,wBAAwB,CAAC,MAAM,CAAC,CAAC;IAChD,IAAI,CAAC;QACH,OAAO,MAAM;YACX,CAAC,CAAC,MAAM,oBAAoB,CAAC,MAAM,EAAE,UAAU,CAAC;YAChD,CAAC,CAAC,MAAM,qBAAqB,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACtD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,sBAAsB,EAAE,CAAC;YAC5C,MAAM,KAAK,CAAC;QACd,CAAC;QACD,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IACvF,CAAC;AACH,CAAC","sourcesContent":["import { hasCompoundFileSignature, sanitizeBiffWorkbook } from \"../biff/workbook.js\";\nimport { ExcelSanitizationError, createError } from \"../errors.js\";\nimport { sanitizeOoxmlWorkbook } from \"../ooxml/workbook.js\";\nimport { normalizeOptions } from \"../options.js\";\nimport type { SanitizeExcelOptions } from \"../types.js\";\n\nexport async function sanitizeExcel(\n buffer: Buffer,\n options?: SanitizeExcelOptions,\n): Promise<Buffer> {\n if (!Buffer.isBuffer(buffer)) {\n throw createError(\"ERR_INVALID_ARGUMENT\", {\n message: \"buffer must be a Node.js Buffer\",\n });\n }\n\n const normalized = normalizeOptions(options);\n const isBiff = hasCompoundFileSignature(buffer);\n try {\n return isBiff\n ? await sanitizeBiffWorkbook(buffer, normalized)\n : await sanitizeOoxmlWorkbook(buffer, normalized);\n } catch (error) {\n if (error instanceof ExcelSanitizationError) {\n throw error;\n }\n throw createError(isBiff ? \"ERR_INVALID_XLS\" : \"ERR_INVALID_XLSX\", { cause: error });\n }\n}\n"]}
@@ -1,4 +1,3 @@
1
1
  import type { NormalizedSanitizeOptions } from "../types.js";
2
2
  export declare function hasCompoundFileSignature(buffer: Buffer): boolean;
3
3
  export declare function sanitizeBiffWorkbook(buffer: Buffer, options: NormalizedSanitizeOptions): Promise<Buffer>;
4
- //# sourceMappingURL=workbook.d.ts.map
@@ -0,0 +1,169 @@
1
+ import * as CFB from "cfb";
2
+ import { createError, limitExceeded } from "../errors.js";
3
+ const COMPOUND_FILE_SIGNATURE = Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]);
4
+ const RECORD_HEADER_BYTES = 4;
5
+ const CFB_STORAGE_ENTRY = 1;
6
+ const CFB_STREAM_ENTRY = 2;
7
+ const RECORD_CONTINUE = 0x003c;
8
+ const RECORD_BOUNDSHEET = 0x0085;
9
+ const RECORD_BOF_IDS = new Set([0x0009, 0x0209, 0x0409, 0x0809]);
10
+ // INDEX, DBCELL, and ExtSST cache absolute stream offsets. They are optional
11
+ // acceleration records and must be discarded after other records move.
12
+ const OFFSET_CACHE_RECORD_IDS = new Set([0x00d7, 0x00ff, 0x020b]);
13
+ // Drawing-layer records cover OfficeArt drawings, images, notes/comments,
14
+ // controls, embedded OLE objects, and header/footer or background pictures.
15
+ const DRAWING_RECORD_IDS = new Set([
16
+ 0x001c, // Note
17
+ 0x005d, // Obj
18
+ 0x007f, // ImData
19
+ 0x00e9, // BkHim
20
+ 0x00eb, // MsoDrawingGroup
21
+ 0x00ec, // MsoDrawing
22
+ 0x00ed, // MsoDrawingSelection
23
+ 0x01b6, // TxO
24
+ 0x0866, // HFPicture
25
+ ]);
26
+ export function hasCompoundFileSignature(buffer) {
27
+ return (buffer.length >= COMPOUND_FILE_SIGNATURE.length &&
28
+ buffer.subarray(0, COMPOUND_FILE_SIGNATURE.length).equals(COMPOUND_FILE_SIGNATURE));
29
+ }
30
+ export async function sanitizeBiffWorkbook(buffer, options) {
31
+ enforceInputSize(buffer, options);
32
+ const cfb = CFB.read(buffer, { type: "buffer" });
33
+ enforceContainerLimits(cfb, options);
34
+ const workbookEntry = CFB.find(cfb, "Workbook") ?? CFB.find(cfb, "Book");
35
+ if (!workbookEntry || workbookEntry.type !== CFB_STREAM_ENTRY) {
36
+ throw new Error("Compound file has no BIFF Workbook stream");
37
+ }
38
+ const workbookStream = Buffer.from(workbookEntry.content);
39
+ const records = parseBiffRecords(workbookStream);
40
+ rejectEncryptedWorkbook(records);
41
+ enforceWorksheetCount(records, options.requireSingleWorksheet);
42
+ const sanitizedStream = rebuildWorkbookStream(records);
43
+ workbookEntry.content = sanitizedStream;
44
+ workbookEntry.size = sanitizedStream.length;
45
+ removeEmbeddedObjectEntries(cfb);
46
+ return Buffer.from(CFB.write(cfb, { type: "buffer", fileType: "cfb" }));
47
+ }
48
+ function enforceInputSize(buffer, options) {
49
+ const configured = options.limits.maxUncompressedBytes;
50
+ if (buffer.length > configured) {
51
+ throw limitExceeded("maxUncompressedBytes", configured, buffer.length);
52
+ }
53
+ }
54
+ function enforceContainerLimits(cfb, options) {
55
+ const entries = cfb.FileIndex.filter((entry) => entry.type === CFB_STORAGE_ENTRY || entry.type === CFB_STREAM_ENTRY);
56
+ if (entries.length > options.limits.maxEntryCount) {
57
+ throw limitExceeded("maxEntryCount", options.limits.maxEntryCount, entries.length);
58
+ }
59
+ const totalBytes = entries.reduce((total, entry) => (entry.type === CFB_STREAM_ENTRY ? total + entry.content.length : total), 0);
60
+ if (totalBytes > options.limits.maxUncompressedBytes) {
61
+ throw limitExceeded("maxUncompressedBytes", options.limits.maxUncompressedBytes, totalBytes);
62
+ }
63
+ }
64
+ function parseBiffRecords(stream) {
65
+ const records = [];
66
+ let offset = 0;
67
+ let removeContinuation = false;
68
+ while (offset < stream.length) {
69
+ if (offset + RECORD_HEADER_BYTES > stream.length) {
70
+ throw new Error("Truncated BIFF record header");
71
+ }
72
+ const id = stream.readUInt16LE(offset);
73
+ const length = stream.readUInt16LE(offset + 2);
74
+ const nextOffset = offset + RECORD_HEADER_BYTES + length;
75
+ if (nextOffset > stream.length) {
76
+ throw new Error("Truncated BIFF record payload");
77
+ }
78
+ const remove = id === RECORD_CONTINUE
79
+ ? removeContinuation
80
+ : DRAWING_RECORD_IDS.has(id) || OFFSET_CACHE_RECORD_IDS.has(id);
81
+ records.push({
82
+ id,
83
+ originalOffset: offset,
84
+ payload: stream.subarray(offset + RECORD_HEADER_BYTES, nextOffset),
85
+ remove,
86
+ });
87
+ if (id !== RECORD_CONTINUE) {
88
+ removeContinuation = remove;
89
+ }
90
+ offset = nextOffset;
91
+ }
92
+ if (records.length === 0 || !RECORD_BOF_IDS.has(records[0]?.id ?? -1)) {
93
+ throw new Error("Workbook stream does not start with a BIFF BOF record");
94
+ }
95
+ return records;
96
+ }
97
+ function rejectEncryptedWorkbook(records) {
98
+ if (records.some((record) => record.id === 0x002f)) {
99
+ throw createError("ERR_INVALID_XLS", {
100
+ message: "Encrypted XLS workbooks are not supported",
101
+ });
102
+ }
103
+ }
104
+ function enforceWorksheetCount(records, required) {
105
+ const count = records.filter((record) => record.id === RECORD_BOUNDSHEET).length;
106
+ if (count === 0) {
107
+ throw new Error("Workbook contains no sheets");
108
+ }
109
+ if (required && count !== 1) {
110
+ throw createError("ERR_WORKSHEET_COUNT", {
111
+ details: { expected: 1, actual: count },
112
+ });
113
+ }
114
+ }
115
+ function rebuildWorkbookStream(records) {
116
+ const newOffsets = new Map();
117
+ let outputLength = 0;
118
+ for (const record of records) {
119
+ if (record.remove) {
120
+ continue;
121
+ }
122
+ newOffsets.set(record.originalOffset, outputLength);
123
+ outputLength += RECORD_HEADER_BYTES + record.payload.length;
124
+ }
125
+ const output = Buffer.allocUnsafe(outputLength);
126
+ let outputOffset = 0;
127
+ for (const record of records) {
128
+ if (record.remove) {
129
+ continue;
130
+ }
131
+ const payload = patchBoundSheetOffset(record, newOffsets);
132
+ output.writeUInt16LE(record.id, outputOffset);
133
+ output.writeUInt16LE(payload.length, outputOffset + 2);
134
+ payload.copy(output, outputOffset + RECORD_HEADER_BYTES);
135
+ outputOffset += RECORD_HEADER_BYTES + payload.length;
136
+ }
137
+ return output;
138
+ }
139
+ function patchBoundSheetOffset(record, newOffsets) {
140
+ if (record.id !== RECORD_BOUNDSHEET) {
141
+ return record.payload;
142
+ }
143
+ if (record.payload.length < 4) {
144
+ throw new Error("Invalid BoundSheet record");
145
+ }
146
+ const originalSheetOffset = record.payload.readUInt32LE(0);
147
+ const newSheetOffset = newOffsets.get(originalSheetOffset);
148
+ if (newSheetOffset === undefined) {
149
+ throw new Error("BoundSheet record points outside the Workbook stream");
150
+ }
151
+ const payload = Buffer.from(record.payload);
152
+ payload.writeUInt32LE(newSheetOffset, 0);
153
+ return payload;
154
+ }
155
+ function removeEmbeddedObjectEntries(cfb) {
156
+ const paths = cfb.FullPaths.filter((path) => path
157
+ .split("/")
158
+ .filter(Boolean)
159
+ .some((segment) => segment.toLowerCase() === "objectpool" ||
160
+ segment.toLowerCase() === "ctls" ||
161
+ /^mbd[0-9a-f]{8}$/i.test(segment)));
162
+ for (let index = paths.length - 1; index >= 0; index -= 1) {
163
+ CFB.utils.cfb_del(cfb, paths[index] ?? "");
164
+ }
165
+ if (paths.length > 0) {
166
+ CFB.utils.cfb_gc(cfb);
167
+ }
168
+ }
169
+ //# sourceMappingURL=workbook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workbook.js","sourceRoot":"","sources":["../../src/biff/workbook.ts"],"names":[],"mappings":"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","sourcesContent":["import * as CFB from \"cfb\";\n\nimport { createError, limitExceeded } from \"../errors.js\";\nimport type { NormalizedSanitizeOptions } from \"../types.js\";\n\nconst COMPOUND_FILE_SIGNATURE = Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]);\nconst RECORD_HEADER_BYTES = 4;\nconst CFB_STORAGE_ENTRY = 1;\nconst CFB_STREAM_ENTRY = 2;\nconst RECORD_CONTINUE = 0x003c;\nconst RECORD_BOUNDSHEET = 0x0085;\nconst RECORD_BOF_IDS = new Set([0x0009, 0x0209, 0x0409, 0x0809]);\n\n// INDEX, DBCELL, and ExtSST cache absolute stream offsets. They are optional\n// acceleration records and must be discarded after other records move.\nconst OFFSET_CACHE_RECORD_IDS = new Set([0x00d7, 0x00ff, 0x020b]);\n\n// Drawing-layer records cover OfficeArt drawings, images, notes/comments,\n// controls, embedded OLE objects, and header/footer or background pictures.\nconst DRAWING_RECORD_IDS = new Set([\n 0x001c, // Note\n 0x005d, // Obj\n 0x007f, // ImData\n 0x00e9, // BkHim\n 0x00eb, // MsoDrawingGroup\n 0x00ec, // MsoDrawing\n 0x00ed, // MsoDrawingSelection\n 0x01b6, // TxO\n 0x0866, // HFPicture\n]);\n\ninterface BiffRecord {\n id: number;\n originalOffset: number;\n payload: Buffer;\n remove: boolean;\n}\n\nexport function hasCompoundFileSignature(buffer: Buffer): boolean {\n return (\n buffer.length >= COMPOUND_FILE_SIGNATURE.length &&\n buffer.subarray(0, COMPOUND_FILE_SIGNATURE.length).equals(COMPOUND_FILE_SIGNATURE)\n );\n}\n\nexport async function sanitizeBiffWorkbook(\n buffer: Buffer,\n options: NormalizedSanitizeOptions,\n): Promise<Buffer> {\n enforceInputSize(buffer, options);\n const cfb = CFB.read(buffer, { type: \"buffer\" });\n enforceContainerLimits(cfb, options);\n\n const workbookEntry = CFB.find(cfb, \"Workbook\") ?? CFB.find(cfb, \"Book\");\n if (!workbookEntry || workbookEntry.type !== CFB_STREAM_ENTRY) {\n throw new Error(\"Compound file has no BIFF Workbook stream\");\n }\n\n const workbookStream = Buffer.from(workbookEntry.content);\n const records = parseBiffRecords(workbookStream);\n rejectEncryptedWorkbook(records);\n enforceWorksheetCount(records, options.requireSingleWorksheet);\n\n const sanitizedStream = rebuildWorkbookStream(records);\n workbookEntry.content = sanitizedStream;\n workbookEntry.size = sanitizedStream.length;\n removeEmbeddedObjectEntries(cfb);\n\n return Buffer.from(CFB.write(cfb, { type: \"buffer\", fileType: \"cfb\" }));\n}\n\nfunction enforceInputSize(buffer: Buffer, options: NormalizedSanitizeOptions): void {\n const configured = options.limits.maxUncompressedBytes;\n if (buffer.length > configured) {\n throw limitExceeded(\"maxUncompressedBytes\", configured, buffer.length);\n }\n}\n\nfunction enforceContainerLimits(cfb: CFB.CFB$Container, options: NormalizedSanitizeOptions): void {\n const entries = cfb.FileIndex.filter(\n (entry) => entry.type === CFB_STORAGE_ENTRY || entry.type === CFB_STREAM_ENTRY,\n );\n if (entries.length > options.limits.maxEntryCount) {\n throw limitExceeded(\"maxEntryCount\", options.limits.maxEntryCount, entries.length);\n }\n\n const totalBytes = entries.reduce(\n (total, entry) => (entry.type === CFB_STREAM_ENTRY ? total + entry.content.length : total),\n 0,\n );\n if (totalBytes > options.limits.maxUncompressedBytes) {\n throw limitExceeded(\"maxUncompressedBytes\", options.limits.maxUncompressedBytes, totalBytes);\n }\n}\n\nfunction parseBiffRecords(stream: Buffer): BiffRecord[] {\n const records: BiffRecord[] = [];\n let offset = 0;\n let removeContinuation = false;\n\n while (offset < stream.length) {\n if (offset + RECORD_HEADER_BYTES > stream.length) {\n throw new Error(\"Truncated BIFF record header\");\n }\n\n const id = stream.readUInt16LE(offset);\n const length = stream.readUInt16LE(offset + 2);\n const nextOffset = offset + RECORD_HEADER_BYTES + length;\n if (nextOffset > stream.length) {\n throw new Error(\"Truncated BIFF record payload\");\n }\n\n const remove: boolean =\n id === RECORD_CONTINUE\n ? removeContinuation\n : DRAWING_RECORD_IDS.has(id) || OFFSET_CACHE_RECORD_IDS.has(id);\n records.push({\n id,\n originalOffset: offset,\n payload: stream.subarray(offset + RECORD_HEADER_BYTES, nextOffset),\n remove,\n });\n if (id !== RECORD_CONTINUE) {\n removeContinuation = remove;\n }\n offset = nextOffset;\n }\n\n if (records.length === 0 || !RECORD_BOF_IDS.has(records[0]?.id ?? -1)) {\n throw new Error(\"Workbook stream does not start with a BIFF BOF record\");\n }\n return records;\n}\n\nfunction rejectEncryptedWorkbook(records: readonly BiffRecord[]): void {\n if (records.some((record) => record.id === 0x002f)) {\n throw createError(\"ERR_INVALID_XLS\", {\n message: \"Encrypted XLS workbooks are not supported\",\n });\n }\n}\n\nfunction enforceWorksheetCount(records: readonly BiffRecord[], required: boolean): void {\n const count = records.filter((record) => record.id === RECORD_BOUNDSHEET).length;\n if (count === 0) {\n throw new Error(\"Workbook contains no sheets\");\n }\n if (required && count !== 1) {\n throw createError(\"ERR_WORKSHEET_COUNT\", {\n details: { expected: 1, actual: count },\n });\n }\n}\n\nfunction rebuildWorkbookStream(records: readonly BiffRecord[]): Buffer {\n const newOffsets = new Map<number, number>();\n let outputLength = 0;\n for (const record of records) {\n if (record.remove) {\n continue;\n }\n newOffsets.set(record.originalOffset, outputLength);\n outputLength += RECORD_HEADER_BYTES + record.payload.length;\n }\n\n const output = Buffer.allocUnsafe(outputLength);\n let outputOffset = 0;\n for (const record of records) {\n if (record.remove) {\n continue;\n }\n\n const payload = patchBoundSheetOffset(record, newOffsets);\n output.writeUInt16LE(record.id, outputOffset);\n output.writeUInt16LE(payload.length, outputOffset + 2);\n payload.copy(output, outputOffset + RECORD_HEADER_BYTES);\n outputOffset += RECORD_HEADER_BYTES + payload.length;\n }\n return output;\n}\n\nfunction patchBoundSheetOffset(\n record: BiffRecord,\n newOffsets: ReadonlyMap<number, number>,\n): Buffer {\n if (record.id !== RECORD_BOUNDSHEET) {\n return record.payload;\n }\n if (record.payload.length < 4) {\n throw new Error(\"Invalid BoundSheet record\");\n }\n\n const originalSheetOffset = record.payload.readUInt32LE(0);\n const newSheetOffset = newOffsets.get(originalSheetOffset);\n if (newSheetOffset === undefined) {\n throw new Error(\"BoundSheet record points outside the Workbook stream\");\n }\n\n const payload = Buffer.from(record.payload);\n payload.writeUInt32LE(newSheetOffset, 0);\n return payload;\n}\n\nfunction removeEmbeddedObjectEntries(cfb: CFB.CFB$Container): void {\n const paths = cfb.FullPaths.filter((path) =>\n path\n .split(\"/\")\n .filter(Boolean)\n .some(\n (segment) =>\n segment.toLowerCase() === \"objectpool\" ||\n segment.toLowerCase() === \"ctls\" ||\n /^mbd[0-9a-f]{8}$/i.test(segment),\n ),\n );\n\n for (let index = paths.length - 1; index >= 0; index -= 1) {\n CFB.utils.cfb_del(cfb, paths[index] ?? \"\");\n }\n if (paths.length > 0) {\n CFB.utils.cfb_gc(cfb);\n }\n}\n"]}
@@ -3,4 +3,3 @@ export declare const DEFAULT_LIMITS: Readonly<ExcelSanitizationLimits>;
3
3
  export declare const SANITIZE_OPTION_NAMES: ReadonlySet<string>;
4
4
  export declare const FILE_OPTION_NAMES: ReadonlySet<string>;
5
5
  export declare const LIMIT_NAMES: ReadonlySet<string>;
6
- //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1,28 @@
1
+ export const DEFAULT_LIMITS = Object.freeze({
2
+ maxEntryCount: 10_000,
3
+ maxUncompressedBytes: 512 * 1024 * 1024,
4
+ maxXmlBytes: 512 * 1024 * 1024,
5
+ maxWorksheetXmlBytes: 256 * 1024 * 1024,
6
+ maxStylesXmlBytes: 64 * 1024 * 1024,
7
+ maxMetadataXmlBytes: 16 * 1024 * 1024,
8
+ maxRelationshipsXmlBytes: 8 * 1024 * 1024,
9
+ });
10
+ export const SANITIZE_OPTION_NAMES = new Set([
11
+ "requireSingleWorksheet",
12
+ "limits",
13
+ ]);
14
+ export const FILE_OPTION_NAMES = new Set([
15
+ "requireSingleWorksheet",
16
+ "limits",
17
+ "overwrite",
18
+ ]);
19
+ export const LIMIT_NAMES = new Set([
20
+ "maxEntryCount",
21
+ "maxUncompressedBytes",
22
+ "maxXmlBytes",
23
+ "maxWorksheetXmlBytes",
24
+ "maxStylesXmlBytes",
25
+ "maxMetadataXmlBytes",
26
+ "maxRelationshipsXmlBytes",
27
+ ]);
28
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,cAAc,GAAsC,MAAM,CAAC,MAAM,CAAC;IAC7E,aAAa,EAAE,MAAM;IACrB,oBAAoB,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI;IACvC,WAAW,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI;IAC9B,oBAAoB,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI;IACvC,iBAAiB,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;IACnC,mBAAmB,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;IACrC,wBAAwB,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;CAC1C,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,qBAAqB,GAAwB,IAAI,GAAG,CAAC;IAChE,wBAAwB;IACxB,QAAQ;CACT,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,iBAAiB,GAAwB,IAAI,GAAG,CAAC;IAC5D,wBAAwB;IACxB,QAAQ;IACR,WAAW;CACZ,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,WAAW,GAAwB,IAAI,GAAG,CAAC;IACtD,eAAe;IACf,sBAAsB;IACtB,aAAa;IACb,sBAAsB;IACtB,mBAAmB;IACnB,qBAAqB;IACrB,0BAA0B;CAC3B,CAAC,CAAC","sourcesContent":["import type { ExcelSanitizationLimits } from \"./types.js\";\n\nexport const DEFAULT_LIMITS: Readonly<ExcelSanitizationLimits> = Object.freeze({\n maxEntryCount: 10_000,\n maxUncompressedBytes: 512 * 1024 * 1024,\n maxXmlBytes: 512 * 1024 * 1024,\n maxWorksheetXmlBytes: 256 * 1024 * 1024,\n maxStylesXmlBytes: 64 * 1024 * 1024,\n maxMetadataXmlBytes: 16 * 1024 * 1024,\n maxRelationshipsXmlBytes: 8 * 1024 * 1024,\n});\n\nexport const SANITIZE_OPTION_NAMES: ReadonlySet<string> = new Set([\n \"requireSingleWorksheet\",\n \"limits\",\n]);\n\nexport const FILE_OPTION_NAMES: ReadonlySet<string> = new Set([\n \"requireSingleWorksheet\",\n \"limits\",\n \"overwrite\",\n]);\n\nexport const LIMIT_NAMES: ReadonlySet<string> = new Set([\n \"maxEntryCount\",\n \"maxUncompressedBytes\",\n \"maxXmlBytes\",\n \"maxWorksheetXmlBytes\",\n \"maxStylesXmlBytes\",\n \"maxMetadataXmlBytes\",\n \"maxRelationshipsXmlBytes\",\n]);\n"]}
package/dist/errors.d.ts CHANGED
@@ -17,4 +17,3 @@ export declare class ExcelSanitizationError extends Error {
17
17
  export declare function createError(code: ExcelSanitizationErrorCode, options?: CreateErrorOptions): ExcelSanitizationError;
18
18
  export declare function limitExceeded(limit: SafetyLimitName, configured: number, actual: number, partName?: string): ExcelSanitizationError;
19
19
  export {};
20
- //# sourceMappingURL=errors.d.ts.map
package/dist/errors.js ADDED
@@ -0,0 +1,38 @@
1
+ const ERROR_MESSAGES = Object.freeze({
2
+ ERR_INVALID_ARGUMENT: "Invalid argument",
3
+ ERR_UNSUPPORTED_EXTENSION: "Supported extensions are .xls, .xlsx, and .xlsm; input and output formats must match",
4
+ ERR_INVALID_XLS: "Invalid or unsupported XLS workbook",
5
+ ERR_INVALID_XLSX: "Invalid or unsupported OOXML workbook",
6
+ ERR_LIMIT_EXCEEDED: "Excel workbook exceeds a configured safety limit",
7
+ ERR_WORKSHEET_COUNT: "The workbook must contain exactly one worksheet",
8
+ ERR_SAME_PATH: "Input and output paths must be different",
9
+ ERR_OUTPUT_EXISTS: "The output file already exists",
10
+ ERR_FILE_READ: "Unable to read the input Excel file",
11
+ ERR_FILE_WRITE: "Unable to write the sanitized Excel file",
12
+ });
13
+ export class ExcelSanitizationError extends Error {
14
+ name = "ExcelSanitizationError";
15
+ code;
16
+ details;
17
+ constructor(code, message, options = {}) {
18
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
19
+ this.code = code;
20
+ if (options.details !== undefined) {
21
+ this.details = Object.freeze({ ...options.details });
22
+ }
23
+ }
24
+ }
25
+ export function createError(code, options = {}) {
26
+ return new ExcelSanitizationError(code, options.message ?? ERROR_MESSAGES[code], options);
27
+ }
28
+ export function limitExceeded(limit, configured, actual, partName) {
29
+ return createError("ERR_LIMIT_EXCEEDED", {
30
+ details: {
31
+ limit,
32
+ configured,
33
+ actual,
34
+ ...(partName === undefined ? {} : { partName }),
35
+ },
36
+ });
37
+ }
38
+ //# sourceMappingURL=errors.js.map