pivotkit-xlsx 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -1
- package/dist/chunks/chunk-KDJGTIXW.js +1 -0
- package/dist/chunks/{chunk-GPNA7WRL.js → chunk-TEWBHPQ4.js} +1 -1
- package/dist/chunks/{import-csv-QZDVETIW.js → import-csv-TRZA6M7W.js} +1 -1
- package/dist/chunks/{import-json-GKBVWJSK.js → import-json-YR5ADN4H.js} +1 -1
- package/dist/chunks/reader-S5MTRZAR.js +9 -0
- package/dist/index.js +1 -1
- package/package.json +6 -3
- package/src/reader.js +30 -20
- package/src/xml.js +360 -45
- package/src/zip.js +3 -3
- package/types/xml.d.ts +48 -4
- package/dist/chunks/chunk-FI6VROG7.js +0 -1
- package/dist/chunks/reader-VGOKIFNW.js +0 -9
package/src/xml.js
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* unpacked into memory, parsed and turned into empty rows. A row is kept when it has a cell with
|
|
10
10
|
* content (`</c>`: a value, an inline string or a formula); a row of empty cells (`<c r="A9" s="1"/>`)
|
|
11
11
|
* or a self-closing `<row/>` goes. Rows are numbered by their cells' `r` attributes, so leaving rows
|
|
12
|
-
* out moves no value.
|
|
12
|
+
* out moves no value. Anything between rows that is no row, and what follows `</sheetData>` but
|
|
13
|
+
* for the root's end tag, is left out too: read-excel-file reads nothing there.
|
|
14
|
+
* - `createXmlGuard`: whitespace runs cut and over-long tokens refused, so read-excel-file's parser
|
|
15
|
+
* gets no token that costs it quadratic time.
|
|
13
16
|
*
|
|
14
17
|
* Not part of the public API.
|
|
15
18
|
*/
|
|
@@ -24,15 +27,25 @@ const ENTITY = ascii("ENTITY");
|
|
|
24
27
|
const SHEET_DATA = ascii("<sheetData");
|
|
25
28
|
const SHEET_DATA_END = ascii("</sheetData>");
|
|
26
29
|
const ROW = ascii("<row");
|
|
30
|
+
const COMMENT_START = ascii("<!--");
|
|
31
|
+
const COMMENT_END = ascii("-->");
|
|
32
|
+
const CDATA_START = ascii("<![CDATA[");
|
|
33
|
+
const CDATA_END = ascii("]]>");
|
|
34
|
+
const PI_END = ascii("?>");
|
|
35
|
+
const GT = ascii(">");
|
|
27
36
|
const ROW_END = ascii("</row>");
|
|
28
37
|
const CELL_END = ascii("</c>");
|
|
29
|
-
|
|
38
|
+
// an error cell's type; the byte before it is whitespace (searched without it: a row padded with
|
|
39
|
+
// spaces would make every byte a place to compare)
|
|
40
|
+
const ERROR_TYPE = ascii('t="e"');
|
|
30
41
|
|
|
31
|
-
/** Index of `pattern` in `buf
|
|
42
|
+
/** Index of `pattern` in `buf[from, to)`, or -1. */
|
|
32
43
|
export function indexOf(buf, pattern, from = 0, to = buf.length) {
|
|
33
44
|
const first = pattern[0];
|
|
34
45
|
const last = to - pattern.length;
|
|
35
|
-
|
|
46
|
+
// bounded, so a search for the first byte never runs past `to`
|
|
47
|
+
const view = to < buf.length ? buf.subarray(0, to) : buf;
|
|
48
|
+
for (let i = view.indexOf(first, from); i !== -1 && i <= last; i = view.indexOf(first, i + 1)) {
|
|
36
49
|
let k = 1;
|
|
37
50
|
while (k < pattern.length && buf[i + k] === pattern[k]) k++;
|
|
38
51
|
if (k === pattern.length) return i;
|
|
@@ -63,15 +76,20 @@ export function createXmlCheck(file, part) {
|
|
|
63
76
|
throw fileError("XML_DECLARATION", file, `the part "${part}" is not UTF-8 XML, which a workbook always is; it is refused.`, { part });
|
|
64
77
|
}
|
|
65
78
|
first = false;
|
|
66
|
-
|
|
79
|
+
// a declaration split between the last piece and this one is in their joint; the piece is
|
|
80
|
+
// searched as it is, not copied (a piece can be megabytes)
|
|
81
|
+
if (tail.length) search(concat(tail, chunk.subarray(0, 9)));
|
|
82
|
+
search(chunk);
|
|
83
|
+
tail = chunk.length >= 9 ? chunk.slice(chunk.length - 9) : concat(tail, chunk).slice(-9);
|
|
84
|
+
};
|
|
85
|
+
function search(buf) {
|
|
67
86
|
// "!" is rare in a workbook's XML (comments, CDATA, text); "<" is in every tag
|
|
68
87
|
for (let i = buf.indexOf(BANG, 1); i !== -1; i = buf.indexOf(BANG, i + 1)) {
|
|
69
88
|
if (buf[i - 1] === LT && (wordAt(buf, i + 1, DOCTYPE) || wordAt(buf, i + 1, ENTITY))) {
|
|
70
89
|
throw fileError("XML_DECLARATION", file, `the part "${part}" declares a document type or entities, which a workbook never needs; it is refused.`, { part });
|
|
71
90
|
}
|
|
72
91
|
}
|
|
73
|
-
|
|
74
|
-
};
|
|
92
|
+
}
|
|
75
93
|
}
|
|
76
94
|
|
|
77
95
|
/** Checks a whole XML part. */
|
|
@@ -86,49 +104,321 @@ function concat(a, b) {
|
|
|
86
104
|
return out;
|
|
87
105
|
}
|
|
88
106
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
/**
|
|
108
|
+
* A run of XML whitespace longer than this, in text or between a tag's attributes, is cut to this
|
|
109
|
+
* length: four times the most characters an Excel cell holds (32,767), so no value Excel writes changes.
|
|
110
|
+
*/
|
|
111
|
+
export const SPACE_RUN = 128 * 1024;
|
|
112
|
+
/**
|
|
113
|
+
* The longest text, tag, comment, CDATA section or processing instruction of an XML part handed to
|
|
114
|
+
* read-excel-file, counted after whitespace runs are cut. An Excel cell holds at most 32,767
|
|
115
|
+
* characters (at most about 300 KB as XML); a longer token is refused.
|
|
116
|
+
*/
|
|
117
|
+
export const MAX_TOKEN = 1024 * 1024;
|
|
118
|
+
/**
|
|
119
|
+
* The longest tag: the parser reads a tag byte by byte in JavaScript, again from its start with every
|
|
120
|
+
* piece, where it finds a text's end with a native search. A sheet's tags are a few dozen bytes.
|
|
121
|
+
*/
|
|
122
|
+
export const MAX_TAG = 256 * 1024;
|
|
123
|
+
|
|
124
|
+
const TEXT = 0;
|
|
125
|
+
const TAG = 1;
|
|
126
|
+
const OPENING = 2; // after "<", until it is clear whether a comment, a CDATA section or a tag starts
|
|
127
|
+
const MARKUP = 3; // a comment, a CDATA section or a processing instruction: ends at `ending`
|
|
128
|
+
const CDATA_HEAD = ascii("![CDATA[");
|
|
129
|
+
const COMMENT_HEAD = ascii("!--");
|
|
130
|
+
const isSpace = (b) => b === 0x20 || b === 0x09 || b === 0x0a || b === 0x0d;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Passes an XML part on to `out`, piece by piece, with every run of whitespace in its text and
|
|
134
|
+
* between a tag's attributes cut to SPACE_RUN bytes, and refuses (DAMAGED) a tag longer than MAX_TAG
|
|
135
|
+
* and a text, comment, CDATA section or processing instruction longer than MAX_TOKEN.
|
|
136
|
+
*
|
|
137
|
+
* read-excel-file parses a sheet in pieces; a token that is not finished at the end of a piece is
|
|
138
|
+
* read again from its start with the next one, and it calls itself once per piece. A token of tens
|
|
139
|
+
* of megabytes (a workbook a few dozen kilobytes small whose XML is padded with spaces) costs it
|
|
140
|
+
* quadratic time, a gigabyte of memory and a stack overflow. Tokens it gets from here are short. The
|
|
141
|
+
* scan follows what that parser takes as one token: a quoted attribute value is part of its tag
|
|
142
|
+
* even with a ">" in it, a comment ends at "-->", a CDATA section at "]]>", a processing
|
|
143
|
+
* instruction at "?>".
|
|
144
|
+
* @param {{ write: (bytes: Uint8Array, from?: number, to?: number) => void }} out
|
|
145
|
+
* @param {string} file for messages
|
|
146
|
+
* @param {string} part for messages
|
|
147
|
+
*/
|
|
148
|
+
export function createXmlGuard(out, file, part) {
|
|
149
|
+
let mode = TEXT;
|
|
150
|
+
let quote = 0; // in a tag: the quote of the attribute value it is in, or 0
|
|
151
|
+
let length = 0; // bytes of the current token passed on
|
|
152
|
+
let spaces = 0; // whitespace bytes in a row, in text or in a tag outside quotes
|
|
153
|
+
/** @type {number[]} */
|
|
154
|
+
let head = []; // the bytes after "<" while OPENING
|
|
155
|
+
// MARKUP ends at ">" after `close1` ("?", "-", "]"), and after `close2` before that unless it is 0
|
|
156
|
+
let close1 = 0;
|
|
157
|
+
let close2 = 0;
|
|
158
|
+
let last1 = 0; // the last two bytes seen in MARKUP
|
|
159
|
+
let last2 = 0;
|
|
160
|
+
const tooLong = () =>
|
|
161
|
+
fileError("DAMAGED", file, `the part "${part}" holds a text of more than 1 MiB (an Excel cell holds at most 32,767 characters); the workbook is refused.`, { part });
|
|
162
|
+
const tagTooLong = () => fileError("DAMAGED", file, `the part "${part}" holds a tag of more than 256 KiB; the workbook is refused.`, { part });
|
|
163
|
+
const startsAs = (word) => head.length <= word.length && head.every((b, k) => b === word[k]);
|
|
164
|
+
/** Starts a comment, CDATA section or processing instruction that ends with `close` and ">". */
|
|
165
|
+
const endMarkup = (close) => {
|
|
166
|
+
close1 = close.at(-1) ?? 0;
|
|
167
|
+
close2 = close.length > 1 ? close[0] : 0;
|
|
168
|
+
// the end may overlap the start, as the parser's search for it does ("<!-->", "<?>")
|
|
169
|
+
last1 = head.at(-1) ?? 0;
|
|
170
|
+
last2 = head.at(-2) ?? 0;
|
|
171
|
+
return MARKUP;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
/**
|
|
176
|
+
* @param {Uint8Array} bytes
|
|
177
|
+
* @param {number} [from]
|
|
178
|
+
* @param {number} [to]
|
|
179
|
+
*/
|
|
180
|
+
write(bytes, from = 0, to = bytes.length) {
|
|
181
|
+
// the state in locals while the loops run (a closure's variables are slower to reach)
|
|
182
|
+
let m = mode;
|
|
183
|
+
let q = quote;
|
|
184
|
+
let sp = spaces;
|
|
185
|
+
let len = length;
|
|
186
|
+
// bounded, so a search for "<" never runs past `to`
|
|
187
|
+
const view = to < bytes.length ? bytes.subarray(0, to) : bytes;
|
|
188
|
+
let start = from; // the range not yet passed on
|
|
189
|
+
let i = from;
|
|
190
|
+
while (i < to) {
|
|
191
|
+
if (m === TEXT) {
|
|
192
|
+
// most text is short ("<v>12</v>") or none ("><"): a few bytes by hand, then a search
|
|
193
|
+
let stop = i;
|
|
194
|
+
const near = Math.min(to, i + 16);
|
|
195
|
+
while (stop < near && bytes[stop] !== LT) stop++;
|
|
196
|
+
if (stop === near && near < to) {
|
|
197
|
+
const lt = view.indexOf(LT, near);
|
|
198
|
+
stop = lt === -1 ? to : lt;
|
|
199
|
+
}
|
|
200
|
+
if (sp + (stop - i) > SPACE_RUN) {
|
|
201
|
+
// a run may pass the cut: byte by byte
|
|
202
|
+
while (i < stop) {
|
|
203
|
+
const b = bytes[i];
|
|
204
|
+
if (b === 0x20 || b === 0x09 || b === 0x0a || b === 0x0d) {
|
|
205
|
+
if (sp === SPACE_RUN) {
|
|
206
|
+
// past the cut: the rest of the run is left out
|
|
207
|
+
if (start < i) out.write(bytes, start, i);
|
|
208
|
+
while (i < stop && isSpace(bytes[i])) i++;
|
|
209
|
+
start = i;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
sp++;
|
|
213
|
+
} else {
|
|
214
|
+
sp = 0;
|
|
215
|
+
}
|
|
216
|
+
i++;
|
|
217
|
+
if (++len > MAX_TOKEN) throw tooLong();
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
len += stop - i;
|
|
221
|
+
if (len > MAX_TOKEN) throw tooLong();
|
|
222
|
+
// the whitespace at the end of the text, where a run may go on in the next piece
|
|
223
|
+
let k = stop;
|
|
224
|
+
while (k > i && isSpace(bytes[k - 1])) k--;
|
|
225
|
+
sp = k === i ? sp + (stop - i) : stop - k;
|
|
226
|
+
i = stop;
|
|
227
|
+
}
|
|
228
|
+
if (i < to) {
|
|
229
|
+
// at "<": a tag, unless a comment, CDATA section or processing instruction starts
|
|
230
|
+
sp = 0;
|
|
231
|
+
len = 1;
|
|
232
|
+
i++;
|
|
233
|
+
if (i < to && bytes[i] !== BANG && bytes[i] !== 0x3f) {
|
|
234
|
+
m = TAG;
|
|
235
|
+
q = 0;
|
|
236
|
+
} else {
|
|
237
|
+
m = OPENING;
|
|
238
|
+
head = [];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} else if (m === TAG) {
|
|
242
|
+
for (; i < to; i++) {
|
|
243
|
+
const b = bytes[i];
|
|
244
|
+
if (q !== 0) {
|
|
245
|
+
if (b === q) q = 0;
|
|
246
|
+
} else if (b === 0x3e) {
|
|
247
|
+
break;
|
|
248
|
+
} else if (b === 0x20 || b === 0x09 || b === 0x0a || b === 0x0d) {
|
|
249
|
+
if (sp === SPACE_RUN) {
|
|
250
|
+
if (start < i) out.write(bytes, start, i);
|
|
251
|
+
while (i < to && isSpace(bytes[i])) i++;
|
|
252
|
+
start = i;
|
|
253
|
+
i--;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
sp++;
|
|
257
|
+
} else {
|
|
258
|
+
sp = 0;
|
|
259
|
+
if (b === 0x22 || b === 0x27) q = b;
|
|
260
|
+
}
|
|
261
|
+
if (++len > MAX_TAG) throw tagTooLong();
|
|
262
|
+
}
|
|
263
|
+
if (i < to) {
|
|
264
|
+
m = TEXT;
|
|
265
|
+
sp = 0;
|
|
266
|
+
len = 0;
|
|
267
|
+
i++;
|
|
268
|
+
}
|
|
269
|
+
} else if (m === OPENING) {
|
|
270
|
+
const b = bytes[i++];
|
|
271
|
+
head.push(b);
|
|
272
|
+
if (++len > MAX_TOKEN) throw tooLong();
|
|
273
|
+
if (head.length === 1 && b === 0x3f) {
|
|
274
|
+
m = endMarkup([0x3f]); // <? ... ?>
|
|
275
|
+
} else if (head[0] === BANG && startsAs(COMMENT_HEAD)) {
|
|
276
|
+
if (head.length === COMMENT_HEAD.length) m = endMarkup([0x2d, 0x2d]);
|
|
277
|
+
} else if (head[0] === BANG && startsAs(CDATA_HEAD)) {
|
|
278
|
+
if (head.length === CDATA_HEAD.length) m = endMarkup([0x5d, 0x5d]);
|
|
279
|
+
} else {
|
|
280
|
+
// a tag: what was read of it so far may hold a quote or its end
|
|
281
|
+
m = TAG;
|
|
282
|
+
q = 0;
|
|
283
|
+
sp = 0;
|
|
284
|
+
for (const h of head) {
|
|
285
|
+
if (q !== 0) {
|
|
286
|
+
if (h === q) q = 0;
|
|
287
|
+
} else if (h === 0x22 || h === 0x27) {
|
|
288
|
+
q = h;
|
|
289
|
+
} else if (h === 0x3e) {
|
|
290
|
+
m = TEXT;
|
|
291
|
+
len = 0;
|
|
292
|
+
sp = 0;
|
|
293
|
+
break;
|
|
294
|
+
} else {
|
|
295
|
+
sp = isSpace(h) ? sp + 1 : 0;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
// MARKUP: a comment, a CDATA section or a processing instruction
|
|
301
|
+
for (; i < to; i++) {
|
|
302
|
+
const b = bytes[i];
|
|
303
|
+
if (++len > MAX_TOKEN) throw tooLong();
|
|
304
|
+
if (b === 0x3e && last1 === close1 && (close2 === 0 || last2 === close2)) {
|
|
305
|
+
m = TEXT;
|
|
306
|
+
len = 0;
|
|
307
|
+
i++;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
last2 = last1;
|
|
311
|
+
last1 = b;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (start < to) out.write(bytes, start, to);
|
|
316
|
+
mode = m;
|
|
317
|
+
quote = q;
|
|
318
|
+
spaces = sp;
|
|
319
|
+
length = len;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Where the value of the attribute `r` of the tag in buf[from, stop) starts, or -1: after whitespace,
|
|
326
|
+
* with whitespace around "=" and either quote, as an XML parser reads it; quoted values are skipped.
|
|
327
|
+
*/
|
|
328
|
+
function refAt(buf, from, stop) {
|
|
329
|
+
for (let k = from; k < stop; k++) {
|
|
330
|
+
const b = buf[k];
|
|
331
|
+
if (b === 0x22 || b === 0x27) {
|
|
332
|
+
k++;
|
|
333
|
+
while (k < stop && buf[k] !== b) k++;
|
|
334
|
+
} else if (isSpace(b) && buf[k + 1] === 0x72) {
|
|
335
|
+
let q = k + 2;
|
|
336
|
+
while (q < stop && isSpace(buf[q])) q++;
|
|
337
|
+
if (q < stop && buf[q] === 0x3d) {
|
|
338
|
+
q++;
|
|
339
|
+
while (q < stop && isSpace(buf[q])) q++;
|
|
340
|
+
if (q < stop && (buf[q] === 0x22 || buf[q] === 0x27)) {
|
|
341
|
+
q++;
|
|
342
|
+
while (q < stop && isSpace(buf[q])) q++;
|
|
343
|
+
return q;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
97
346
|
}
|
|
98
347
|
}
|
|
99
|
-
return
|
|
348
|
+
return -1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Where the tag starting at buf[from] ends: its ">" outside quotes, a "<" (never in a tag), or `to`. */
|
|
352
|
+
function tagEndAt(buf, from, to) {
|
|
353
|
+
for (let k = from; k < to; k++) {
|
|
354
|
+
const b = buf[k];
|
|
355
|
+
if (b === 0x3e || b === LT) return k;
|
|
356
|
+
if (b === 0x22 || b === 0x27) {
|
|
357
|
+
k++;
|
|
358
|
+
while (k < to && buf[k] !== b && buf[k] !== LT) k++;
|
|
359
|
+
if (buf[k] === LT) return k;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return to;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** The row number of a `<row ...>` tag (its `r` attribute), or 0. */
|
|
366
|
+
function rowNumber(buf, from, to) {
|
|
367
|
+
const at = refAt(buf, from + 4, to);
|
|
368
|
+
let n = 0;
|
|
369
|
+
if (at !== -1) for (let k = at; k < to && buf[k] >= 0x30 && buf[k] <= 0x39 && n <= 1e12; k++) n = n * 10 + (buf[k] - 0x30);
|
|
370
|
+
return n;
|
|
100
371
|
}
|
|
101
372
|
|
|
102
373
|
/** The largest row (or start of a sheet before its <sheetData>) the filter holds while it waits for its end. */
|
|
103
374
|
const MAX_PENDING = 64 * 1024 * 1024;
|
|
104
|
-
|
|
375
|
+
/** The size from which a row waiting for its end is squeezed by createXmlGuard. */
|
|
376
|
+
const SQUEEZE_AT = 1024 * 1024;
|
|
377
|
+
const CELL = ascii("<c");
|
|
378
|
+
|
|
379
|
+
/** XML that starts at a "<", through createXmlGuard. */
|
|
380
|
+
function squeeze(buf, file, part) {
|
|
381
|
+
/** @type {Uint8Array[]} */
|
|
382
|
+
const pieces = [];
|
|
383
|
+
let size = 0;
|
|
384
|
+
createXmlGuard({ write: (b, from = 0, to = b.length) => {
|
|
385
|
+
pieces.push(b.subarray(from, to));
|
|
386
|
+
size += to - from;
|
|
387
|
+
} }, file, part).write(buf);
|
|
388
|
+
if (pieces.length === 1) return pieces[0];
|
|
389
|
+
const out = new Uint8Array(size);
|
|
390
|
+
let at = 0;
|
|
391
|
+
for (const p of pieces) {
|
|
392
|
+
out.set(p, at);
|
|
393
|
+
at += p.length;
|
|
394
|
+
}
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
105
397
|
|
|
106
398
|
/** The highest column (1 for A) and row of the cells' `r` attributes in buf[from, to). */
|
|
107
399
|
function cellExtent(buf, from, to) {
|
|
108
400
|
let column = 0;
|
|
109
401
|
let row = 0;
|
|
110
|
-
for (let i = indexOf(buf, CELL, from, to); i !== -1; i = indexOf(buf, CELL, i +
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
402
|
+
for (let i = indexOf(buf, CELL, from, to); i !== -1; i = indexOf(buf, CELL, i + 2, to)) {
|
|
403
|
+
if (!isSpace(buf[i + 2])) continue; // <col>, <cfRule>
|
|
404
|
+
// each tag is scanned once, up to its end: the time stays linear in the row's length
|
|
405
|
+
const stop = tagEndAt(buf, i + 1, to);
|
|
406
|
+
let q = refAt(buf, i + 2, stop);
|
|
407
|
+
if (q === -1) continue;
|
|
408
|
+
let c = 0;
|
|
409
|
+
for (; q < stop && ((buf[q] | 0x20) >= 0x61 && (buf[q] | 0x20) <= 0x7a) && c <= 1e7; q++) c = c * 26 + ((buf[q] | 0x20) - 0x60);
|
|
410
|
+
let r = 0;
|
|
411
|
+
for (; q < stop && buf[q] >= 0x30 && buf[q] <= 0x39 && r <= 1e12; q++) r = r * 10 + (buf[q] - 0x30);
|
|
412
|
+
column = Math.max(column, c);
|
|
413
|
+
row = Math.max(row, r);
|
|
125
414
|
}
|
|
126
415
|
return { column, row };
|
|
127
416
|
}
|
|
128
417
|
|
|
129
418
|
/**
|
|
130
|
-
* Streams a sheet's XML and leaves out its rows without a value
|
|
131
|
-
*
|
|
419
|
+
* Streams a sheet's XML and leaves out its rows without a value and everything after `</sheetData>`
|
|
420
|
+
* but the last end tag (the root's): read-excel-file reads nothing there. Feed pieces with `push`; the
|
|
421
|
+
* XML kept goes to `out.write`; `end()` gives the row numbers seen.
|
|
132
422
|
*
|
|
133
423
|
* It also checks where the values are against the limits, before read-excel-file sees them: the
|
|
134
424
|
* parser makes a dense array of the sheet's extent (every row up to the last row with a value, each
|
|
@@ -136,17 +426,21 @@ function cellExtent(buf, from, to) {
|
|
|
136
426
|
* small the file is. A value beyond `maxRow` or `maxColumn`, or an extent of more than `maxCells`,
|
|
137
427
|
* stops the read with LIMIT_EXCEEDED.
|
|
138
428
|
* @param {{ write: (bytes: Uint8Array, from?: number, to?: number) => void }} out
|
|
139
|
-
* @param {{ file?: string, maxRow?: number, maxColumn?: number, maxCells?: number }} [limits]
|
|
429
|
+
* @param {{ file?: string, part?: string, maxRow?: number, maxColumn?: number, maxCells?: number }} [limits] `file` and `part` name them in messages
|
|
140
430
|
*/
|
|
141
431
|
export function createSheetFilter(out, limits = {}) {
|
|
142
432
|
const file = limits.file ?? "file";
|
|
433
|
+
const part = limits.part ?? "sheet";
|
|
143
434
|
let widest = 0;
|
|
144
435
|
let state = 0; // 0 before <sheetData>, 1 inside, 2 after
|
|
436
|
+
/** @type {Uint8Array} */
|
|
145
437
|
let pending = new Uint8Array(0);
|
|
146
438
|
let lastValueRow = 0;
|
|
147
439
|
let lastRow = 0;
|
|
148
440
|
let dropped = 0;
|
|
149
441
|
let errors = 0;
|
|
442
|
+
/** After the sheet's data: the last end tag seen. */
|
|
443
|
+
let closing = new Uint8Array(0);
|
|
150
444
|
|
|
151
445
|
const run = (final) => {
|
|
152
446
|
const buf = pending;
|
|
@@ -172,9 +466,21 @@ export function createSheetFilter(out, limits = {}) {
|
|
|
172
466
|
continue;
|
|
173
467
|
}
|
|
174
468
|
if (state === 2) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
469
|
+
// nothing after the sheet's data is read (merged cells, conditional formats, print
|
|
470
|
+
// settings): it is skipped, and only the last end tag (the root's) goes on, in end()
|
|
471
|
+
const lt = buf.indexOf(LT, i);
|
|
472
|
+
if (lt === -1) {
|
|
473
|
+
i = buf.length;
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
const gt = buf.indexOf(0x3e, lt);
|
|
477
|
+
if (gt === -1) {
|
|
478
|
+
i = final ? buf.length : lt;
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
if (buf[lt + 1] === 0x2f && gt - lt < 1024) closing = buf.slice(lt, gt + 1);
|
|
482
|
+
i = gt + 1;
|
|
483
|
+
continue;
|
|
178
484
|
}
|
|
179
485
|
// inside <sheetData>: whole rows at a time
|
|
180
486
|
const lt = buf.indexOf(LT, i);
|
|
@@ -183,19 +489,22 @@ export function createSheetFilter(out, limits = {}) {
|
|
|
183
489
|
break;
|
|
184
490
|
}
|
|
185
491
|
if (indexOf(buf, SHEET_DATA_END, lt, lt + SHEET_DATA_END.length) === lt) {
|
|
492
|
+
out.write(buf, lt, lt + SHEET_DATA_END.length);
|
|
186
493
|
state = 2;
|
|
187
|
-
i = lt;
|
|
494
|
+
i = lt + SHEET_DATA_END.length;
|
|
188
495
|
continue;
|
|
189
496
|
}
|
|
190
497
|
if (indexOf(buf, ROW, lt, lt + ROW.length) !== lt) {
|
|
191
|
-
// anything else in sheetData (a comment, say) is
|
|
192
|
-
|
|
193
|
-
|
|
498
|
+
// anything else in sheetData (a comment, say) is left out: read-excel-file reads rows
|
|
499
|
+
// only. A comment, CDATA section or processing instruction ends where the parser ends
|
|
500
|
+
// it, so a row inside one stays out too.
|
|
501
|
+
const close = buf[lt + 1] === 0x3f ? PI_END : indexOf(buf, COMMENT_START, lt, lt + 4) === lt ? COMMENT_END : indexOf(buf, CDATA_START, lt, lt + 9) === lt ? CDATA_END : GT;
|
|
502
|
+
const end = indexOf(buf, close, lt + 1);
|
|
503
|
+
if (end === -1) {
|
|
194
504
|
i = lt;
|
|
195
505
|
break;
|
|
196
506
|
}
|
|
197
|
-
|
|
198
|
-
i = gt + 1;
|
|
507
|
+
i = end + close.length;
|
|
199
508
|
continue;
|
|
200
509
|
}
|
|
201
510
|
const tagEnd = buf.indexOf(0x3e, lt);
|
|
@@ -227,13 +536,18 @@ export function createSheetFilter(out, limits = {}) {
|
|
|
227
536
|
if (limits.maxCells !== undefined && at * widest > limits.maxCells) throw limitError("importCells", at * widest, limits.maxCells, file, "values (rows x columns)");
|
|
228
537
|
out.write(buf, lt, stop);
|
|
229
538
|
lastValueRow = Math.max(lastValueRow, at);
|
|
230
|
-
for (let e = indexOf(buf, ERROR_TYPE, tagEnd, end); e !== -1; e = indexOf(buf, ERROR_TYPE, e + 1, end))
|
|
539
|
+
for (let e = indexOf(buf, ERROR_TYPE, tagEnd, end); e !== -1; e = indexOf(buf, ERROR_TYPE, e + 1, end)) {
|
|
540
|
+
if (isSpace(buf[e - 1])) errors++;
|
|
541
|
+
}
|
|
231
542
|
} else {
|
|
232
543
|
dropped++;
|
|
233
544
|
}
|
|
234
545
|
i = stop;
|
|
235
546
|
}
|
|
236
547
|
pending = i < buf.length ? buf.slice(i) : new Uint8Array(0);
|
|
548
|
+
// a row waiting for its end that grows large is squeezed (whitespace runs cut, over-long
|
|
549
|
+
// tokens refused) while it waits, so whitespace in a row is never held
|
|
550
|
+
if (state === 1 && pending.length > SQUEEZE_AT) pending = squeeze(pending, file, part);
|
|
237
551
|
if (pending.length > MAX_PENDING) {
|
|
238
552
|
throw fileError("DAMAGED", file, "a row of the sheet is larger than 64 MiB, or the sheet's XML is broken; the workbook is refused.");
|
|
239
553
|
}
|
|
@@ -272,6 +586,7 @@ export function createSheetFilter(out, limits = {}) {
|
|
|
272
586
|
waiting = [];
|
|
273
587
|
run(true);
|
|
274
588
|
if (pending.length) out.write(pending);
|
|
589
|
+
if (closing.length) out.write(closing);
|
|
275
590
|
return { lastRow, lastValueRow, dropped, errors };
|
|
276
591
|
}
|
|
277
592
|
};
|
package/src/zip.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { Inflate } from "fflate";
|
|
16
|
-
import { fileError } from "pivotkit-core/import";
|
|
16
|
+
import { fileError, limitError } from "pivotkit-core/import";
|
|
17
17
|
|
|
18
18
|
const EOCD = 0x06054b50;
|
|
19
19
|
const EOCD64_LOCATOR = 0x07064b50;
|
|
@@ -64,7 +64,7 @@ export function readDirectory(bytes, file, limits) {
|
|
|
64
64
|
offset = u64(bytes, at + 48);
|
|
65
65
|
}
|
|
66
66
|
if (count > limits.archiveEntries) {
|
|
67
|
-
throw
|
|
67
|
+
throw limitError("archiveEntries", count, limits.archiveEntries, file, "parts in its ZIP directory");
|
|
68
68
|
}
|
|
69
69
|
if (offset + size > bytes.length) throw broken("directory outside the file");
|
|
70
70
|
/** @type {ZipEntry[]} */
|
|
@@ -105,7 +105,7 @@ export function readDirectory(bytes, file, limits) {
|
|
|
105
105
|
}
|
|
106
106
|
total += unpacked;
|
|
107
107
|
if (total > limits.unpackedBytes) {
|
|
108
|
-
throw
|
|
108
|
+
throw limitError("unpackedBytes", total, limits.unpackedBytes, file, "bytes unpacked, by its ZIP directory; a ZIP bomb or a workbook too large to load");
|
|
109
109
|
}
|
|
110
110
|
entries.push({ name, method, compressedSize, size: unpacked, localOffset });
|
|
111
111
|
p = extraEnd + commentLength;
|
package/types/xml.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Index of `pattern` in `buf
|
|
1
|
+
/** Index of `pattern` in `buf[from, to)`, or -1. */
|
|
2
2
|
export declare function indexOf(buf: any, pattern: any, from?: number, to?: any): any;
|
|
3
3
|
/**
|
|
4
4
|
* A check of an XML part's bytes, fed piece by piece: throws at a `<!DOCTYPE` or `<!ENTITY`.
|
|
@@ -9,8 +9,51 @@ export declare function createXmlCheck(file: string, part: string): (chunk: any)
|
|
|
9
9
|
/** Checks a whole XML part. */
|
|
10
10
|
export declare function checkXml(bytes: any, file: any, part: any): void;
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* A run of XML whitespace longer than this, in text or between a tag's attributes, is cut to this
|
|
13
|
+
* length: four times the most characters an Excel cell holds (32,767), so no value Excel writes changes.
|
|
14
|
+
*/
|
|
15
|
+
export declare const SPACE_RUN: number;
|
|
16
|
+
/**
|
|
17
|
+
* The longest text, tag, comment, CDATA section or processing instruction of an XML part handed to
|
|
18
|
+
* read-excel-file, counted after whitespace runs are cut. An Excel cell holds at most 32,767
|
|
19
|
+
* characters (at most about 300 KB as XML); a longer token is refused.
|
|
20
|
+
*/
|
|
21
|
+
export declare const MAX_TOKEN: number;
|
|
22
|
+
/**
|
|
23
|
+
* The longest tag: the parser reads a tag byte by byte in JavaScript, again from its start with every
|
|
24
|
+
* piece, where it finds a text's end with a native search. A sheet's tags are a few dozen bytes.
|
|
25
|
+
*/
|
|
26
|
+
export declare const MAX_TAG: number;
|
|
27
|
+
/**
|
|
28
|
+
* Passes an XML part on to `out`, piece by piece, with every run of whitespace in its text and
|
|
29
|
+
* between a tag's attributes cut to SPACE_RUN bytes, and refuses (DAMAGED) a tag longer than MAX_TAG
|
|
30
|
+
* and a text, comment, CDATA section or processing instruction longer than MAX_TOKEN.
|
|
31
|
+
*
|
|
32
|
+
* read-excel-file parses a sheet in pieces; a token that is not finished at the end of a piece is
|
|
33
|
+
* read again from its start with the next one, and it calls itself once per piece. A token of tens
|
|
34
|
+
* of megabytes (a workbook a few dozen kilobytes small whose XML is padded with spaces) costs it
|
|
35
|
+
* quadratic time, a gigabyte of memory and a stack overflow. Tokens it gets from here are short. The
|
|
36
|
+
* scan follows what that parser takes as one token: a quoted attribute value is part of its tag
|
|
37
|
+
* even with a ">" in it, a comment ends at "-->", a CDATA section at "]]>", a processing
|
|
38
|
+
* instruction at "?>".
|
|
39
|
+
* @param {{ write: (bytes: Uint8Array, from?: number, to?: number) => void }} out
|
|
40
|
+
* @param {string} file for messages
|
|
41
|
+
* @param {string} part for messages
|
|
42
|
+
*/
|
|
43
|
+
export declare function createXmlGuard(out: {
|
|
44
|
+
write: (bytes: Uint8Array, from?: number, to?: number) => void;
|
|
45
|
+
}, file: string, part: string): {
|
|
46
|
+
/**
|
|
47
|
+
* @param {Uint8Array} bytes
|
|
48
|
+
* @param {number} [from]
|
|
49
|
+
* @param {number} [to]
|
|
50
|
+
*/
|
|
51
|
+
write(bytes: Uint8Array, from?: number, to?: number): void;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Streams a sheet's XML and leaves out its rows without a value and everything after `</sheetData>`
|
|
55
|
+
* but the last end tag (the root's): read-excel-file reads nothing there. Feed pieces with `push`; the
|
|
56
|
+
* XML kept goes to `out.write`; `end()` gives the row numbers seen.
|
|
14
57
|
*
|
|
15
58
|
* It also checks where the values are against the limits, before read-excel-file sees them: the
|
|
16
59
|
* parser makes a dense array of the sheet's extent (every row up to the last row with a value, each
|
|
@@ -18,12 +61,13 @@ export declare function checkXml(bytes: any, file: any, part: any): void;
|
|
|
18
61
|
* small the file is. A value beyond `maxRow` or `maxColumn`, or an extent of more than `maxCells`,
|
|
19
62
|
* stops the read with LIMIT_EXCEEDED.
|
|
20
63
|
* @param {{ write: (bytes: Uint8Array, from?: number, to?: number) => void }} out
|
|
21
|
-
* @param {{ file?: string, maxRow?: number, maxColumn?: number, maxCells?: number }} [limits]
|
|
64
|
+
* @param {{ file?: string, part?: string, maxRow?: number, maxColumn?: number, maxCells?: number }} [limits] `file` and `part` name them in messages
|
|
22
65
|
*/
|
|
23
66
|
export declare function createSheetFilter(out: {
|
|
24
67
|
write: (bytes: Uint8Array, from?: number, to?: number) => void;
|
|
25
68
|
}, limits?: {
|
|
26
69
|
file?: string;
|
|
70
|
+
part?: string;
|
|
27
71
|
maxRow?: number;
|
|
28
72
|
maxColumn?: number;
|
|
29
73
|
maxCells?: number;
|