@push.rocks/smartlinuxrepo 1.1.1 → 1.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/alpine/apkindexreader.d.ts +87 -0
- package/dist_ts/alpine/apkindexreader.js +193 -0
- package/dist_ts/arch/indexreader.d.ts +35 -0
- package/dist_ts/arch/indexreader.js +45 -0
- package/dist_ts/arch/reader.js +2 -2
- package/dist_ts/debian/indexbuilder.js +8 -23
- package/dist_ts/debian/indexreader.d.ts +107 -0
- package/dist_ts/debian/indexreader.js +274 -0
- package/dist_ts/debian/reader.js +6 -2
- package/dist_ts/debian/rules.d.ts +18 -0
- package/dist_ts/debian/rules.js +81 -2
- package/dist_ts/index.d.ts +14 -4
- package/dist_ts/index.js +9 -3
- package/dist_ts/readers/decompress.d.ts +14 -5
- package/dist_ts/readers/decompress.js +401 -26
- package/dist_ts/readers/document.d.ts +36 -0
- package/dist_ts/readers/document.js +110 -0
- package/dist_ts/rpm/repodata.d.ts +3 -2
- package/dist_ts/rpm/repodata.js +5 -4
- package/dist_ts/rpm/repodatareader.d.ts +173 -0
- package/dist_ts/rpm/repodatareader.js +414 -0
- package/dist_ts/rpm/xmlreader.d.ts +70 -0
- package/dist_ts/rpm/xmlreader.js +576 -0
- package/dist_ts/shared/errors.d.ts +12 -4
- package/dist_ts/shared/errors.js +1 -1
- package/dist_ts/shared/limits.d.ts +9 -2
- package/dist_ts/shared/limits.js +5 -2
- package/dist_ts/shared/types.d.ts +2 -0
- package/dist_ts/signing/verifiers.d.ts +43 -0
- package/dist_ts/signing/verifiers.js +81 -2
- package/package.json +1 -1
- package/readme.md +56 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/alpine/apkindexreader.ts +265 -0
- package/ts/arch/indexreader.ts +66 -0
- package/ts/arch/reader.ts +3 -2
- package/ts/debian/indexbuilder.ts +13 -23
- package/ts/debian/indexreader.ts +372 -0
- package/ts/debian/reader.ts +7 -2
- package/ts/debian/rules.ts +83 -1
- package/ts/index.ts +40 -2
- package/ts/readers/decompress.ts +442 -32
- package/ts/readers/document.ts +136 -0
- package/ts/rpm/repodata.ts +4 -3
- package/ts/rpm/repodatareader.ts +582 -0
- package/ts/rpm/xmlreader.ts +680 -0
- package/ts/shared/errors.ts +13 -4
- package/ts/shared/limits.ts +10 -2
- package/ts/shared/types.ts +3 -0
- package/ts/signing/verifiers.ts +113 -1
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import { LinuxRepoError, limitExceeded } from '../shared/errors.js';
|
|
2
|
+
import type { ILinuxPackageLimits } from '../shared/limits.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A strict, iterative XML 1.0 scanner over the bytes of one repodata document,
|
|
6
|
+
* the reading side of `xml.ts`. It checks well-formedness and hands the
|
|
7
|
+
* elements of each unit (a child of the root) to a handler, without building a
|
|
8
|
+
* tree. Every refusal is `malformed-index` with the byte offset in
|
|
9
|
+
* `detail.value`:
|
|
10
|
+
* - the document must be strict UTF-8 holding only XML characters; an
|
|
11
|
+
* optional UTF-8 byte order mark and XML declaration may lead it, and a
|
|
12
|
+
* declared `encoding` other than UTF-8 (ASCII case-insensitive) is refused;
|
|
13
|
+
* - `<!DOCTYPE` (or any other `<!` declaration) is refused, so there are no
|
|
14
|
+
* entity definitions: only `< > & ' "` and numeric
|
|
15
|
+
* references to XML characters are accepted;
|
|
16
|
+
* - tags must match, attribute names must be unique within a tag and be
|
|
17
|
+
* separated by whitespace, values are quoted and hold no `<`, text holds no
|
|
18
|
+
* `]]>`, comments hold no `--`, a processing instruction's target is not
|
|
19
|
+
* `xml` (except the declaration at the start), and there is exactly one root;
|
|
20
|
+
* - comments, processing instructions and whitespace may sit before and after
|
|
21
|
+
* the root; inside it only whitespace may sit between units; inside a unit
|
|
22
|
+
* CDATA sections, comments, processing instructions and empty-element tags
|
|
23
|
+
* are accepted.
|
|
24
|
+
*
|
|
25
|
+
* Names are matched literally with their prefixes (no namespace processing);
|
|
26
|
+
* a name starts with an ASCII letter, `_`, `:` or a non-ASCII character and
|
|
27
|
+
* continues with those, digits, `.` and `-` (every non-ASCII character counts
|
|
28
|
+
* as a name character, a simplification of the XML name production). Values
|
|
29
|
+
* handed to the handler are decoded: references replaced, line ends
|
|
30
|
+
* normalized (CRLF and CR to LF), and in attribute values literal tabs and
|
|
31
|
+
* line ends become spaces (XML 1.0 sections 2.11 and 3.3.3).
|
|
32
|
+
*
|
|
33
|
+
* Bounds: one start tag by `maxHeaderBytes`, one unit by `maxMetadataBytes`,
|
|
34
|
+
* units and open elements at a time by `maxEntries`.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
export interface IXmlAttribute {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly value: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Receives the elements of one unit; `depth` 0 is the unit element itself. */
|
|
43
|
+
export interface IXmlUnitHandler {
|
|
44
|
+
/** An element starts; returning true captures its direct character data for `end`. */
|
|
45
|
+
start(depthArg: number, nameArg: string, attributesArg: readonly IXmlAttribute[]): boolean;
|
|
46
|
+
/** An element ends; `textArg` is its captured character data, or null when not captured. */
|
|
47
|
+
end(depthArg: number, nameArg: string, textArg: string | null): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface IXmlScanOptions {
|
|
51
|
+
/** The required root element name. */
|
|
52
|
+
rootName: string;
|
|
53
|
+
/** The unit element name among the root's children. */
|
|
54
|
+
unitName: string;
|
|
55
|
+
/** Whether other root children are accepted (repomd); otherwise they are refused. */
|
|
56
|
+
keepOtherChildren: boolean;
|
|
57
|
+
limits: Readonly<ILinuxPackageLimits>;
|
|
58
|
+
/** A handler for the unit starting at `startArg` (the offset of its `<`). */
|
|
59
|
+
createUnit(startArg: number): IXmlUnitHandler;
|
|
60
|
+
/** A unit ended: its bytes are `[startArg, endArg)`. */
|
|
61
|
+
unit(handlerArg: IXmlUnitHandler, startArg: number, endArg: number): void;
|
|
62
|
+
/** Another root child ended (only with `keepOtherChildren`). */
|
|
63
|
+
other(nameArg: string, startArg: number, endArg: number): void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Where the root element's tags sit in the document. */
|
|
67
|
+
export interface IXmlFrameScan {
|
|
68
|
+
rootStart: number;
|
|
69
|
+
rootStartEnd: number;
|
|
70
|
+
rootAttributes: IXmlAttribute[];
|
|
71
|
+
/** The root end tag `[rootEndStart, rootEnd)`; both equal `rootStartEnd` for an empty-element root. */
|
|
72
|
+
rootEndStart: number;
|
|
73
|
+
rootEnd: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const LT = 0x3c;
|
|
77
|
+
const GT = 0x3e;
|
|
78
|
+
const AMP = 0x26;
|
|
79
|
+
const SLASH = 0x2f;
|
|
80
|
+
const BANG = 0x21;
|
|
81
|
+
const QUESTION = 0x3f;
|
|
82
|
+
const EQUALS = 0x3d;
|
|
83
|
+
const QUOTE = 0x22;
|
|
84
|
+
const APOSTROPHE = 0x27;
|
|
85
|
+
const SEMICOLON = 0x3b;
|
|
86
|
+
const HASH = 0x23;
|
|
87
|
+
const CR = 0x0d;
|
|
88
|
+
const LF = 0x0a;
|
|
89
|
+
const TAB = 0x09;
|
|
90
|
+
const SPACE = 0x20;
|
|
91
|
+
const BRACKET_CLOSE = 0x5d;
|
|
92
|
+
|
|
93
|
+
const utf8 = new TextDecoder('utf-8');
|
|
94
|
+
const ascii = (textArg: string) => Uint8Array.from(textArg, (characterArg) => characterArg.charCodeAt(0));
|
|
95
|
+
const COMMENT_OPEN = ascii('<!--');
|
|
96
|
+
const CDATA_OPEN = ascii('<![CDATA[');
|
|
97
|
+
const XML_DECLARATION_OPEN = ascii('<?xml');
|
|
98
|
+
const PREDEFINED: ReadonlyMap<string, string> = new Map([
|
|
99
|
+
['lt', '<'],
|
|
100
|
+
['gt', '>'],
|
|
101
|
+
['amp', '&'],
|
|
102
|
+
['apos', "'"],
|
|
103
|
+
['quot', '"'],
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
function isSpace(byteArg: number): boolean {
|
|
107
|
+
return byteArg === SPACE || byteArg === TAB || byteArg === LF || byteArg === CR;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isNameStart(byteArg: number): boolean {
|
|
111
|
+
return (
|
|
112
|
+
(byteArg >= 0x61 && byteArg <= 0x7a) ||
|
|
113
|
+
(byteArg >= 0x41 && byteArg <= 0x5a) ||
|
|
114
|
+
byteArg === 0x5f ||
|
|
115
|
+
byteArg === 0x3a ||
|
|
116
|
+
byteArg >= 0x80
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isNameChar(byteArg: number): boolean {
|
|
121
|
+
return isNameStart(byteArg) || (byteArg >= 0x30 && byteArg <= 0x39) || byteArg === 0x2e || byteArg === 0x2d;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isXmlCharacter(codePointArg: number): boolean {
|
|
125
|
+
return (
|
|
126
|
+
codePointArg === 0x09 ||
|
|
127
|
+
codePointArg === 0x0a ||
|
|
128
|
+
codePointArg === 0x0d ||
|
|
129
|
+
(codePointArg >= 0x20 && codePointArg <= 0xd7ff) ||
|
|
130
|
+
(codePointArg >= 0xe000 && codePointArg <= 0xfffd) ||
|
|
131
|
+
(codePointArg >= 0x10000 && codePointArg <= 0x10ffff)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface IStartTag {
|
|
136
|
+
name: string;
|
|
137
|
+
nameStart: number;
|
|
138
|
+
nameEnd: number;
|
|
139
|
+
attributes: IXmlAttribute[];
|
|
140
|
+
/** Offset after the tag's `>`. */
|
|
141
|
+
end: number;
|
|
142
|
+
selfClosing: boolean;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface IOpenElement {
|
|
146
|
+
name: string;
|
|
147
|
+
nameStart: number;
|
|
148
|
+
nameEnd: number;
|
|
149
|
+
text: string[] | null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Scans one document; see the module comment. */
|
|
153
|
+
export function scanXmlDocument(documentArg: Uint8Array, optionsArg: IXmlScanOptions): IXmlFrameScan {
|
|
154
|
+
return new XmlScanner(documentArg, optionsArg).scan();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
class XmlScanner {
|
|
158
|
+
private readonly doc: Uint8Array;
|
|
159
|
+
private readonly buffer: Buffer;
|
|
160
|
+
private readonly limits: Readonly<ILinuxPackageLimits>;
|
|
161
|
+
|
|
162
|
+
constructor(
|
|
163
|
+
documentArg: Uint8Array,
|
|
164
|
+
private readonly options: IXmlScanOptions,
|
|
165
|
+
) {
|
|
166
|
+
this.doc = documentArg;
|
|
167
|
+
this.buffer = Buffer.from(documentArg.buffer, documentArg.byteOffset, documentArg.byteLength);
|
|
168
|
+
this.limits = options.limits;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public scan(): IXmlFrameScan {
|
|
172
|
+
const doc = this.doc;
|
|
173
|
+
this.checkCharacters();
|
|
174
|
+
let pos = 0;
|
|
175
|
+
if (doc[0] === 0xef && doc[1] === 0xbb && doc[2] === 0xbf) {
|
|
176
|
+
pos = 3;
|
|
177
|
+
}
|
|
178
|
+
if (this.startsWith(pos, XML_DECLARATION_OPEN) && (isSpace(doc[pos + 5]) || doc[pos + 5] === QUESTION)) {
|
|
179
|
+
pos = this.xmlDeclaration(pos);
|
|
180
|
+
}
|
|
181
|
+
pos = this.misc(pos);
|
|
182
|
+
if (pos >= doc.length || doc[pos] !== LT || !isNameStart(doc[pos + 1])) {
|
|
183
|
+
throw this.malformed('the document has no root element', pos);
|
|
184
|
+
}
|
|
185
|
+
const rootStart = pos;
|
|
186
|
+
const root = this.startTag(pos);
|
|
187
|
+
if (root.name !== this.options.rootName) {
|
|
188
|
+
throw this.malformed(`the root element is <${root.name}>, expected <${this.options.rootName}>`, pos);
|
|
189
|
+
}
|
|
190
|
+
let rootEndStart = root.end;
|
|
191
|
+
let rootEnd = root.end;
|
|
192
|
+
if (!root.selfClosing) {
|
|
193
|
+
pos = root.end;
|
|
194
|
+
let units = 0;
|
|
195
|
+
for (;;) {
|
|
196
|
+
while (pos < doc.length && isSpace(doc[pos])) {
|
|
197
|
+
pos += 1;
|
|
198
|
+
}
|
|
199
|
+
if (pos >= doc.length) {
|
|
200
|
+
throw this.malformed(`the <${root.name}> element is not closed`, rootStart);
|
|
201
|
+
}
|
|
202
|
+
if (doc[pos] !== LT) {
|
|
203
|
+
throw this.malformed(`character data between the units of <${root.name}>`, pos);
|
|
204
|
+
}
|
|
205
|
+
if (doc[pos + 1] === SLASH) {
|
|
206
|
+
rootEndStart = pos;
|
|
207
|
+
rootEnd = this.endTag(pos, { name: root.name, nameStart: root.nameStart, nameEnd: root.nameEnd, text: null });
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (!isNameStart(doc[pos + 1])) {
|
|
211
|
+
throw this.malformed(`markup other than an element between the units of <${root.name}>`, pos);
|
|
212
|
+
}
|
|
213
|
+
const tag = this.startTag(pos);
|
|
214
|
+
units += 1;
|
|
215
|
+
if (units > this.limits.maxEntries) {
|
|
216
|
+
throw limitExceeded('maxEntries', this.limits.maxEntries, `<${root.name}> unit count`);
|
|
217
|
+
}
|
|
218
|
+
if (tag.name === this.options.unitName) {
|
|
219
|
+
const handler = this.options.createUnit(pos);
|
|
220
|
+
const end = this.element(pos, tag, handler);
|
|
221
|
+
this.options.unit(handler, pos, end);
|
|
222
|
+
pos = end;
|
|
223
|
+
} else if (this.options.keepOtherChildren) {
|
|
224
|
+
const end = this.element(pos, tag, null);
|
|
225
|
+
this.options.other(tag.name, pos, end);
|
|
226
|
+
pos = end;
|
|
227
|
+
} else {
|
|
228
|
+
throw this.malformed(`<${root.name}> holds a <${tag.name}> element, expected <${this.options.unitName}>`, pos);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
pos = this.misc(rootEnd);
|
|
233
|
+
if (pos < doc.length) {
|
|
234
|
+
throw this.malformed('content after the root element', pos);
|
|
235
|
+
}
|
|
236
|
+
return { rootStart, rootStartEnd: root.end, rootAttributes: root.attributes, rootEndStart, rootEnd };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Strict UTF-8 holding only XML characters (XML 1.0 `Char`). */
|
|
240
|
+
private checkCharacters(): void {
|
|
241
|
+
const doc = this.doc;
|
|
242
|
+
const length = doc.length;
|
|
243
|
+
let i = 0;
|
|
244
|
+
while (i < length) {
|
|
245
|
+
const b0 = doc[i];
|
|
246
|
+
if (b0 < 0x80) {
|
|
247
|
+
if (b0 < 0x20 && b0 !== TAB && b0 !== LF && b0 !== CR) {
|
|
248
|
+
throw this.malformed('the document holds a control character that is not an XML character', i);
|
|
249
|
+
}
|
|
250
|
+
i += 1;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
let need: number;
|
|
254
|
+
let codePoint: number;
|
|
255
|
+
let low = 0x80;
|
|
256
|
+
let high = 0xbf;
|
|
257
|
+
if (b0 >= 0xc2 && b0 <= 0xdf) {
|
|
258
|
+
need = 1;
|
|
259
|
+
codePoint = b0 & 0x1f;
|
|
260
|
+
} else if (b0 >= 0xe0 && b0 <= 0xef) {
|
|
261
|
+
need = 2;
|
|
262
|
+
codePoint = b0 & 0x0f;
|
|
263
|
+
if (b0 === 0xe0) {
|
|
264
|
+
low = 0xa0;
|
|
265
|
+
} else if (b0 === 0xed) {
|
|
266
|
+
high = 0x9f;
|
|
267
|
+
}
|
|
268
|
+
} else if (b0 >= 0xf0 && b0 <= 0xf4) {
|
|
269
|
+
need = 3;
|
|
270
|
+
codePoint = b0 & 0x07;
|
|
271
|
+
if (b0 === 0xf0) {
|
|
272
|
+
low = 0x90;
|
|
273
|
+
} else if (b0 === 0xf4) {
|
|
274
|
+
high = 0x8f;
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
throw this.malformed('the document is not valid UTF-8', i);
|
|
278
|
+
}
|
|
279
|
+
if (i + need >= length) {
|
|
280
|
+
throw this.malformed('the document is not valid UTF-8', i);
|
|
281
|
+
}
|
|
282
|
+
for (let k = 1; k <= need; k++) {
|
|
283
|
+
const b = doc[i + k];
|
|
284
|
+
if (b < (k === 1 ? low : 0x80) || b > (k === 1 ? high : 0xbf)) {
|
|
285
|
+
throw this.malformed('the document is not valid UTF-8', i);
|
|
286
|
+
}
|
|
287
|
+
codePoint = (codePoint << 6) | (b & 0x3f);
|
|
288
|
+
}
|
|
289
|
+
if (!isXmlCharacter(codePoint)) {
|
|
290
|
+
throw this.malformed('the document holds a code point that is not an XML character', i);
|
|
291
|
+
}
|
|
292
|
+
i += need + 1;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** `<?xml version="1.x" [encoding="UTF-8"] [standalone="yes|no"] ?>`; returns the offset after it. */
|
|
297
|
+
private xmlDeclaration(startArg: number): number {
|
|
298
|
+
const doc = this.doc;
|
|
299
|
+
let pos = startArg + XML_DECLARATION_OPEN.length;
|
|
300
|
+
const pseudo: Array<[string, string]> = [];
|
|
301
|
+
for (;;) {
|
|
302
|
+
const before = pos;
|
|
303
|
+
while (pos < doc.length && isSpace(doc[pos])) {
|
|
304
|
+
pos += 1;
|
|
305
|
+
}
|
|
306
|
+
if (doc[pos] === QUESTION && doc[pos + 1] === GT) {
|
|
307
|
+
pos += 2;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
if (pos === before || !isNameStart(doc[pos])) {
|
|
311
|
+
throw this.malformed('a malformed XML declaration', startArg);
|
|
312
|
+
}
|
|
313
|
+
const nameStart = pos;
|
|
314
|
+
while (pos < doc.length && isNameChar(doc[pos])) {
|
|
315
|
+
pos += 1;
|
|
316
|
+
}
|
|
317
|
+
const name = this.decode(nameStart, pos);
|
|
318
|
+
pos = this.skipSpace(pos);
|
|
319
|
+
if (doc[pos] !== EQUALS) {
|
|
320
|
+
throw this.malformed('a malformed XML declaration', startArg);
|
|
321
|
+
}
|
|
322
|
+
pos = this.skipSpace(pos + 1);
|
|
323
|
+
const quote = doc[pos];
|
|
324
|
+
if (quote !== QUOTE && quote !== APOSTROPHE) {
|
|
325
|
+
throw this.malformed('a malformed XML declaration', startArg);
|
|
326
|
+
}
|
|
327
|
+
const valueEnd = doc.indexOf(quote, pos + 1);
|
|
328
|
+
if (valueEnd < 0) {
|
|
329
|
+
throw this.malformed('a malformed XML declaration', startArg);
|
|
330
|
+
}
|
|
331
|
+
pseudo.push([name, this.decode(pos + 1, valueEnd)]);
|
|
332
|
+
pos = valueEnd + 1;
|
|
333
|
+
if (pos - startArg > this.limits.maxHeaderBytes) {
|
|
334
|
+
throw limitExceeded('maxHeaderBytes', this.limits.maxHeaderBytes, 'XML declaration');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const names = pseudo.map(([name]) => name);
|
|
338
|
+
const expected = ['version', 'encoding', 'standalone'].filter((name) => names.includes(name));
|
|
339
|
+
if (names.length === 0 || names[0] !== 'version' || names.join() !== expected.join()) {
|
|
340
|
+
throw this.malformed('a malformed XML declaration', startArg);
|
|
341
|
+
}
|
|
342
|
+
for (const [name, value] of pseudo) {
|
|
343
|
+
if (name === 'version' && !/^1\.[0-9]+$/.test(value)) {
|
|
344
|
+
throw this.malformed('a malformed XML declaration version', startArg);
|
|
345
|
+
}
|
|
346
|
+
if (name === 'encoding' && value.toLowerCase() !== 'utf-8') {
|
|
347
|
+
throw this.malformed(`the document declares the encoding ${JSON.stringify(value)}, not UTF-8`, startArg);
|
|
348
|
+
}
|
|
349
|
+
if (name === 'standalone' && value !== 'yes' && value !== 'no') {
|
|
350
|
+
throw this.malformed('a malformed XML declaration standalone value', startArg);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return pos;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Whitespace, comments and processing instructions (prolog and trailer); returns the first other offset. */
|
|
357
|
+
private misc(startArg: number): number {
|
|
358
|
+
const doc = this.doc;
|
|
359
|
+
let pos = startArg;
|
|
360
|
+
for (;;) {
|
|
361
|
+
while (pos < doc.length && isSpace(doc[pos])) {
|
|
362
|
+
pos += 1;
|
|
363
|
+
}
|
|
364
|
+
if (pos >= doc.length || doc[pos] !== LT) {
|
|
365
|
+
return pos;
|
|
366
|
+
}
|
|
367
|
+
if (doc[pos + 1] === BANG) {
|
|
368
|
+
if (!this.startsWith(pos, COMMENT_OPEN)) {
|
|
369
|
+
throw this.malformed('a declaration (<!DOCTYPE or another <!) outside the root element', pos);
|
|
370
|
+
}
|
|
371
|
+
pos = this.comment(pos);
|
|
372
|
+
} else if (doc[pos + 1] === QUESTION) {
|
|
373
|
+
pos = this.processingInstruction(pos);
|
|
374
|
+
} else {
|
|
375
|
+
return pos;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** `<!-- ... -->` without `--` inside; returns the offset after it. */
|
|
381
|
+
private comment(startArg: number): number {
|
|
382
|
+
const close = this.buffer.indexOf('--', startArg + COMMENT_OPEN.length, 'latin1');
|
|
383
|
+
if (close < 0) {
|
|
384
|
+
throw this.malformed('a comment is not closed', startArg);
|
|
385
|
+
}
|
|
386
|
+
if (this.doc[close + 2] !== GT) {
|
|
387
|
+
throw this.malformed('a comment holds "--"', close);
|
|
388
|
+
}
|
|
389
|
+
return close + 3;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** `<?target ...?>` with a target other than `xml`; returns the offset after it. */
|
|
393
|
+
private processingInstruction(startArg: number): number {
|
|
394
|
+
const doc = this.doc;
|
|
395
|
+
let pos = startArg + 2;
|
|
396
|
+
if (!isNameStart(doc[pos])) {
|
|
397
|
+
throw this.malformed('a processing instruction without a target', startArg);
|
|
398
|
+
}
|
|
399
|
+
while (pos < doc.length && isNameChar(doc[pos])) {
|
|
400
|
+
pos += 1;
|
|
401
|
+
}
|
|
402
|
+
if (this.decode(startArg + 2, pos).toLowerCase() === 'xml') {
|
|
403
|
+
throw this.malformed('an XML declaration that is not at the start of the document', startArg);
|
|
404
|
+
}
|
|
405
|
+
if (!(doc[pos] === QUESTION && doc[pos + 1] === GT) && !isSpace(doc[pos])) {
|
|
406
|
+
throw this.malformed('a malformed processing instruction target', startArg);
|
|
407
|
+
}
|
|
408
|
+
const close = this.buffer.indexOf('?>', pos, 'latin1');
|
|
409
|
+
if (close < 0) {
|
|
410
|
+
throw this.malformed('a processing instruction is not closed', startArg);
|
|
411
|
+
}
|
|
412
|
+
return close + 2;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** `<name attr="value" ...>` or `<name .../>`. */
|
|
416
|
+
private startTag(startArg: number): IStartTag {
|
|
417
|
+
const doc = this.doc;
|
|
418
|
+
const limit = startArg + this.limits.maxHeaderBytes;
|
|
419
|
+
let pos = startArg + 1;
|
|
420
|
+
const nameStart = pos;
|
|
421
|
+
while (pos < doc.length && isNameChar(doc[pos])) {
|
|
422
|
+
pos += 1;
|
|
423
|
+
}
|
|
424
|
+
const nameEnd = pos;
|
|
425
|
+
if (pos > limit) {
|
|
426
|
+
throw limitExceeded('maxHeaderBytes', this.limits.maxHeaderBytes, 'start tag');
|
|
427
|
+
}
|
|
428
|
+
const name = this.decode(nameStart, nameEnd);
|
|
429
|
+
const attributes: IXmlAttribute[] = [];
|
|
430
|
+
const seen = new Set<string>();
|
|
431
|
+
for (;;) {
|
|
432
|
+
if (pos > limit) {
|
|
433
|
+
throw limitExceeded('maxHeaderBytes', this.limits.maxHeaderBytes, `<${name}> start tag`);
|
|
434
|
+
}
|
|
435
|
+
const afterName = pos;
|
|
436
|
+
pos = this.skipSpace(pos);
|
|
437
|
+
if (pos >= doc.length) {
|
|
438
|
+
throw this.malformed(`the <${name}> start tag is not closed`, startArg);
|
|
439
|
+
}
|
|
440
|
+
if (doc[pos] === GT) {
|
|
441
|
+
return { name, nameStart, nameEnd, attributes, end: pos + 1, selfClosing: false };
|
|
442
|
+
}
|
|
443
|
+
if (doc[pos] === SLASH && doc[pos + 1] === GT) {
|
|
444
|
+
return { name, nameStart, nameEnd, attributes, end: pos + 2, selfClosing: true };
|
|
445
|
+
}
|
|
446
|
+
if (pos === afterName || !isNameStart(doc[pos])) {
|
|
447
|
+
throw this.malformed(`a malformed <${name}> start tag`, pos);
|
|
448
|
+
}
|
|
449
|
+
const attributeStart = pos;
|
|
450
|
+
while (pos < doc.length && isNameChar(doc[pos])) {
|
|
451
|
+
pos += 1;
|
|
452
|
+
}
|
|
453
|
+
if (pos > limit) {
|
|
454
|
+
throw limitExceeded('maxHeaderBytes', this.limits.maxHeaderBytes, `<${name}> start tag`);
|
|
455
|
+
}
|
|
456
|
+
const attributeName = this.decode(attributeStart, pos);
|
|
457
|
+
if (seen.has(attributeName)) {
|
|
458
|
+
throw this.malformed(`the <${name}> start tag repeats the attribute ${attributeName}`, attributeStart);
|
|
459
|
+
}
|
|
460
|
+
seen.add(attributeName);
|
|
461
|
+
pos = this.skipSpace(pos);
|
|
462
|
+
if (doc[pos] !== EQUALS) {
|
|
463
|
+
throw this.malformed(`the attribute ${attributeName} has no value`, attributeStart);
|
|
464
|
+
}
|
|
465
|
+
pos = this.skipSpace(pos + 1);
|
|
466
|
+
const quote = doc[pos];
|
|
467
|
+
if (quote !== QUOTE && quote !== APOSTROPHE) {
|
|
468
|
+
throw this.malformed(`the attribute ${attributeName} value is not quoted`, pos);
|
|
469
|
+
}
|
|
470
|
+
const valueEnd = doc.indexOf(quote, pos + 1);
|
|
471
|
+
if (valueEnd < 0 || valueEnd > limit) {
|
|
472
|
+
if (valueEnd > limit) {
|
|
473
|
+
throw limitExceeded('maxHeaderBytes', this.limits.maxHeaderBytes, `<${name}> start tag`);
|
|
474
|
+
}
|
|
475
|
+
throw this.malformed(`the attribute ${attributeName} value is not closed`, pos);
|
|
476
|
+
}
|
|
477
|
+
attributes.push({ name: attributeName, value: this.characterData(pos + 1, valueEnd, true, true) as string });
|
|
478
|
+
pos = valueEnd + 1;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** `</name S?>` matching `openArg`; returns the offset after it. */
|
|
483
|
+
private endTag(startArg: number, openArg: IOpenElement): number {
|
|
484
|
+
const doc = this.doc;
|
|
485
|
+
let pos = startArg + 2;
|
|
486
|
+
const nameStart = pos;
|
|
487
|
+
while (pos < doc.length && isNameChar(doc[pos])) {
|
|
488
|
+
pos += 1;
|
|
489
|
+
}
|
|
490
|
+
const length = openArg.nameEnd - openArg.nameStart;
|
|
491
|
+
if (pos - nameStart !== length || this.buffer.compare(this.buffer, openArg.nameStart, openArg.nameEnd, nameStart, pos) !== 0) {
|
|
492
|
+
throw this.malformed(`an end tag does not match <${openArg.name}>`, startArg);
|
|
493
|
+
}
|
|
494
|
+
pos = this.skipSpace(pos);
|
|
495
|
+
if (doc[pos] !== GT) {
|
|
496
|
+
throw this.malformed(`a malformed </${openArg.name}> end tag`, startArg);
|
|
497
|
+
}
|
|
498
|
+
return pos + 1;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** The content of an element through its end tag; returns the offset after the end tag. */
|
|
502
|
+
private element(startArg: number, tagArg: IStartTag, handlerArg: IXmlUnitHandler | null): number {
|
|
503
|
+
const doc = this.doc;
|
|
504
|
+
const maxEnd = startArg + this.limits.maxMetadataBytes;
|
|
505
|
+
const capture = handlerArg !== null && handlerArg.start(0, tagArg.name, tagArg.attributes);
|
|
506
|
+
if (tagArg.selfClosing) {
|
|
507
|
+
handlerArg?.end(0, tagArg.name, capture ? '' : null);
|
|
508
|
+
return tagArg.end;
|
|
509
|
+
}
|
|
510
|
+
const stack: IOpenElement[] = [{ name: tagArg.name, nameStart: tagArg.nameStart, nameEnd: tagArg.nameEnd, text: capture ? [] : null }];
|
|
511
|
+
let pos = tagArg.end;
|
|
512
|
+
while (stack.length > 0) {
|
|
513
|
+
if (pos > maxEnd) {
|
|
514
|
+
throw limitExceeded('maxMetadataBytes', this.limits.maxMetadataBytes, `<${tagArg.name}> element`);
|
|
515
|
+
}
|
|
516
|
+
if (pos >= doc.length) {
|
|
517
|
+
throw this.malformed(`the <${stack[stack.length - 1].name}> element is not closed`, startArg);
|
|
518
|
+
}
|
|
519
|
+
const top = stack[stack.length - 1];
|
|
520
|
+
if (doc[pos] !== LT) {
|
|
521
|
+
let textEnd = doc.indexOf(LT, pos);
|
|
522
|
+
if (textEnd < 0) {
|
|
523
|
+
textEnd = doc.length;
|
|
524
|
+
}
|
|
525
|
+
const text = this.characterData(pos, textEnd, top.text !== null, false);
|
|
526
|
+
if (text !== null) {
|
|
527
|
+
top.text!.push(text);
|
|
528
|
+
}
|
|
529
|
+
pos = textEnd;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const next = doc[pos + 1];
|
|
533
|
+
if (next === SLASH) {
|
|
534
|
+
const end = this.endTag(pos, top);
|
|
535
|
+
stack.pop();
|
|
536
|
+
handlerArg?.end(stack.length, top.name, top.text === null ? null : top.text.join(''));
|
|
537
|
+
pos = end;
|
|
538
|
+
} else if (next === BANG) {
|
|
539
|
+
if (this.startsWith(pos, COMMENT_OPEN)) {
|
|
540
|
+
pos = this.comment(pos);
|
|
541
|
+
} else if (this.startsWith(pos, CDATA_OPEN)) {
|
|
542
|
+
const close = this.buffer.indexOf(']]>', pos + CDATA_OPEN.length, 'latin1');
|
|
543
|
+
if (close < 0) {
|
|
544
|
+
throw this.malformed('a CDATA section is not closed', pos);
|
|
545
|
+
}
|
|
546
|
+
if (top.text !== null) {
|
|
547
|
+
top.text.push(normalizeLineEnds(this.decode(pos + CDATA_OPEN.length, close)));
|
|
548
|
+
}
|
|
549
|
+
pos = close + 3;
|
|
550
|
+
} else {
|
|
551
|
+
throw this.malformed('a declaration inside an element', pos);
|
|
552
|
+
}
|
|
553
|
+
} else if (next === QUESTION) {
|
|
554
|
+
pos = this.processingInstruction(pos);
|
|
555
|
+
} else if (isNameStart(next)) {
|
|
556
|
+
// the root, the unit's open elements and the new one
|
|
557
|
+
if (stack.length + 2 > this.limits.maxEntries) {
|
|
558
|
+
throw limitExceeded('maxEntries', this.limits.maxEntries, 'open XML elements');
|
|
559
|
+
}
|
|
560
|
+
const tag = this.startTag(pos);
|
|
561
|
+
const depth = stack.length;
|
|
562
|
+
const captureChild = handlerArg !== null && handlerArg.start(depth, tag.name, tag.attributes);
|
|
563
|
+
if (tag.selfClosing) {
|
|
564
|
+
handlerArg?.end(depth, tag.name, captureChild ? '' : null);
|
|
565
|
+
} else {
|
|
566
|
+
stack.push({ name: tag.name, nameStart: tag.nameStart, nameEnd: tag.nameEnd, text: captureChild ? [] : null });
|
|
567
|
+
}
|
|
568
|
+
pos = tag.end;
|
|
569
|
+
} else {
|
|
570
|
+
throw this.malformed('a "<" that starts no markup', pos);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (pos > maxEnd) {
|
|
574
|
+
throw limitExceeded('maxMetadataBytes', this.limits.maxMetadataBytes, `<${tagArg.name}> element`);
|
|
575
|
+
}
|
|
576
|
+
return pos;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Checks character data or an attribute value in `[startArg, endArg)` (no
|
|
581
|
+
* `]]>` in text, no `<` in values, only the accepted references) and, when
|
|
582
|
+
* `decodeArg`, returns its decoded value; otherwise null.
|
|
583
|
+
*/
|
|
584
|
+
private characterData(startArg: number, endArg: number, decodeArg: boolean, attributeArg: boolean): string | null {
|
|
585
|
+
const doc = this.doc;
|
|
586
|
+
const parts: string[] = [];
|
|
587
|
+
let runStart = startArg;
|
|
588
|
+
for (let i = startArg; i < endArg; i++) {
|
|
589
|
+
const b = doc[i];
|
|
590
|
+
if (b === LT) {
|
|
591
|
+
throw this.malformed('an attribute value holds "<"', i);
|
|
592
|
+
}
|
|
593
|
+
if (b === GT && !attributeArg && i - 2 >= startArg && doc[i - 1] === BRACKET_CLOSE && doc[i - 2] === BRACKET_CLOSE) {
|
|
594
|
+
throw this.malformed('character data holds "]]>"', i - 2);
|
|
595
|
+
}
|
|
596
|
+
if (b !== AMP) {
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
const semicolon = doc.indexOf(SEMICOLON, i + 1);
|
|
600
|
+
if (semicolon < 0 || semicolon >= endArg || semicolon - i > 12) {
|
|
601
|
+
throw this.malformed('a reference is not terminated', i);
|
|
602
|
+
}
|
|
603
|
+
const replacement = this.reference(i, semicolon);
|
|
604
|
+
if (decodeArg) {
|
|
605
|
+
parts.push(this.literal(runStart, i, attributeArg), replacement);
|
|
606
|
+
}
|
|
607
|
+
i = semicolon;
|
|
608
|
+
runStart = semicolon + 1;
|
|
609
|
+
}
|
|
610
|
+
if (!decodeArg) {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
parts.push(this.literal(runStart, endArg, attributeArg));
|
|
614
|
+
return parts.join('');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** The literal run `[startArg, endArg)`: line ends normalized, and in attribute values whitespace to spaces. */
|
|
618
|
+
private literal(startArg: number, endArg: number, attributeArg: boolean): string {
|
|
619
|
+
if (startArg === endArg) {
|
|
620
|
+
return '';
|
|
621
|
+
}
|
|
622
|
+
const text = normalizeLineEnds(this.decode(startArg, endArg));
|
|
623
|
+
return attributeArg ? text.replace(/[\t\n]/g, ' ') : text;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** The character a reference `&...;` stands for. */
|
|
627
|
+
private reference(startArg: number, semicolonArg: number): string {
|
|
628
|
+
const doc = this.doc;
|
|
629
|
+
if (doc[startArg + 1] === HASH) {
|
|
630
|
+
const hex = doc[startArg + 2] === 0x78;
|
|
631
|
+
const digits = this.decode(startArg + (hex ? 3 : 2), semicolonArg);
|
|
632
|
+
if (!(hex ? /^[0-9a-fA-F]+$/ : /^[0-9]+$/).test(digits)) {
|
|
633
|
+
throw this.malformed('a malformed character reference', startArg);
|
|
634
|
+
}
|
|
635
|
+
const codePoint = Number.parseInt(digits, hex ? 16 : 10);
|
|
636
|
+
if (!isXmlCharacter(codePoint)) {
|
|
637
|
+
throw this.malformed('a character reference to a character XML does not allow', startArg);
|
|
638
|
+
}
|
|
639
|
+
return String.fromCodePoint(codePoint);
|
|
640
|
+
}
|
|
641
|
+
const replacement = PREDEFINED.get(this.decode(startArg + 1, semicolonArg));
|
|
642
|
+
if (replacement === undefined) {
|
|
643
|
+
throw this.malformed('a reference to an undeclared entity', startArg);
|
|
644
|
+
}
|
|
645
|
+
return replacement;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private skipSpace(posArg: number): number {
|
|
649
|
+
let pos = posArg;
|
|
650
|
+
while (pos < this.doc.length && isSpace(this.doc[pos])) {
|
|
651
|
+
pos += 1;
|
|
652
|
+
}
|
|
653
|
+
return pos;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private startsWith(posArg: number, bytesArg: Uint8Array): boolean {
|
|
657
|
+
if (posArg + bytesArg.length > this.doc.length) {
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
for (let i = 0; i < bytesArg.length; i++) {
|
|
661
|
+
if (this.doc[posArg + i] !== bytesArg[i]) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
private decode(startArg: number, endArg: number): string {
|
|
669
|
+
return utf8.decode(this.doc.subarray(startArg, endArg));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private malformed(messageArg: string, offsetArg: number): LinuxRepoError {
|
|
673
|
+
return new LinuxRepoError('malformed-index', messageArg, { field: `<${this.options.rootName}> document`, value: offsetArg });
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** XML 1.0 section 2.11: CRLF and lone CR become LF. */
|
|
678
|
+
function normalizeLineEnds(textArg: string): string {
|
|
679
|
+
return textArg.includes('\r') ? textArg.replace(/\r\n?/g, '\n') : textArg;
|
|
680
|
+
}
|