@patrickdk77/office-defuse 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +230 -2
- package/dist/context.d.ts +38 -0
- package/dist/context.js +100 -0
- package/dist/crc32.d.ts +2 -0
- package/dist/crc32.js +18 -0
- package/dist/engine.d.ts +5 -0
- package/dist/engine.js +151 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.js +33 -0
- package/dist/findings.d.ts +25 -0
- package/dist/findings.js +169 -0
- package/dist/formats.d.ts +10 -0
- package/dist/formats.js +46 -0
- package/dist/formulas.d.ts +22 -0
- package/dist/formulas.js +77 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +30 -0
- package/dist/index.mjs +3 -0
- package/dist/io.d.ts +23 -0
- package/dist/io.js +139 -0
- package/dist/links.d.ts +26 -0
- package/dist/links.js +145 -0
- package/dist/odf.d.ts +3 -0
- package/dist/odf.js +297 -0
- package/dist/ooxml.d.ts +8 -0
- package/dist/ooxml.js +516 -0
- package/dist/score.d.ts +17 -0
- package/dist/score.js +63 -0
- package/dist/types.d.ts +247 -0
- package/dist/types.js +133 -0
- package/dist/xlsb.d.ts +19 -0
- package/dist/xlsb.js +101 -0
- package/dist/xml.d.ts +256 -0
- package/dist/xml.js +1105 -0
- package/dist/zip-writer.d.ts +38 -0
- package/dist/zip-writer.js +182 -0
- package/dist/zip.d.ts +55 -0
- package/dist/zip.js +535 -0
- package/package.json +96 -4
package/dist/xml.js
ADDED
|
@@ -0,0 +1,1105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.XmlTokenizer = exports.isNcName = exports.unknownEntity = exports.showReferences = exports.UNKNOWN = exports.XMLNS_NS = exports.XML_NS = void 0;
|
|
4
|
+
exports.attr = attr;
|
|
5
|
+
const node_util_1 = require("node:util");
|
|
6
|
+
const errors_1 = require("./errors");
|
|
7
|
+
const types_1 = require("./types");
|
|
8
|
+
exports.XML_NS = 'http://www.w3.org/XML/1998/namespace';
|
|
9
|
+
exports.XMLNS_NS = 'http://www.w3.org/2000/xmlns/';
|
|
10
|
+
/**
|
|
11
|
+
* Stands in text for a reference to an external entity, whose content office-defuse never fetches: UNKNOWN, the
|
|
12
|
+
* entity's name, UNKNOWN again. U+FFFF cannot occur in XML, so nothing a part holds is taken for it.
|
|
13
|
+
*/
|
|
14
|
+
exports.UNKNOWN = String.fromCharCode(0xffff);
|
|
15
|
+
/** One reference to an external entity, with the entity's name as its group. Built from UNKNOWN so the source stays ASCII. */
|
|
16
|
+
const UNKNOWN_NAME = `${exports.UNKNOWN}([^${exports.UNKNOWN}]*)${exports.UNKNOWN}`;
|
|
17
|
+
const UNKNOWN_REFERENCE = new RegExp(UNKNOWN_NAME, 'g');
|
|
18
|
+
const UNKNOWN_FIRST = new RegExp(UNKNOWN_NAME);
|
|
19
|
+
/** A value with each reference to an external entity written back as `&name;`, for a finding to show. */
|
|
20
|
+
const showReferences = (s) => (s.includes(exports.UNKNOWN) ? s.replace(UNKNOWN_REFERENCE, '&$1;') : s);
|
|
21
|
+
exports.showReferences = showReferences;
|
|
22
|
+
/** The name of the first external entity whose content stands in `s`, or undefined when none does. */
|
|
23
|
+
const unknownEntity = (s) => UNKNOWN_FIRST.exec(s)?.[1];
|
|
24
|
+
exports.unknownEntity = unknownEntity;
|
|
25
|
+
/** The value of the attribute with this namespace and local name. */
|
|
26
|
+
function attr(el, ns, local) {
|
|
27
|
+
for (const a of el.attributes)
|
|
28
|
+
if (a.local === local && a.ns === ns)
|
|
29
|
+
return a.value;
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
// XML 1.0 fifth edition NameStartChar and the further NameChar ranges, without the colon, which namespaces reserve.
|
|
33
|
+
const NAME_START = 'A-Z_a-z\\u{C0}-\\u{D6}\\u{D8}-\\u{F6}\\u{F8}-\\u{2FF}\\u{370}-\\u{37D}\\u{37F}-\\u{1FFF}\\u{200C}-\\u{200D}\\u{2070}-\\u{218F}\\u{2C00}-\\u{2FEF}\\u{3001}-\\u{D7FF}\\u{F900}-\\u{FDCF}\\u{FDF0}-\\u{FFFD}\\u{10000}-\\u{EFFFF}';
|
|
34
|
+
const NAME_MORE = '\\-.0-9\\u{B7}\\u{300}-\\u{36F}\\u{203F}-\\u{2040}';
|
|
35
|
+
const NC_NAME = new RegExp(`^[${NAME_START}][${NAME_START}${NAME_MORE}]*$`, 'u');
|
|
36
|
+
/** A name token, as an enumerated attribute type lists them. */
|
|
37
|
+
const NMTOKEN = new RegExp(`^[${NAME_START}${NAME_MORE}:]+$`, 'u');
|
|
38
|
+
/** An XML name without a colon. */
|
|
39
|
+
const isNcName = (s) => NC_NAME.test(s);
|
|
40
|
+
exports.isNcName = isNcName;
|
|
41
|
+
/** A name with at most one colon, between two names without one. */
|
|
42
|
+
const isQName = (s) => {
|
|
43
|
+
const colon = s.indexOf(':');
|
|
44
|
+
return colon < 0 ? (0, exports.isNcName)(s) : (0, exports.isNcName)(s.slice(0, colon)) && (0, exports.isNcName)(s.slice(colon + 1));
|
|
45
|
+
};
|
|
46
|
+
const PREDEFINED = { lt: '<', gt: '>', amp: '&', quot: '"', apos: "'" };
|
|
47
|
+
/** Longest reference read: "&#x" with eight digits and ";" is 12, and leading zeros are legal. */
|
|
48
|
+
const MAX_REFERENCE = 64;
|
|
49
|
+
/** The characters a public identifier may hold. */
|
|
50
|
+
const PUBID = /^[-a-zA-Z0-9 \r\n'()+,./:=?;!*#@$_%]*$/;
|
|
51
|
+
/** Characters that end a name inside a document type declaration. */
|
|
52
|
+
const NAME_END = /[\s>[\]"'%;&<]/;
|
|
53
|
+
/** Attribute types other than CDATA, whose values are normalized further. NOTATION and enumerations are read apart. */
|
|
54
|
+
const TOKENIZED_TYPES = new Set(['ID', 'IDREF', 'IDREFS', 'ENTITY', 'ENTITIES', 'NMTOKEN', 'NMTOKENS']);
|
|
55
|
+
const LIMIT_DETAILS = new Set([types_1.OfficeDetail.XmlDepth, types_1.OfficeDetail.XmlTokenSize, types_1.OfficeDetail.XmlEntityDepth, types_1.OfficeDetail.XmlEntityExpansions, types_1.OfficeDetail.XmlEntitySize]);
|
|
56
|
+
const isSpace = (c) => c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d;
|
|
57
|
+
/** A character XML 1.0 allows. The decoders leave no lone surrogates, so the halves of a pair pass here. */
|
|
58
|
+
const isXmlChar = (c) => c === 0x09 || c === 0x0a || c === 0x0d || (c >= 0x20 && c !== 0xfffe && c !== 0xffff);
|
|
59
|
+
const isCodePointAllowed = (c) => c === 0x09 || c === 0x0a || c === 0x0d || (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd) || (c >= 0x10000 && c <= 0x10ffff);
|
|
60
|
+
/** The further normalization of XML 1.0 (3.3.3) for an attribute declared with a type other than CDATA. */
|
|
61
|
+
const tokenize = (v) => v.replace(/ {2,}/g, ' ').replace(/^ | $/g, '');
|
|
62
|
+
/** A cursor over a whole document type declaration, or over the text of a parameter entity read inside one. */
|
|
63
|
+
class Cursor {
|
|
64
|
+
s;
|
|
65
|
+
normalized;
|
|
66
|
+
fail;
|
|
67
|
+
i;
|
|
68
|
+
/**
|
|
69
|
+
* `normalized` marks the text of a parameter entity. Its line ends were normalized where it was declared, so a CR
|
|
70
|
+
* in it, which only a character reference makes, stays a character of its own.
|
|
71
|
+
*/
|
|
72
|
+
constructor(s, start, normalized, fail) {
|
|
73
|
+
this.s = s;
|
|
74
|
+
this.normalized = normalized;
|
|
75
|
+
this.fail = fail;
|
|
76
|
+
this.i = start;
|
|
77
|
+
}
|
|
78
|
+
/** Skips white space and tells whether there was any. */
|
|
79
|
+
space() {
|
|
80
|
+
const from = this.i;
|
|
81
|
+
while (this.i < this.s.length && isSpace(this.s.charCodeAt(this.i)))
|
|
82
|
+
this.i++;
|
|
83
|
+
return this.i > from;
|
|
84
|
+
}
|
|
85
|
+
needSpace(where) {
|
|
86
|
+
if (!this.space())
|
|
87
|
+
this.fail(`No white space ${where}`);
|
|
88
|
+
}
|
|
89
|
+
/** Steps over `word` when it is next, and tells whether it was. */
|
|
90
|
+
skip(word) {
|
|
91
|
+
if (!this.s.startsWith(word, this.i))
|
|
92
|
+
return false;
|
|
93
|
+
this.i += word.length;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/** Everything up to white space or a delimiter, for the caller to check as a name. */
|
|
97
|
+
name() {
|
|
98
|
+
const from = this.i;
|
|
99
|
+
while (this.i < this.s.length && !NAME_END.test(this.s[this.i]))
|
|
100
|
+
this.i++;
|
|
101
|
+
return this.s.slice(from, this.i);
|
|
102
|
+
}
|
|
103
|
+
/** A quoted literal, without its quotes. */
|
|
104
|
+
literal(what) {
|
|
105
|
+
const q = this.s[this.i];
|
|
106
|
+
if (q !== '"' && q !== "'")
|
|
107
|
+
this.fail(`${what} is not quoted`);
|
|
108
|
+
const from = this.i + 1;
|
|
109
|
+
const close = this.s.indexOf(q, from);
|
|
110
|
+
if (close < 0)
|
|
111
|
+
this.fail(`${what} is not closed`);
|
|
112
|
+
this.i = close + 1;
|
|
113
|
+
return this.s.slice(from, close);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Steps past the ">" that ends an element type or notation declaration, over quoted literals. A parameter entity
|
|
117
|
+
* reference may not stand inside a declaration of the internal subset, and nothing else there uses "%".
|
|
118
|
+
*/
|
|
119
|
+
declarationEnd() {
|
|
120
|
+
for (; this.i < this.s.length; this.i++) {
|
|
121
|
+
const c = this.s[this.i];
|
|
122
|
+
if (c === '>') {
|
|
123
|
+
this.i++;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (c === '%')
|
|
127
|
+
this.fail('A parameter entity reference inside a declaration');
|
|
128
|
+
if (c === '"' || c === "'") {
|
|
129
|
+
const close = this.s.indexOf(c, this.i + 1);
|
|
130
|
+
if (close < 0)
|
|
131
|
+
break;
|
|
132
|
+
this.i = close;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
this.fail('A declaration in the internal subset does not end');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A streaming XML 1.0 tokenizer with namespaces. It refuses references to entities nothing declares, encodings other
|
|
140
|
+
* than UTF-8 and UTF-16, and anything not well-formed. It holds one markup token at a time; text goes to the handler
|
|
141
|
+
* in pieces.
|
|
142
|
+
*
|
|
143
|
+
* A document type declaration goes to `notes.doctype`. No external DTD or other external entity is ever loaded. The
|
|
144
|
+
* internal subset is read as XML 1.0 (5.1) has a processor read it that reads internal parameter entities and no
|
|
145
|
+
* external ones: it keeps the general entities and attribute-list declarations, reads the text of each internal
|
|
146
|
+
* parameter entity it refers to, and skips the entity and attribute-list declarations after a parameter entity it
|
|
147
|
+
* does not read, unless the XML declaration says standalone="yes".
|
|
148
|
+
*
|
|
149
|
+
* A reference to an internal entity is expanded, within the entity limits, and the handler sees its text, markup
|
|
150
|
+
* included, as if the part held it. A reference to an external entity in content goes to `notes.externalEntity`, and
|
|
151
|
+
* stands in the text as UNKNOWN marks it. One in an attribute value, directly or through other entities, is refused,
|
|
152
|
+
* as XML 1.0 refuses it. An attribute an element leaves out gets the default its attribute-list declaration gives, and
|
|
153
|
+
* an attribute declared with a type other than CDATA is normalized further, as expat does. Each default added counts
|
|
154
|
+
* as an expansion against the entity limits.
|
|
155
|
+
*/
|
|
156
|
+
class XmlTokenizer {
|
|
157
|
+
handler;
|
|
158
|
+
limits;
|
|
159
|
+
part;
|
|
160
|
+
decoder;
|
|
161
|
+
head = new Uint8Array(0);
|
|
162
|
+
buf = '';
|
|
163
|
+
pos = 0;
|
|
164
|
+
/** Offset in the decoded text of buf[0]. */
|
|
165
|
+
base = 0;
|
|
166
|
+
stack = [];
|
|
167
|
+
rootSeen = false;
|
|
168
|
+
doctypeSeen = false;
|
|
169
|
+
/** The XML declaration says standalone="yes". */
|
|
170
|
+
standalone = false;
|
|
171
|
+
encoding = 'utf-8';
|
|
172
|
+
/** The last text piece ended in CR, so an LF that starts the next piece belongs to the same line end. */
|
|
173
|
+
lastCr = false;
|
|
174
|
+
scan;
|
|
175
|
+
/** General entities the internal subset declares, by name. */
|
|
176
|
+
entities = new Map();
|
|
177
|
+
/** Parameter entities the internal subset declares, by name. */
|
|
178
|
+
parameters = new Map();
|
|
179
|
+
/** What the attribute-list declarations say, by element type as written. */
|
|
180
|
+
declared = new Map();
|
|
181
|
+
/** The entities being expanded, outermost first. A parameter entity's name has "%" in front. */
|
|
182
|
+
active = [];
|
|
183
|
+
/** While entity text is read, the offsets of the outermost reference, which its elements report. */
|
|
184
|
+
span;
|
|
185
|
+
/** The stack depth at which the entity text being read started. Its end tags close nothing below it. */
|
|
186
|
+
floor = 0;
|
|
187
|
+
vml;
|
|
188
|
+
notes;
|
|
189
|
+
budget;
|
|
190
|
+
/**
|
|
191
|
+
* `options.vml` reads the dialect Office writes in VML drawing parts, which is not well-formed XML: it skips the
|
|
192
|
+
* `<![if ...]>` and `<![endif]>` markers around content and takes an unclosed `<br>` as an empty element. Both
|
|
193
|
+
* branches of a marker reach the handler.
|
|
194
|
+
*/
|
|
195
|
+
constructor(handler, limits, part, options = {}) {
|
|
196
|
+
this.handler = handler;
|
|
197
|
+
this.limits = limits;
|
|
198
|
+
this.part = part;
|
|
199
|
+
this.vml = options.vml ?? false;
|
|
200
|
+
this.notes = options.notes ?? {};
|
|
201
|
+
this.budget = options.budget ?? { expansions: 0, size: 0 };
|
|
202
|
+
}
|
|
203
|
+
stop(detail, message) {
|
|
204
|
+
const category = LIMIT_DETAILS.has(detail) ? types_1.OfficeCategory.Limit : types_1.OfficeCategory.Xml;
|
|
205
|
+
throw new errors_1.StopError(category, detail, `${this.part}: ${message}`, { part: this.part, reason: message }, this.part);
|
|
206
|
+
}
|
|
207
|
+
malformed(message) {
|
|
208
|
+
return this.stop(types_1.OfficeDetail.Malformed, message);
|
|
209
|
+
}
|
|
210
|
+
write(bytes) {
|
|
211
|
+
if (this.decoder) {
|
|
212
|
+
this.feed(this.decode(this.decoder, bytes, true), false);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
this.head = this.head.length ? Buffer.concat([this.head, bytes]) : bytes;
|
|
216
|
+
if (this.head.length >= 4)
|
|
217
|
+
this.begin();
|
|
218
|
+
}
|
|
219
|
+
end() {
|
|
220
|
+
const decoder = this.decoder ?? this.begin();
|
|
221
|
+
this.feed(this.decode(decoder, new Uint8Array(0), false), true);
|
|
222
|
+
if (this.stack.length)
|
|
223
|
+
this.malformed(`<${this.stack[this.stack.length - 1].el.qname}> is never closed`);
|
|
224
|
+
if (!this.rootSeen)
|
|
225
|
+
this.malformed('There is no root element');
|
|
226
|
+
}
|
|
227
|
+
/** Picks the encoding from the first bytes, as XML 1.0 Appendix F does, and reads the bytes held so far. */
|
|
228
|
+
begin() {
|
|
229
|
+
const b = this.head;
|
|
230
|
+
const at = (i) => (i < b.length ? b[i] : -1);
|
|
231
|
+
if ((at(0) === 0 && at(1) === 0) || (at(0) === 0xff && at(1) === 0xfe && at(2) === 0 && at(3) === 0) || (at(0) === 0x3c && at(1) === 0 && at(2) === 0 && at(3) === 0)) {
|
|
232
|
+
this.stop(types_1.OfficeDetail.Encoding, 'UTF-32 is not an encoding Office reads');
|
|
233
|
+
}
|
|
234
|
+
let skip = 0;
|
|
235
|
+
if (at(0) === 0xef && at(1) === 0xbb && at(2) === 0xbf)
|
|
236
|
+
skip = 3;
|
|
237
|
+
else if (at(0) === 0xff && at(1) === 0xfe) {
|
|
238
|
+
this.encoding = 'utf-16le';
|
|
239
|
+
skip = 2;
|
|
240
|
+
}
|
|
241
|
+
else if (at(0) === 0xfe && at(1) === 0xff) {
|
|
242
|
+
this.encoding = 'utf-16be';
|
|
243
|
+
skip = 2;
|
|
244
|
+
}
|
|
245
|
+
else if (at(0) === 0x3c && at(1) === 0 && at(2) === 0x3f && at(3) === 0)
|
|
246
|
+
this.encoding = 'utf-16le';
|
|
247
|
+
else if (at(0) === 0 && at(1) === 0x3c && at(2) === 0 && at(3) === 0x3f)
|
|
248
|
+
this.encoding = 'utf-16be';
|
|
249
|
+
const decoder = new node_util_1.TextDecoder(this.encoding, { fatal: true, ignoreBOM: true });
|
|
250
|
+
this.decoder = decoder;
|
|
251
|
+
this.head = new Uint8Array(0);
|
|
252
|
+
this.feed(this.decode(decoder, b.subarray(skip), true), false);
|
|
253
|
+
return decoder;
|
|
254
|
+
}
|
|
255
|
+
decode(decoder, bytes, more) {
|
|
256
|
+
try {
|
|
257
|
+
return decoder.decode(bytes, { stream: more });
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return this.stop(types_1.OfficeDetail.Encoding, `Bytes that are not valid ${this.encoding.toUpperCase()}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/** Checks the XML declaration against the encoding the bytes are in, and notes whether it says standalone="yes". */
|
|
264
|
+
declaration(body) {
|
|
265
|
+
const version = /\bversion\s*=\s*(["'])([^"']*)\1/.exec(body)?.[2];
|
|
266
|
+
if (version !== '1.0')
|
|
267
|
+
this.malformed(`XML version ${version ?? 'missing'}`);
|
|
268
|
+
this.standalone = /\bstandalone\s*=\s*(["'])yes\1/.test(body);
|
|
269
|
+
const declared = /\bencoding\s*=\s*(["'])([^"']*)\1/.exec(body)?.[2]?.toLowerCase();
|
|
270
|
+
if (declared === undefined)
|
|
271
|
+
return;
|
|
272
|
+
const ok = this.encoding === 'utf-8' ? declared === 'utf-8' || declared === 'utf8' : declared === 'utf-16' || declared === this.encoding;
|
|
273
|
+
if (!ok)
|
|
274
|
+
this.stop(types_1.OfficeDetail.Encoding, `The declaration says ${declared}, the bytes are ${this.encoding.toUpperCase()}`);
|
|
275
|
+
}
|
|
276
|
+
feed(text, final) {
|
|
277
|
+
this.buf = this.pos < this.buf.length ? this.buf.slice(this.pos) + text : text;
|
|
278
|
+
this.base += this.pos;
|
|
279
|
+
this.pos = 0;
|
|
280
|
+
this.loop(final);
|
|
281
|
+
}
|
|
282
|
+
/** Reads the text and markup in buf from pos. Entity text is read with `final` set, since all of it is there. */
|
|
283
|
+
loop(final) {
|
|
284
|
+
for (;;) {
|
|
285
|
+
const lt = this.buf.indexOf('<', this.pos);
|
|
286
|
+
if (lt !== this.pos) {
|
|
287
|
+
// Text up to the next tag, or what can be decoded of it so far.
|
|
288
|
+
let end = lt < 0 ? this.buf.length : lt;
|
|
289
|
+
if (lt < 0 && !final) {
|
|
290
|
+
const amp = this.buf.lastIndexOf('&');
|
|
291
|
+
if (amp >= this.pos && this.buf.indexOf(';', amp) < 0) {
|
|
292
|
+
if (this.buf.length - amp > MAX_REFERENCE)
|
|
293
|
+
this.malformed('An unterminated reference');
|
|
294
|
+
end = amp;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (end > this.pos)
|
|
298
|
+
this.text(this.buf.slice(this.pos, end), this.base + this.pos);
|
|
299
|
+
this.pos = end;
|
|
300
|
+
if (lt < 0)
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
// At the end of the part, markup is read whole or refused, so nothing is left over.
|
|
304
|
+
if (!this.markup(final))
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/** The offset in the decoded text of buf[i], or while entity text is read, that of the reference to it. */
|
|
309
|
+
at(i, end = false) {
|
|
310
|
+
if (this.span)
|
|
311
|
+
return this.span[end ? 1 : 0];
|
|
312
|
+
return this.base + i;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Character data at `offset` in the decoded text. References are expanded and line ends turned into LF. Entity
|
|
316
|
+
* text had its line ends normalized where it was declared, so a CR in it, which only a character reference makes,
|
|
317
|
+
* stays.
|
|
318
|
+
*/
|
|
319
|
+
text(raw, offset) {
|
|
320
|
+
const normalized = this.span !== undefined;
|
|
321
|
+
let out = '';
|
|
322
|
+
let from = 0;
|
|
323
|
+
let i = 0;
|
|
324
|
+
if (!normalized && this.lastCr && raw.charCodeAt(0) === 0x0a) {
|
|
325
|
+
from = 1;
|
|
326
|
+
i = 1;
|
|
327
|
+
}
|
|
328
|
+
for (; i < raw.length; i++) {
|
|
329
|
+
const c = raw.charCodeAt(i);
|
|
330
|
+
if (c === 0x26) {
|
|
331
|
+
const semi = raw.indexOf(';', i);
|
|
332
|
+
if (semi < 0 || semi - i > MAX_REFERENCE)
|
|
333
|
+
this.malformed('An unterminated reference');
|
|
334
|
+
const name = raw.slice(i + 1, semi);
|
|
335
|
+
const plain = this.plainReference(name);
|
|
336
|
+
if (plain !== undefined)
|
|
337
|
+
out += raw.slice(from, i) + plain;
|
|
338
|
+
else {
|
|
339
|
+
this.characters(out + raw.slice(from, i));
|
|
340
|
+
out = '';
|
|
341
|
+
this.contentReference(name, offset + i, offset + semi + 1);
|
|
342
|
+
}
|
|
343
|
+
i = semi;
|
|
344
|
+
from = semi + 1;
|
|
345
|
+
}
|
|
346
|
+
else if (c === 0x0d && !normalized) {
|
|
347
|
+
out += `${raw.slice(from, i)}\n`;
|
|
348
|
+
if (raw.charCodeAt(i + 1) === 0x0a)
|
|
349
|
+
i++;
|
|
350
|
+
from = i + 1;
|
|
351
|
+
}
|
|
352
|
+
else if (!isXmlChar(c)) {
|
|
353
|
+
this.malformed(`Character U+${c.toString(16).padStart(4, '0')} is not allowed in XML`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (!normalized)
|
|
357
|
+
this.lastCr = raw.charCodeAt(raw.length - 1) === 0x0d;
|
|
358
|
+
this.characters(from === 0 ? raw : out + raw.slice(from));
|
|
359
|
+
}
|
|
360
|
+
/** Hands decoded text to the handler. Outside the root element only white space may stand. */
|
|
361
|
+
characters(decoded) {
|
|
362
|
+
if (!this.stack.length) {
|
|
363
|
+
for (let i = 0; i < decoded.length; i++)
|
|
364
|
+
if (!isSpace(decoded.charCodeAt(i)))
|
|
365
|
+
this.malformed('Text outside the root element');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (decoded.length)
|
|
369
|
+
this.handler.text?.(decoded);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Normalizes an attribute value as XML 1.0 (3.3.3) says: references expanded, and each white space character a
|
|
373
|
+
* space, except one written as a character reference. `normalized` marks entity text, whose line ends were
|
|
374
|
+
* normalized where it was declared, so a CR and LF in it are two characters.
|
|
375
|
+
*/
|
|
376
|
+
attributeValue(raw, normalized) {
|
|
377
|
+
let out = '';
|
|
378
|
+
let from = 0;
|
|
379
|
+
for (let i = 0; i < raw.length; i++) {
|
|
380
|
+
const c = raw.charCodeAt(i);
|
|
381
|
+
if (c === 0x26) {
|
|
382
|
+
const semi = raw.indexOf(';', i);
|
|
383
|
+
if (semi < 0 || semi - i > MAX_REFERENCE)
|
|
384
|
+
this.malformed('An unterminated reference');
|
|
385
|
+
const name = raw.slice(i + 1, semi);
|
|
386
|
+
out += raw.slice(from, i) + (this.plainReference(name) ?? this.attributeReference(name));
|
|
387
|
+
i = semi;
|
|
388
|
+
from = semi + 1;
|
|
389
|
+
}
|
|
390
|
+
else if (c === 0x0d && !normalized) {
|
|
391
|
+
out += `${raw.slice(from, i)} `;
|
|
392
|
+
if (raw.charCodeAt(i + 1) === 0x0a)
|
|
393
|
+
i++;
|
|
394
|
+
from = i + 1;
|
|
395
|
+
}
|
|
396
|
+
else if (c === 0x0a || c === 0x09 || c === 0x0d) {
|
|
397
|
+
out += `${raw.slice(from, i)} `;
|
|
398
|
+
from = i + 1;
|
|
399
|
+
}
|
|
400
|
+
else if (c === 0x3c) {
|
|
401
|
+
this.malformed('"<" in an attribute value');
|
|
402
|
+
}
|
|
403
|
+
else if (!isXmlChar(c)) {
|
|
404
|
+
this.malformed(`Character U+${c.toString(16).padStart(4, '0')} is not allowed in XML`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return from === 0 ? raw : out + raw.slice(from);
|
|
408
|
+
}
|
|
409
|
+
/** The text a predefined entity or a character reference stands for, or undefined for any other entity. */
|
|
410
|
+
plainReference(name) {
|
|
411
|
+
if (Object.hasOwn(PREDEFINED, name))
|
|
412
|
+
return PREDEFINED[name];
|
|
413
|
+
if (name.startsWith('#'))
|
|
414
|
+
return this.characterReference(name);
|
|
415
|
+
if (!(0, exports.isNcName)(name))
|
|
416
|
+
this.malformed('A malformed reference');
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
characterReference(name) {
|
|
420
|
+
const m = /^#(?:x([0-9a-fA-F]+)|([0-9]+))$/.exec(name);
|
|
421
|
+
if (!m)
|
|
422
|
+
this.malformed('A malformed reference');
|
|
423
|
+
const code = m[1] !== undefined ? Number.parseInt(m[1], 16) : Number.parseInt(m[2], 10);
|
|
424
|
+
if (!isCodePointAllowed(code))
|
|
425
|
+
this.malformed(`A character reference to U+${code.toString(16)}`);
|
|
426
|
+
return String.fromCodePoint(code);
|
|
427
|
+
}
|
|
428
|
+
/** The entity a reference names. A reference to one nothing declares is refused. */
|
|
429
|
+
entity(name) {
|
|
430
|
+
const e = this.entities.get(name);
|
|
431
|
+
if (!e)
|
|
432
|
+
this.malformed(`A reference to entity "${name}", which nothing declares`);
|
|
433
|
+
return e;
|
|
434
|
+
}
|
|
435
|
+
/** Starts expanding an entity, within the limits. */
|
|
436
|
+
enter(name, text) {
|
|
437
|
+
if (this.active.includes(name))
|
|
438
|
+
this.malformed(`Entity "${name}" refers to itself`);
|
|
439
|
+
if (this.active.length >= this.limits.entityDepth)
|
|
440
|
+
this.stop(types_1.OfficeDetail.XmlEntityDepth, `Entity references nest deeper than ${this.limits.entityDepth}`);
|
|
441
|
+
this.charge(text.length);
|
|
442
|
+
this.active.push(name);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Counts one expansion of `size` characters of declared text, an entity's or an attribute default's, against the
|
|
446
|
+
* limits, so a document type cannot make the part cost more than they allow.
|
|
447
|
+
*/
|
|
448
|
+
charge(size) {
|
|
449
|
+
this.budget.expansions += 1;
|
|
450
|
+
if (this.budget.expansions > this.limits.entityExpansions)
|
|
451
|
+
this.stop(types_1.OfficeDetail.XmlEntityExpansions, `More than ${this.limits.entityExpansions} entities and attribute defaults expanded`);
|
|
452
|
+
this.budget.size += size;
|
|
453
|
+
if (this.budget.size > this.limits.entitySize)
|
|
454
|
+
this.stop(types_1.OfficeDetail.XmlEntitySize, `More than ${this.limits.entitySize} characters of entity text and attribute defaults expanded`);
|
|
455
|
+
}
|
|
456
|
+
/** A reference to a general entity in content. Its text is read as content, in place of the reference. */
|
|
457
|
+
contentReference(name, start, end) {
|
|
458
|
+
if (!this.stack.length)
|
|
459
|
+
this.malformed('An entity reference outside the root element');
|
|
460
|
+
const e = this.entity(name);
|
|
461
|
+
const span = this.span ?? [start, end];
|
|
462
|
+
if (!('text' in e)) {
|
|
463
|
+
if (e.notation !== undefined)
|
|
464
|
+
this.malformed(`A reference to entity "${name}", whose data is not XML`);
|
|
465
|
+
this.notes.externalEntity?.({ name, ...(e.publicId === undefined ? {} : { publicId: e.publicId }), systemId: e.systemId, start: span[0], end: span[1] });
|
|
466
|
+
this.characters(exports.UNKNOWN + name + exports.UNKNOWN);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
this.enter(name, e.text);
|
|
470
|
+
const { buf, pos, base, floor, span: outer } = this;
|
|
471
|
+
this.buf = e.text;
|
|
472
|
+
this.pos = 0;
|
|
473
|
+
this.base = 0;
|
|
474
|
+
this.floor = this.stack.length;
|
|
475
|
+
this.span = span;
|
|
476
|
+
this.loop(true);
|
|
477
|
+
if (this.stack.length !== this.floor)
|
|
478
|
+
this.malformed(`The text of entity "${name}" leaves <${this.stack[this.stack.length - 1].el.qname}> open`);
|
|
479
|
+
this.buf = buf;
|
|
480
|
+
this.pos = pos;
|
|
481
|
+
this.base = base;
|
|
482
|
+
this.floor = floor;
|
|
483
|
+
this.span = outer;
|
|
484
|
+
this.active.pop();
|
|
485
|
+
}
|
|
486
|
+
/** A reference to a general entity in an attribute value. Its text is normalized into the value. */
|
|
487
|
+
attributeReference(name) {
|
|
488
|
+
const e = this.entity(name);
|
|
489
|
+
if (!('text' in e))
|
|
490
|
+
this.malformed(`A reference to external entity "${name}" in an attribute value`);
|
|
491
|
+
this.enter(name, e.text);
|
|
492
|
+
const value = this.attributeValue(e.text, true);
|
|
493
|
+
this.active.pop();
|
|
494
|
+
return value;
|
|
495
|
+
}
|
|
496
|
+
/** Reads one markup token at pos. Returns false when the buffer does not yet hold all of it. */
|
|
497
|
+
markup(final) {
|
|
498
|
+
const b = this.buf;
|
|
499
|
+
const p = this.pos;
|
|
500
|
+
// Enough to tell every kind of markup apart: "<!NOTATION" is the longest opening.
|
|
501
|
+
if (b.length - p < 10 && !final)
|
|
502
|
+
return false;
|
|
503
|
+
let close;
|
|
504
|
+
if (b.startsWith('<?', p)) {
|
|
505
|
+
close = b.indexOf('?>', p + 2);
|
|
506
|
+
if (close < 0)
|
|
507
|
+
return this.wait(final);
|
|
508
|
+
this.instruction(b.slice(p + 2, close));
|
|
509
|
+
return this.advance(close + 2);
|
|
510
|
+
}
|
|
511
|
+
if (b.startsWith('<!--', p)) {
|
|
512
|
+
close = b.indexOf('-->', p + 4);
|
|
513
|
+
if (close < 0)
|
|
514
|
+
return this.wait(final);
|
|
515
|
+
this.checkChars(b, p + 4, close);
|
|
516
|
+
return this.advance(close + 3);
|
|
517
|
+
}
|
|
518
|
+
if (b.startsWith('<![CDATA[', p)) {
|
|
519
|
+
close = b.indexOf(']]>', p + 9);
|
|
520
|
+
if (close < 0)
|
|
521
|
+
return this.wait(final);
|
|
522
|
+
if (!this.stack.length)
|
|
523
|
+
this.malformed('A CDATA section outside the root element');
|
|
524
|
+
const raw = b.slice(p + 9, close);
|
|
525
|
+
this.checkChars(raw, 0, raw.length);
|
|
526
|
+
const t = this.span ? raw : raw.replace(/\r\n?/g, '\n');
|
|
527
|
+
if (t.length)
|
|
528
|
+
this.handler.text?.(t);
|
|
529
|
+
this.lastCr = false;
|
|
530
|
+
return this.advance(close + 3);
|
|
531
|
+
}
|
|
532
|
+
if (this.vml && b.startsWith('<![', p)) {
|
|
533
|
+
close = b.indexOf(']>', p + 3);
|
|
534
|
+
if (close < 0)
|
|
535
|
+
return this.wait(final);
|
|
536
|
+
const marker = b.slice(p + 3, close);
|
|
537
|
+
if (!/^(if\s|endif$)/.test(marker))
|
|
538
|
+
this.malformed('An unknown <![ marker');
|
|
539
|
+
this.checkChars(marker, 0, marker.length);
|
|
540
|
+
return this.advance(close + 2);
|
|
541
|
+
}
|
|
542
|
+
if (b.startsWith('<!DOCTYPE', p)) {
|
|
543
|
+
if (this.rootSeen || this.doctypeSeen)
|
|
544
|
+
this.malformed('A document type declaration after the root element, or a second one');
|
|
545
|
+
close = this.scanDoctype(b, p);
|
|
546
|
+
if (close < 0)
|
|
547
|
+
return this.wait(final);
|
|
548
|
+
this.doctype(b.slice(p, close), this.at(p), this.at(close));
|
|
549
|
+
return this.advance(close);
|
|
550
|
+
}
|
|
551
|
+
if (b.startsWith('<!', p)) {
|
|
552
|
+
if (/^<!(ENTITY|ELEMENT|ATTLIST|NOTATION)/.test(b.slice(p, p + 10)))
|
|
553
|
+
this.malformed('A markup declaration outside the document type declaration');
|
|
554
|
+
this.malformed('An unknown <! declaration');
|
|
555
|
+
}
|
|
556
|
+
if (b.startsWith('</', p)) {
|
|
557
|
+
close = b.indexOf('>', p + 2);
|
|
558
|
+
if (close < 0)
|
|
559
|
+
return this.wait(final);
|
|
560
|
+
this.endTag(b.slice(p + 2, close), this.at(p), this.at(close + 1, true));
|
|
561
|
+
return this.advance(close + 1);
|
|
562
|
+
}
|
|
563
|
+
// A start tag ends at the first ">" outside a quoted value.
|
|
564
|
+
let i = p + 1;
|
|
565
|
+
for (;;) {
|
|
566
|
+
if (i >= b.length)
|
|
567
|
+
return this.wait(final);
|
|
568
|
+
const c = b.charCodeAt(i);
|
|
569
|
+
if (c === 0x22 || c === 0x27) {
|
|
570
|
+
const q = b.indexOf(c === 0x22 ? '"' : "'", i + 1);
|
|
571
|
+
if (q < 0)
|
|
572
|
+
return this.wait(final);
|
|
573
|
+
i = q + 1;
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (c === 0x3e)
|
|
577
|
+
break;
|
|
578
|
+
if (c === 0x3c)
|
|
579
|
+
this.malformed('"<" inside a tag');
|
|
580
|
+
i++;
|
|
581
|
+
}
|
|
582
|
+
this.startTag(b.slice(p + 1, i), this.at(p), this.at(i + 1, true));
|
|
583
|
+
return this.advance(i + 1);
|
|
584
|
+
}
|
|
585
|
+
wait(final) {
|
|
586
|
+
if (final)
|
|
587
|
+
this.malformed(this.span ? 'Entity text ends inside markup' : 'The part ends inside markup');
|
|
588
|
+
if (this.buf.length - this.pos > this.limits.tokenSize)
|
|
589
|
+
this.stop(types_1.OfficeDetail.XmlTokenSize, `A token is longer than ${this.limits.tokenSize} characters`);
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
advance(to) {
|
|
593
|
+
if (to - this.pos > this.limits.tokenSize)
|
|
594
|
+
this.stop(types_1.OfficeDetail.XmlTokenSize, `A token is longer than ${this.limits.tokenSize} characters`);
|
|
595
|
+
this.pos = to;
|
|
596
|
+
this.lastCr = false;
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
checkChars(s, from, to) {
|
|
600
|
+
for (let i = from; i < to; i++) {
|
|
601
|
+
const c = s.charCodeAt(i);
|
|
602
|
+
if (!isXmlChar(c))
|
|
603
|
+
this.malformed(`Character U+${c.toString(16).padStart(4, '0')} is not allowed in XML`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
instruction(body) {
|
|
607
|
+
this.checkChars(body, 0, body.length);
|
|
608
|
+
const space = body.search(/\s/);
|
|
609
|
+
const target = space < 0 ? body : body.slice(0, space);
|
|
610
|
+
if (target.toLowerCase() === 'xml') {
|
|
611
|
+
if (this.span || this.base + this.pos !== 0)
|
|
612
|
+
this.malformed('An XML declaration that is not at the start');
|
|
613
|
+
this.declaration(body);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (!(0, exports.isNcName)(target))
|
|
617
|
+
this.malformed('A processing instruction without a valid target');
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Finds the end of the document type declaration at p: the ">" outside its literals and its internal subset, where
|
|
621
|
+
* comments, processing instructions and declarations may hold either. Returns the offset after it, or -1 when buf
|
|
622
|
+
* does not hold all of it yet. The scan goes on from where it stopped.
|
|
623
|
+
*/
|
|
624
|
+
scanDoctype(b, p) {
|
|
625
|
+
const s = this.scan ?? { at: 9, mode: 'outside', quote: '' };
|
|
626
|
+
this.scan = s;
|
|
627
|
+
let i = p + s.at;
|
|
628
|
+
for (; i < b.length; i++) {
|
|
629
|
+
const c = b[i];
|
|
630
|
+
if (s.quote) {
|
|
631
|
+
if (c === s.quote)
|
|
632
|
+
s.quote = '';
|
|
633
|
+
}
|
|
634
|
+
else if (s.mode === 'outside' || s.mode === 'declaration') {
|
|
635
|
+
if (c === '"' || c === "'")
|
|
636
|
+
s.quote = c;
|
|
637
|
+
else if (c === '[' && s.mode === 'outside')
|
|
638
|
+
s.mode = 'subset';
|
|
639
|
+
else if (c === '>' && s.mode === 'declaration')
|
|
640
|
+
s.mode = 'subset';
|
|
641
|
+
else if (c === '>') {
|
|
642
|
+
this.scan = undefined;
|
|
643
|
+
return i + 1;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
else if (s.mode === 'subset') {
|
|
647
|
+
if (c === ']')
|
|
648
|
+
s.mode = 'outside';
|
|
649
|
+
else if (c === '<') {
|
|
650
|
+
// Nothing whole is that short, and "<!--" may not have arrived whole yet.
|
|
651
|
+
if (b.length - i < 4)
|
|
652
|
+
break;
|
|
653
|
+
if (b.startsWith('<!--', i)) {
|
|
654
|
+
s.mode = 'comment';
|
|
655
|
+
i += 3;
|
|
656
|
+
}
|
|
657
|
+
else if (b[i + 1] === '?') {
|
|
658
|
+
s.mode = 'instruction';
|
|
659
|
+
i += 1;
|
|
660
|
+
}
|
|
661
|
+
else
|
|
662
|
+
s.mode = 'declaration';
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
const end = s.mode === 'comment' ? '-->' : '?>';
|
|
667
|
+
if (c === end[0]) {
|
|
668
|
+
if (b.length - i < end.length)
|
|
669
|
+
break;
|
|
670
|
+
if (b.startsWith(end, i)) {
|
|
671
|
+
s.mode = 'subset';
|
|
672
|
+
i += end.length - 1;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
s.at = i - p;
|
|
678
|
+
return -1;
|
|
679
|
+
}
|
|
680
|
+
/** Reads a whole document type declaration, keeping the entities and attributes its internal subset declares. */
|
|
681
|
+
doctype(token, start, end) {
|
|
682
|
+
this.checkChars(token, 0, token.length);
|
|
683
|
+
const r = new Cursor(token, 9, false, m => this.malformed(m));
|
|
684
|
+
r.needSpace('after <!DOCTYPE');
|
|
685
|
+
const name = r.name();
|
|
686
|
+
if (!isQName(name))
|
|
687
|
+
this.malformed(`"${name.slice(0, 40)}" is not a valid name`);
|
|
688
|
+
const id = r.space() ? this.externalId(r) : undefined;
|
|
689
|
+
if (id)
|
|
690
|
+
r.space();
|
|
691
|
+
const s = { unread: false, entities: 0, externalEntities: 0, attributeDefaults: 0, skipped: 0 };
|
|
692
|
+
const internalSubset = r.skip('[');
|
|
693
|
+
if (internalSubset) {
|
|
694
|
+
this.declarations(r, s, false);
|
|
695
|
+
r.space();
|
|
696
|
+
}
|
|
697
|
+
// The scan ended the token at a ">", so it is the last character.
|
|
698
|
+
if (r.i !== token.length - 1)
|
|
699
|
+
this.malformed('The document type declaration does not end where it should');
|
|
700
|
+
this.doctypeSeen = true;
|
|
701
|
+
const { entities, externalEntities, attributeDefaults, skipped } = s;
|
|
702
|
+
this.notes.doctype?.({ name, ...id, internalSubset, entities, externalEntities, attributeDefaults, skipped, start, end });
|
|
703
|
+
}
|
|
704
|
+
/** SYSTEM and a system literal, or PUBLIC and a public and a system literal, or undefined when neither is next. */
|
|
705
|
+
externalId(r) {
|
|
706
|
+
if (r.skip('SYSTEM')) {
|
|
707
|
+
r.needSpace('after SYSTEM');
|
|
708
|
+
return { systemId: r.literal('A system identifier') };
|
|
709
|
+
}
|
|
710
|
+
if (!r.skip('PUBLIC'))
|
|
711
|
+
return undefined;
|
|
712
|
+
r.needSpace('after PUBLIC');
|
|
713
|
+
const publicId = r.literal('A public identifier');
|
|
714
|
+
if (!PUBID.test(publicId))
|
|
715
|
+
this.malformed('A public identifier holds a character it may not');
|
|
716
|
+
r.needSpace('after the public identifier');
|
|
717
|
+
return { publicId, systemId: r.literal('A system identifier') };
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Reads markup declarations: the internal subset up to and past its "]", or with `nested` the whole text of a
|
|
721
|
+
* parameter entity.
|
|
722
|
+
*/
|
|
723
|
+
declarations(r, s, nested) {
|
|
724
|
+
for (;;) {
|
|
725
|
+
r.space();
|
|
726
|
+
if (nested && r.i === r.s.length)
|
|
727
|
+
return;
|
|
728
|
+
if (!nested && r.skip(']'))
|
|
729
|
+
return;
|
|
730
|
+
if (r.skip('<!--')) {
|
|
731
|
+
const close = r.s.indexOf('-->', r.i);
|
|
732
|
+
if (close < 0)
|
|
733
|
+
this.malformed('A comment in the internal subset does not end');
|
|
734
|
+
r.i = close + 3;
|
|
735
|
+
}
|
|
736
|
+
else if (r.skip('<?')) {
|
|
737
|
+
const close = r.s.indexOf('?>', r.i);
|
|
738
|
+
if (close < 0)
|
|
739
|
+
this.malformed('A processing instruction in the internal subset does not end');
|
|
740
|
+
const target = r.s.slice(r.i, close).split(/\s/, 1)[0];
|
|
741
|
+
if (!(0, exports.isNcName)(target) || target.toLowerCase() === 'xml')
|
|
742
|
+
this.malformed('A processing instruction without a valid target');
|
|
743
|
+
r.i = close + 2;
|
|
744
|
+
}
|
|
745
|
+
else if (r.skip('<!ENTITY'))
|
|
746
|
+
this.entityDeclaration(r, s);
|
|
747
|
+
else if (r.skip('<!ATTLIST'))
|
|
748
|
+
this.attributeListDeclaration(r, s);
|
|
749
|
+
else if (r.skip('<!ELEMENT') || r.skip('<!NOTATION')) {
|
|
750
|
+
r.needSpace('after the declaration keyword');
|
|
751
|
+
r.declarationEnd();
|
|
752
|
+
}
|
|
753
|
+
else if (r.skip('%'))
|
|
754
|
+
this.parameterReference(r, s);
|
|
755
|
+
else if (r.skip('<!['))
|
|
756
|
+
this.malformed('A conditional section, which only an external subset may hold');
|
|
757
|
+
else
|
|
758
|
+
this.malformed('An unknown declaration in the internal subset');
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* A parameter entity reference between declarations. An internal entity's text is read as declarations, with a
|
|
763
|
+
* space on each side as XML 1.0 (4.4.8) says. An external or undeclared one is not read, and the entity and
|
|
764
|
+
* attribute-list declarations after it are skipped, since it could have declared the same names first.
|
|
765
|
+
*/
|
|
766
|
+
parameterReference(r, s) {
|
|
767
|
+
const name = r.name();
|
|
768
|
+
if (!(0, exports.isNcName)(name) || !r.skip(';'))
|
|
769
|
+
this.malformed('A malformed parameter entity reference');
|
|
770
|
+
const e = this.parameters.get(name);
|
|
771
|
+
if (!e || !('text' in e)) {
|
|
772
|
+
if (!this.standalone)
|
|
773
|
+
s.unread = true;
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
this.enter(`%${name}`, e.text);
|
|
777
|
+
this.declarations(new Cursor(` ${e.text} `, 0, true, m => this.malformed(m)), s, true);
|
|
778
|
+
this.active.pop();
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Reads an entity declaration after its keyword, and keeps the entity unless it comes after a parameter entity that
|
|
782
|
+
* was not read, or gives one of the predefined names, or a name already declared: the first declaration binds.
|
|
783
|
+
*/
|
|
784
|
+
entityDeclaration(r, s) {
|
|
785
|
+
r.needSpace('after <!ENTITY');
|
|
786
|
+
const parameter = r.skip('%');
|
|
787
|
+
if (parameter)
|
|
788
|
+
r.needSpace('after %');
|
|
789
|
+
const name = r.name();
|
|
790
|
+
if (!(0, exports.isNcName)(name))
|
|
791
|
+
this.malformed(`"${name.slice(0, 40)}" is not a valid entity name`);
|
|
792
|
+
r.needSpace('after the entity name');
|
|
793
|
+
let entity;
|
|
794
|
+
const q = r.s[r.i];
|
|
795
|
+
if (q === '"' || q === "'")
|
|
796
|
+
entity = { text: this.entityValue(r.literal('An entity value'), r.normalized) };
|
|
797
|
+
else {
|
|
798
|
+
const id = this.externalId(r);
|
|
799
|
+
if (!id)
|
|
800
|
+
this.malformed('An entity declaration with neither a value nor an external identifier');
|
|
801
|
+
entity = id;
|
|
802
|
+
if (!parameter && r.space() && r.skip('NDATA')) {
|
|
803
|
+
r.needSpace('after NDATA');
|
|
804
|
+
const notation = r.name();
|
|
805
|
+
if (!(0, exports.isNcName)(notation))
|
|
806
|
+
this.malformed('NDATA without a notation name');
|
|
807
|
+
entity = { ...id, notation };
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
r.space();
|
|
811
|
+
if (!r.skip('>'))
|
|
812
|
+
this.malformed('An entity declaration does not end where it should');
|
|
813
|
+
if (s.unread) {
|
|
814
|
+
s.skipped++;
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const map = parameter ? this.parameters : this.entities;
|
|
818
|
+
if (map.has(name) || (!parameter && Object.hasOwn(PREDEFINED, name)))
|
|
819
|
+
return;
|
|
820
|
+
map.set(name, entity);
|
|
821
|
+
if (parameter)
|
|
822
|
+
return;
|
|
823
|
+
s.entities++;
|
|
824
|
+
if (!('text' in entity))
|
|
825
|
+
s.externalEntities++;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* The text an entity value expands to: line ends normalized, unless `normalized` says they were, and character
|
|
829
|
+
* references replaced. References to general entities stay as written, to be expanded where the entity is used. A
|
|
830
|
+
* parameter entity reference cannot stand inside a declaration in the internal subset.
|
|
831
|
+
*/
|
|
832
|
+
entityValue(raw, normalized) {
|
|
833
|
+
let out = '';
|
|
834
|
+
let from = 0;
|
|
835
|
+
for (let i = 0; i < raw.length; i++) {
|
|
836
|
+
const c = raw.charCodeAt(i);
|
|
837
|
+
if (c === 0x25)
|
|
838
|
+
this.malformed('A parameter entity reference inside a declaration');
|
|
839
|
+
else if (c === 0x26) {
|
|
840
|
+
const semi = raw.indexOf(';', i);
|
|
841
|
+
if (semi < 0)
|
|
842
|
+
this.malformed('An unterminated reference');
|
|
843
|
+
const name = raw.slice(i + 1, semi);
|
|
844
|
+
if (name.startsWith('#')) {
|
|
845
|
+
out += raw.slice(from, i) + this.characterReference(name);
|
|
846
|
+
from = semi + 1;
|
|
847
|
+
}
|
|
848
|
+
else if (!(0, exports.isNcName)(name))
|
|
849
|
+
this.malformed('A malformed reference');
|
|
850
|
+
i = semi;
|
|
851
|
+
}
|
|
852
|
+
else if (c === 0x0d && !normalized) {
|
|
853
|
+
out += `${raw.slice(from, i)}\n`;
|
|
854
|
+
if (raw.charCodeAt(i + 1) === 0x0a)
|
|
855
|
+
i++;
|
|
856
|
+
from = i + 1;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return out + raw.slice(from);
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Reads an attribute-list declaration after its keyword. Each attribute it names for the first time is kept, with
|
|
863
|
+
* its default normalized now, as the entities declared so far give it. After a parameter entity that was not read,
|
|
864
|
+
* the declaration is skipped.
|
|
865
|
+
*/
|
|
866
|
+
attributeListDeclaration(r, s) {
|
|
867
|
+
r.needSpace('after <!ATTLIST');
|
|
868
|
+
const element = r.name();
|
|
869
|
+
if (!isQName(element))
|
|
870
|
+
this.malformed(`"${element.slice(0, 40)}" is not a valid name`);
|
|
871
|
+
const defs = [];
|
|
872
|
+
for (;;) {
|
|
873
|
+
const spaced = r.space();
|
|
874
|
+
if (r.skip('>'))
|
|
875
|
+
break;
|
|
876
|
+
if (!spaced)
|
|
877
|
+
this.malformed('No white space before an attribute definition');
|
|
878
|
+
const name = r.name();
|
|
879
|
+
if (!isQName(name))
|
|
880
|
+
this.malformed(`"${name.slice(0, 40)}" is not a valid name`);
|
|
881
|
+
r.needSpace('after the attribute name');
|
|
882
|
+
const tokenized = this.attributeType(r);
|
|
883
|
+
r.needSpace('after the attribute type');
|
|
884
|
+
let raw;
|
|
885
|
+
if (!r.skip('#REQUIRED') && !r.skip('#IMPLIED')) {
|
|
886
|
+
if (r.skip('#FIXED'))
|
|
887
|
+
r.needSpace('after #FIXED');
|
|
888
|
+
raw = r.literal('A default value');
|
|
889
|
+
if (raw.includes('<'))
|
|
890
|
+
this.malformed('"<" in an attribute value');
|
|
891
|
+
}
|
|
892
|
+
defs.push([name, tokenized, raw]);
|
|
893
|
+
}
|
|
894
|
+
if (s.unread) {
|
|
895
|
+
s.skipped++;
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
let declared = this.declared.get(element);
|
|
899
|
+
if (!declared) {
|
|
900
|
+
declared = { names: new Set(), tokenized: new Set(), defaults: [] };
|
|
901
|
+
this.declared.set(element, declared);
|
|
902
|
+
}
|
|
903
|
+
for (const [name, tokenized, raw] of defs) {
|
|
904
|
+
const value = raw === undefined ? undefined : this.attributeValue(raw, r.normalized);
|
|
905
|
+
if (declared.names.has(name))
|
|
906
|
+
continue;
|
|
907
|
+
declared.names.add(name);
|
|
908
|
+
if (tokenized)
|
|
909
|
+
declared.tokenized.add(name);
|
|
910
|
+
if (value === undefined)
|
|
911
|
+
continue;
|
|
912
|
+
declared.defaults.push([name, tokenized ? tokenize(value) : value]);
|
|
913
|
+
s.attributeDefaults++;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
/** Reads an attribute type and tells whether its values are tokens, normalized further than CDATA. */
|
|
917
|
+
attributeType(r) {
|
|
918
|
+
if (r.s[r.i] === '(') {
|
|
919
|
+
this.enumeration(r);
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
const type = r.name();
|
|
923
|
+
if (type === 'CDATA')
|
|
924
|
+
return false;
|
|
925
|
+
if (type === 'NOTATION') {
|
|
926
|
+
r.needSpace('after NOTATION');
|
|
927
|
+
this.enumeration(r);
|
|
928
|
+
}
|
|
929
|
+
else if (!TOKENIZED_TYPES.has(type))
|
|
930
|
+
this.malformed(`"${type.slice(0, 40)}" is not an attribute type`);
|
|
931
|
+
return true;
|
|
932
|
+
}
|
|
933
|
+
/** An enumerated type: name tokens between parentheses, separated by "|". */
|
|
934
|
+
enumeration(r) {
|
|
935
|
+
if (!r.skip('('))
|
|
936
|
+
this.malformed('An enumerated attribute type does not start with "("');
|
|
937
|
+
for (;;) {
|
|
938
|
+
r.space();
|
|
939
|
+
const from = r.i;
|
|
940
|
+
while (r.i < r.s.length && !/[\s|()>]/.test(r.s[r.i]))
|
|
941
|
+
r.i++;
|
|
942
|
+
if (!NMTOKEN.test(r.s.slice(from, r.i)))
|
|
943
|
+
this.malformed('An enumerated attribute type holds something other than a name token');
|
|
944
|
+
r.space();
|
|
945
|
+
if (r.skip(')'))
|
|
946
|
+
return;
|
|
947
|
+
if (!r.skip('|'))
|
|
948
|
+
this.malformed('An enumerated attribute type does not end with ")"');
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
lookup(prefix) {
|
|
952
|
+
if (prefix === 'xml')
|
|
953
|
+
return exports.XML_NS;
|
|
954
|
+
for (let i = this.stack.length - 1; i >= 0; i--) {
|
|
955
|
+
const v = this.stack[i].scope?.get(prefix);
|
|
956
|
+
if (v !== undefined)
|
|
957
|
+
return v;
|
|
958
|
+
}
|
|
959
|
+
return prefix === '' ? '' : undefined;
|
|
960
|
+
}
|
|
961
|
+
split(qname) {
|
|
962
|
+
const colon = qname.indexOf(':');
|
|
963
|
+
if (colon < 0) {
|
|
964
|
+
if (!(0, exports.isNcName)(qname))
|
|
965
|
+
this.malformed(`"${qname.slice(0, 40)}" is not a valid name`);
|
|
966
|
+
return ['', qname];
|
|
967
|
+
}
|
|
968
|
+
const prefix = qname.slice(0, colon);
|
|
969
|
+
const local = qname.slice(colon + 1);
|
|
970
|
+
if (!(0, exports.isNcName)(prefix) || !(0, exports.isNcName)(local))
|
|
971
|
+
this.malformed(`"${qname.slice(0, 40)}" is not a valid name`);
|
|
972
|
+
return [prefix, local];
|
|
973
|
+
}
|
|
974
|
+
startTag(body, start, end) {
|
|
975
|
+
if (this.rootSeen && !this.stack.length)
|
|
976
|
+
this.malformed('A second root element');
|
|
977
|
+
let i = 0;
|
|
978
|
+
const n = body.length;
|
|
979
|
+
while (i < n && !isSpace(body.charCodeAt(i)) && body[i] !== '/')
|
|
980
|
+
i++;
|
|
981
|
+
const qname = body.slice(0, i);
|
|
982
|
+
const raw = [];
|
|
983
|
+
let selfClosing = this.vml && qname.toLowerCase() === 'br';
|
|
984
|
+
for (;;) {
|
|
985
|
+
const before = i;
|
|
986
|
+
while (i < n && isSpace(body.charCodeAt(i)))
|
|
987
|
+
i++;
|
|
988
|
+
if (i >= n)
|
|
989
|
+
break;
|
|
990
|
+
if (body[i] === '/') {
|
|
991
|
+
if (i !== n - 1)
|
|
992
|
+
this.malformed('"/" inside a tag');
|
|
993
|
+
selfClosing = true;
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
if (i === before)
|
|
997
|
+
this.malformed('Attributes without white space between them');
|
|
998
|
+
const nameStart = i;
|
|
999
|
+
while (i < n && !isSpace(body.charCodeAt(i)) && body[i] !== '=')
|
|
1000
|
+
i++;
|
|
1001
|
+
const name = body.slice(nameStart, i);
|
|
1002
|
+
while (i < n && isSpace(body.charCodeAt(i)))
|
|
1003
|
+
i++;
|
|
1004
|
+
if (body[i] !== '=')
|
|
1005
|
+
this.malformed(`Attribute ${name.slice(0, 40)} has no value`);
|
|
1006
|
+
i++;
|
|
1007
|
+
while (i < n && isSpace(body.charCodeAt(i)))
|
|
1008
|
+
i++;
|
|
1009
|
+
const quote = body[i];
|
|
1010
|
+
if (quote !== '"' && quote !== "'")
|
|
1011
|
+
this.malformed(`Attribute ${name.slice(0, 40)} has an unquoted value`);
|
|
1012
|
+
// markup() ended the tag at the first ">" outside quotes, so the value closes inside the tag.
|
|
1013
|
+
const close = body.indexOf(quote, i + 1);
|
|
1014
|
+
raw.push([name, this.attributeValue(body.slice(i + 1, close), this.span !== undefined)]);
|
|
1015
|
+
i = close + 1;
|
|
1016
|
+
}
|
|
1017
|
+
// Attribute-list declarations normalize the attributes they type, and add the defaults the tag leaves out. Each
|
|
1018
|
+
// default added counts against the entity limits, so declarations cannot multiply the work of a long part.
|
|
1019
|
+
const declared = this.declared.get(qname);
|
|
1020
|
+
if (declared) {
|
|
1021
|
+
const given = new Set();
|
|
1022
|
+
for (const a of raw) {
|
|
1023
|
+
given.add(a[0]);
|
|
1024
|
+
if (declared.tokenized.has(a[0]))
|
|
1025
|
+
a[1] = tokenize(a[1]);
|
|
1026
|
+
}
|
|
1027
|
+
for (const [name, value] of declared.defaults) {
|
|
1028
|
+
if (given.has(name))
|
|
1029
|
+
continue;
|
|
1030
|
+
this.charge(value.length);
|
|
1031
|
+
raw.push([name, value]);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const scope = new Map();
|
|
1035
|
+
const plain = [];
|
|
1036
|
+
const qnames = new Set();
|
|
1037
|
+
for (const [name, value] of raw) {
|
|
1038
|
+
if (qnames.has(name))
|
|
1039
|
+
this.malformed(`Attribute ${name.slice(0, 40)} appears twice`);
|
|
1040
|
+
qnames.add(name);
|
|
1041
|
+
if (name === 'xmlns') {
|
|
1042
|
+
if (value === exports.XML_NS || value === exports.XMLNS_NS)
|
|
1043
|
+
this.malformed('The default namespace bound to a reserved namespace');
|
|
1044
|
+
scope.set('', value);
|
|
1045
|
+
}
|
|
1046
|
+
else if (name.startsWith('xmlns:')) {
|
|
1047
|
+
const prefix = name.slice(6);
|
|
1048
|
+
if (!(0, exports.isNcName)(prefix))
|
|
1049
|
+
this.malformed(`"${name.slice(0, 40)}" is not a valid namespace declaration`);
|
|
1050
|
+
if (prefix === 'xmlns')
|
|
1051
|
+
this.malformed('The xmlns prefix declared');
|
|
1052
|
+
if (value === '')
|
|
1053
|
+
this.malformed(`Prefix ${prefix} bound to no namespace`);
|
|
1054
|
+
if ((prefix === 'xml') !== (value === exports.XML_NS) || value === exports.XMLNS_NS)
|
|
1055
|
+
this.malformed(`Prefix ${prefix} bound to a reserved namespace`);
|
|
1056
|
+
scope.set(prefix, value);
|
|
1057
|
+
}
|
|
1058
|
+
else
|
|
1059
|
+
plain.push([name, value]);
|
|
1060
|
+
}
|
|
1061
|
+
const depth = this.stack.length + 1;
|
|
1062
|
+
if (depth > this.limits.depth)
|
|
1063
|
+
this.stop(types_1.OfficeDetail.XmlDepth, `Elements nest deeper than ${this.limits.depth}`);
|
|
1064
|
+
const el = { ns: '', local: '', qname, attributes: [], depth, start, end };
|
|
1065
|
+
const open = { el, scope: scope.size ? scope : undefined };
|
|
1066
|
+
this.stack.push(open);
|
|
1067
|
+
const [prefix, local] = this.split(qname);
|
|
1068
|
+
const ns = this.lookup(prefix);
|
|
1069
|
+
if (ns === undefined)
|
|
1070
|
+
this.malformed(`Prefix ${prefix} is not declared`);
|
|
1071
|
+
el.ns = ns;
|
|
1072
|
+
el.local = local;
|
|
1073
|
+
const expanded = new Set();
|
|
1074
|
+
for (const [name, value] of plain) {
|
|
1075
|
+
const [p, l] = this.split(name);
|
|
1076
|
+
const ans = p === '' ? '' : this.lookup(p);
|
|
1077
|
+
if (ans === undefined)
|
|
1078
|
+
this.malformed(`Prefix ${p} is not declared`);
|
|
1079
|
+
const key = `${ans} ${l}`;
|
|
1080
|
+
if (expanded.has(key))
|
|
1081
|
+
this.malformed(`Attribute ${l} appears twice in one namespace`);
|
|
1082
|
+
expanded.add(key);
|
|
1083
|
+
el.attributes.push({ ns: ans, local: l, qname: name, value });
|
|
1084
|
+
}
|
|
1085
|
+
this.rootSeen = true;
|
|
1086
|
+
this.handler.start?.(el);
|
|
1087
|
+
if (selfClosing) {
|
|
1088
|
+
this.stack.pop();
|
|
1089
|
+
this.handler.end?.(el, start, end);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
endTag(body, start, end) {
|
|
1093
|
+
let n = body.length;
|
|
1094
|
+
while (n > 0 && isSpace(body.charCodeAt(n - 1)))
|
|
1095
|
+
n--;
|
|
1096
|
+
const qname = body.slice(0, n);
|
|
1097
|
+
const top = this.stack.length > this.floor ? this.stack.pop() : undefined;
|
|
1098
|
+
if (!top)
|
|
1099
|
+
this.malformed(this.floor ? `</${qname.slice(0, 40)}> closes an element from outside the entity text it stands in` : `</${qname.slice(0, 40)}> closes nothing`);
|
|
1100
|
+
if (top.el.qname !== qname)
|
|
1101
|
+
this.malformed(`</${qname.slice(0, 40)}> closes <${top.el.qname.slice(0, 40)}>`);
|
|
1102
|
+
this.handler.end?.(top.el, start, end);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
exports.XmlTokenizer = XmlTokenizer;
|