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
|
@@ -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"]}
|
package/dist/constants.d.ts
CHANGED
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAyBA,MAAM,cAAc,GAAyD,MAAM,CAAC,MAAM,CAAC;IACzF,oBAAoB,EAAE,kBAAkB;IACxC,yBAAyB,EACvB,sFAAsF;IACxF,eAAe,EAAE,qCAAqC;IACtD,gBAAgB,EAAE,uCAAuC;IACzD,kBAAkB,EAAE,kDAAkD;IACtE,mBAAmB,EAAE,iDAAiD;IACtE,aAAa,EAAE,0CAA0C;IACzD,iBAAiB,EAAE,gCAAgC;IACnD,aAAa,EAAE,qCAAqC;IACpD,cAAc,EAAE,0CAA0C;CAC3D,CAAC,CAAC;AAEH,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IAC7B,IAAI,GAAG,wBAAwB,CAAC;IACzC,IAAI,CAA6B;IACjC,OAAO,CAAqC;IAErD,YACE,IAAgC,EAChC,OAAe,EACf,OAAO,GAAkC,EAAE;QAE3C,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QACnF,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAClC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,WAAW,CACzB,IAAgC,EAChC,OAAO,GAAuB,EAAE;IAEhC,OAAO,IAAI,sBAAsB,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,IAAI,cAAc,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;AAC5F,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,KAAsB,EACtB,UAAkB,EAClB,MAAc,EACd,QAAiB;IAEjB,OAAO,WAAW,CAAC,oBAAoB,EAAE;QACvC,OAAO,EAAE;YACP,KAAK;YACL,UAAU;YACV,MAAM;YACN,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;SAChD;KACF,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { ExcelSanitizationLimits } from \"./types.js\";\n\nexport type ExcelSanitizationErrorCode =\n | \"ERR_INVALID_ARGUMENT\"\n | \"ERR_UNSUPPORTED_EXTENSION\"\n | \"ERR_INVALID_XLS\"\n | \"ERR_INVALID_XLSX\"\n | \"ERR_LIMIT_EXCEEDED\"\n | \"ERR_WORKSHEET_COUNT\"\n | \"ERR_SAME_PATH\"\n | \"ERR_OUTPUT_EXISTS\"\n | \"ERR_FILE_READ\"\n | \"ERR_FILE_WRITE\";\n\nexport interface ExcelSanitizationErrorOptions {\n cause?: unknown;\n details?: Readonly<Record<string, unknown>>;\n}\n\ninterface CreateErrorOptions extends ExcelSanitizationErrorOptions {\n message?: string;\n}\n\nexport type SafetyLimitName = keyof ExcelSanitizationLimits | \"partBytes\";\n\nconst ERROR_MESSAGES: Readonly<Record<ExcelSanitizationErrorCode, string>> = Object.freeze({\n ERR_INVALID_ARGUMENT: \"Invalid argument\",\n ERR_UNSUPPORTED_EXTENSION:\n \"Supported extensions are .xls, .xlsx, and .xlsm; input and output formats must match\",\n ERR_INVALID_XLS: \"Invalid or unsupported XLS workbook\",\n ERR_INVALID_XLSX: \"Invalid or unsupported OOXML workbook\",\n ERR_LIMIT_EXCEEDED: \"Excel workbook exceeds a configured safety limit\",\n ERR_WORKSHEET_COUNT: \"The workbook must contain exactly one worksheet\",\n ERR_SAME_PATH: \"Input and output paths must be different\",\n ERR_OUTPUT_EXISTS: \"The output file already exists\",\n ERR_FILE_READ: \"Unable to read the input Excel file\",\n ERR_FILE_WRITE: \"Unable to write the sanitized Excel file\",\n});\n\nexport class ExcelSanitizationError extends Error {\n override readonly name = \"ExcelSanitizationError\";\n readonly code: ExcelSanitizationErrorCode;\n readonly details?: Readonly<Record<string, unknown>>;\n\n constructor(\n code: ExcelSanitizationErrorCode,\n message: string,\n options: ExcelSanitizationErrorOptions = {},\n ) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.code = code;\n if (options.details !== undefined) {\n this.details = Object.freeze({ ...options.details });\n }\n }\n}\n\nexport function createError(\n code: ExcelSanitizationErrorCode,\n options: CreateErrorOptions = {},\n): ExcelSanitizationError {\n return new ExcelSanitizationError(code, options.message ?? ERROR_MESSAGES[code], options);\n}\n\nexport function limitExceeded(\n limit: SafetyLimitName,\n configured: number,\n actual: number,\n partName?: string,\n): ExcelSanitizationError {\n return createError(\"ERR_LIMIT_EXCEEDED\", {\n details: {\n limit,\n configured,\n actual,\n ...(partName === undefined ? {} : { partName }),\n },\n });\n}\n"]}
|
package/dist/formats.d.ts
CHANGED
|
@@ -2,4 +2,3 @@ export type ExcelFileFormat = "xls" | "xlsx" | "xlsm";
|
|
|
2
2
|
export declare function excelFileFormat(name: unknown): ExcelFileFormat | undefined;
|
|
3
3
|
export declare function isExcelFilename(name: unknown): name is string;
|
|
4
4
|
export declare function isXlsxFilename(name: unknown): name is string;
|
|
5
|
-
//# sourceMappingURL=formats.d.ts.map
|
package/dist/formats.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function excelFileFormat(name) {
|
|
2
|
+
if (typeof name !== "string") {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
const extension = /\.(xls|xlsx|xlsm)$/i.exec(name)?.[1]?.toLowerCase();
|
|
6
|
+
return extension === "xls" || extension === "xlsx" || extension === "xlsm"
|
|
7
|
+
? extension
|
|
8
|
+
: undefined;
|
|
9
|
+
}
|
|
10
|
+
export function isExcelFilename(name) {
|
|
11
|
+
return excelFileFormat(name) !== undefined;
|
|
12
|
+
}
|
|
13
|
+
export function isXlsxFilename(name) {
|
|
14
|
+
return excelFileFormat(name) === "xlsx";
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=formats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formats.js","sourceRoot":"","sources":["../src/formats.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,eAAe,CAAC,IAAa;IAC3C,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,SAAS,GAAG,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;IACvE,OAAO,SAAS,KAAK,KAAK,IAAI,SAAS,KAAK,MAAM,IAAI,SAAS,KAAK,MAAM;QACxE,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAa;IAC3C,OAAO,eAAe,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAa;IAC1C,OAAO,eAAe,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC;AAC1C,CAAC","sourcesContent":["export type ExcelFileFormat = \"xls\" | \"xlsx\" | \"xlsm\";\n\nexport function excelFileFormat(name: unknown): ExcelFileFormat | undefined {\n if (typeof name !== \"string\") {\n return undefined;\n }\n const extension = /\\.(xls|xlsx|xlsm)$/i.exec(name)?.[1]?.toLowerCase();\n return extension === \"xls\" || extension === \"xlsx\" || extension === \"xlsm\"\n ? extension\n : undefined;\n}\n\nexport function isExcelFilename(name: unknown): name is string {\n return excelFileFormat(name) !== undefined;\n}\n\nexport function isXlsxFilename(name: unknown): name is string {\n return excelFileFormat(name) === \"xlsx\";\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,3 @@ export { ExcelSanitizationError } from "./errors.js";
|
|
|
5
5
|
export type { ExcelSanitizationErrorCode, ExcelSanitizationErrorOptions } from "./errors.js";
|
|
6
6
|
export type { ExcelFileFormat } from "./formats.js";
|
|
7
7
|
export type { ExcelSanitizationLimits, SanitizeExcelFileOptions, SanitizeExcelOptions, } from "./types.js";
|
|
8
|
-
//# sourceMappingURL=index.d.ts.map
|