clear-excel 1.0.3 → 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.
- package/CHANGELOG.md +28 -0
- package/README.md +26 -6
- 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,304 @@
|
|
|
1
|
+
const XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";
|
|
2
|
+
const XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
|
|
3
|
+
const XML_ENTITIES = new Map([
|
|
4
|
+
["amp", "&"],
|
|
5
|
+
["quot", '"'],
|
|
6
|
+
["apos", "'"],
|
|
7
|
+
["lt", "<"],
|
|
8
|
+
["gt", ">"],
|
|
9
|
+
]);
|
|
10
|
+
const SPREADSHEET_NAMESPACES = new Set([
|
|
11
|
+
"",
|
|
12
|
+
"http://schemas.openxmlformats.org/spreadsheetml/2006/main",
|
|
13
|
+
"http://purl.oclc.org/ooxml/spreadsheetml/main",
|
|
14
|
+
]);
|
|
15
|
+
const XML_NAME = /^[A-Za-z_\u00c0-\uffff][A-Za-z0-9_.:\-\u00b7\u00c0-\uffff]*$/;
|
|
16
|
+
export function getAttribute(xml, name) {
|
|
17
|
+
return openingAttributes(xml).find((attribute) => attribute.name === name)?.value;
|
|
18
|
+
}
|
|
19
|
+
function openingAttributes(xml) {
|
|
20
|
+
if (!xml.startsWith("<")) {
|
|
21
|
+
return parseAttributes(` ${xml}`, -1);
|
|
22
|
+
}
|
|
23
|
+
const opening = xml.slice(1, findTagEnd(xml, 0) - 1);
|
|
24
|
+
const name = opening.match(/^\S+?(?=[\s/]|$)/)?.[0] ?? "";
|
|
25
|
+
return parseAttributes(opening.slice(name.length).replace(/\/\s*$/, ""), name.length + 1);
|
|
26
|
+
}
|
|
27
|
+
export function replaceAttribute(xml, name, value) {
|
|
28
|
+
const attribute = openingAttributes(xml).find((candidate) => candidate.name === name);
|
|
29
|
+
return attribute === undefined
|
|
30
|
+
? xml
|
|
31
|
+
: xml.slice(0, attribute.valueStart) + value + xml.slice(attribute.valueEnd);
|
|
32
|
+
}
|
|
33
|
+
export function getNamespacedAttribute(element, name, namespaceUris) {
|
|
34
|
+
return element.attributes.find((attribute) => attribute.localName === name && namespaceUris.includes(attribute.namespaceUri))?.value;
|
|
35
|
+
}
|
|
36
|
+
export function decodeXml(value) {
|
|
37
|
+
return value.replace(/&([^;]+);/g, (_entity, reference) => {
|
|
38
|
+
const replacement = XML_ENTITIES.get(reference);
|
|
39
|
+
if (replacement !== undefined) {
|
|
40
|
+
return replacement;
|
|
41
|
+
}
|
|
42
|
+
const numeric = reference.startsWith("#x")
|
|
43
|
+
? /^#x[\da-f]+$/i.test(reference) && Number.parseInt(reference.slice(2), 16)
|
|
44
|
+
: /^#\d+$/.test(reference) && Number.parseInt(reference.slice(1), 10);
|
|
45
|
+
if (typeof numeric === "number" &&
|
|
46
|
+
(numeric === 9 || numeric === 10 || numeric === 13 || numeric >= 32) &&
|
|
47
|
+
numeric <= 0x10ffff &&
|
|
48
|
+
!(numeric >= 0xd800 && numeric <= 0xdfff)) {
|
|
49
|
+
return String.fromCodePoint(numeric);
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`Invalid XML character reference: &${reference};`);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export function isSpreadsheetElement(element, name) {
|
|
55
|
+
return (SPREADSHEET_NAMESPACES.has(element.namespaceUri) &&
|
|
56
|
+
(name === undefined || element.localName === name));
|
|
57
|
+
}
|
|
58
|
+
// The scanner keeps only the open-element stack, preserving source spans without
|
|
59
|
+
// materializing a DOM or interpreting text, CDATA, comments, or processing instructions.
|
|
60
|
+
function* scanXml(xml) {
|
|
61
|
+
const stack = [];
|
|
62
|
+
let position = 0;
|
|
63
|
+
while (position < xml.length) {
|
|
64
|
+
const start = xml.indexOf("<", position);
|
|
65
|
+
if (start === -1) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
if (xml.startsWith("<!--", start) ||
|
|
69
|
+
xml.startsWith("<![CDATA[", start) ||
|
|
70
|
+
xml.startsWith("<?", start)) {
|
|
71
|
+
const delimiter = xml.startsWith("<!--", start)
|
|
72
|
+
? "-->"
|
|
73
|
+
: xml.startsWith("<?", start)
|
|
74
|
+
? "?>"
|
|
75
|
+
: "]]>";
|
|
76
|
+
const end = xml.indexOf(delimiter, start + (delimiter === "?>" ? 2 : delimiter === "-->" ? 4 : 9));
|
|
77
|
+
if (end === -1) {
|
|
78
|
+
throw new Error("Unterminated XML comment, CDATA, or processing instruction");
|
|
79
|
+
}
|
|
80
|
+
position = end + delimiter.length;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (xml.startsWith("<!", start)) {
|
|
84
|
+
throw new Error("Unsupported XML declaration");
|
|
85
|
+
}
|
|
86
|
+
const end = findTagEnd(xml, start);
|
|
87
|
+
const body = xml.slice(start + 1, end - 1);
|
|
88
|
+
if (body.startsWith("/")) {
|
|
89
|
+
const name = body.slice(1).trim();
|
|
90
|
+
const frame = stack.pop();
|
|
91
|
+
if (!frame || frame.element.name !== name) {
|
|
92
|
+
throw new Error(`Mismatched XML closing element: ${name}`);
|
|
93
|
+
}
|
|
94
|
+
frame.element.closingStart = start;
|
|
95
|
+
frame.element.end = end;
|
|
96
|
+
yield { element: frame.element, opening: false };
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const name = body.match(/^\S+?(?=[\s/]|$)/)?.[0] ?? "";
|
|
100
|
+
validateName(name);
|
|
101
|
+
const selfClosing = /\/\s*$/.test(body);
|
|
102
|
+
const attributes = parseAttributes(body.slice(name.length).replace(/\/\s*$/, ""), start + 1 + name.length);
|
|
103
|
+
const inherited = stack.at(-1)?.namespaces ?? new Map([["xml", XML_NAMESPACE]]);
|
|
104
|
+
const declarations = attributes.filter((attribute) => attribute.name === "xmlns" || attribute.name.startsWith("xmlns:"));
|
|
105
|
+
let namespaces = inherited;
|
|
106
|
+
if (declarations.length > 0) {
|
|
107
|
+
const updated = new Map(inherited);
|
|
108
|
+
for (const attribute of declarations) {
|
|
109
|
+
const prefix = attribute.name === "xmlns" ? "" : attribute.name.slice(6);
|
|
110
|
+
if (prefix === "xmlns" ||
|
|
111
|
+
(prefix === "xml") !== (attribute.value === XML_NAMESPACE) ||
|
|
112
|
+
attribute.value === XMLNS_NAMESPACE) {
|
|
113
|
+
throw new Error(`Invalid XML namespace declaration: ${attribute.name}`);
|
|
114
|
+
}
|
|
115
|
+
updated.set(prefix, attribute.value);
|
|
116
|
+
}
|
|
117
|
+
namespaces = updated;
|
|
118
|
+
}
|
|
119
|
+
const attributeNames = new Set();
|
|
120
|
+
for (const attribute of attributes) {
|
|
121
|
+
attribute.namespaceUri =
|
|
122
|
+
attribute.name === "xmlns" || attribute.name.startsWith("xmlns:")
|
|
123
|
+
? XMLNS_NAMESPACE
|
|
124
|
+
: resolveNamespace(attribute.name, namespaces, false);
|
|
125
|
+
const expandedName = `${attribute.namespaceUri}\0${attribute.localName}`;
|
|
126
|
+
if (attributeNames.has(expandedName)) {
|
|
127
|
+
throw new Error(`Duplicate XML attribute: ${attribute.name}`);
|
|
128
|
+
}
|
|
129
|
+
attributeNames.add(expandedName);
|
|
130
|
+
}
|
|
131
|
+
const element = {
|
|
132
|
+
name,
|
|
133
|
+
localName: name.split(":").at(-1) ?? name,
|
|
134
|
+
namespaceUri: resolveNamespace(name, namespaces, true),
|
|
135
|
+
attributes,
|
|
136
|
+
start,
|
|
137
|
+
openingEnd: end,
|
|
138
|
+
closingStart: end,
|
|
139
|
+
end,
|
|
140
|
+
parent: stack.at(-1)?.element,
|
|
141
|
+
};
|
|
142
|
+
yield { element, opening: true };
|
|
143
|
+
if (selfClosing) {
|
|
144
|
+
yield { element, opening: false };
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
stack.push({ element, namespaces });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
position = end;
|
|
151
|
+
}
|
|
152
|
+
if (stack.length > 0) {
|
|
153
|
+
throw new Error(`Unclosed XML element: ${stack.at(-1)?.element.name}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function findTagEnd(xml, start) {
|
|
157
|
+
let quote = "";
|
|
158
|
+
for (let position = start + 1; position < xml.length; position += 1) {
|
|
159
|
+
const character = xml[position];
|
|
160
|
+
if (quote) {
|
|
161
|
+
if (character === quote) {
|
|
162
|
+
quote = "";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else if (character === '"' || character === "'") {
|
|
166
|
+
quote = character;
|
|
167
|
+
}
|
|
168
|
+
else if (character === ">") {
|
|
169
|
+
return position + 1;
|
|
170
|
+
}
|
|
171
|
+
else if (character === "<") {
|
|
172
|
+
throw new Error("Invalid XML opening element");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
throw new Error("Unterminated XML opening element");
|
|
176
|
+
}
|
|
177
|
+
function validateName(name) {
|
|
178
|
+
if (!XML_NAME.test(name) || name.split(":").length > 2 || name.endsWith(":")) {
|
|
179
|
+
throw new Error(`Invalid XML name: ${name}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function parseAttributes(xml, offset) {
|
|
183
|
+
const attributes = [];
|
|
184
|
+
const names = new Set();
|
|
185
|
+
let position = 0;
|
|
186
|
+
while (position < xml.length) {
|
|
187
|
+
const match = /^\s+([^\s=]+)\s*=\s*(["'])/.exec(xml.slice(position));
|
|
188
|
+
if (!match) {
|
|
189
|
+
if (xml.slice(position).trim() !== "") {
|
|
190
|
+
throw new Error("Invalid XML attributes");
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
const name = match[1] ?? "";
|
|
195
|
+
validateName(name);
|
|
196
|
+
if (names.has(name)) {
|
|
197
|
+
throw new Error(`Duplicate XML attribute: ${name}`);
|
|
198
|
+
}
|
|
199
|
+
names.add(name);
|
|
200
|
+
const valueStart = position + match[0].length;
|
|
201
|
+
const valueEnd = xml.indexOf(match[2] ?? '"', valueStart);
|
|
202
|
+
if (valueEnd === -1 || xml.slice(valueStart, valueEnd).includes("<")) {
|
|
203
|
+
throw new Error("Invalid XML attribute value");
|
|
204
|
+
}
|
|
205
|
+
attributes.push({
|
|
206
|
+
name,
|
|
207
|
+
localName: name.split(":").at(-1) ?? name,
|
|
208
|
+
namespaceUri: "",
|
|
209
|
+
value: decodeXml(xml.slice(valueStart, valueEnd)),
|
|
210
|
+
valueStart: offset + valueStart,
|
|
211
|
+
valueEnd: offset + valueEnd,
|
|
212
|
+
});
|
|
213
|
+
position = valueEnd + 1;
|
|
214
|
+
}
|
|
215
|
+
return attributes;
|
|
216
|
+
}
|
|
217
|
+
function resolveNamespace(name, namespaces, element) {
|
|
218
|
+
const colon = name.indexOf(":");
|
|
219
|
+
if (colon === -1) {
|
|
220
|
+
return element ? (namespaces.get("") ?? "") : "";
|
|
221
|
+
}
|
|
222
|
+
const prefix = name.slice(0, colon);
|
|
223
|
+
const namespace = namespaces.get(prefix);
|
|
224
|
+
if (!namespace) {
|
|
225
|
+
throw new Error(`Undeclared XML namespace prefix: ${prefix}`);
|
|
226
|
+
}
|
|
227
|
+
return namespace;
|
|
228
|
+
}
|
|
229
|
+
export function findElements(xml, name) {
|
|
230
|
+
const elements = [];
|
|
231
|
+
for (const { element, opening } of scanXml(xml)) {
|
|
232
|
+
if (opening && element.localName === name) {
|
|
233
|
+
elements.push(element);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return elements;
|
|
237
|
+
}
|
|
238
|
+
export function mapOpeningElements(xml, transform) {
|
|
239
|
+
const edits = [];
|
|
240
|
+
for (const { element, opening } of scanXml(xml)) {
|
|
241
|
+
if (opening) {
|
|
242
|
+
const original = xml.slice(element.start, element.openingEnd);
|
|
243
|
+
const text = transform(element, original);
|
|
244
|
+
if (text !== original) {
|
|
245
|
+
edits.push({ start: element.start, end: element.openingEnd, text });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return applyEdits(xml, edits);
|
|
250
|
+
}
|
|
251
|
+
export function removeElements(xml, predicate) {
|
|
252
|
+
const edits = [];
|
|
253
|
+
for (const { element, opening } of scanXml(xml)) {
|
|
254
|
+
if (!opening && predicate(element)) {
|
|
255
|
+
edits.push({ start: element.start, end: element.end, text: "" });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Elements close inside-out; removing an outer element also removes its descendants.
|
|
259
|
+
// oxlint-disable-next-line unicorn/no-array-sort
|
|
260
|
+
edits.sort((left, right) => left.start - right.start || right.end - left.end);
|
|
261
|
+
return applyEdits(xml, edits);
|
|
262
|
+
}
|
|
263
|
+
function applyEdits(xml, edits) {
|
|
264
|
+
const chunks = [];
|
|
265
|
+
let position = 0;
|
|
266
|
+
for (const edit of edits) {
|
|
267
|
+
if (edit.start < position) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
chunks.push(xml.slice(position, edit.start), edit.text);
|
|
271
|
+
position = edit.end;
|
|
272
|
+
}
|
|
273
|
+
chunks.push(xml.slice(position));
|
|
274
|
+
return chunks.join("");
|
|
275
|
+
}
|
|
276
|
+
export function findCollection(xml, name) {
|
|
277
|
+
const element = findElements(xml, name).find((candidate) => isSpreadsheetElement(candidate));
|
|
278
|
+
return element === undefined
|
|
279
|
+
? undefined
|
|
280
|
+
: {
|
|
281
|
+
body: xml.slice(element.openingEnd, element.closingStart),
|
|
282
|
+
full: xml.slice(element.start, element.end),
|
|
283
|
+
opening: xml.slice(element.start, element.openingEnd),
|
|
284
|
+
element,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
export function replaceCollection(xml, name, body, count) {
|
|
288
|
+
const collection = findCollection(xml, name);
|
|
289
|
+
if (!collection) {
|
|
290
|
+
return xml;
|
|
291
|
+
}
|
|
292
|
+
const attribute = collection.element.attributes.find((candidate) => candidate.name === "count");
|
|
293
|
+
const opening = (attribute
|
|
294
|
+
? collection.opening.slice(0, attribute.valueStart - collection.element.start) +
|
|
295
|
+
count +
|
|
296
|
+
collection.opening.slice(attribute.valueEnd - collection.element.start)
|
|
297
|
+
: collection.opening.replace(/\/?\s*>$/, ` count="${count}">`)).replace(/\/\s*>$/, ">");
|
|
298
|
+
return (xml.slice(0, collection.element.start) +
|
|
299
|
+
opening +
|
|
300
|
+
body +
|
|
301
|
+
`</${collection.element.name}>` +
|
|
302
|
+
xml.slice(collection.element.end));
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=xml.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xml.js","sourceRoot":"","sources":["../../src/ooxml/xml.ts"],"names":[],"mappings":"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","sourcesContent":["const XML_NAMESPACE = \"http://www.w3.org/XML/1998/namespace\";\nconst XMLNS_NAMESPACE = \"http://www.w3.org/2000/xmlns/\";\nconst XML_ENTITIES = new Map([\n [\"amp\", \"&\"],\n [\"quot\", '\"'],\n [\"apos\", \"'\"],\n [\"lt\", \"<\"],\n [\"gt\", \">\"],\n]);\nconst SPREADSHEET_NAMESPACES = new Set([\n \"\",\n \"http://schemas.openxmlformats.org/spreadsheetml/2006/main\",\n \"http://purl.oclc.org/ooxml/spreadsheetml/main\",\n]);\nconst XML_NAME = /^[A-Za-z_\\u00c0-\\uffff][A-Za-z0-9_.:\\-\\u00b7\\u00c0-\\uffff]*$/;\n\ninterface XmlAttribute {\n name: string;\n localName: string;\n namespaceUri: string;\n value: string;\n valueStart: number;\n valueEnd: number;\n}\n\nexport interface XmlElement {\n name: string;\n localName: string;\n namespaceUri: string;\n attributes: XmlAttribute[];\n start: number;\n openingEnd: number;\n closingStart: number;\n end: number;\n parent: XmlElement | undefined;\n}\n\ninterface XmlCollection {\n body: string;\n full: string;\n opening: string;\n element: XmlElement;\n}\n\ninterface XmlEdit {\n start: number;\n end: number;\n text: string;\n}\n\ninterface XmlFrame {\n element: XmlElement;\n namespaces: ReadonlyMap<string, string>;\n}\n\nexport function getAttribute(xml: string, name: string): string | undefined {\n return openingAttributes(xml).find((attribute) => attribute.name === name)?.value;\n}\n\nfunction openingAttributes(xml: string): XmlAttribute[] {\n if (!xml.startsWith(\"<\")) {\n return parseAttributes(` ${xml}`, -1);\n }\n const opening = xml.slice(1, findTagEnd(xml, 0) - 1);\n const name = opening.match(/^\\S+?(?=[\\s/]|$)/)?.[0] ?? \"\";\n return parseAttributes(opening.slice(name.length).replace(/\\/\\s*$/, \"\"), name.length + 1);\n}\n\nexport function replaceAttribute(xml: string, name: string, value: string): string {\n const attribute = openingAttributes(xml).find((candidate) => candidate.name === name);\n return attribute === undefined\n ? xml\n : xml.slice(0, attribute.valueStart) + value + xml.slice(attribute.valueEnd);\n}\n\nexport function getNamespacedAttribute(\n element: XmlElement,\n name: string,\n namespaceUris: readonly string[],\n): string | undefined {\n return element.attributes.find(\n (attribute) => attribute.localName === name && namespaceUris.includes(attribute.namespaceUri),\n )?.value;\n}\n\nexport function decodeXml(value: string): string {\n return value.replace(/&([^;]+);/g, (_entity, reference: string) => {\n const replacement = XML_ENTITIES.get(reference);\n if (replacement !== undefined) {\n return replacement;\n }\n const numeric = reference.startsWith(\"#x\")\n ? /^#x[\\da-f]+$/i.test(reference) && Number.parseInt(reference.slice(2), 16)\n : /^#\\d+$/.test(reference) && Number.parseInt(reference.slice(1), 10);\n if (\n typeof numeric === \"number\" &&\n (numeric === 9 || numeric === 10 || numeric === 13 || numeric >= 32) &&\n numeric <= 0x10ffff &&\n !(numeric >= 0xd800 && numeric <= 0xdfff)\n ) {\n return String.fromCodePoint(numeric);\n }\n throw new Error(`Invalid XML character reference: &${reference};`);\n });\n}\n\nexport function isSpreadsheetElement(element: XmlElement, name?: string): boolean {\n return (\n SPREADSHEET_NAMESPACES.has(element.namespaceUri) &&\n (name === undefined || element.localName === name)\n );\n}\n\n// The scanner keeps only the open-element stack, preserving source spans without\n// materializing a DOM or interpreting text, CDATA, comments, or processing instructions.\nfunction* scanXml(xml: string): Generator<{ element: XmlElement; opening: boolean }> {\n const stack: XmlFrame[] = [];\n let position = 0;\n while (position < xml.length) {\n const start = xml.indexOf(\"<\", position);\n if (start === -1) {\n break;\n }\n if (\n xml.startsWith(\"<!--\", start) ||\n xml.startsWith(\"<![CDATA[\", start) ||\n xml.startsWith(\"<?\", start)\n ) {\n const delimiter = xml.startsWith(\"<!--\", start)\n ? \"-->\"\n : xml.startsWith(\"<?\", start)\n ? \"?>\"\n : \"]]>\";\n const end = xml.indexOf(\n delimiter,\n start + (delimiter === \"?>\" ? 2 : delimiter === \"-->\" ? 4 : 9),\n );\n if (end === -1) {\n throw new Error(\"Unterminated XML comment, CDATA, or processing instruction\");\n }\n position = end + delimiter.length;\n continue;\n }\n if (xml.startsWith(\"<!\", start)) {\n throw new Error(\"Unsupported XML declaration\");\n }\n const end = findTagEnd(xml, start);\n const body = xml.slice(start + 1, end - 1);\n if (body.startsWith(\"/\")) {\n const name = body.slice(1).trim();\n const frame = stack.pop();\n if (!frame || frame.element.name !== name) {\n throw new Error(`Mismatched XML closing element: ${name}`);\n }\n frame.element.closingStart = start;\n frame.element.end = end;\n yield { element: frame.element, opening: false };\n } else {\n const name = body.match(/^\\S+?(?=[\\s/]|$)/)?.[0] ?? \"\";\n validateName(name);\n const selfClosing = /\\/\\s*$/.test(body);\n const attributes = parseAttributes(\n body.slice(name.length).replace(/\\/\\s*$/, \"\"),\n start + 1 + name.length,\n );\n const inherited = stack.at(-1)?.namespaces ?? new Map([[\"xml\", XML_NAMESPACE]]);\n const declarations = attributes.filter(\n (attribute) => attribute.name === \"xmlns\" || attribute.name.startsWith(\"xmlns:\"),\n );\n let namespaces = inherited;\n if (declarations.length > 0) {\n const updated = new Map(inherited);\n for (const attribute of declarations) {\n const prefix = attribute.name === \"xmlns\" ? \"\" : attribute.name.slice(6);\n if (\n prefix === \"xmlns\" ||\n (prefix === \"xml\") !== (attribute.value === XML_NAMESPACE) ||\n attribute.value === XMLNS_NAMESPACE\n ) {\n throw new Error(`Invalid XML namespace declaration: ${attribute.name}`);\n }\n updated.set(prefix, attribute.value);\n }\n namespaces = updated;\n }\n const attributeNames = new Set<string>();\n for (const attribute of attributes) {\n attribute.namespaceUri =\n attribute.name === \"xmlns\" || attribute.name.startsWith(\"xmlns:\")\n ? XMLNS_NAMESPACE\n : resolveNamespace(attribute.name, namespaces, false);\n const expandedName = `${attribute.namespaceUri}\\0${attribute.localName}`;\n if (attributeNames.has(expandedName)) {\n throw new Error(`Duplicate XML attribute: ${attribute.name}`);\n }\n attributeNames.add(expandedName);\n }\n const element: XmlElement = {\n name,\n localName: name.split(\":\").at(-1) ?? name,\n namespaceUri: resolveNamespace(name, namespaces, true),\n attributes,\n start,\n openingEnd: end,\n closingStart: end,\n end,\n parent: stack.at(-1)?.element,\n };\n yield { element, opening: true };\n if (selfClosing) {\n yield { element, opening: false };\n } else {\n stack.push({ element, namespaces });\n }\n }\n position = end;\n }\n if (stack.length > 0) {\n throw new Error(`Unclosed XML element: ${stack.at(-1)?.element.name}`);\n }\n}\n\nfunction findTagEnd(xml: string, start: number): number {\n let quote = \"\";\n for (let position = start + 1; position < xml.length; position += 1) {\n const character = xml[position];\n if (quote) {\n if (character === quote) {\n quote = \"\";\n }\n } else if (character === '\"' || character === \"'\") {\n quote = character;\n } else if (character === \">\") {\n return position + 1;\n } else if (character === \"<\") {\n throw new Error(\"Invalid XML opening element\");\n }\n }\n throw new Error(\"Unterminated XML opening element\");\n}\n\nfunction validateName(name: string): void {\n if (!XML_NAME.test(name) || name.split(\":\").length > 2 || name.endsWith(\":\")) {\n throw new Error(`Invalid XML name: ${name}`);\n }\n}\n\nfunction parseAttributes(xml: string, offset: number): XmlAttribute[] {\n const attributes: XmlAttribute[] = [];\n const names = new Set<string>();\n let position = 0;\n while (position < xml.length) {\n const match = /^\\s+([^\\s=]+)\\s*=\\s*([\"'])/.exec(xml.slice(position));\n if (!match) {\n if (xml.slice(position).trim() !== \"\") {\n throw new Error(\"Invalid XML attributes\");\n }\n break;\n }\n const name = match[1] ?? \"\";\n validateName(name);\n if (names.has(name)) {\n throw new Error(`Duplicate XML attribute: ${name}`);\n }\n names.add(name);\n const valueStart = position + match[0].length;\n const valueEnd = xml.indexOf(match[2] ?? '\"', valueStart);\n if (valueEnd === -1 || xml.slice(valueStart, valueEnd).includes(\"<\")) {\n throw new Error(\"Invalid XML attribute value\");\n }\n attributes.push({\n name,\n localName: name.split(\":\").at(-1) ?? name,\n namespaceUri: \"\",\n value: decodeXml(xml.slice(valueStart, valueEnd)),\n valueStart: offset + valueStart,\n valueEnd: offset + valueEnd,\n });\n position = valueEnd + 1;\n }\n return attributes;\n}\n\nfunction resolveNamespace(\n name: string,\n namespaces: ReadonlyMap<string, string>,\n element: boolean,\n): string {\n const colon = name.indexOf(\":\");\n if (colon === -1) {\n return element ? (namespaces.get(\"\") ?? \"\") : \"\";\n }\n const prefix = name.slice(0, colon);\n const namespace = namespaces.get(prefix);\n if (!namespace) {\n throw new Error(`Undeclared XML namespace prefix: ${prefix}`);\n }\n return namespace;\n}\n\nexport function findElements(xml: string, name: string): XmlElement[] {\n const elements: XmlElement[] = [];\n for (const { element, opening } of scanXml(xml)) {\n if (opening && element.localName === name) {\n elements.push(element);\n }\n }\n return elements;\n}\n\nexport function mapOpeningElements(\n xml: string,\n transform: (element: XmlElement, opening: string) => string,\n): string {\n const edits: XmlEdit[] = [];\n for (const { element, opening } of scanXml(xml)) {\n if (opening) {\n const original = xml.slice(element.start, element.openingEnd);\n const text = transform(element, original);\n if (text !== original) {\n edits.push({ start: element.start, end: element.openingEnd, text });\n }\n }\n }\n return applyEdits(xml, edits);\n}\n\nexport function removeElements(xml: string, predicate: (element: XmlElement) => boolean): string {\n const edits: XmlEdit[] = [];\n for (const { element, opening } of scanXml(xml)) {\n if (!opening && predicate(element)) {\n edits.push({ start: element.start, end: element.end, text: \"\" });\n }\n }\n // Elements close inside-out; removing an outer element also removes its descendants.\n // oxlint-disable-next-line unicorn/no-array-sort\n edits.sort((left, right) => left.start - right.start || right.end - left.end);\n return applyEdits(xml, edits);\n}\n\nfunction applyEdits(xml: string, edits: readonly XmlEdit[]): string {\n const chunks: string[] = [];\n let position = 0;\n for (const edit of edits) {\n if (edit.start < position) {\n continue;\n }\n chunks.push(xml.slice(position, edit.start), edit.text);\n position = edit.end;\n }\n chunks.push(xml.slice(position));\n return chunks.join(\"\");\n}\n\nexport function findCollection(xml: string, name: string): XmlCollection | undefined {\n const element = findElements(xml, name).find((candidate) => isSpreadsheetElement(candidate));\n return element === undefined\n ? undefined\n : {\n body: xml.slice(element.openingEnd, element.closingStart),\n full: xml.slice(element.start, element.end),\n opening: xml.slice(element.start, element.openingEnd),\n element,\n };\n}\n\nexport function replaceCollection(xml: string, name: string, body: string, count: number): string {\n const collection = findCollection(xml, name);\n if (!collection) {\n return xml;\n }\n const attribute = collection.element.attributes.find((candidate) => candidate.name === \"count\");\n const opening = (\n attribute\n ? collection.opening.slice(0, attribute.valueStart - collection.element.start) +\n count +\n collection.opening.slice(attribute.valueEnd - collection.element.start)\n : collection.opening.replace(/\\/?\\s*>$/, ` count=\"${count}\">`)\n ).replace(/\\/\\s*>$/, \">\");\n return (\n xml.slice(0, collection.element.start) +\n opening +\n body +\n `</${collection.element.name}>` +\n xml.slice(collection.element.end)\n );\n}\n"]}
|
package/dist/options.d.ts
CHANGED
|
@@ -2,4 +2,3 @@ import type { ExcelSanitizationLimits, NormalizedFileOptions, NormalizedSanitize
|
|
|
2
2
|
export declare function normalizeLimits(overrides: unknown): Readonly<ExcelSanitizationLimits>;
|
|
3
3
|
export declare function normalizeOptions(options: unknown, fileOptions: true): NormalizedFileOptions;
|
|
4
4
|
export declare function normalizeOptions(options?: unknown, fileOptions?: false): NormalizedSanitizeOptions;
|
|
5
|
-
//# sourceMappingURL=options.d.ts.map
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { DEFAULT_LIMITS, FILE_OPTION_NAMES, LIMIT_NAMES, SANITIZE_OPTION_NAMES, } from "./constants.js";
|
|
2
|
+
import { createError } from "./errors.js";
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function validateKnownKeys(value, knownKeys, label) {
|
|
7
|
+
for (const key of Object.keys(value)) {
|
|
8
|
+
if (!knownKeys.has(key)) {
|
|
9
|
+
throw createError("ERR_INVALID_ARGUMENT", {
|
|
10
|
+
message: `Unknown ${label}: ${key}`,
|
|
11
|
+
details: { key },
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function isLimitName(name) {
|
|
17
|
+
return LIMIT_NAMES.has(name);
|
|
18
|
+
}
|
|
19
|
+
export function normalizeLimits(overrides) {
|
|
20
|
+
if (overrides === undefined) {
|
|
21
|
+
return DEFAULT_LIMITS;
|
|
22
|
+
}
|
|
23
|
+
if (!isRecord(overrides)) {
|
|
24
|
+
throw createError("ERR_INVALID_ARGUMENT", {
|
|
25
|
+
message: "options.limits must be an object",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
validateKnownKeys(overrides, LIMIT_NAMES, "limit");
|
|
29
|
+
const limits = { ...DEFAULT_LIMITS };
|
|
30
|
+
for (const [name, value] of Object.entries(overrides)) {
|
|
31
|
+
if (!isLimitName(name)) {
|
|
32
|
+
throw createError("ERR_INVALID_ARGUMENT", {
|
|
33
|
+
message: `Unknown limit: ${name}`,
|
|
34
|
+
details: { key: name },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
38
|
+
throw createError("ERR_INVALID_ARGUMENT", {
|
|
39
|
+
message: `options.limits.${name} must be a positive safe integer`,
|
|
40
|
+
details: { limit: name, value },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
limits[name] = value;
|
|
44
|
+
}
|
|
45
|
+
return Object.freeze(limits);
|
|
46
|
+
}
|
|
47
|
+
export function normalizeOptions(options, fileOptions = false) {
|
|
48
|
+
if (options !== undefined && !isRecord(options)) {
|
|
49
|
+
throw createError("ERR_INVALID_ARGUMENT", {
|
|
50
|
+
message: "options must be an object",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const rawOptions = options ?? {};
|
|
54
|
+
validateKnownKeys(rawOptions, fileOptions ? FILE_OPTION_NAMES : SANITIZE_OPTION_NAMES, "option");
|
|
55
|
+
if (rawOptions.requireSingleWorksheet !== undefined &&
|
|
56
|
+
typeof rawOptions.requireSingleWorksheet !== "boolean") {
|
|
57
|
+
throw createError("ERR_INVALID_ARGUMENT", {
|
|
58
|
+
message: "options.requireSingleWorksheet must be a boolean",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
let normalizedOverwrite = false;
|
|
62
|
+
if (fileOptions) {
|
|
63
|
+
const overwrite = rawOptions.overwrite;
|
|
64
|
+
if (overwrite !== undefined && typeof overwrite !== "boolean") {
|
|
65
|
+
throw createError("ERR_INVALID_ARGUMENT", {
|
|
66
|
+
message: "options.overwrite must be a boolean",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
normalizedOverwrite = overwrite ?? false;
|
|
70
|
+
}
|
|
71
|
+
const normalized = {
|
|
72
|
+
requireSingleWorksheet: rawOptions.requireSingleWorksheet ?? false,
|
|
73
|
+
limits: normalizeLimits(rawOptions.limits),
|
|
74
|
+
};
|
|
75
|
+
if (!fileOptions) {
|
|
76
|
+
return normalized;
|
|
77
|
+
}
|
|
78
|
+
return { ...normalized, overwrite: normalizedOverwrite };
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DEFAULT_LIMITS,\n FILE_OPTION_NAMES,\n LIMIT_NAMES,\n SANITIZE_OPTION_NAMES,\n} from \"./constants.js\";\nimport { createError } from \"./errors.js\";\nimport type {\n ExcelSanitizationLimits,\n NormalizedFileOptions,\n NormalizedSanitizeOptions,\n} from \"./types.js\";\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction validateKnownKeys(\n value: Readonly<Record<string, unknown>>,\n knownKeys: ReadonlySet<string>,\n label: string,\n): void {\n for (const key of Object.keys(value)) {\n if (!knownKeys.has(key)) {\n throw createError(\"ERR_INVALID_ARGUMENT\", {\n message: `Unknown ${label}: ${key}`,\n details: { key },\n });\n }\n }\n}\n\nfunction isLimitName(name: string): name is keyof ExcelSanitizationLimits {\n return LIMIT_NAMES.has(name);\n}\n\nexport function normalizeLimits(overrides: unknown): Readonly<ExcelSanitizationLimits> {\n if (overrides === undefined) {\n return DEFAULT_LIMITS;\n }\n if (!isRecord(overrides)) {\n throw createError(\"ERR_INVALID_ARGUMENT\", {\n message: \"options.limits must be an object\",\n });\n }\n\n validateKnownKeys(overrides, LIMIT_NAMES, \"limit\");\n const limits: ExcelSanitizationLimits = { ...DEFAULT_LIMITS };\n for (const [name, value] of Object.entries(overrides)) {\n if (!isLimitName(name)) {\n throw createError(\"ERR_INVALID_ARGUMENT\", {\n message: `Unknown limit: ${name}`,\n details: { key: name },\n });\n }\n if (typeof value !== \"number\" || !Number.isSafeInteger(value) || value <= 0) {\n throw createError(\"ERR_INVALID_ARGUMENT\", {\n message: `options.limits.${name} must be a positive safe integer`,\n details: { limit: name, value },\n });\n }\n limits[name] = value;\n }\n return Object.freeze(limits);\n}\n\nexport function normalizeOptions(options: unknown, fileOptions: true): NormalizedFileOptions;\nexport function normalizeOptions(options?: unknown, fileOptions?: false): NormalizedSanitizeOptions;\nexport function normalizeOptions(\n options?: unknown,\n fileOptions = false,\n): NormalizedSanitizeOptions | NormalizedFileOptions {\n if (options !== undefined && !isRecord(options)) {\n throw createError(\"ERR_INVALID_ARGUMENT\", {\n message: \"options must be an object\",\n });\n }\n\n const rawOptions = options ?? {};\n validateKnownKeys(rawOptions, fileOptions ? FILE_OPTION_NAMES : SANITIZE_OPTION_NAMES, \"option\");\n if (\n rawOptions.requireSingleWorksheet !== undefined &&\n typeof rawOptions.requireSingleWorksheet !== \"boolean\"\n ) {\n throw createError(\"ERR_INVALID_ARGUMENT\", {\n message: \"options.requireSingleWorksheet must be a boolean\",\n });\n }\n\n let normalizedOverwrite = false;\n if (fileOptions) {\n const overwrite = rawOptions.overwrite;\n if (overwrite !== undefined && typeof overwrite !== \"boolean\") {\n throw createError(\"ERR_INVALID_ARGUMENT\", {\n message: \"options.overwrite must be a boolean\",\n });\n }\n normalizedOverwrite = overwrite ?? false;\n }\n\n const normalized: NormalizedSanitizeOptions = {\n requireSingleWorksheet: rawOptions.requireSingleWorksheet ?? false,\n limits: normalizeLimits(rawOptions.limits),\n };\n if (!fileOptions) {\n return normalized;\n }\n return { ...normalized, overwrite: normalizedOverwrite };\n}\n"]}
|
package/dist/types.d.ts
CHANGED
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export interface ExcelSanitizationLimits {\n maxEntryCount: number;\n maxUncompressedBytes: number;\n maxXmlBytes: number;\n maxWorksheetXmlBytes: number;\n maxStylesXmlBytes: number;\n maxMetadataXmlBytes: number;\n maxRelationshipsXmlBytes: number;\n}\n\nexport interface SanitizeExcelOptions {\n requireSingleWorksheet?: boolean;\n limits?: Partial<ExcelSanitizationLimits>;\n}\n\nexport interface SanitizeExcelFileOptions extends SanitizeExcelOptions {\n overwrite?: boolean;\n}\n\nexport interface NormalizedSanitizeOptions {\n requireSingleWorksheet: boolean;\n limits: Readonly<ExcelSanitizationLimits>;\n}\n\nexport interface NormalizedFileOptions extends NormalizedSanitizeOptions {\n overwrite: boolean;\n}\n\nexport interface ReadBudget {\n consumedBytes: number;\n limitBytes: number;\n}\n"]}
|
|
@@ -4,4 +4,3 @@ export declare function validateZipCentralDirectory(buffer: Buffer, limits: Read
|
|
|
4
4
|
export declare function validatePackageEntryName(name: string): void;
|
|
5
5
|
export declare function findEndOfCentralDirectory(buffer: Buffer): number | undefined;
|
|
6
6
|
export declare function rejectZip64ExtraFields(buffer: Buffer, extraOffset: number, extraLength: number): void;
|
|
7
|
-
//# sourceMappingURL=central-directory.d.ts.map
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { limitExceeded } from "../errors.js";
|
|
2
|
+
const END_RECORD_MINIMUM_SIZE = 22;
|
|
3
|
+
const CENTRAL_DIRECTORY_ENTRY_MINIMUM_SIZE = 46;
|
|
4
|
+
const MAX_ZIP_COMMENT_SIZE = 65_535;
|
|
5
|
+
const END_RECORD_SIGNATURE = 0x06054b50;
|
|
6
|
+
const CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50;
|
|
7
|
+
const ZIP64_END_LOCATOR_SIGNATURE = 0x07064b50;
|
|
8
|
+
export function hasZipSignature(buffer) {
|
|
9
|
+
return buffer.length >= 2 && buffer[0] === 0x50 && buffer[1] === 0x4b;
|
|
10
|
+
}
|
|
11
|
+
export function validateZipCentralDirectory(buffer, limits) {
|
|
12
|
+
const endRecord = readEndRecord(buffer, limits);
|
|
13
|
+
const centralDirectoryEnd = endRecord.centralDirectoryOffset + endRecord.centralDirectorySize;
|
|
14
|
+
if (endRecord.centralDirectoryOffset > endRecord.offset ||
|
|
15
|
+
centralDirectoryEnd > endRecord.offset ||
|
|
16
|
+
centralDirectoryEnd > buffer.length) {
|
|
17
|
+
throw new Error("Invalid ZIP central-directory bounds");
|
|
18
|
+
}
|
|
19
|
+
let entryOffset = endRecord.centralDirectoryOffset;
|
|
20
|
+
let totalUncompressedSize = 0;
|
|
21
|
+
for (let index = 0; index < endRecord.totalEntries; index += 1) {
|
|
22
|
+
const entry = readCentralDirectoryEntry(buffer, entryOffset, centralDirectoryEnd, endRecord.centralDirectoryOffset);
|
|
23
|
+
totalUncompressedSize += entry.uncompressedSize;
|
|
24
|
+
if (totalUncompressedSize > limits.maxUncompressedBytes) {
|
|
25
|
+
throw limitExceeded("maxUncompressedBytes", limits.maxUncompressedBytes, totalUncompressedSize);
|
|
26
|
+
}
|
|
27
|
+
entryOffset = entry.nextOffset;
|
|
28
|
+
}
|
|
29
|
+
if (entryOffset !== centralDirectoryEnd) {
|
|
30
|
+
throw new Error("ZIP central-directory entry count is inconsistent");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function readEndRecord(buffer, limits) {
|
|
34
|
+
const offset = findEndOfCentralDirectory(buffer);
|
|
35
|
+
if (offset === undefined || offset + END_RECORD_MINIMUM_SIZE > buffer.length) {
|
|
36
|
+
throw new Error("ZIP end-of-central-directory record is missing");
|
|
37
|
+
}
|
|
38
|
+
const diskNumber = buffer.readUInt16LE(offset + 4);
|
|
39
|
+
const centralDirectoryDisk = buffer.readUInt16LE(offset + 6);
|
|
40
|
+
const entriesOnDisk = buffer.readUInt16LE(offset + 8);
|
|
41
|
+
const totalEntries = buffer.readUInt16LE(offset + 10);
|
|
42
|
+
const centralDirectorySize = buffer.readUInt32LE(offset + 12);
|
|
43
|
+
const centralDirectoryOffset = buffer.readUInt32LE(offset + 16);
|
|
44
|
+
const commentLength = buffer.readUInt16LE(offset + 20);
|
|
45
|
+
if (offset + END_RECORD_MINIMUM_SIZE + commentLength > buffer.length) {
|
|
46
|
+
throw new Error("Truncated ZIP comment");
|
|
47
|
+
}
|
|
48
|
+
if (diskNumber !== 0 || centralDirectoryDisk !== 0 || entriesOnDisk !== totalEntries) {
|
|
49
|
+
throw new Error("Multi-disk ZIP files are not supported");
|
|
50
|
+
}
|
|
51
|
+
if (totalEntries === 0xffff ||
|
|
52
|
+
centralDirectorySize === 0xffffffff ||
|
|
53
|
+
centralDirectoryOffset === 0xffffffff ||
|
|
54
|
+
(offset >= 20 && buffer.readUInt32LE(offset - 20) === ZIP64_END_LOCATOR_SIGNATURE)) {
|
|
55
|
+
throw new Error("ZIP64 files are not supported");
|
|
56
|
+
}
|
|
57
|
+
if (totalEntries > limits.maxEntryCount) {
|
|
58
|
+
throw limitExceeded("maxEntryCount", limits.maxEntryCount, totalEntries);
|
|
59
|
+
}
|
|
60
|
+
return { offset, totalEntries, centralDirectoryOffset, centralDirectorySize };
|
|
61
|
+
}
|
|
62
|
+
function readCentralDirectoryEntry(buffer, entryOffset, centralDirectoryEnd, centralDirectoryOffset) {
|
|
63
|
+
if (entryOffset + CENTRAL_DIRECTORY_ENTRY_MINIMUM_SIZE > centralDirectoryEnd ||
|
|
64
|
+
buffer.readUInt32LE(entryOffset) !== CENTRAL_DIRECTORY_ENTRY_SIGNATURE) {
|
|
65
|
+
throw new Error("Malformed ZIP central-directory entry");
|
|
66
|
+
}
|
|
67
|
+
const compressedSize = buffer.readUInt32LE(entryOffset + 20);
|
|
68
|
+
const uncompressedSize = buffer.readUInt32LE(entryOffset + 24);
|
|
69
|
+
const fileNameLength = buffer.readUInt16LE(entryOffset + 28);
|
|
70
|
+
const extraLength = buffer.readUInt16LE(entryOffset + 30);
|
|
71
|
+
const fileCommentLength = buffer.readUInt16LE(entryOffset + 32);
|
|
72
|
+
const entryDisk = buffer.readUInt16LE(entryOffset + 34);
|
|
73
|
+
const localHeaderOffset = buffer.readUInt32LE(entryOffset + 42);
|
|
74
|
+
if (compressedSize === 0xffffffff ||
|
|
75
|
+
uncompressedSize === 0xffffffff ||
|
|
76
|
+
localHeaderOffset === 0xffffffff ||
|
|
77
|
+
entryDisk !== 0) {
|
|
78
|
+
throw new Error("ZIP64 or multi-disk entries are not supported");
|
|
79
|
+
}
|
|
80
|
+
const fileNameOffset = entryOffset + CENTRAL_DIRECTORY_ENTRY_MINIMUM_SIZE;
|
|
81
|
+
const extraOffset = fileNameOffset + fileNameLength;
|
|
82
|
+
const nextOffset = extraOffset + extraLength + fileCommentLength;
|
|
83
|
+
if (nextOffset > centralDirectoryEnd) {
|
|
84
|
+
throw new Error("Truncated ZIP central-directory entry");
|
|
85
|
+
}
|
|
86
|
+
validatePackageEntryName(buffer.subarray(fileNameOffset, extraOffset).toString("utf8"));
|
|
87
|
+
rejectZip64ExtraFields(buffer, extraOffset, extraLength);
|
|
88
|
+
if (localHeaderOffset >= centralDirectoryOffset) {
|
|
89
|
+
throw new Error("Invalid ZIP local-header offset");
|
|
90
|
+
}
|
|
91
|
+
return { nextOffset, uncompressedSize };
|
|
92
|
+
}
|
|
93
|
+
export function validatePackageEntryName(name) {
|
|
94
|
+
const normalized = name.replaceAll("\\", "/");
|
|
95
|
+
if (normalized.includes("\0") ||
|
|
96
|
+
normalized.startsWith("/") ||
|
|
97
|
+
/^[A-Za-z]:\//.test(normalized) ||
|
|
98
|
+
normalized.split("/").includes("..")) {
|
|
99
|
+
throw new Error(`Unsafe ZIP entry name: ${name}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function findEndOfCentralDirectory(buffer) {
|
|
103
|
+
const minimumOffset = Math.max(0, buffer.length - MAX_ZIP_COMMENT_SIZE - END_RECORD_MINIMUM_SIZE);
|
|
104
|
+
for (let offset = buffer.length - END_RECORD_MINIMUM_SIZE; offset >= minimumOffset; offset -= 1) {
|
|
105
|
+
if (buffer.readUInt32LE(offset) === END_RECORD_SIGNATURE &&
|
|
106
|
+
offset + END_RECORD_MINIMUM_SIZE + buffer.readUInt16LE(offset + 20) === buffer.length) {
|
|
107
|
+
return offset;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
export function rejectZip64ExtraFields(buffer, extraOffset, extraLength) {
|
|
113
|
+
const extraEnd = extraOffset + extraLength;
|
|
114
|
+
let offset = extraOffset;
|
|
115
|
+
while (offset < extraEnd) {
|
|
116
|
+
if (offset + 4 > extraEnd) {
|
|
117
|
+
throw new Error("Malformed ZIP extra field");
|
|
118
|
+
}
|
|
119
|
+
const headerId = buffer.readUInt16LE(offset);
|
|
120
|
+
const fieldLength = buffer.readUInt16LE(offset + 2);
|
|
121
|
+
offset += 4;
|
|
122
|
+
if (offset + fieldLength > extraEnd) {
|
|
123
|
+
throw new Error("Truncated ZIP extra field");
|
|
124
|
+
}
|
|
125
|
+
if (headerId === 0x0001) {
|
|
126
|
+
throw new Error("ZIP64 files are not supported");
|
|
127
|
+
}
|
|
128
|
+
offset += fieldLength;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=central-directory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"central-directory.js","sourceRoot":"","sources":["../../src/zip/central-directory.ts"],"names":[],"mappings":"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","sourcesContent":["import { limitExceeded } from \"../errors.js\";\nimport type { ExcelSanitizationLimits } from \"../types.js\";\n\nconst END_RECORD_MINIMUM_SIZE = 22;\nconst CENTRAL_DIRECTORY_ENTRY_MINIMUM_SIZE = 46;\nconst MAX_ZIP_COMMENT_SIZE = 65_535;\nconst END_RECORD_SIGNATURE = 0x06054b50;\nconst CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50;\nconst ZIP64_END_LOCATOR_SIGNATURE = 0x07064b50;\n\ninterface EndRecord {\n offset: number;\n totalEntries: number;\n centralDirectoryOffset: number;\n centralDirectorySize: number;\n}\n\ninterface CentralDirectoryEntry {\n nextOffset: number;\n uncompressedSize: number;\n}\n\nexport function hasZipSignature(buffer: Buffer): boolean {\n return buffer.length >= 2 && buffer[0] === 0x50 && buffer[1] === 0x4b;\n}\n\nexport function validateZipCentralDirectory(\n buffer: Buffer,\n limits: Readonly<ExcelSanitizationLimits>,\n): void {\n const endRecord = readEndRecord(buffer, limits);\n const centralDirectoryEnd = endRecord.centralDirectoryOffset + endRecord.centralDirectorySize;\n\n if (\n endRecord.centralDirectoryOffset > endRecord.offset ||\n centralDirectoryEnd > endRecord.offset ||\n centralDirectoryEnd > buffer.length\n ) {\n throw new Error(\"Invalid ZIP central-directory bounds\");\n }\n\n let entryOffset = endRecord.centralDirectoryOffset;\n let totalUncompressedSize = 0;\n for (let index = 0; index < endRecord.totalEntries; index += 1) {\n const entry = readCentralDirectoryEntry(\n buffer,\n entryOffset,\n centralDirectoryEnd,\n endRecord.centralDirectoryOffset,\n );\n totalUncompressedSize += entry.uncompressedSize;\n if (totalUncompressedSize > limits.maxUncompressedBytes) {\n throw limitExceeded(\n \"maxUncompressedBytes\",\n limits.maxUncompressedBytes,\n totalUncompressedSize,\n );\n }\n entryOffset = entry.nextOffset;\n }\n\n if (entryOffset !== centralDirectoryEnd) {\n throw new Error(\"ZIP central-directory entry count is inconsistent\");\n }\n}\n\nfunction readEndRecord(buffer: Buffer, limits: Readonly<ExcelSanitizationLimits>): EndRecord {\n const offset = findEndOfCentralDirectory(buffer);\n if (offset === undefined || offset + END_RECORD_MINIMUM_SIZE > buffer.length) {\n throw new Error(\"ZIP end-of-central-directory record is missing\");\n }\n\n const diskNumber = buffer.readUInt16LE(offset + 4);\n const centralDirectoryDisk = buffer.readUInt16LE(offset + 6);\n const entriesOnDisk = buffer.readUInt16LE(offset + 8);\n const totalEntries = buffer.readUInt16LE(offset + 10);\n const centralDirectorySize = buffer.readUInt32LE(offset + 12);\n const centralDirectoryOffset = buffer.readUInt32LE(offset + 16);\n const commentLength = buffer.readUInt16LE(offset + 20);\n\n if (offset + END_RECORD_MINIMUM_SIZE + commentLength > buffer.length) {\n throw new Error(\"Truncated ZIP comment\");\n }\n if (diskNumber !== 0 || centralDirectoryDisk !== 0 || entriesOnDisk !== totalEntries) {\n throw new Error(\"Multi-disk ZIP files are not supported\");\n }\n if (\n totalEntries === 0xffff ||\n centralDirectorySize === 0xffffffff ||\n centralDirectoryOffset === 0xffffffff ||\n (offset >= 20 && buffer.readUInt32LE(offset - 20) === ZIP64_END_LOCATOR_SIGNATURE)\n ) {\n throw new Error(\"ZIP64 files are not supported\");\n }\n if (totalEntries > limits.maxEntryCount) {\n throw limitExceeded(\"maxEntryCount\", limits.maxEntryCount, totalEntries);\n }\n\n return { offset, totalEntries, centralDirectoryOffset, centralDirectorySize };\n}\n\nfunction readCentralDirectoryEntry(\n buffer: Buffer,\n entryOffset: number,\n centralDirectoryEnd: number,\n centralDirectoryOffset: number,\n): CentralDirectoryEntry {\n if (\n entryOffset + CENTRAL_DIRECTORY_ENTRY_MINIMUM_SIZE > centralDirectoryEnd ||\n buffer.readUInt32LE(entryOffset) !== CENTRAL_DIRECTORY_ENTRY_SIGNATURE\n ) {\n throw new Error(\"Malformed ZIP central-directory entry\");\n }\n\n const compressedSize = buffer.readUInt32LE(entryOffset + 20);\n const uncompressedSize = buffer.readUInt32LE(entryOffset + 24);\n const fileNameLength = buffer.readUInt16LE(entryOffset + 28);\n const extraLength = buffer.readUInt16LE(entryOffset + 30);\n const fileCommentLength = buffer.readUInt16LE(entryOffset + 32);\n const entryDisk = buffer.readUInt16LE(entryOffset + 34);\n const localHeaderOffset = buffer.readUInt32LE(entryOffset + 42);\n\n if (\n compressedSize === 0xffffffff ||\n uncompressedSize === 0xffffffff ||\n localHeaderOffset === 0xffffffff ||\n entryDisk !== 0\n ) {\n throw new Error(\"ZIP64 or multi-disk entries are not supported\");\n }\n\n const fileNameOffset = entryOffset + CENTRAL_DIRECTORY_ENTRY_MINIMUM_SIZE;\n const extraOffset = fileNameOffset + fileNameLength;\n const nextOffset = extraOffset + extraLength + fileCommentLength;\n if (nextOffset > centralDirectoryEnd) {\n throw new Error(\"Truncated ZIP central-directory entry\");\n }\n\n validatePackageEntryName(buffer.subarray(fileNameOffset, extraOffset).toString(\"utf8\"));\n rejectZip64ExtraFields(buffer, extraOffset, extraLength);\n if (localHeaderOffset >= centralDirectoryOffset) {\n throw new Error(\"Invalid ZIP local-header offset\");\n }\n\n return { nextOffset, uncompressedSize };\n}\n\nexport function validatePackageEntryName(name: string): void {\n const normalized = name.replaceAll(\"\\\\\", \"/\");\n if (\n normalized.includes(\"\\0\") ||\n normalized.startsWith(\"/\") ||\n /^[A-Za-z]:\\//.test(normalized) ||\n normalized.split(\"/\").includes(\"..\")\n ) {\n throw new Error(`Unsafe ZIP entry name: ${name}`);\n }\n}\n\nexport function findEndOfCentralDirectory(buffer: Buffer): number | undefined {\n const minimumOffset = Math.max(0, buffer.length - MAX_ZIP_COMMENT_SIZE - END_RECORD_MINIMUM_SIZE);\n for (let offset = buffer.length - END_RECORD_MINIMUM_SIZE; offset >= minimumOffset; offset -= 1) {\n if (\n buffer.readUInt32LE(offset) === END_RECORD_SIGNATURE &&\n offset + END_RECORD_MINIMUM_SIZE + buffer.readUInt16LE(offset + 20) === buffer.length\n ) {\n return offset;\n }\n }\n return undefined;\n}\n\nexport function rejectZip64ExtraFields(\n buffer: Buffer,\n extraOffset: number,\n extraLength: number,\n): void {\n const extraEnd = extraOffset + extraLength;\n let offset = extraOffset;\n while (offset < extraEnd) {\n if (offset + 4 > extraEnd) {\n throw new Error(\"Malformed ZIP extra field\");\n }\n const headerId = buffer.readUInt16LE(offset);\n const fieldLength = buffer.readUInt16LE(offset + 2);\n offset += 4;\n if (offset + fieldLength > extraEnd) {\n throw new Error(\"Truncated ZIP extra field\");\n }\n if (headerId === 0x0001) {\n throw new Error(\"ZIP64 files are not supported\");\n }\n offset += fieldLength;\n }\n}\n"]}
|