clear-excel 1.0.3 → 1.0.5
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.
- package/CHANGELOG.md +36 -0
- package/README.md +68 -165
- package/dist/api/sanitize-excel-file.d.ts +0 -1
- package/dist/api/sanitize-excel-file.js +176 -0
- package/dist/api/sanitize-excel-file.js.map +1 -0
- package/dist/api/sanitize-excel.d.ts +0 -1
- package/dist/api/sanitize-excel.js +25 -0
- package/dist/api/sanitize-excel.js.map +1 -0
- package/dist/biff/workbook.d.ts +0 -1
- package/dist/biff/workbook.js +169 -0
- package/dist/biff/workbook.js.map +1 -0
- package/dist/constants.d.ts +0 -1
- package/dist/constants.js +28 -0
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +0 -1
- package/dist/errors.js +38 -0
- package/dist/errors.js.map +1 -0
- package/dist/formats.d.ts +0 -1
- package/dist/formats.js +16 -0
- package/dist/formats.js.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +4 -2776
- package/dist/index.js.map +1 -1
- package/dist/ooxml/cleanup.d.ts +0 -1
- package/dist/ooxml/cleanup.js +30 -0
- package/dist/ooxml/cleanup.js.map +1 -0
- package/dist/ooxml/constants.d.ts +0 -1
- package/dist/ooxml/constants.js +19 -0
- package/dist/ooxml/constants.js.map +1 -0
- package/dist/ooxml/parts.d.ts +1 -1
- package/dist/ooxml/parts.js +94 -0
- package/dist/ooxml/parts.js.map +1 -0
- package/dist/ooxml/relationships.d.ts +0 -1
- package/dist/ooxml/relationships.js +132 -0
- package/dist/ooxml/relationships.js.map +1 -0
- package/dist/ooxml/styles.d.ts +0 -1
- package/dist/ooxml/styles.js +67 -0
- package/dist/ooxml/styles.js.map +1 -0
- package/dist/ooxml/workbook.d.ts +0 -1
- package/dist/ooxml/workbook.js +119 -0
- package/dist/ooxml/workbook.js.map +1 -0
- package/dist/ooxml/xml.d.ts +26 -2
- package/dist/ooxml/xml.js +304 -0
- package/dist/ooxml/xml.js.map +1 -0
- package/dist/options.d.ts +0 -1
- package/dist/options.js +80 -0
- package/dist/options.js.map +1 -0
- package/dist/types.d.ts +0 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/zip/central-directory.d.ts +0 -1
- package/dist/zip/central-directory.js +131 -0
- package/dist/zip/central-directory.js.map +1 -0
- package/package.json +5 -10
- package/dist/api/sanitize-excel-file.d.ts.map +0 -1
- package/dist/api/sanitize-excel.d.ts.map +0 -1
- package/dist/biff/workbook.d.ts.map +0 -1
- package/dist/constants.d.ts.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/formats.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/ooxml/cleanup.d.ts.map +0 -1
- package/dist/ooxml/constants.d.ts.map +0 -1
- package/dist/ooxml/parts.d.ts.map +0 -1
- package/dist/ooxml/relationships.d.ts.map +0 -1
- package/dist/ooxml/styles.d.ts.map +0 -1
- package/dist/ooxml/workbook.d.ts.map +0 -1
- package/dist/ooxml/xml.d.ts.map +0 -1
- package/dist/options.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/zip/central-directory.d.ts.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,40 @@
|
|
|
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.5] - 2026-10-11
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- Simplify the README to focus on installation, usage, options, errors, and
|
|
11
|
+
supported workbook limitations.
|
|
12
|
+
|
|
13
|
+
## [1.0.4] - 2026-10-09
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Preserve existing output files when exclusive creation fails and remove only
|
|
18
|
+
partial files created by the current write.
|
|
19
|
+
- Reject source-file aliases through symbolic links and hard links, and preserve
|
|
20
|
+
existing output permissions during atomic overwrite.
|
|
21
|
+
- Preserve worksheet text, CDATA, XML comments, and unrelated elements during
|
|
22
|
+
namespace-aware drawing and comment cleanup.
|
|
23
|
+
- Resolve sheet relationship IDs by namespace URI and reject missing or invalid
|
|
24
|
+
relationships before pruning styles.
|
|
25
|
+
- Decode UTF-8 and UTF-16 XML consistently and update encoding declarations when
|
|
26
|
+
writing UTF-8 output.
|
|
27
|
+
- Enforce the actual aggregate uncompressed size of all retained ZIP parts,
|
|
28
|
+
including binary VBA projects with forged ZIP size metadata.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Replace Vite with one NodeNext TypeScript build for modular ESM output and
|
|
33
|
+
declarations, eliminating the duplicate bundled `cfb` dependency.
|
|
34
|
+
- Clean build artifacts before compilation, embed sources in JavaScript maps,
|
|
35
|
+
and stop publishing declaration maps that reference unavailable source files.
|
|
36
|
+
- Move the transitive `uuid` override to pnpm workspace configuration.
|
|
37
|
+
- Verify the installed package with real `.xls`, `.xlsx`, and `.xlsm` workbooks
|
|
38
|
+
through both APIs, including retained values, formulas, merges, and macros.
|
|
39
|
+
|
|
6
40
|
## [1.0.0] - 2026-08-19
|
|
7
41
|
|
|
8
42
|
### Changed
|
|
@@ -21,5 +55,7 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
21
55
|
- Initial release with buffer and file APIs for sanitizing `.xls`, `.xlsx`,
|
|
22
56
|
and `.xlsm` workbooks.
|
|
23
57
|
|
|
58
|
+
[1.0.5]: https://www.npmjs.com/package/clear-excel/v/1.0.5
|
|
59
|
+
[1.0.4]: https://www.npmjs.com/package/clear-excel/v/1.0.4
|
|
24
60
|
[1.0.0]: https://www.npmjs.com/package/clear-excel/v/1.0.0
|
|
25
61
|
[0.1.0]: https://www.npmjs.com/package/clear-excel/v/0.1.0
|
package/README.md
CHANGED
|
@@ -1,63 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
# clear-excel
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Remove images, drawings, comments, controls, and embedded objects from `.xls`,
|
|
4
|
+
`.xlsx`, and `.xlsm` workbooks. Cell values, formulas, merged cells, worksheets,
|
|
5
|
+
and used styles are preserved. Unused styles are also removed from `.xlsx` and
|
|
6
|
+
`.xlsm` workbooks.
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
without rebuilding their data model.</strong></p>
|
|
8
|
+
## Install
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<a href="https://github.com/Vlad1slav1vanov/clear-excel/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/Vlad1slav1vanov/clear-excel/ci.yml?branch=main&style=flat-square&logo=github&label=CI"></a>
|
|
12
|
-
<a href="https://www.npmjs.com/package/clear-excel"><img alt="Node.js" src="https://img.shields.io/node/v/clear-excel?style=flat-square&logo=nodedotjs"></a>
|
|
13
|
-
<a href="./LICENSE"><img alt="License" src="https://img.shields.io/npm/l/clear-excel?style=flat-square"></a>
|
|
14
|
-
</p>
|
|
15
|
-
|
|
16
|
-
<p>
|
|
17
|
-
<a href="https://www.npmjs.com/package/clear-excel">npm</a> ·
|
|
18
|
-
<a href="https://github.com/Vlad1slav1vanov/clear-excel/releases">Releases</a> ·
|
|
19
|
-
<a href="./CHANGELOG.md">Changelog</a> ·
|
|
20
|
-
<a href="./CONTRIBUTING.md">Contributing</a> ·
|
|
21
|
-
<a href="./SECURITY.md">Security</a>
|
|
22
|
-
</p>
|
|
23
|
-
|
|
24
|
-
</div>
|
|
25
|
-
|
|
26
|
-
`clear-excel` is an ESM-only Node.js library for sanitizing legacy `.xls` and
|
|
27
|
-
OOXML `.xlsx`/`.xlsm` workbooks while preserving worksheet data and structure.
|
|
28
|
-
It has no framework or consumer-side build requirement.
|
|
29
|
-
|
|
30
|
-
## What it removes
|
|
31
|
-
|
|
32
|
-
- drawings, images, comments, controls, and embedded OLE objects;
|
|
33
|
-
- VML drawings, threaded comments, and person metadata in OOXML workbooks;
|
|
34
|
-
- unused `cellXfs` style records in OOXML workbooks;
|
|
35
|
-
- BIFF drawing, note, object, and picture records plus embedded object storages
|
|
36
|
-
in legacy `.xls` workbooks.
|
|
37
|
-
|
|
38
|
-
Values, formulas and cached results, merged cells, worksheets, and styles that
|
|
39
|
-
are still referenced by cells, rows, or columns are retained. `.xlsm` VBA
|
|
40
|
-
projects and legacy `.xls` VBA storages are preserved; the library does not
|
|
41
|
-
execute or remove macros.
|
|
10
|
+
```sh
|
|
11
|
+
npm install clear-excel
|
|
12
|
+
```
|
|
42
13
|
|
|
43
|
-
|
|
14
|
+
Requires Node.js 22 or newer. ESM only; TypeScript declarations are included.
|
|
44
15
|
|
|
45
|
-
|
|
46
|
-
- Excel 97-2003 BIFF8 `.xls`, OOXML `.xlsx`, or macro-enabled `.xlsm` input.
|
|
16
|
+
## Usage
|
|
47
17
|
|
|
48
|
-
|
|
18
|
+
### Files
|
|
49
19
|
|
|
50
|
-
|
|
20
|
+
```js
|
|
21
|
+
import { sanitizeExcelFile } from "clear-excel";
|
|
51
22
|
|
|
52
|
-
|
|
53
|
-
pnpm add clear-excel
|
|
23
|
+
await sanitizeExcelFile("report.xlsx", "report.cleaned.xlsx");
|
|
54
24
|
```
|
|
55
25
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
26
|
+
`sanitizeExcelFile(inputPath, outputPath, options?)` returns `Promise<void>`.
|
|
27
|
+
Input and output must refer to different files and use the same supported
|
|
28
|
+
extension (`.xls`, `.xlsx`, or `.xlsm`, case-insensitive). The output directory
|
|
29
|
+
must already exist. To replace an existing output file, set `overwrite: true`.
|
|
59
30
|
|
|
60
|
-
|
|
31
|
+
### Buffers
|
|
61
32
|
|
|
62
33
|
```js
|
|
63
34
|
import { readFile, writeFile } from "node:fs/promises";
|
|
@@ -68,86 +39,65 @@ const output = await sanitizeExcel(input);
|
|
|
68
39
|
await writeFile("report.cleaned.xlsx", output);
|
|
69
40
|
```
|
|
70
41
|
|
|
71
|
-
`sanitizeExcel` accepts a Node.js `Buffer
|
|
72
|
-
|
|
73
|
-
|
|
42
|
+
`sanitizeExcel(buffer, options?)` accepts a Node.js `Buffer` and returns
|
|
43
|
+
`Promise<Buffer>`. The input buffer is not modified; the output keeps the
|
|
44
|
+
original workbook format.
|
|
45
|
+
|
|
46
|
+
### Filename checks
|
|
74
47
|
|
|
75
|
-
|
|
48
|
+
The package also exports `isExcelFilename(name)` for `.xls`, `.xlsx`, and `.xlsm`,
|
|
49
|
+
and `isXlsxFilename(name)` for `.xlsx` only. Both return a boolean, ignore case,
|
|
50
|
+
and check the extension without reading the file.
|
|
76
51
|
|
|
77
|
-
|
|
78
|
-
ECMAScript imports. Runtime exports, option types, error codes, and sanitizing
|
|
79
|
-
behavior otherwise remain compatible with version 0.1.
|
|
52
|
+
## Options
|
|
80
53
|
|
|
81
|
-
|
|
54
|
+
| Option | Default | Description |
|
|
55
|
+
| ------------------------ | ---------------- | ------------------------------------------------------- |
|
|
56
|
+
| `requireSingleWorksheet` | `false` | Reject workbooks that do not contain exactly one sheet. |
|
|
57
|
+
| `limits` | `DEFAULT_LIMITS` | Override individual size and entry-count limits. |
|
|
58
|
+
| `overwrite` | `false` | Replace an existing output; `sanitizeExcelFile` only. |
|
|
82
59
|
|
|
83
60
|
```js
|
|
84
61
|
import { sanitizeExcelFile } from "clear-excel";
|
|
85
62
|
|
|
86
|
-
await sanitizeExcelFile("report.xlsx", "report.cleaned.xlsx");
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
The input and output paths must be different and use the same supported
|
|
90
|
-
extension: `.xls`, `.xlsx`, or `.xlsm` (case-insensitive). The file API does not
|
|
91
|
-
convert between formats. The output directory must already exist. An existing
|
|
92
|
-
output file is not replaced unless `overwrite: true` is explicitly supplied:
|
|
93
|
-
|
|
94
|
-
```js
|
|
95
63
|
await sanitizeExcelFile("report.xlsx", "report.cleaned.xlsx", {
|
|
96
64
|
overwrite: true,
|
|
97
65
|
requireSingleWorksheet: true,
|
|
66
|
+
limits: {
|
|
67
|
+
maxUncompressedBytes: 128 * 1024 * 1024,
|
|
68
|
+
},
|
|
98
69
|
});
|
|
99
70
|
```
|
|
100
71
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Both sanitizing functions accept these options:
|
|
104
|
-
|
|
105
|
-
| Option | Type | Default | Description |
|
|
106
|
-
| ------------------------ | --------- | --------- | ------------------------------------------------------- |
|
|
107
|
-
| `requireSingleWorksheet` | `boolean` | `false` | Reject workbooks that do not contain exactly one sheet. |
|
|
108
|
-
| `limits` | `object` | See below | Override individual ZIP/XML safety limits. |
|
|
109
|
-
|
|
110
|
-
`sanitizeExcelFile` additionally accepts `overwrite`, which defaults to
|
|
111
|
-
`false`.
|
|
72
|
+
### Limits
|
|
112
73
|
|
|
113
|
-
The exported `DEFAULT_LIMITS`
|
|
74
|
+
The exported `DEFAULT_LIMITS` contains these defaults. Only specified values
|
|
75
|
+
are overridden; each must be a positive safe integer. Sizes are in bytes.
|
|
114
76
|
|
|
115
77
|
| Limit | Default |
|
|
116
78
|
| -------------------------- | ---------------------------: |
|
|
117
79
|
| `maxEntryCount` | 10,000 entries |
|
|
118
80
|
| `maxUncompressedBytes` | 512 MiB |
|
|
119
|
-
| `maxXmlBytes` |
|
|
81
|
+
| `maxXmlBytes` | 512 MiB aggregate XML reads |
|
|
120
82
|
| `maxWorksheetXmlBytes` | 256 MiB per worksheet |
|
|
121
83
|
| `maxStylesXmlBytes` | 64 MiB |
|
|
122
84
|
| `maxMetadataXmlBytes` | 16 MiB per metadata part |
|
|
123
85
|
| `maxRelationshipsXmlBytes` | 8 MiB per relationships part |
|
|
124
86
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
```js
|
|
128
|
-
const output = await sanitizeExcel(input, {
|
|
129
|
-
limits: {
|
|
130
|
-
maxUncompressedBytes: 128 * 1024 * 1024,
|
|
131
|
-
maxWorksheetXmlBytes: 64 * 1024 * 1024,
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Every limit must be a positive safe integer. Raising limits can significantly
|
|
137
|
-
increase memory usage. For `.xls`, `maxEntryCount` and
|
|
138
|
-
`maxUncompressedBytes` apply to the CFB container; the XML-specific limits apply
|
|
139
|
-
only to `.xlsx` and `.xlsm`.
|
|
87
|
+
For `.xls`, only `maxEntryCount` and `maxUncompressedBytes` apply. XML limits
|
|
88
|
+
apply to `.xlsx` and `.xlsm`.
|
|
140
89
|
|
|
141
90
|
## Errors
|
|
142
91
|
|
|
143
|
-
|
|
144
|
-
instead of matching the
|
|
92
|
+
Both sanitizing functions reject with `ExcelSanitizationError`. Check `code`
|
|
93
|
+
instead of matching the error message. `details` contains additional context
|
|
94
|
+
when available, and `cause` holds the original error when one exists.
|
|
145
95
|
|
|
146
96
|
```js
|
|
147
|
-
import { ExcelSanitizationError,
|
|
97
|
+
import { ExcelSanitizationError, sanitizeExcelFile } from "clear-excel";
|
|
148
98
|
|
|
149
99
|
try {
|
|
150
|
-
await
|
|
100
|
+
await sanitizeExcelFile("report.xlsx", "report.cleaned.xlsx");
|
|
151
101
|
} catch (error) {
|
|
152
102
|
if (error instanceof ExcelSanitizationError) {
|
|
153
103
|
console.error(error.code, error.details);
|
|
@@ -156,73 +106,26 @@ try {
|
|
|
156
106
|
}
|
|
157
107
|
```
|
|
158
108
|
|
|
159
|
-
| Code | Meaning
|
|
160
|
-
| --------------------------- |
|
|
161
|
-
| `ERR_INVALID_ARGUMENT` | An argument or option has an invalid type or value.
|
|
162
|
-
| `ERR_UNSUPPORTED_EXTENSION` | Paths use an unsupported or mismatched extension.
|
|
163
|
-
| `ERR_INVALID_XLS` | The
|
|
164
|
-
| `ERR_INVALID_XLSX` | The
|
|
165
|
-
| `ERR_LIMIT_EXCEEDED` | A configured
|
|
166
|
-
| `ERR_WORKSHEET_COUNT` | Exactly one worksheet was required.
|
|
167
|
-
| `ERR_SAME_PATH` | Input and output
|
|
168
|
-
| `ERR_OUTPUT_EXISTS` | Output exists and `overwrite` is false.
|
|
169
|
-
| `ERR_FILE_READ` | The input could not be read.
|
|
170
|
-
| `ERR_FILE_WRITE` | The
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
directory, rejects ZIP64 and multi-disk archives, rejects unsafe entry paths,
|
|
179
|
-
and checks declared entry counts and expansion sizes. Required XML parts are
|
|
180
|
-
inflated lazily with both per-part and aggregate runtime budgets, including
|
|
181
|
-
protection when ZIP size metadata has been forged.
|
|
182
|
-
|
|
183
|
-
For `.xls`, the library validates CFB entry and byte limits, parses the BIFF
|
|
184
|
-
record stream without evaluating formulas, removes drawing-layer records, and
|
|
185
|
-
rewrites worksheet offsets after records move. Workbook records are not
|
|
186
|
-
converted through an OOXML or generic tabular representation.
|
|
187
|
-
|
|
188
|
-
JSZip still loads the ZIP directory and regenerates the complete workbook in
|
|
189
|
-
memory. Plan for memory usage above the input size, especially when increasing
|
|
190
|
-
the defaults. The sanitizer does not evaluate formulas, execute embedded
|
|
191
|
-
content, or execute VBA projects.
|
|
192
|
-
|
|
193
|
-
## Development
|
|
194
|
-
|
|
195
|
-
Install dependencies and run the complete verification suite:
|
|
196
|
-
|
|
197
|
-
```sh
|
|
198
|
-
pnpm install --frozen-lockfile
|
|
199
|
-
pnpm run check
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
`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.
|
|
206
|
-
|
|
207
|
-
Useful development commands:
|
|
208
|
-
|
|
209
|
-
```sh
|
|
210
|
-
pnpm run format
|
|
211
|
-
pnpm run lint
|
|
212
|
-
pnpm run check:types
|
|
213
|
-
pnpm test
|
|
214
|
-
pnpm run build
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the contribution and release
|
|
218
|
-
process.
|
|
219
|
-
|
|
220
|
-
## Security
|
|
221
|
-
|
|
222
|
-
Please report vulnerabilities privately by following
|
|
223
|
-
[SECURITY.md](./SECURITY.md). Do not open a public issue for a suspected
|
|
224
|
-
security vulnerability.
|
|
109
|
+
| Code | Meaning |
|
|
110
|
+
| --------------------------- | ---------------------------------------------------- |
|
|
111
|
+
| `ERR_INVALID_ARGUMENT` | An argument or option has an invalid type or value. |
|
|
112
|
+
| `ERR_UNSUPPORTED_EXTENSION` | Paths use an unsupported or mismatched extension. |
|
|
113
|
+
| `ERR_INVALID_XLS` | The XLS workbook is malformed or unsupported. |
|
|
114
|
+
| `ERR_INVALID_XLSX` | The XLSX/XLSM workbook is malformed or unsupported. |
|
|
115
|
+
| `ERR_LIMIT_EXCEEDED` | A configured size or entry-count limit was exceeded. |
|
|
116
|
+
| `ERR_WORKSHEET_COUNT` | Exactly one worksheet was required. |
|
|
117
|
+
| `ERR_SAME_PATH` | Input and output refer to the same file. |
|
|
118
|
+
| `ERR_OUTPUT_EXISTS` | Output exists and `overwrite` is false. |
|
|
119
|
+
| `ERR_FILE_READ` | The input could not be read. |
|
|
120
|
+
| `ERR_FILE_WRITE` | The output could not be written. |
|
|
121
|
+
|
|
122
|
+
## Limitations
|
|
123
|
+
|
|
124
|
+
- `.xls` input must use Excel 97–2003 BIFF8. Encrypted `.xls` files are not supported.
|
|
125
|
+
- VBA macros are preserved. The library does not execute macros or evaluate formulas.
|
|
126
|
+
- Workbooks are processed in memory. Memory usage can exceed the input size,
|
|
127
|
+
especially when increasing limits.
|
|
225
128
|
|
|
226
129
|
## License
|
|
227
130
|
|
|
228
|
-
[MIT](./LICENSE)
|
|
131
|
+
[MIT](./LICENSE)
|
|
@@ -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"]}
|
|
@@ -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"]}
|
package/dist/biff/workbook.d.ts
CHANGED
|
@@ -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
|