@push.rocks/smartlinuxrepo 0.0.0-stage → 1.0.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/.smartconfig.json +65 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/alpine/apkindex.d.ts +39 -0
- package/dist_ts/alpine/apkindex.js +211 -0
- package/dist_ts/alpine/pkginfo.d.ts +45 -0
- package/dist_ts/alpine/pkginfo.js +159 -0
- package/dist_ts/alpine/reader.d.ts +20 -0
- package/dist_ts/alpine/reader.js +89 -0
- package/dist_ts/arch/builder.d.ts +41 -0
- package/dist_ts/arch/builder.js +234 -0
- package/dist_ts/arch/reader.d.ts +16 -0
- package/dist_ts/arch/reader.js +271 -0
- package/dist_ts/arch/signer.d.ts +21 -0
- package/dist_ts/arch/signer.js +66 -0
- package/dist_ts/arch/vercmp.d.ts +12 -0
- package/dist_ts/arch/vercmp.js +170 -0
- package/dist_ts/debian/control.d.ts +22 -0
- package/dist_ts/debian/control.js +137 -0
- package/dist_ts/debian/gostring.d.ts +26 -0
- package/dist_ts/debian/gostring.js +145 -0
- package/dist_ts/debian/indexbuilder.d.ts +47 -0
- package/dist_ts/debian/indexbuilder.js +299 -0
- package/dist_ts/debian/mailaddress.d.ts +12 -0
- package/dist_ts/debian/mailaddress.js +736 -0
- package/dist_ts/debian/reader.d.ts +30 -0
- package/dist_ts/debian/reader.js +110 -0
- package/dist_ts/index.d.ts +23 -0
- package/dist_ts/index.js +18 -0
- package/dist_ts/plugins.d.ts +8 -0
- package/dist_ts/plugins.js +11 -0
- package/dist_ts/readers/ar.d.ts +31 -0
- package/dist_ts/readers/ar.js +87 -0
- package/dist_ts/readers/bytes.d.ts +58 -0
- package/dist_ts/readers/bytes.js +147 -0
- package/dist_ts/readers/bytesource.d.ts +40 -0
- package/dist_ts/readers/bytesource.js +154 -0
- package/dist_ts/readers/decompress.d.ts +43 -0
- package/dist_ts/readers/decompress.js +269 -0
- package/dist_ts/readers/gzipmember.d.ts +32 -0
- package/dist_ts/readers/gzipmember.js +176 -0
- package/dist_ts/readers/tar.d.ts +85 -0
- package/dist_ts/readers/tar.js +731 -0
- package/dist_ts/readers/text.d.ts +16 -0
- package/dist_ts/readers/text.js +96 -0
- package/dist_ts/rpm/builder.d.ts +28 -0
- package/dist_ts/rpm/builder.js +208 -0
- package/dist_ts/rpm/header.d.ts +138 -0
- package/dist_ts/rpm/header.js +546 -0
- package/dist_ts/rpm/reader.d.ts +20 -0
- package/dist_ts/rpm/reader.js +306 -0
- package/dist_ts/rpm/repoconfig.d.ts +15 -0
- package/dist_ts/rpm/repoconfig.js +38 -0
- package/dist_ts/rpm/repodata.d.ts +44 -0
- package/dist_ts/rpm/repodata.js +148 -0
- package/dist_ts/rpm/sign.d.ts +30 -0
- package/dist_ts/rpm/sign.js +183 -0
- package/dist_ts/rpm/updates.d.ts +38 -0
- package/dist_ts/rpm/updates.js +265 -0
- package/dist_ts/rpm/xml.d.ts +18 -0
- package/dist_ts/rpm/xml.js +90 -0
- package/dist_ts/shared/architecture.d.ts +8 -0
- package/dist_ts/shared/architecture.js +17 -0
- package/dist_ts/shared/errors.d.ts +38 -0
- package/dist_ts/shared/errors.js +22 -0
- package/dist_ts/shared/gopath.d.ts +13 -0
- package/dist_ts/shared/gopath.js +53 -0
- package/dist_ts/shared/gostrings.d.ts +14 -0
- package/dist_ts/shared/gostrings.js +29 -0
- package/dist_ts/shared/limits.d.ts +47 -0
- package/dist_ts/shared/limits.js +57 -0
- package/dist_ts/shared/types.d.ts +213 -0
- package/dist_ts/shared/types.js +13 -0
- package/dist_ts/shared/url.d.ts +18 -0
- package/dist_ts/shared/url.js +371 -0
- package/dist_ts/shared/validation.d.ts +21 -0
- package/dist_ts/shared/validation.js +58 -0
- package/dist_ts/signing/keys.d.ts +36 -0
- package/dist_ts/signing/keys.js +89 -0
- package/dist_ts/signing/openpgpframing.d.ts +33 -0
- package/dist_ts/signing/openpgpframing.js +93 -0
- package/dist_ts/signing/signers.d.ts +63 -0
- package/dist_ts/signing/signers.js +248 -0
- package/dist_ts/writers/gotar.d.ts +43 -0
- package/dist_ts/writers/gotar.js +218 -0
- package/license.md +19 -0
- package/package.json +56 -3
- package/readme.md +176 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/alpine/apkindex.ts +241 -0
- package/ts/alpine/pkginfo.ts +183 -0
- package/ts/alpine/reader.ts +96 -0
- package/ts/arch/builder.ts +283 -0
- package/ts/arch/reader.ts +307 -0
- package/ts/arch/signer.ts +70 -0
- package/ts/arch/vercmp.ts +188 -0
- package/ts/debian/control.ts +150 -0
- package/ts/debian/gostring.ts +150 -0
- package/ts/debian/indexbuilder.ts +355 -0
- package/ts/debian/mailaddress.ts +754 -0
- package/ts/debian/reader.ts +126 -0
- package/ts/index.ts +46 -0
- package/ts/plugins.ts +13 -0
- package/ts/readers/ar.ts +102 -0
- package/ts/readers/bytes.ts +182 -0
- package/ts/readers/bytesource.ts +164 -0
- package/ts/readers/decompress.ts +332 -0
- package/ts/readers/gzipmember.ts +209 -0
- package/ts/readers/tar.ts +835 -0
- package/ts/readers/text.ts +91 -0
- package/ts/rpm/builder.ts +253 -0
- package/ts/rpm/header.ts +628 -0
- package/ts/rpm/reader.ts +317 -0
- package/ts/rpm/repoconfig.ts +45 -0
- package/ts/rpm/repodata.ts +219 -0
- package/ts/rpm/sign.ts +200 -0
- package/ts/rpm/updates.ts +314 -0
- package/ts/rpm/xml.ts +101 -0
- package/ts/shared/architecture.ts +16 -0
- package/ts/shared/errors.ts +78 -0
- package/ts/shared/gopath.ts +54 -0
- package/ts/shared/gostrings.ts +31 -0
- package/ts/shared/limits.ts +93 -0
- package/ts/shared/types.ts +251 -0
- package/ts/shared/url.ts +387 -0
- package/ts/shared/validation.ts +71 -0
- package/ts/signing/keys.ts +110 -0
- package/ts/signing/openpgpframing.ts +115 -0
- package/ts/signing/signers.ts +315 -0
- package/ts/writers/gotar.ts +256 -0
- package/README.md +0 -3
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { LinuxRepoError, limitExceeded, truncated } from '../shared/errors.js';
|
|
3
|
+
import type { ILinuxPackageLimits } from '../shared/limits.js';
|
|
4
|
+
import { READ_CHUNK_BYTES, takeFront, type IByteReader } from './bytes.js';
|
|
5
|
+
|
|
6
|
+
/** The stream compressions the readers decode. */
|
|
7
|
+
export type TLinuxCompression = 'none' | 'gzip' | 'xz' | 'zstd';
|
|
8
|
+
|
|
9
|
+
export interface IDecompressOptions {
|
|
10
|
+
/** Bound on the decompressed bytes delivered. */
|
|
11
|
+
maxOutputBytes: number;
|
|
12
|
+
outputLimit: keyof ILinuxPackageLimits;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A reader over the decompressed bytes of an inner reader. Pulling is lazy
|
|
17
|
+
* and bounded: decompressed output past `maxOutputBytes` fails with
|
|
18
|
+
* `limit-exceeded` and stops the decoder. `close()` releases the decoder and
|
|
19
|
+
* must be called (see `withDecompressingReader`).
|
|
20
|
+
*
|
|
21
|
+
* - gzip (multi-member) and zstd come from `node:zlib`, xz from
|
|
22
|
+
* `@napi-rs/lzma` `xz.decompressStream` (64 KiB chunks, cancellable).
|
|
23
|
+
* - An error from the inner reader (the caller's source, or a codec limit on
|
|
24
|
+
* it) propagates unchanged.
|
|
25
|
+
* - A compressed stream that ends early is `truncated`; corrupt data is
|
|
26
|
+
* `malformed-archive` with the decoder error as `cause`.
|
|
27
|
+
* - The decoder reads the inner reader ahead and never hands bytes back, so
|
|
28
|
+
* the compressed stream must run to the end of the inner reader (a whole
|
|
29
|
+
* package, or a `LimitedReader` over one archive member).
|
|
30
|
+
* - Opening a zstd decoder on a Node.js without nodejs/node#64593 throws a
|
|
31
|
+
* plain `Error` (see `isZstdDecodingSafe`): a server fault, not a verdict
|
|
32
|
+
* on the package.
|
|
33
|
+
*/
|
|
34
|
+
export interface IDecompressingReader extends IByteReader {
|
|
35
|
+
readonly outputBytes: number;
|
|
36
|
+
close(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface IChunkSource {
|
|
40
|
+
next(): Promise<Uint8Array | null>;
|
|
41
|
+
close(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function openDecompressingReader(
|
|
45
|
+
inputArg: IByteReader,
|
|
46
|
+
compressionArg: TLinuxCompression,
|
|
47
|
+
optionsArg: IDecompressOptions,
|
|
48
|
+
): IDecompressingReader {
|
|
49
|
+
let source: IChunkSource;
|
|
50
|
+
switch (compressionArg) {
|
|
51
|
+
case 'none':
|
|
52
|
+
source = new PassThroughSource(inputArg);
|
|
53
|
+
break;
|
|
54
|
+
case 'gzip':
|
|
55
|
+
source = new ZlibSource(inputArg, 'gzip');
|
|
56
|
+
break;
|
|
57
|
+
case 'zstd':
|
|
58
|
+
if (!isZstdDecodingSafe(process.versions.node)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`zstd decoding needs Node.js ${ZSTD_NODE_FLOOR} (nodejs/node#64593 reports a zstd frame that ends early); ` +
|
|
61
|
+
`this process runs Node.js ${process.versions.node}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
source = new ZlibSource(inputArg, 'zstd');
|
|
65
|
+
break;
|
|
66
|
+
case 'xz':
|
|
67
|
+
source = new XzSource(inputArg);
|
|
68
|
+
break;
|
|
69
|
+
default:
|
|
70
|
+
throw new LinuxRepoError('unsupported-compression', `unknown compression "${String(compressionArg)}"`, {
|
|
71
|
+
field: 'compression',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return new BoundedOutputReader(source, optionsArg);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The Node.js releases whose node:zlib reports a truncated zstd frame. */
|
|
78
|
+
const ZSTD_NODE_FLOOR = '^24.20.0 || >=26.6.0';
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* True when Node.js `versionArg` (a `process.versions.node` string) carries
|
|
82
|
+
* nodejs/node#64593, so node:zlib reports a zstd frame that ends early as
|
|
83
|
+
* `Z_BUF_ERROR` instead of returning a prefix: 24.x from 24.20.0, 26.x from
|
|
84
|
+
* 26.6.0, and every major from 27 on. Anything else, including all of 22.x,
|
|
85
|
+
* 23.x and 25.x, is false.
|
|
86
|
+
*/
|
|
87
|
+
export function isZstdDecodingSafe(versionArg: string): boolean {
|
|
88
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:$|[-+])/.exec(versionArg);
|
|
89
|
+
if (match === null) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const major = Number(match[1]);
|
|
93
|
+
const minor = Number(match[2]);
|
|
94
|
+
if (major === 24) {
|
|
95
|
+
return minor >= 20;
|
|
96
|
+
}
|
|
97
|
+
if (major === 26) {
|
|
98
|
+
return minor >= 6;
|
|
99
|
+
}
|
|
100
|
+
return major >= 27;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Runs `bodyArg` with a decompressing reader and always closes it. */
|
|
104
|
+
export async function withDecompressingReader<T>(
|
|
105
|
+
inputArg: IByteReader,
|
|
106
|
+
compressionArg: TLinuxCompression,
|
|
107
|
+
optionsArg: IDecompressOptions,
|
|
108
|
+
bodyArg: (readerArg: IDecompressingReader) => Promise<T>,
|
|
109
|
+
): Promise<T> {
|
|
110
|
+
const reader = openDecompressingReader(inputArg, compressionArg, optionsArg);
|
|
111
|
+
try {
|
|
112
|
+
return await bodyArg(reader);
|
|
113
|
+
} finally {
|
|
114
|
+
await reader.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class BoundedOutputReader implements IDecompressingReader {
|
|
119
|
+
private delivered = 0;
|
|
120
|
+
private readonly pushedBack: Uint8Array[] = [];
|
|
121
|
+
private closed = false;
|
|
122
|
+
|
|
123
|
+
constructor(
|
|
124
|
+
private readonly source: IChunkSource,
|
|
125
|
+
private readonly options: IDecompressOptions,
|
|
126
|
+
) {}
|
|
127
|
+
|
|
128
|
+
public get outputBytes(): number {
|
|
129
|
+
return this.delivered;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public async read(maxArg: number): Promise<Uint8Array | null> {
|
|
133
|
+
const pushed = this.pushedBack.pop();
|
|
134
|
+
if (pushed !== undefined) {
|
|
135
|
+
return takeFront(pushed, maxArg, this.pushedBack);
|
|
136
|
+
}
|
|
137
|
+
if (this.closed) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const chunk = await this.source.next();
|
|
141
|
+
if (chunk === null) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
this.delivered += chunk.length;
|
|
145
|
+
if (this.delivered > this.options.maxOutputBytes) {
|
|
146
|
+
await this.close();
|
|
147
|
+
throw limitExceeded(this.options.outputLimit, this.options.maxOutputBytes, 'decompressed data');
|
|
148
|
+
}
|
|
149
|
+
return takeFront(chunk, maxArg, this.pushedBack);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public unread(bytesArg: Uint8Array): void {
|
|
153
|
+
if (bytesArg.length > 0) {
|
|
154
|
+
this.pushedBack.push(bytesArg);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public async close(): Promise<void> {
|
|
159
|
+
this.pushedBack.length = 0;
|
|
160
|
+
if (this.closed) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this.closed = true;
|
|
164
|
+
await this.source.close();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
class PassThroughSource implements IChunkSource {
|
|
169
|
+
constructor(private readonly input: IByteReader) {}
|
|
170
|
+
|
|
171
|
+
public next(): Promise<Uint8Array | null> {
|
|
172
|
+
return this.input.read(READ_CHUNK_BYTES);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public async close(): Promise<void> {}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** gzip (multi-member, like Go's default) and zstd through `node:zlib`, with stream backpressure. */
|
|
179
|
+
class ZlibSource implements IChunkSource {
|
|
180
|
+
private readonly kind: 'gzip' | 'zstd';
|
|
181
|
+
private readonly feeder: plugins.stream.Readable;
|
|
182
|
+
private readonly decoder: plugins.zlib.Gunzip | plugins.zlib.ZstdDecompress;
|
|
183
|
+
private readonly iterator: AsyncIterator<Buffer>;
|
|
184
|
+
private inputFailed = false;
|
|
185
|
+
private inputError: unknown = undefined;
|
|
186
|
+
private finished = false;
|
|
187
|
+
|
|
188
|
+
constructor(inputArg: IByteReader, kindArg: 'gzip' | 'zstd') {
|
|
189
|
+
this.kind = kindArg;
|
|
190
|
+
const pump = async function* (this: ZlibSource): AsyncGenerator<Uint8Array> {
|
|
191
|
+
for (;;) {
|
|
192
|
+
let chunk: Uint8Array | null;
|
|
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
|
+
}
|
|
200
|
+
if (chunk === null) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
yield chunk;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
this.feeder = plugins.stream.Readable.from(pump.call(this), { objectMode: false });
|
|
207
|
+
this.decoder = kindArg === 'gzip' ? plugins.zlib.createGunzip() : plugins.zlib.createZstdDecompress();
|
|
208
|
+
// errors surface through the iterator below; the callback only completes the pipeline
|
|
209
|
+
plugins.stream.pipeline(this.feeder, this.decoder, () => {});
|
|
210
|
+
this.iterator = this.decoder[Symbol.asyncIterator]();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
public async next(): Promise<Uint8Array | null> {
|
|
214
|
+
if (this.finished) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const result = await this.iterator.next();
|
|
219
|
+
if (result.done) {
|
|
220
|
+
this.finished = true;
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
return result.value;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
this.finished = true;
|
|
226
|
+
if (this.inputFailed) {
|
|
227
|
+
throw this.inputError;
|
|
228
|
+
}
|
|
229
|
+
throw mapZlibError(error, this.kind);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
public async close(): Promise<void> {
|
|
234
|
+
this.finished = true;
|
|
235
|
+
this.feeder.destroy();
|
|
236
|
+
this.decoder.destroy();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function mapZlibError(errorArg: unknown, kindArg: 'gzip' | 'zstd'): LinuxRepoError {
|
|
241
|
+
const code = (errorArg as { code?: unknown } | null)?.code;
|
|
242
|
+
// gunzip, and zstd on the Node.js floor, report an input that ended inside the stream as Z_BUF_ERROR
|
|
243
|
+
if (code === 'Z_BUF_ERROR') {
|
|
244
|
+
const error = truncated(`${kindArg} stream`);
|
|
245
|
+
return new LinuxRepoError(error.code, error.message, error.detail, { cause: errorArg });
|
|
246
|
+
}
|
|
247
|
+
return new LinuxRepoError('malformed-archive', `corrupt ${kindArg} data`, { field: `${kindArg} stream` }, {
|
|
248
|
+
cause: errorArg,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* xz through `@napi-rs/lzma` `xz.decompressStream`, the one bounded decoder of
|
|
254
|
+
* that package (its class API buffers whole outputs). The input is a
|
|
255
|
+
* pull-based web stream that never reads ahead; cancelling the output stops it.
|
|
256
|
+
*/
|
|
257
|
+
class XzSource implements IChunkSource {
|
|
258
|
+
private readonly reader: ReadableStreamDefaultReader<Uint8Array>;
|
|
259
|
+
private inputFailed = false;
|
|
260
|
+
private inputError: unknown = undefined;
|
|
261
|
+
private inputEnded = false;
|
|
262
|
+
private finished = false;
|
|
263
|
+
private stopped = false;
|
|
264
|
+
|
|
265
|
+
constructor(inputArg: IByteReader) {
|
|
266
|
+
const input = new ReadableStream<Uint8Array>(
|
|
267
|
+
{
|
|
268
|
+
pull: async (controller) => {
|
|
269
|
+
if (this.stopped) {
|
|
270
|
+
controller.close();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
let chunk: Uint8Array | null;
|
|
274
|
+
try {
|
|
275
|
+
chunk = await inputArg.read(READ_CHUNK_BYTES);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
this.inputFailed = true;
|
|
278
|
+
this.inputError = error;
|
|
279
|
+
controller.error(error);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (chunk === null) {
|
|
283
|
+
this.inputEnded = true;
|
|
284
|
+
controller.close();
|
|
285
|
+
} else {
|
|
286
|
+
controller.enqueue(chunk);
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
cancel: () => {
|
|
290
|
+
this.stopped = true;
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{ highWaterMark: 0 },
|
|
294
|
+
);
|
|
295
|
+
this.reader = plugins.lzma.xz.decompressStream(input).getReader();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
public async next(): Promise<Uint8Array | null> {
|
|
299
|
+
if (this.finished) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const result = await this.reader.read();
|
|
304
|
+
if (result.done) {
|
|
305
|
+
this.finished = true;
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
return result.value;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
this.finished = true;
|
|
311
|
+
if (this.inputFailed) {
|
|
312
|
+
throw this.inputError;
|
|
313
|
+
}
|
|
314
|
+
// @napi-rs/lzma 1.5.3 reports a decoder that ran out of input as Rust's
|
|
315
|
+
// read_exact error; with the input at its end that is a truncated stream
|
|
316
|
+
const message = error instanceof Error ? error.message : '';
|
|
317
|
+
if (this.inputEnded && message.includes('failed to fill whole buffer')) {
|
|
318
|
+
const base = truncated('xz stream');
|
|
319
|
+
throw new LinuxRepoError(base.code, base.message, base.detail, { cause: error });
|
|
320
|
+
}
|
|
321
|
+
throw new LinuxRepoError('malformed-archive', 'corrupt xz data', { field: 'xz stream' }, { cause: error });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
public async close(): Promise<void> {
|
|
326
|
+
this.stopped = true;
|
|
327
|
+
if (!this.finished) {
|
|
328
|
+
this.finished = true;
|
|
329
|
+
await this.reader.cancel();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { LinuxRepoError, limitExceeded, truncated } from '../shared/errors.js';
|
|
3
|
+
import type { ILinuxPackageLimits } from '../shared/limits.js';
|
|
4
|
+
import { concatBytes, READ_CHUNK_BYTES, readExact, readUpTo, type IByteReader } from './bytes.js';
|
|
5
|
+
|
|
6
|
+
const FLAG_HCRC = 0x02;
|
|
7
|
+
const FLAG_EXTRA = 0x04;
|
|
8
|
+
const FLAG_NAME = 0x08;
|
|
9
|
+
const FLAG_COMMENT = 0x10;
|
|
10
|
+
|
|
11
|
+
export interface IGzipMemberOptions {
|
|
12
|
+
/** Bound on the member's own bytes (header, deflate data and trailer). */
|
|
13
|
+
maxMemberBytes: number;
|
|
14
|
+
memberLimit: keyof ILinuxPackageLimits;
|
|
15
|
+
/** Bound on the member's decompressed bytes. */
|
|
16
|
+
maxOutputBytes: number;
|
|
17
|
+
outputLimit: keyof ILinuxPackageLimits;
|
|
18
|
+
/** Bound on the header's FNAME and FCOMMENT strings. */
|
|
19
|
+
maxHeaderBytes: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IGzipMember {
|
|
23
|
+
/** The decompressed member. */
|
|
24
|
+
output: Uint8Array;
|
|
25
|
+
/** The member's own byte count: header, deflate data and trailer. */
|
|
26
|
+
memberBytes: number;
|
|
27
|
+
/** Lowercase hex SHA-1 of exactly the member's bytes. */
|
|
28
|
+
memberSha1: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reads exactly one gzip member (RFC 1952) from `inputArg` and leaves the
|
|
33
|
+
* reader positioned at the first byte after it, so concatenated members (an
|
|
34
|
+
* apk v2 package) can be read one at a time with their exact boundaries.
|
|
35
|
+
*
|
|
36
|
+
* The header is parsed here; the deflate data is inflated with
|
|
37
|
+
* `zlib.createInflateRaw()`, whose `bytesWritten` counts only the bytes the
|
|
38
|
+
* deflate stream consumed, and the bytes past the stream's end are pushed back
|
|
39
|
+
* with `unread`. The trailer's CRC-32 and ISIZE are checked. Returns `null`
|
|
40
|
+
* when the input ends exactly where a member would start.
|
|
41
|
+
*/
|
|
42
|
+
export async function readGzipMember(
|
|
43
|
+
inputArg: IByteReader,
|
|
44
|
+
optionsArg: IGzipMemberOptions,
|
|
45
|
+
): Promise<IGzipMember | null> {
|
|
46
|
+
const sha1 = plugins.crypto.createHash('sha1');
|
|
47
|
+
let memberBytes = 0;
|
|
48
|
+
const account = (bytesArg: Uint8Array) => {
|
|
49
|
+
memberBytes += bytesArg.length;
|
|
50
|
+
if (memberBytes > optionsArg.maxMemberBytes) {
|
|
51
|
+
throw limitExceeded(optionsArg.memberLimit, optionsArg.maxMemberBytes, 'gzip member');
|
|
52
|
+
}
|
|
53
|
+
sha1.update(bytesArg);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// header
|
|
57
|
+
const fixed = await readUpTo(inputArg, 10);
|
|
58
|
+
if (fixed.length === 0) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (fixed[0] !== 0x1f || (fixed.length > 1 && fixed[1] !== 0x8b) || (fixed.length > 2 && fixed[2] !== 0x08)) {
|
|
62
|
+
throw new LinuxRepoError('malformed-archive', 'input is not a gzip member', { field: 'gzip header' });
|
|
63
|
+
}
|
|
64
|
+
if (fixed.length < 10) {
|
|
65
|
+
throw truncated('gzip header');
|
|
66
|
+
}
|
|
67
|
+
account(fixed);
|
|
68
|
+
let headerCrc = plugins.zlib.crc32(fixed);
|
|
69
|
+
const flags = fixed[3];
|
|
70
|
+
if ((flags & FLAG_EXTRA) !== 0) {
|
|
71
|
+
const lengthBytes = await readExact(inputArg, 2, 'gzip header');
|
|
72
|
+
account(lengthBytes);
|
|
73
|
+
headerCrc = plugins.zlib.crc32(lengthBytes, headerCrc);
|
|
74
|
+
const extra = await readExact(inputArg, lengthBytes[0] | (lengthBytes[1] << 8), 'gzip header');
|
|
75
|
+
account(extra);
|
|
76
|
+
headerCrc = plugins.zlib.crc32(extra, headerCrc);
|
|
77
|
+
}
|
|
78
|
+
for (const flag of [FLAG_NAME, FLAG_COMMENT]) {
|
|
79
|
+
if ((flags & flag) !== 0) {
|
|
80
|
+
const text = await readZeroTerminated(inputArg, optionsArg.maxHeaderBytes);
|
|
81
|
+
account(text);
|
|
82
|
+
headerCrc = plugins.zlib.crc32(text, headerCrc);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if ((flags & FLAG_HCRC) !== 0) {
|
|
86
|
+
const stored = await readExact(inputArg, 2, 'gzip header');
|
|
87
|
+
account(stored);
|
|
88
|
+
if ((stored[0] | (stored[1] << 8)) !== (headerCrc & 0xffff)) {
|
|
89
|
+
throw new LinuxRepoError('malformed-archive', 'gzip header CRC mismatch', { field: 'gzip header' });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// deflate data
|
|
94
|
+
const output = await inflateRawMember(inputArg, optionsArg, account);
|
|
95
|
+
|
|
96
|
+
// trailer
|
|
97
|
+
const trailer = await readExact(inputArg, 8, 'gzip trailer');
|
|
98
|
+
account(trailer);
|
|
99
|
+
const view = new DataView(trailer.buffer, trailer.byteOffset, trailer.byteLength);
|
|
100
|
+
if (view.getUint32(0, true) !== output.crc || view.getUint32(4, true) !== output.bytes.length % 0x1_0000_0000) {
|
|
101
|
+
throw new LinuxRepoError('malformed-archive', 'gzip trailer does not match the data', { field: 'gzip trailer' });
|
|
102
|
+
}
|
|
103
|
+
return { output: output.bytes, memberBytes, memberSha1: sha1.digest('hex') };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function inflateRawMember(
|
|
107
|
+
inputArg: IByteReader,
|
|
108
|
+
optionsArg: IGzipMemberOptions,
|
|
109
|
+
accountArg: (bytesArg: Uint8Array) => void,
|
|
110
|
+
): Promise<{ bytes: Uint8Array; crc: number }> {
|
|
111
|
+
const inflate = plugins.zlib.createInflateRaw();
|
|
112
|
+
const chunks: Uint8Array[] = [];
|
|
113
|
+
let outputBytes = 0;
|
|
114
|
+
let crc = 0;
|
|
115
|
+
let failure: LinuxRepoError | null = null;
|
|
116
|
+
let streamError: Error | null = null;
|
|
117
|
+
inflate.on('data', (chunk: Buffer) => {
|
|
118
|
+
if (failure !== null) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
outputBytes += chunk.length;
|
|
122
|
+
if (outputBytes > optionsArg.maxOutputBytes) {
|
|
123
|
+
failure = limitExceeded(optionsArg.outputLimit, optionsArg.maxOutputBytes, 'decompressed gzip member');
|
|
124
|
+
inflate.destroy();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
crc = plugins.zlib.crc32(chunk, crc);
|
|
128
|
+
chunks.push(chunk);
|
|
129
|
+
});
|
|
130
|
+
inflate.on('error', (error: Error) => {
|
|
131
|
+
streamError ??= error;
|
|
132
|
+
});
|
|
133
|
+
try {
|
|
134
|
+
for (;;) {
|
|
135
|
+
const chunk = await inputArg.read(READ_CHUNK_BYTES);
|
|
136
|
+
if (chunk === null) {
|
|
137
|
+
throw truncated('gzip member');
|
|
138
|
+
}
|
|
139
|
+
const before = inflate.bytesWritten;
|
|
140
|
+
try {
|
|
141
|
+
await writeChunk(inflate, chunk);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw failure ?? mapInflateError(streamError ?? error);
|
|
144
|
+
}
|
|
145
|
+
if (failure !== null) {
|
|
146
|
+
throw failure;
|
|
147
|
+
}
|
|
148
|
+
const consumed = inflate.bytesWritten - before;
|
|
149
|
+
accountArg(chunk.subarray(0, consumed));
|
|
150
|
+
if (consumed < chunk.length) {
|
|
151
|
+
inputArg.unread(chunk.subarray(consumed));
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!inflate.readableEnded) {
|
|
156
|
+
await new Promise<void>((resolve) => inflate.once('end', resolve));
|
|
157
|
+
}
|
|
158
|
+
if (failure !== null) {
|
|
159
|
+
throw failure;
|
|
160
|
+
}
|
|
161
|
+
return { bytes: concatBytes(chunks, outputBytes), crc };
|
|
162
|
+
} finally {
|
|
163
|
+
inflate.destroy();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Writes one chunk and waits until the stream processed it, failing if the stream closes first. */
|
|
168
|
+
function writeChunk(streamArg: plugins.zlib.InflateRaw, chunkArg: Uint8Array): Promise<void> {
|
|
169
|
+
return new Promise<void>((resolve, reject) => {
|
|
170
|
+
const onClose = () => reject(new Error('inflate stream closed'));
|
|
171
|
+
streamArg.once('close', onClose);
|
|
172
|
+
streamArg.write(chunkArg, (error) => {
|
|
173
|
+
streamArg.off('close', onClose);
|
|
174
|
+
if (error) {
|
|
175
|
+
reject(error);
|
|
176
|
+
} else {
|
|
177
|
+
resolve();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function mapInflateError(errorArg: unknown): LinuxRepoError {
|
|
184
|
+
return new LinuxRepoError('malformed-archive', 'gzip member has corrupt deflate data', { field: 'gzip member' }, {
|
|
185
|
+
cause: errorArg,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function readZeroTerminated(inputArg: IByteReader, maxBytesArg: number): Promise<Uint8Array> {
|
|
190
|
+
const parts: Uint8Array[] = [];
|
|
191
|
+
let total = 0;
|
|
192
|
+
for (;;) {
|
|
193
|
+
const chunk = await inputArg.read(READ_CHUNK_BYTES);
|
|
194
|
+
if (chunk === null) {
|
|
195
|
+
throw truncated('gzip header');
|
|
196
|
+
}
|
|
197
|
+
const nul = chunk.indexOf(0);
|
|
198
|
+
const take = nul < 0 ? chunk : chunk.subarray(0, nul + 1);
|
|
199
|
+
total += take.length;
|
|
200
|
+
if (total > maxBytesArg) {
|
|
201
|
+
throw limitExceeded('maxHeaderBytes', maxBytesArg, 'gzip header string');
|
|
202
|
+
}
|
|
203
|
+
parts.push(take);
|
|
204
|
+
if (nul >= 0) {
|
|
205
|
+
inputArg.unread(chunk.subarray(nul + 1));
|
|
206
|
+
return concatBytes(parts, total);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|