@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
package/ts/readers/decompress.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
import { LinuxRepoError, limitExceeded, truncated } from '../shared/errors.js';
|
|
3
3
|
import type { ILinuxPackageLimits } from '../shared/limits.js';
|
|
4
|
+
import type { TLinuxCompression } from '../shared/types.js';
|
|
4
5
|
import { READ_CHUNK_BYTES, takeFront, type IByteReader } from './bytes.js';
|
|
5
6
|
|
|
6
|
-
/** The stream compressions the readers decode. */
|
|
7
|
-
export type TLinuxCompression = 'none' | 'gzip' | 'xz' | 'zstd';
|
|
8
|
-
|
|
9
7
|
export interface IDecompressOptions {
|
|
10
8
|
/** Bound on the decompressed bytes delivered. */
|
|
11
9
|
maxOutputBytes: number;
|
|
12
10
|
outputLimit: keyof ILinuxPackageLimits;
|
|
11
|
+
/**
|
|
12
|
+
* Bound on the Zstandard frames of a zstd stream (skippable frames do not
|
|
13
|
+
* count): one more is `limit-exceeded` on `maxEntries`.
|
|
14
|
+
*/
|
|
15
|
+
maxZstdFrames: number;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
/**
|
|
@@ -18,15 +21,20 @@ export interface IDecompressOptions {
|
|
|
18
21
|
* `limit-exceeded` and stops the decoder. `close()` releases the decoder and
|
|
19
22
|
* must be called (see `withDecompressingReader`).
|
|
20
23
|
*
|
|
21
|
-
* - gzip (multi-member)
|
|
22
|
-
*
|
|
24
|
+
* - gzip (multi-member) comes from `node:zlib` `createGunzip`; zstd from one
|
|
25
|
+
* `node:zlib` `createZstdDecompress` fed by the codec's RFC 8878 frame walk,
|
|
26
|
+
* at most `maxZstdFrames` Zstandard frames; xz from `@napi-rs/lzma`
|
|
27
|
+
* `xz.decompressStream` (64 KiB chunks, cancellable).
|
|
23
28
|
* - An error from the inner reader (the caller's source, or a codec limit on
|
|
24
29
|
* it) propagates unchanged.
|
|
25
30
|
* - A compressed stream that ends early is `truncated`; corrupt data is
|
|
26
31
|
* `malformed-archive` with the decoder error as `cause`.
|
|
27
32
|
* - The decoder reads the inner reader ahead and never hands bytes back, so
|
|
28
33
|
* the compressed stream must run to the end of the inner reader (a whole
|
|
29
|
-
* package, or a `LimitedReader` over one archive member).
|
|
34
|
+
* package, or a `LimitedReader` over one archive member). Bytes after the
|
|
35
|
+
* last gzip member or zstd frame are `malformed-archive` (zstd skippable
|
|
36
|
+
* frames are skipped). A reader closed before the end skips the end check;
|
|
37
|
+
* the zstd walk still refuses such bytes when it has already read them.
|
|
30
38
|
* - Opening a zstd decoder on a Node.js without nodejs/node#64593 throws a
|
|
31
39
|
* plain `Error` (see `isZstdDecodingSafe`): a server fault, not a verdict
|
|
32
40
|
* on the package.
|
|
@@ -52,7 +60,7 @@ export function openDecompressingReader(
|
|
|
52
60
|
source = new PassThroughSource(inputArg);
|
|
53
61
|
break;
|
|
54
62
|
case 'gzip':
|
|
55
|
-
source = new
|
|
63
|
+
source = new GzipSource(inputArg);
|
|
56
64
|
break;
|
|
57
65
|
case 'zstd':
|
|
58
66
|
if (!isZstdDecodingSafe(process.versions.node)) {
|
|
@@ -61,7 +69,7 @@ export function openDecompressingReader(
|
|
|
61
69
|
`this process runs Node.js ${process.versions.node}`,
|
|
62
70
|
);
|
|
63
71
|
}
|
|
64
|
-
source = new
|
|
72
|
+
source = new ZstdSource(inputArg, optionsArg.maxZstdFrames);
|
|
65
73
|
break;
|
|
66
74
|
case 'xz':
|
|
67
75
|
source = new XzSource(inputArg);
|
|
@@ -175,28 +183,36 @@ class PassThroughSource implements IChunkSource {
|
|
|
175
183
|
public async close(): Promise<void> {}
|
|
176
184
|
}
|
|
177
185
|
|
|
178
|
-
/**
|
|
179
|
-
|
|
180
|
-
|
|
186
|
+
/**
|
|
187
|
+
* gzip through `node:zlib` `createGunzip`, with stream backpressure. Gunzip
|
|
188
|
+
* decodes concatenated members, as Go's default reader does, but it stops at a
|
|
189
|
+
* NUL byte after a member and ignores the rest of the input, a valid member
|
|
190
|
+
* included; its `bytesWritten` counts only the bytes it consumed. So when the
|
|
191
|
+
* decoder ends by itself it must have consumed every byte read from the inner
|
|
192
|
+
* reader, and the inner reader must be at its end; anything else is
|
|
193
|
+
* `malformed-archive`. A source closed before that skips the check.
|
|
194
|
+
*/
|
|
195
|
+
class GzipSource implements IChunkSource {
|
|
196
|
+
private readonly input: IByteReader;
|
|
181
197
|
private readonly feeder: plugins.stream.Readable;
|
|
182
|
-
private readonly decoder: plugins.zlib.Gunzip
|
|
198
|
+
private readonly decoder: plugins.zlib.Gunzip;
|
|
183
199
|
private readonly iterator: AsyncIterator<Buffer>;
|
|
200
|
+
/** Bytes read from the inner reader. */
|
|
201
|
+
private inputBytes = 0;
|
|
202
|
+
private inputEnded = false;
|
|
184
203
|
private inputFailed = false;
|
|
185
204
|
private inputError: unknown = undefined;
|
|
205
|
+
/** The read in flight on the inner reader; there is never more than one. */
|
|
206
|
+
private pendingRead: Promise<Uint8Array | null> | null = null;
|
|
207
|
+
/** Set when the decoder has ended: the feeder starts no further read. */
|
|
208
|
+
private stopped = false;
|
|
186
209
|
private finished = false;
|
|
187
210
|
|
|
188
|
-
constructor(inputArg: IByteReader
|
|
189
|
-
this.
|
|
190
|
-
const pump = async function* (this:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
chunk = await inputArg.read(READ_CHUNK_BYTES);
|
|
195
|
-
} catch (error) {
|
|
196
|
-
this.inputFailed = true;
|
|
197
|
-
this.inputError = error;
|
|
198
|
-
throw error;
|
|
199
|
-
}
|
|
211
|
+
constructor(inputArg: IByteReader) {
|
|
212
|
+
this.input = inputArg;
|
|
213
|
+
const pump = async function* (this: GzipSource): AsyncGenerator<Uint8Array> {
|
|
214
|
+
while (!this.stopped) {
|
|
215
|
+
const chunk = await this.readInput(READ_CHUNK_BYTES);
|
|
200
216
|
if (chunk === null) {
|
|
201
217
|
return;
|
|
202
218
|
}
|
|
@@ -204,7 +220,7 @@ class ZlibSource implements IChunkSource {
|
|
|
204
220
|
}
|
|
205
221
|
};
|
|
206
222
|
this.feeder = plugins.stream.Readable.from(pump.call(this), { objectMode: false });
|
|
207
|
-
this.decoder =
|
|
223
|
+
this.decoder = plugins.zlib.createGunzip();
|
|
208
224
|
// errors surface through the iterator below; the callback only completes the pipeline
|
|
209
225
|
plugins.stream.pipeline(this.feeder, this.decoder, () => {});
|
|
210
226
|
this.iterator = this.decoder[Symbol.asyncIterator]();
|
|
@@ -214,27 +230,421 @@ class ZlibSource implements IChunkSource {
|
|
|
214
230
|
if (this.finished) {
|
|
215
231
|
return null;
|
|
216
232
|
}
|
|
233
|
+
let result: IteratorResult<Buffer>;
|
|
217
234
|
try {
|
|
218
|
-
|
|
219
|
-
if (result.done) {
|
|
220
|
-
this.finished = true;
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
return result.value;
|
|
235
|
+
result = await this.iterator.next();
|
|
224
236
|
} catch (error) {
|
|
225
237
|
this.finished = true;
|
|
226
238
|
if (this.inputFailed) {
|
|
227
239
|
throw this.inputError;
|
|
228
240
|
}
|
|
229
|
-
throw mapZlibError(error,
|
|
241
|
+
throw mapZlibError(error, 'gzip');
|
|
230
242
|
}
|
|
243
|
+
if (!result.done) {
|
|
244
|
+
return result.value;
|
|
245
|
+
}
|
|
246
|
+
this.finished = true;
|
|
247
|
+
await this.checkEnd();
|
|
248
|
+
return null;
|
|
231
249
|
}
|
|
232
250
|
|
|
233
251
|
public async close(): Promise<void> {
|
|
234
252
|
this.finished = true;
|
|
253
|
+
this.stopped = true;
|
|
235
254
|
this.feeder.destroy();
|
|
236
255
|
this.decoder.destroy();
|
|
237
256
|
}
|
|
257
|
+
|
|
258
|
+
/** Reads the inner reader and counts what it returns. */
|
|
259
|
+
private readInput(maxArg: number): Promise<Uint8Array | null> {
|
|
260
|
+
const pending = this.input.read(maxArg).then(
|
|
261
|
+
(chunk) => {
|
|
262
|
+
this.pendingRead = null;
|
|
263
|
+
if (chunk === null) {
|
|
264
|
+
this.inputEnded = true;
|
|
265
|
+
} else {
|
|
266
|
+
this.inputBytes += chunk.length;
|
|
267
|
+
}
|
|
268
|
+
return chunk;
|
|
269
|
+
},
|
|
270
|
+
(error: unknown) => {
|
|
271
|
+
this.pendingRead = null;
|
|
272
|
+
this.inputFailed = true;
|
|
273
|
+
this.inputError = error;
|
|
274
|
+
throw error;
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
this.pendingRead = pending;
|
|
278
|
+
return pending;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The decoder ended by itself. It must have consumed every byte read from
|
|
283
|
+
* the inner reader, and one more read must report the end of input.
|
|
284
|
+
*/
|
|
285
|
+
private async checkEnd(): Promise<void> {
|
|
286
|
+
this.stopped = true;
|
|
287
|
+
if (this.pendingRead !== null && this.decoder.bytesWritten === this.inputBytes) {
|
|
288
|
+
// a read the feeder started settles first: its bytes count, and no two reads overlap
|
|
289
|
+
await this.pendingRead;
|
|
290
|
+
}
|
|
291
|
+
if (this.inputFailed) {
|
|
292
|
+
throw this.inputError;
|
|
293
|
+
}
|
|
294
|
+
const consumedAll =
|
|
295
|
+
this.decoder.bytesWritten === this.inputBytes && (this.inputEnded || (await this.readInput(1)) === null);
|
|
296
|
+
if (!consumedAll) {
|
|
297
|
+
throw new LinuxRepoError('malformed-archive', 'bytes follow the last gzip member', { field: 'gzip stream' });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** RFC 8878 §3.1.1: the Zstandard frame magic number 0xFD2FB528, little-endian. */
|
|
303
|
+
const ZSTD_FRAME_MAGIC = Uint8Array.of(0x28, 0xb5, 0x2f, 0xfd);
|
|
304
|
+
/** RFC 8878 §3.1.2: a skippable frame magic number 0x184D2A5?, little-endian, after its first byte 0x5?. */
|
|
305
|
+
const ZSTD_SKIPPABLE_MAGIC = Uint8Array.of(0x50, 0x2a, 0x4d, 0x18);
|
|
306
|
+
/** Dictionary_ID field bytes by Dictionary_ID_Flag (RFC 8878 §3.1.1.1.1). */
|
|
307
|
+
const ZSTD_DICTIONARY_ID_BYTES = [0, 1, 2, 4];
|
|
308
|
+
/** Frame_Content_Size field bytes by Frame_Content_Size_Flag; flag 0 means 1 byte with Single_Segment_Flag, else 0. */
|
|
309
|
+
const ZSTD_CONTENT_SIZE_BYTES = [0, 2, 4, 8];
|
|
310
|
+
const ZSTD_BLOCK_TYPE_RLE = 1;
|
|
311
|
+
const ZSTD_BLOCK_TYPE_RESERVED = 3;
|
|
312
|
+
/** Place values of the bytes of a little-endian integer. */
|
|
313
|
+
const LITTLE_ENDIAN_PLACE = [1, 0x100, 0x10000, 0x1000000];
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Where the frame walk stands: the magic number at a frame start, a skippable
|
|
317
|
+
* frame's size and content, or a Zstandard frame's header descriptor, header
|
|
318
|
+
* fields, block header, block content and checksum.
|
|
319
|
+
*/
|
|
320
|
+
type TZstdWalkState =
|
|
321
|
+
| 'magic'
|
|
322
|
+
| 'skippable-size'
|
|
323
|
+
| 'skippable-body'
|
|
324
|
+
| 'frame-header'
|
|
325
|
+
| 'header-fields'
|
|
326
|
+
| 'block-header'
|
|
327
|
+
| 'block-body'
|
|
328
|
+
| 'checksum';
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* zstd through one `node:zlib` `createZstdDecompress` for the whole stream,
|
|
332
|
+
* fed by the codec's own RFC 8878 frame walk. node:zlib's zstd decoder before
|
|
333
|
+
* Node.js 26 drops the rest of a write after a frame (a second frame in the
|
|
334
|
+
* same write is lost, one in the next write is decoded), and every release
|
|
335
|
+
* ignores non-frame bytes after a frame. So the walk decides the frame
|
|
336
|
+
* boundaries, and no write to the decoder holds the end of one frame and any
|
|
337
|
+
* byte after it: every frame therefore costs at least one write (about 35 µs),
|
|
338
|
+
* which is why `maxZstdFrames` caps the Zstandard frames of a stream.
|
|
339
|
+
* - The walk reads the inner reader in chunks of `READ_CHUNK_BYTES` and parses
|
|
340
|
+
* each chunk synchronously; it writes the longest runs of Zstandard-frame
|
|
341
|
+
* bytes as views into the chunk, each ending at the end of the chunk or at a
|
|
342
|
+
* frame end, so a frame costs one write per chunk it spans, never one per
|
|
343
|
+
* header or block. Magic-number bytes from an earlier read are written as a
|
|
344
|
+
* copy before the run. It keeps no bytes across reads, only the state below.
|
|
345
|
+
* - Skippable frames are skipped, never written; they do not count against
|
|
346
|
+
* the cap. Anything else at a frame start, a set reserved header bit and
|
|
347
|
+
* block type 3 are `malformed-archive`, raised as soon as the byte is read.
|
|
348
|
+
* - Input that ends inside a frame, a magic-number prefix included, or is
|
|
349
|
+
* empty, is `truncated`; at least one Zstandard frame is required.
|
|
350
|
+
* - When the decoder ends by itself it must have consumed every byte written:
|
|
351
|
+
* libzstd and the walk read the same header fields, so one check at the end
|
|
352
|
+
* fails closed. A walk error is rethrown unchanged; a source closed before
|
|
353
|
+
* the end skips the check.
|
|
354
|
+
*/
|
|
355
|
+
class ZstdSource implements IChunkSource {
|
|
356
|
+
private readonly input: IByteReader;
|
|
357
|
+
private readonly maxFrames: number;
|
|
358
|
+
private readonly feeder: plugins.stream.Readable;
|
|
359
|
+
private readonly decoder: plugins.zlib.ZstdDecompress;
|
|
360
|
+
private readonly iterator: AsyncIterator<Buffer>;
|
|
361
|
+
/** Frame bytes the walk has written to the decoder. */
|
|
362
|
+
private written = 0;
|
|
363
|
+
private walkFailed = false;
|
|
364
|
+
private walkError: unknown = undefined;
|
|
365
|
+
/** Set when the source closes: the walk starts no further read. */
|
|
366
|
+
private stopped = false;
|
|
367
|
+
private finished = false;
|
|
368
|
+
|
|
369
|
+
private state: TZstdWalkState = 'magic';
|
|
370
|
+
/** Bytes of the current magic number, skippable frame size or block header read so far. */
|
|
371
|
+
private fieldBytes = 0;
|
|
372
|
+
/** Whether the magic number being read is a skippable frame's. */
|
|
373
|
+
private skippable = false;
|
|
374
|
+
/** A skippable frame size or block header as read so far, little-endian. */
|
|
375
|
+
private fieldValue = 0;
|
|
376
|
+
/** Bytes left in the current header fields, block content, checksum or skippable content. */
|
|
377
|
+
private remaining = 0;
|
|
378
|
+
private descriptor = 0;
|
|
379
|
+
private lastBlock = false;
|
|
380
|
+
private zstandardFrames = 0;
|
|
381
|
+
private sawInput = false;
|
|
382
|
+
|
|
383
|
+
constructor(inputArg: IByteReader, maxFramesArg: number) {
|
|
384
|
+
this.input = inputArg;
|
|
385
|
+
this.maxFrames = maxFramesArg;
|
|
386
|
+
this.feeder = plugins.stream.Readable.from(this.walk(), { objectMode: false });
|
|
387
|
+
this.decoder = plugins.zlib.createZstdDecompress();
|
|
388
|
+
// errors surface through the iterator below; the callback only completes the pipeline
|
|
389
|
+
plugins.stream.pipeline(this.feeder, this.decoder, () => {});
|
|
390
|
+
this.iterator = this.decoder[Symbol.asyncIterator]();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
public async next(): Promise<Uint8Array | null> {
|
|
394
|
+
if (this.finished) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
let result: IteratorResult<Buffer>;
|
|
398
|
+
try {
|
|
399
|
+
result = await this.iterator.next();
|
|
400
|
+
} catch (error) {
|
|
401
|
+
await this.close();
|
|
402
|
+
if (this.walkFailed) {
|
|
403
|
+
throw this.walkError;
|
|
404
|
+
}
|
|
405
|
+
throw mapZlibError(error, 'zstd');
|
|
406
|
+
}
|
|
407
|
+
if (!result.done) {
|
|
408
|
+
return result.value;
|
|
409
|
+
}
|
|
410
|
+
const consumed = this.decoder.bytesWritten;
|
|
411
|
+
await this.close();
|
|
412
|
+
if (this.walkFailed) {
|
|
413
|
+
throw this.walkError;
|
|
414
|
+
}
|
|
415
|
+
if (consumed !== this.written) {
|
|
416
|
+
throw zstdMalformed(`the zstd decoder ended after ${consumed} of the ${this.written} frame bytes`);
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
public async close(): Promise<void> {
|
|
422
|
+
this.finished = true;
|
|
423
|
+
this.stopped = true;
|
|
424
|
+
this.feeder.destroy();
|
|
425
|
+
this.decoder.destroy();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Reads and parses the inner reader chunk by chunk and yields the runs of Zstandard-frame bytes to write. */
|
|
429
|
+
private async *walk(): AsyncGenerator<Uint8Array> {
|
|
430
|
+
const runs: Uint8Array[] = [];
|
|
431
|
+
for (;;) {
|
|
432
|
+
// only errors the walk raises or reads count as walk errors, not ones a destroyed feeder injects at a yield
|
|
433
|
+
try {
|
|
434
|
+
const chunk = await this.input.read(READ_CHUNK_BYTES);
|
|
435
|
+
if (this.stopped) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (chunk === null) {
|
|
439
|
+
this.endOfInput();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
this.parse(chunk, runs);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
this.walkFailed = true;
|
|
445
|
+
this.walkError = error;
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
for (const run of runs) {
|
|
449
|
+
this.written += run.length;
|
|
450
|
+
yield run;
|
|
451
|
+
}
|
|
452
|
+
runs.length = 0;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Advances the state over `chunkArg` and appends the runs of Zstandard-frame bytes it holds to `runsArg`. */
|
|
457
|
+
private parse(chunkArg: Uint8Array, runsArg: Uint8Array[]): void {
|
|
458
|
+
const length = chunkArg.length;
|
|
459
|
+
if (length > 0) {
|
|
460
|
+
this.sawInput = true;
|
|
461
|
+
}
|
|
462
|
+
const outsideFrame = this.state === 'magic' || this.state === 'skippable-size' || this.state === 'skippable-body';
|
|
463
|
+
/** Start of the current run in this chunk, or -1 outside a Zstandard frame. */
|
|
464
|
+
let runStart = outsideFrame ? -1 : 0;
|
|
465
|
+
/** Magic-number bytes read before this chunk. */
|
|
466
|
+
let earlier = this.state === 'magic' ? this.fieldBytes : 0;
|
|
467
|
+
let index = 0;
|
|
468
|
+
while (index < length) {
|
|
469
|
+
switch (this.state) {
|
|
470
|
+
case 'magic': {
|
|
471
|
+
const byte = chunkArg[index];
|
|
472
|
+
if (this.fieldBytes === 0) {
|
|
473
|
+
this.skippable = (byte & 0xf0) === ZSTD_SKIPPABLE_MAGIC[0];
|
|
474
|
+
if (!this.skippable && byte !== ZSTD_FRAME_MAGIC[0]) {
|
|
475
|
+
throw zstdMalformed('bytes at a zstd frame start are not a zstd frame');
|
|
476
|
+
}
|
|
477
|
+
} else if (byte !== (this.skippable ? ZSTD_SKIPPABLE_MAGIC : ZSTD_FRAME_MAGIC)[this.fieldBytes]) {
|
|
478
|
+
throw zstdMalformed('bytes at a zstd frame start are not a zstd frame');
|
|
479
|
+
}
|
|
480
|
+
index += 1;
|
|
481
|
+
this.fieldBytes += 1;
|
|
482
|
+
if (this.fieldBytes < 4) {
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
this.fieldBytes = 0;
|
|
486
|
+
if (this.skippable) {
|
|
487
|
+
this.fieldValue = 0;
|
|
488
|
+
this.state = 'skippable-size';
|
|
489
|
+
} else {
|
|
490
|
+
this.zstandardFrames += 1;
|
|
491
|
+
if (this.zstandardFrames > this.maxFrames) {
|
|
492
|
+
throw limitExceeded('maxEntries', this.maxFrames, 'zstd frames');
|
|
493
|
+
}
|
|
494
|
+
if (earlier > 0) {
|
|
495
|
+
runsArg.push(ZSTD_FRAME_MAGIC.slice(0, earlier));
|
|
496
|
+
runStart = 0;
|
|
497
|
+
} else {
|
|
498
|
+
runStart = index - 4;
|
|
499
|
+
}
|
|
500
|
+
this.state = 'frame-header';
|
|
501
|
+
}
|
|
502
|
+
earlier = 0;
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
case 'skippable-size':
|
|
506
|
+
this.fieldValue += chunkArg[index] * LITTLE_ENDIAN_PLACE[this.fieldBytes];
|
|
507
|
+
index += 1;
|
|
508
|
+
this.fieldBytes += 1;
|
|
509
|
+
if (this.fieldBytes === 4) {
|
|
510
|
+
this.fieldBytes = 0;
|
|
511
|
+
this.remaining = this.fieldValue;
|
|
512
|
+
this.state = this.remaining === 0 ? 'magic' : 'skippable-body';
|
|
513
|
+
}
|
|
514
|
+
break;
|
|
515
|
+
case 'skippable-body': {
|
|
516
|
+
const take = Math.min(this.remaining, length - index);
|
|
517
|
+
index += take;
|
|
518
|
+
this.remaining -= take;
|
|
519
|
+
if (this.remaining === 0) {
|
|
520
|
+
this.state = 'magic';
|
|
521
|
+
}
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
case 'frame-header': {
|
|
525
|
+
const flags = chunkArg[index];
|
|
526
|
+
index += 1;
|
|
527
|
+
if ((flags & 0x08) !== 0) {
|
|
528
|
+
throw zstdMalformed('a zstd frame header sets the reserved bit');
|
|
529
|
+
}
|
|
530
|
+
const contentSizeFlag = flags >> 6;
|
|
531
|
+
const singleSegment = (flags >> 5) & 1;
|
|
532
|
+
this.descriptor = flags;
|
|
533
|
+
this.remaining =
|
|
534
|
+
(singleSegment === 1 ? 0 : 1) +
|
|
535
|
+
ZSTD_DICTIONARY_ID_BYTES[flags & 3] +
|
|
536
|
+
(contentSizeFlag === 0 ? singleSegment : ZSTD_CONTENT_SIZE_BYTES[contentSizeFlag]);
|
|
537
|
+
this.fieldValue = 0;
|
|
538
|
+
this.state = this.remaining === 0 ? 'block-header' : 'header-fields';
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
case 'header-fields': {
|
|
542
|
+
const take = Math.min(this.remaining, length - index);
|
|
543
|
+
index += take;
|
|
544
|
+
this.remaining -= take;
|
|
545
|
+
if (this.remaining === 0) {
|
|
546
|
+
this.state = 'block-header';
|
|
547
|
+
}
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
case 'block-header': {
|
|
551
|
+
this.fieldValue += chunkArg[index] * LITTLE_ENDIAN_PLACE[this.fieldBytes];
|
|
552
|
+
index += 1;
|
|
553
|
+
this.fieldBytes += 1;
|
|
554
|
+
if (this.fieldBytes < 3) {
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
const header = this.fieldValue;
|
|
558
|
+
this.fieldBytes = 0;
|
|
559
|
+
this.fieldValue = 0;
|
|
560
|
+
const blockType = (header >> 1) & 3;
|
|
561
|
+
if (blockType === ZSTD_BLOCK_TYPE_RESERVED) {
|
|
562
|
+
throw zstdMalformed('a zstd block has the reserved block type');
|
|
563
|
+
}
|
|
564
|
+
this.lastBlock = (header & 1) === 1;
|
|
565
|
+
this.remaining = blockType === ZSTD_BLOCK_TYPE_RLE ? 1 : header >>> 3;
|
|
566
|
+
if (this.remaining > 0) {
|
|
567
|
+
this.state = 'block-body';
|
|
568
|
+
} else if (this.endBlock()) {
|
|
569
|
+
runsArg.push(chunkArg.subarray(runStart, index));
|
|
570
|
+
runStart = -1;
|
|
571
|
+
}
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
case 'block-body':
|
|
575
|
+
case 'checksum': {
|
|
576
|
+
const take = Math.min(this.remaining, length - index);
|
|
577
|
+
index += take;
|
|
578
|
+
this.remaining -= take;
|
|
579
|
+
if (this.remaining > 0) {
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
if (this.state === 'checksum' || this.endBlock()) {
|
|
583
|
+
this.state = 'magic';
|
|
584
|
+
runsArg.push(chunkArg.subarray(runStart, index));
|
|
585
|
+
runStart = -1;
|
|
586
|
+
}
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (runStart >= 0 && runStart < length) {
|
|
592
|
+
runsArg.push(chunkArg.subarray(runStart, length));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** A block's content has been read: returns true when that ends the frame, else moves to the next field. */
|
|
597
|
+
private endBlock(): boolean {
|
|
598
|
+
if (!this.lastBlock) {
|
|
599
|
+
this.state = 'block-header';
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
if ((this.descriptor & 0x04) !== 0) {
|
|
603
|
+
this.remaining = 4;
|
|
604
|
+
this.state = 'checksum';
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
this.state = 'magic';
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** The inner reader has ended: only a frame start after at least one Zstandard frame is a clean end. */
|
|
612
|
+
private endOfInput(): void {
|
|
613
|
+
switch (this.state) {
|
|
614
|
+
case 'magic':
|
|
615
|
+
if (this.fieldBytes > 0) {
|
|
616
|
+
throw zstdTruncated('a zstd frame magic number');
|
|
617
|
+
}
|
|
618
|
+
if (this.zstandardFrames > 0) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (!this.sawInput) {
|
|
622
|
+
throw zstdTruncated('the zstd stream');
|
|
623
|
+
}
|
|
624
|
+
throw zstdMalformed('the zstd stream holds no Zstandard frame');
|
|
625
|
+
case 'skippable-size':
|
|
626
|
+
throw zstdTruncated('a zstd skippable frame header');
|
|
627
|
+
case 'skippable-body':
|
|
628
|
+
throw zstdTruncated('a zstd skippable frame');
|
|
629
|
+
case 'frame-header':
|
|
630
|
+
case 'header-fields':
|
|
631
|
+
throw zstdTruncated('a zstd frame header');
|
|
632
|
+
case 'block-header':
|
|
633
|
+
throw zstdTruncated('a zstd block header');
|
|
634
|
+
case 'block-body':
|
|
635
|
+
throw zstdTruncated('a zstd block');
|
|
636
|
+
case 'checksum':
|
|
637
|
+
throw zstdTruncated('a zstd frame checksum');
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function zstdTruncated(whereArg: string): LinuxRepoError {
|
|
643
|
+
return new LinuxRepoError('truncated', `input ended inside ${whereArg}`, { field: 'zstd stream' });
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function zstdMalformed(messageArg: string): LinuxRepoError {
|
|
647
|
+
return new LinuxRepoError('malformed-archive', messageArg, { field: 'zstd stream' });
|
|
238
648
|
}
|
|
239
649
|
|
|
240
650
|
function mapZlibError(errorArg: unknown, kindArg: 'gzip' | 'zstd'): LinuxRepoError {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { LinuxRepoError, limitExceeded } from '../shared/errors.js';
|
|
2
|
+
import type { ILinuxPackageLimits } from '../shared/limits.js';
|
|
3
|
+
import type { TLinuxCompression } from '../shared/types.js';
|
|
4
|
+
import { assertBytes } from '../shared/validation.js';
|
|
5
|
+
import { BufferReader, READ_CHUNK_BYTES } from './bytes.js';
|
|
6
|
+
import { withDecompressingReader } from './decompress.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The shared parts of the index readers (1.2.0): decoding one index document
|
|
10
|
+
* into a buffer the reader owns, and the line lexer of the line formats
|
|
11
|
+
* (Debian `Packages` and `Release`, `APKINDEX`).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const COMPRESSIONS: ReadonlySet<string> = new Set<TLinuxCompression>(['none', 'gzip', 'xz', 'zstd']);
|
|
15
|
+
const LF = 0x0a;
|
|
16
|
+
|
|
17
|
+
/** Refuses a reader argument that is not an object (`invalid-input`, `input`). */
|
|
18
|
+
export function assertReaderInput(inputArg: unknown): asserts inputArg is Record<string, unknown> {
|
|
19
|
+
if (inputArg === null || typeof inputArg !== 'object') {
|
|
20
|
+
throw new LinuxRepoError('invalid-input', 'input must be an object', { field: 'input' });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Refuses a `bytes` argument that is not a `Uint8Array` (`invalid-input`, `bytes`). */
|
|
25
|
+
export function assertReaderBytes(valueArg: unknown): asserts valueArg is Uint8Array {
|
|
26
|
+
assertBytes(valueArg, 'bytes');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Refuses a compression that is not a `TLinuxCompression` (`invalid-input`, `compression`). */
|
|
30
|
+
export function assertLinuxCompression(valueArg: unknown): asserts valueArg is TLinuxCompression {
|
|
31
|
+
if (typeof valueArg !== 'string' || !COMPRESSIONS.has(valueArg)) {
|
|
32
|
+
throw new LinuxRepoError('invalid-input', "compression must be 'none', 'gzip', 'xz' or 'zstd'", {
|
|
33
|
+
field: 'compression',
|
|
34
|
+
value: typeof valueArg === 'string' ? valueArg : typeof valueArg,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Decodes one index document into a new buffer the reader owns, through
|
|
41
|
+
* `withDecompressingReader` for every compression (`'none'` passes the input
|
|
42
|
+
* through and copies it), bounded by `maxIndexBytes`. The decoder's own
|
|
43
|
+
* mapping applies: a stream that ends early is `truncated`, corrupt data
|
|
44
|
+
* `malformed-archive`, output past the bound `limit-exceeded`
|
|
45
|
+
* (`maxIndexBytes`). The result never aliases `bytesArg`.
|
|
46
|
+
*/
|
|
47
|
+
export async function readIndexDocument(
|
|
48
|
+
bytesArg: Uint8Array,
|
|
49
|
+
compressionArg: TLinuxCompression,
|
|
50
|
+
limitsArg: Readonly<ILinuxPackageLimits>,
|
|
51
|
+
): Promise<Uint8Array> {
|
|
52
|
+
if (compressionArg === 'none' && bytesArg.length > limitsArg.maxIndexBytes) {
|
|
53
|
+
throw limitExceeded('maxIndexBytes', limitsArg.maxIndexBytes, 'index document');
|
|
54
|
+
}
|
|
55
|
+
const chunks: Uint8Array[] = [];
|
|
56
|
+
let total = 0;
|
|
57
|
+
await withDecompressingReader(
|
|
58
|
+
new BufferReader(bytesArg),
|
|
59
|
+
compressionArg,
|
|
60
|
+
{ maxOutputBytes: limitsArg.maxIndexBytes, outputLimit: 'maxIndexBytes', maxZstdFrames: limitsArg.maxEntries },
|
|
61
|
+
async (readerArg) => {
|
|
62
|
+
for (let chunk = await readerArg.read(READ_CHUNK_BYTES); chunk !== null; chunk = await readerArg.read(READ_CHUNK_BYTES)) {
|
|
63
|
+
chunks.push(chunk);
|
|
64
|
+
total += chunk.length;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
const decoded = new Uint8Array(total);
|
|
69
|
+
let offset = 0;
|
|
70
|
+
for (const chunk of chunks) {
|
|
71
|
+
decoded.set(chunk, offset);
|
|
72
|
+
offset += chunk.length;
|
|
73
|
+
}
|
|
74
|
+
return decoded;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One line of a line-format document: `[start, end)` without its LF, which sits at `end`. */
|
|
78
|
+
export interface ILine {
|
|
79
|
+
readonly start: number;
|
|
80
|
+
readonly end: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The common line lexer of the line formats: lines end in LF and the last
|
|
85
|
+
* line must have one too (`malformed-index`, `detail.value` the offset of
|
|
86
|
+
* the unterminated line); a line longer than `maxHeaderBytes` (without its
|
|
87
|
+
* LF) is `limit-exceeded`. CR is an ordinary byte; nothing is trimmed. An
|
|
88
|
+
* empty document has no lines.
|
|
89
|
+
*/
|
|
90
|
+
export function* lines(bytesArg: Uint8Array, maxLineBytesArg: number, whatArg: string): Generator<ILine> {
|
|
91
|
+
let start = 0;
|
|
92
|
+
while (start < bytesArg.length) {
|
|
93
|
+
const end = bytesArg.indexOf(LF, start);
|
|
94
|
+
if (end < 0) {
|
|
95
|
+
if (bytesArg.length - start > maxLineBytesArg) {
|
|
96
|
+
throw limitExceeded('maxHeaderBytes', maxLineBytesArg, `${whatArg} line`);
|
|
97
|
+
}
|
|
98
|
+
throw new LinuxRepoError('malformed-index', `the last line of the ${whatArg} has no line feed`, {
|
|
99
|
+
field: whatArg,
|
|
100
|
+
value: start,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (end - start > maxLineBytesArg) {
|
|
104
|
+
throw limitExceeded('maxHeaderBytes', maxLineBytesArg, `${whatArg} line`);
|
|
105
|
+
}
|
|
106
|
+
yield { start, end };
|
|
107
|
+
start = end + 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** True when the line holds only spaces or tabs (at least one). */
|
|
112
|
+
export function isBlankLine(bytesArg: Uint8Array, lineArg: ILine): boolean {
|
|
113
|
+
if (lineArg.end === lineArg.start) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
for (let i = lineArg.start; i < lineArg.end; i++) {
|
|
117
|
+
if (bytesArg[i] !== 0x20 && bytesArg[i] !== 0x09) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Counts one more unit of a document against `maxEntries`. */
|
|
125
|
+
export function countUnit(countArg: number, limitsArg: Readonly<ILinuxPackageLimits>, whatArg: string): void {
|
|
126
|
+
if (countArg > limitsArg.maxEntries) {
|
|
127
|
+
throw limitExceeded('maxEntries', limitsArg.maxEntries, `${whatArg} count`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Refuses one unit larger than `maxMetadataBytes`. */
|
|
132
|
+
export function checkUnitBytes(lengthArg: number, limitsArg: Readonly<ILinuxPackageLimits>, whatArg: string): void {
|
|
133
|
+
if (lengthArg > limitsArg.maxMetadataBytes) {
|
|
134
|
+
throw limitExceeded('maxMetadataBytes', limitsArg.maxMetadataBytes, whatArg);
|
|
135
|
+
}
|
|
136
|
+
}
|