@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.
Files changed (131) hide show
  1. package/.smartconfig.json +65 -0
  2. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  3. package/dist_ts/00_commitinfo_data.js +9 -0
  4. package/dist_ts/alpine/apkindex.d.ts +39 -0
  5. package/dist_ts/alpine/apkindex.js +211 -0
  6. package/dist_ts/alpine/pkginfo.d.ts +45 -0
  7. package/dist_ts/alpine/pkginfo.js +159 -0
  8. package/dist_ts/alpine/reader.d.ts +20 -0
  9. package/dist_ts/alpine/reader.js +89 -0
  10. package/dist_ts/arch/builder.d.ts +41 -0
  11. package/dist_ts/arch/builder.js +234 -0
  12. package/dist_ts/arch/reader.d.ts +16 -0
  13. package/dist_ts/arch/reader.js +271 -0
  14. package/dist_ts/arch/signer.d.ts +21 -0
  15. package/dist_ts/arch/signer.js +66 -0
  16. package/dist_ts/arch/vercmp.d.ts +12 -0
  17. package/dist_ts/arch/vercmp.js +170 -0
  18. package/dist_ts/debian/control.d.ts +22 -0
  19. package/dist_ts/debian/control.js +137 -0
  20. package/dist_ts/debian/gostring.d.ts +26 -0
  21. package/dist_ts/debian/gostring.js +145 -0
  22. package/dist_ts/debian/indexbuilder.d.ts +47 -0
  23. package/dist_ts/debian/indexbuilder.js +299 -0
  24. package/dist_ts/debian/mailaddress.d.ts +12 -0
  25. package/dist_ts/debian/mailaddress.js +736 -0
  26. package/dist_ts/debian/reader.d.ts +30 -0
  27. package/dist_ts/debian/reader.js +110 -0
  28. package/dist_ts/index.d.ts +23 -0
  29. package/dist_ts/index.js +18 -0
  30. package/dist_ts/plugins.d.ts +8 -0
  31. package/dist_ts/plugins.js +11 -0
  32. package/dist_ts/readers/ar.d.ts +31 -0
  33. package/dist_ts/readers/ar.js +87 -0
  34. package/dist_ts/readers/bytes.d.ts +58 -0
  35. package/dist_ts/readers/bytes.js +147 -0
  36. package/dist_ts/readers/bytesource.d.ts +40 -0
  37. package/dist_ts/readers/bytesource.js +154 -0
  38. package/dist_ts/readers/decompress.d.ts +43 -0
  39. package/dist_ts/readers/decompress.js +269 -0
  40. package/dist_ts/readers/gzipmember.d.ts +32 -0
  41. package/dist_ts/readers/gzipmember.js +176 -0
  42. package/dist_ts/readers/tar.d.ts +85 -0
  43. package/dist_ts/readers/tar.js +731 -0
  44. package/dist_ts/readers/text.d.ts +16 -0
  45. package/dist_ts/readers/text.js +96 -0
  46. package/dist_ts/rpm/builder.d.ts +28 -0
  47. package/dist_ts/rpm/builder.js +208 -0
  48. package/dist_ts/rpm/header.d.ts +138 -0
  49. package/dist_ts/rpm/header.js +546 -0
  50. package/dist_ts/rpm/reader.d.ts +20 -0
  51. package/dist_ts/rpm/reader.js +306 -0
  52. package/dist_ts/rpm/repoconfig.d.ts +15 -0
  53. package/dist_ts/rpm/repoconfig.js +38 -0
  54. package/dist_ts/rpm/repodata.d.ts +44 -0
  55. package/dist_ts/rpm/repodata.js +148 -0
  56. package/dist_ts/rpm/sign.d.ts +30 -0
  57. package/dist_ts/rpm/sign.js +183 -0
  58. package/dist_ts/rpm/updates.d.ts +38 -0
  59. package/dist_ts/rpm/updates.js +265 -0
  60. package/dist_ts/rpm/xml.d.ts +18 -0
  61. package/dist_ts/rpm/xml.js +90 -0
  62. package/dist_ts/shared/architecture.d.ts +8 -0
  63. package/dist_ts/shared/architecture.js +17 -0
  64. package/dist_ts/shared/errors.d.ts +38 -0
  65. package/dist_ts/shared/errors.js +22 -0
  66. package/dist_ts/shared/gopath.d.ts +13 -0
  67. package/dist_ts/shared/gopath.js +53 -0
  68. package/dist_ts/shared/gostrings.d.ts +14 -0
  69. package/dist_ts/shared/gostrings.js +29 -0
  70. package/dist_ts/shared/limits.d.ts +47 -0
  71. package/dist_ts/shared/limits.js +57 -0
  72. package/dist_ts/shared/types.d.ts +213 -0
  73. package/dist_ts/shared/types.js +13 -0
  74. package/dist_ts/shared/url.d.ts +18 -0
  75. package/dist_ts/shared/url.js +371 -0
  76. package/dist_ts/shared/validation.d.ts +21 -0
  77. package/dist_ts/shared/validation.js +58 -0
  78. package/dist_ts/signing/keys.d.ts +36 -0
  79. package/dist_ts/signing/keys.js +89 -0
  80. package/dist_ts/signing/openpgpframing.d.ts +33 -0
  81. package/dist_ts/signing/openpgpframing.js +93 -0
  82. package/dist_ts/signing/signers.d.ts +63 -0
  83. package/dist_ts/signing/signers.js +248 -0
  84. package/dist_ts/writers/gotar.d.ts +43 -0
  85. package/dist_ts/writers/gotar.js +218 -0
  86. package/license.md +19 -0
  87. package/package.json +56 -3
  88. package/readme.md +176 -0
  89. package/ts/00_commitinfo_data.ts +8 -0
  90. package/ts/alpine/apkindex.ts +241 -0
  91. package/ts/alpine/pkginfo.ts +183 -0
  92. package/ts/alpine/reader.ts +96 -0
  93. package/ts/arch/builder.ts +283 -0
  94. package/ts/arch/reader.ts +307 -0
  95. package/ts/arch/signer.ts +70 -0
  96. package/ts/arch/vercmp.ts +188 -0
  97. package/ts/debian/control.ts +150 -0
  98. package/ts/debian/gostring.ts +150 -0
  99. package/ts/debian/indexbuilder.ts +355 -0
  100. package/ts/debian/mailaddress.ts +754 -0
  101. package/ts/debian/reader.ts +126 -0
  102. package/ts/index.ts +46 -0
  103. package/ts/plugins.ts +13 -0
  104. package/ts/readers/ar.ts +102 -0
  105. package/ts/readers/bytes.ts +182 -0
  106. package/ts/readers/bytesource.ts +164 -0
  107. package/ts/readers/decompress.ts +332 -0
  108. package/ts/readers/gzipmember.ts +209 -0
  109. package/ts/readers/tar.ts +835 -0
  110. package/ts/readers/text.ts +91 -0
  111. package/ts/rpm/builder.ts +253 -0
  112. package/ts/rpm/header.ts +628 -0
  113. package/ts/rpm/reader.ts +317 -0
  114. package/ts/rpm/repoconfig.ts +45 -0
  115. package/ts/rpm/repodata.ts +219 -0
  116. package/ts/rpm/sign.ts +200 -0
  117. package/ts/rpm/updates.ts +314 -0
  118. package/ts/rpm/xml.ts +101 -0
  119. package/ts/shared/architecture.ts +16 -0
  120. package/ts/shared/errors.ts +78 -0
  121. package/ts/shared/gopath.ts +54 -0
  122. package/ts/shared/gostrings.ts +31 -0
  123. package/ts/shared/limits.ts +93 -0
  124. package/ts/shared/types.ts +251 -0
  125. package/ts/shared/url.ts +387 -0
  126. package/ts/shared/validation.ts +71 -0
  127. package/ts/signing/keys.ts +110 -0
  128. package/ts/signing/openpgpframing.ts +115 -0
  129. package/ts/signing/signers.ts +315 -0
  130. package/ts/writers/gotar.ts +256 -0
  131. package/README.md +0 -3
@@ -0,0 +1,835 @@
1
+ import { LinuxRepoError, limitExceeded, truncated } from '../shared/errors.js';
2
+ import type { ILinuxPackageLimits } from '../shared/limits.js';
3
+ import { concatBytes, LimitedReader, readExact, readUpTo, type IByteReader } from './bytes.js';
4
+ import { decodeGoUtf8, decodeLatin1 } from './text.js';
5
+
6
+ const BLOCK = 512;
7
+ /** Type flags whose header size never has data (Go `isHeaderOnlyType`). */
8
+ const HEADER_ONLY_TYPES = new Set(['1', '2', '3', '4', '5', '6']);
9
+ const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
10
+ const INT64_MIN = -(1n << 63n);
11
+ const INT64_MAX = (1n << 63n) - 1n;
12
+ /**
13
+ * Go `maxSpecialFileSize` (format.go:147-149): the most Go reads of one PAX
14
+ * header, GNU long name or long link, old GNU sparse map or PAX GNU 1.0
15
+ * sparse map, whatever `maxHeaderBytes` allows.
16
+ */
17
+ const MAX_SPECIAL_FILE_SIZE = 1 << 20;
18
+
19
+ export interface ITarEntry {
20
+ /**
21
+ * The final name: PAX `path`, else GNU long name, else USTAR prefix + name;
22
+ * a recognised PAX GNU sparse header's `GNU.sparse.name` wins. Bytes are
23
+ * decoded as Gitea's JSON stores a Go string (`decodeGoUtf8`). The name is
24
+ * reported as it stands: no normalisation of `./`, `..` or absolute paths.
25
+ */
26
+ name: string;
27
+ /** The link target; empty for `'g'`, as Go returns it. */
28
+ linkName: string;
29
+ /**
30
+ * The header's mode field as Go `archive/tar` parses it (octal or base-256;
31
+ * a field Go cannot parse fails the archive, as in Go); 0 for `'g'`, as Go
32
+ * returns it.
33
+ */
34
+ mode: bigint;
35
+ /**
36
+ * The type flag. Go's legacy `\0` becomes `'0'`, or `'5'` when the name ends
37
+ * with `/`. `'g'` is a PAX global header, returned as an entry as Go does.
38
+ */
39
+ typeflag: string;
40
+ /**
41
+ * Data bytes readable through `body` (0 for header-only types and `'g'`);
42
+ * for a sparse entry, the real size Go reports, any non-negative int64.
43
+ * Stored data above `Number.MAX_SAFE_INTEGER` bytes is refused.
44
+ */
45
+ size: bigint;
46
+ /**
47
+ * An old GNU (`'S'`) or PAX GNU 0.0, 0.1 or 1.0 sparse entry whose map Go
48
+ * accepts. The codec does not expand holes: reading its data (through
49
+ * `body` or `readTarEntryData`) is refused with `unsupported-package-format`,
50
+ * a known difference from Gitea; skipping it works.
51
+ */
52
+ sparse: boolean;
53
+ body: LimitedReader;
54
+ }
55
+
56
+ export interface ITarReaderOptions {
57
+ /** Entries returned per archive. */
58
+ maxEntries: number;
59
+ /**
60
+ * One header chain: header blocks, PAX records, GNU long names and old GNU
61
+ * sparse extension blocks. A PAX GNU 1.0 map, which heads the entry's data,
62
+ * is bounded by Go's 1 MiB cap only.
63
+ */
64
+ maxHeaderBytes: number;
65
+ }
66
+
67
+ interface IHeaderBlock {
68
+ name: string;
69
+ linkName: string;
70
+ mode: bigint;
71
+ typeflag: string;
72
+ size: bigint;
73
+ format: TTarFormat;
74
+ }
75
+
76
+ /** Go `getFormat`'s header formats (format.go:178-201); PAX headers are `'ustar'`. */
77
+ type TTarFormat = 'v7' | 'ustar' | 'star' | 'gnu';
78
+
79
+ /**
80
+ * A bounded ustar reader following Go `archive/tar` (Gitea's reader):
81
+ * USTAR, PAX (`x`, `g`), GNU long names (`L`, `K`), STAR prefixes, Go's
82
+ * pre-1.8 GNU prefix fallback, base-256 numbers and signed or unsigned
83
+ * checksums. A header Go refuses for an unparseable numeric field is refused. The archive ends at two zero
84
+ * blocks, at a clean end of input on a header boundary (apk segments have no
85
+ * end marker), or when the input ends inside the padding after an entry.
86
+ * Sparse maps (old GNU, PAX GNU 0.0, 0.1 and 1.0) are read and validated as
87
+ * Go does; the holes are not expanded, so a sparse entry's data can only be
88
+ * skipped.
89
+ */
90
+ export class TarReader {
91
+ private done = false;
92
+ private current: LimitedReader | null = null;
93
+ private pad = 0;
94
+ private entries = 0;
95
+
96
+ constructor(
97
+ private readonly input: IByteReader,
98
+ private readonly options: ITarReaderOptions,
99
+ ) {}
100
+
101
+ public async next(): Promise<ITarEntry | null> {
102
+ if (this.done) {
103
+ return null;
104
+ }
105
+ if (this.current !== null) {
106
+ await this.current.skipRest();
107
+ this.current = null;
108
+ }
109
+ if (!(await this.consumePadding())) {
110
+ return this.finish();
111
+ }
112
+
113
+ let chainBytes = 0;
114
+ let paxRecords: Map<string, string> | null = null;
115
+ let gnuLongName: string | null = null;
116
+ let gnuLongLink: string | null = null;
117
+ const addChain = (bytesArg: number) => {
118
+ chainBytes += bytesArg;
119
+ if (chainBytes > this.options.maxHeaderBytes) {
120
+ throw limitExceeded('maxHeaderBytes', this.options.maxHeaderBytes, 'tar header chain');
121
+ }
122
+ };
123
+
124
+ for (;;) {
125
+ const block = await readUpTo(this.input, BLOCK);
126
+ if (block.length === 0) {
127
+ return this.finish();
128
+ }
129
+ if (block.length < BLOCK) {
130
+ throw truncated('tar header');
131
+ }
132
+ if (isZeroBlock(block)) {
133
+ const second = await readUpTo(this.input, BLOCK);
134
+ if (second.length === 0) {
135
+ return this.finish();
136
+ }
137
+ if (second.length < BLOCK) {
138
+ throw truncated('tar end-of-archive marker');
139
+ }
140
+ if (isZeroBlock(second)) {
141
+ return this.finish();
142
+ }
143
+ throw malformed('a zero block is followed by a non-zero block');
144
+ }
145
+ addChain(BLOCK);
146
+ const header = parseHeaderBlock(block);
147
+
148
+ if (header.typeflag === 'x' || header.typeflag === 'g') {
149
+ const size = specialFileSize(header, 'PAX header');
150
+ addChain(size);
151
+ const data = await readExact(this.input, size, 'tar PAX header');
152
+ this.pad = padding(size);
153
+ const records = parsePax(data);
154
+ if (header.typeflag === 'g') {
155
+ this.countEntry();
156
+ this.current = new LimitedReader(this.input, 0, 'tar entry data');
157
+ // Go returns a global header's name, type flag and records only, and
158
+ // ignores mergePAX's error for it (reader.go:107-116); after a failing
159
+ // record Go's name depends on map order, the codec takes `path`
160
+ const name = records.get('path') || header.name;
161
+ return { name, linkName: '', mode: 0n, typeflag: 'g', size: 0n, sparse: false, body: this.current };
162
+ }
163
+ paxRecords = records;
164
+ if (!(await this.consumePadding())) {
165
+ return this.finish();
166
+ }
167
+ continue;
168
+ }
169
+
170
+ if (header.typeflag === 'L' || header.typeflag === 'K') {
171
+ const size = specialFileSize(header, 'GNU long name or link');
172
+ addChain(size);
173
+ const data = await readExact(this.input, size, 'tar GNU long name');
174
+ this.pad = padding(size);
175
+ const value = decodeGoUtf8(cutAtNul(data));
176
+ if (header.typeflag === 'L') {
177
+ gnuLongName = value;
178
+ } else {
179
+ gnuLongLink = value;
180
+ }
181
+ if (!(await this.consumePadding())) {
182
+ return this.finish();
183
+ }
184
+ continue;
185
+ }
186
+
187
+ // a real entry: Go mergePAX (reader.go:261-305), then the GNU long names
188
+ let name = header.name;
189
+ let linkName = header.linkName;
190
+ let size = header.size;
191
+ if (paxRecords !== null) {
192
+ checkPaxNumbers(paxRecords);
193
+ const path = paxRecords.get('path');
194
+ if (path) {
195
+ name = path;
196
+ }
197
+ const linkPath = paxRecords.get('linkpath');
198
+ if (linkPath) {
199
+ linkName = linkPath;
200
+ }
201
+ const paxSize = paxRecords.get('size');
202
+ if (paxSize) {
203
+ size = parsePaxInt(paxSize, 'size');
204
+ }
205
+ }
206
+ if (gnuLongName !== null && gnuLongName !== '') {
207
+ name = gnuLongName;
208
+ }
209
+ if (gnuLongLink !== null && gnuLongLink !== '') {
210
+ linkName = gnuLongLink;
211
+ }
212
+ let typeflag = header.typeflag;
213
+ if (typeflag === '\0') {
214
+ typeflag = name.endsWith('/') ? '5' : '0';
215
+ }
216
+ const dataSize = HEADER_ONLY_TYPES.has(typeflag) ? 0n : size;
217
+ if (dataSize < 0n) {
218
+ throw malformed('negative entry size');
219
+ }
220
+ if (dataSize > MAX_SAFE) {
221
+ throw malformed('entry size is out of range');
222
+ }
223
+ const length = Number(dataSize);
224
+ let body = new LimitedReader(this.input, length, 'tar entry data');
225
+ // Go handleSparseFile (reader.go:195-214): the reported size becomes the real size
226
+ let reportedSize = dataSize;
227
+ let sparse = false;
228
+ if (typeflag === 'S') {
229
+ reportedSize = await this.readOldGnuSparseMap(block, header.format, addChain);
230
+ sparse = true;
231
+ } else if (paxRecords !== null) {
232
+ const version = gnuSparseVersion(paxRecords);
233
+ if (version !== null) {
234
+ // Go readGNUSparsePAXHeaders (reader.go:220-259)
235
+ const sparseName = paxRecords.get('GNU.sparse.name') ?? '';
236
+ if (sparseName !== '') {
237
+ name = sparseName;
238
+ }
239
+ const realSizeText = paxRecords.get('GNU.sparse.size') || paxRecords.get('GNU.sparse.realsize') || '';
240
+ const realSize = realSizeText === '' ? size : parsePaxInt(realSizeText, 'GNU sparse size');
241
+ const check = new SparseMapCheck(realSize);
242
+ if (version === '1.0') {
243
+ await checkGnuSparseMap1x0(body, check);
244
+ } else {
245
+ checkGnuSparseMap0x1(paxRecords, check);
246
+ }
247
+ if (HEADER_ONLY_TYPES.has(typeflag) || !check.valid) {
248
+ throw malformed('invalid sparse map');
249
+ }
250
+ reportedSize = realSize;
251
+ sparse = true;
252
+ }
253
+ }
254
+ this.countEntry();
255
+ if (sparse) {
256
+ body = new SparseEntryBody(this.input, body.unreadBytes, name);
257
+ }
258
+ this.current = body;
259
+ this.pad = padding(length);
260
+ return { name, linkName, mode: header.mode, typeflag, size: reportedSize, sparse, body };
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Go `readOldGNUSparseMap` (reader.go:478-525) and the map check of
266
+ * `handleSparseFile`; returns the real size. The map fills the header's
267
+ * sparse array and extension blocks; Go gives up once its arrays reach
268
+ * 1 MiB (`errSparseTooLong`), whatever `maxHeaderBytes` allows.
269
+ */
270
+ private async readOldGnuSparseMap(
271
+ blockArg: Uint8Array,
272
+ formatArg: TTarFormat,
273
+ addChainArg: (bytesArg: number) => void,
274
+ ): Promise<bigint> {
275
+ if (formatArg !== 'gnu') {
276
+ throw malformed('old GNU sparse entry outside the GNU format');
277
+ }
278
+ const realSize = parseNumeric(blockArg.subarray(483, 495), 'GNU sparse real size');
279
+ const check = new SparseMapCheck(realSize);
280
+ let array = blockArg.subarray(386, 483); // format.go:276
281
+ let totalSize = array.length;
282
+ while (totalSize < MAX_SPECIAL_FILE_SIZE) {
283
+ if (!checkOldGnuSparseArray(array, check)) {
284
+ if (!check.valid) {
285
+ throw malformed('invalid sparse map');
286
+ }
287
+ return realSize;
288
+ }
289
+ addChainArg(BLOCK);
290
+ array = await readExact(this.input, BLOCK, 'tar sparse header'); // a whole block (format.go:173)
291
+ totalSize += array.length;
292
+ }
293
+ throw malformed('the old GNU sparse map is longer than 1 MiB');
294
+ }
295
+
296
+ private countEntry(): void {
297
+ this.entries += 1;
298
+ if (this.entries > this.options.maxEntries) {
299
+ throw limitExceeded('maxEntries', this.options.maxEntries, 'tar entry count');
300
+ }
301
+ }
302
+
303
+ /** Reads the pending padding; false when the input ended inside it (a clean end, as in Go). */
304
+ private async consumePadding(): Promise<boolean> {
305
+ if (this.pad === 0) {
306
+ return true;
307
+ }
308
+ const expected = this.pad;
309
+ this.pad = 0;
310
+ const bytes = await readUpTo(this.input, expected);
311
+ return bytes.length === expected;
312
+ }
313
+
314
+ private finish(): null {
315
+ this.done = true;
316
+ this.current = null;
317
+ return null;
318
+ }
319
+ }
320
+
321
+ /**
322
+ * The stored fragments of a sparse entry. The codec does not expand holes,
323
+ * so reading is refused; `skipRest`, and so `TarReader.next`, skips them.
324
+ */
325
+ class SparseEntryBody extends LimitedReader {
326
+ constructor(
327
+ innerArg: IByteReader,
328
+ lengthArg: number,
329
+ private readonly entryName: string,
330
+ ) {
331
+ super(innerArg, lengthArg, 'tar entry data');
332
+ }
333
+
334
+ public override async read(): Promise<Uint8Array | null> {
335
+ throw sparseDataRefused(this.entryName);
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Reads a whole entry's data into memory, refusing entries larger than
341
+ * `maxBytesArg` before reading them, and sparse entries at all.
342
+ */
343
+ export async function readTarEntryData(
344
+ entryArg: ITarEntry,
345
+ maxBytesArg: number,
346
+ limitArg: keyof ILinuxPackageLimits,
347
+ ): Promise<Uint8Array> {
348
+ if (entryArg.sparse) {
349
+ throw sparseDataRefused(entryArg.name);
350
+ }
351
+ if (entryArg.size > BigInt(maxBytesArg)) {
352
+ throw limitExceeded(limitArg, maxBytesArg, `tar entry ${entryArg.name}`);
353
+ }
354
+ return readExact(entryArg.body, Number(entryArg.size), 'tar entry data');
355
+ }
356
+
357
+ /** Go `block.getFormat`'s magic detection (format.go:187-200); the checksum is checked first. */
358
+ function headerFormat(blockArg: Uint8Array): TTarFormat {
359
+ const magic = decodeLatin1(blockArg.subarray(257, 263));
360
+ const version = decodeLatin1(blockArg.subarray(263, 265));
361
+ const trailer = decodeLatin1(blockArg.subarray(508, 512));
362
+ if (magic === 'ustar\0' && trailer === 'tar\0') {
363
+ return 'star';
364
+ }
365
+ if (magic === 'ustar\0') {
366
+ return 'ustar'; // USTAR or PAX
367
+ }
368
+ if (magic === 'ustar ' && version === ' \0') {
369
+ return 'gnu';
370
+ }
371
+ return 'v7';
372
+ }
373
+
374
+ /**
375
+ * Go `Reader.readHeader` (reader.go:356-468): every numeric field Go parses
376
+ * with its fatal parser fails the archive here too, although only size and
377
+ * mode are kept.
378
+ */
379
+ function parseHeaderBlock(blockArg: Uint8Array): IHeaderBlock {
380
+ verifyChecksum(blockArg);
381
+ const format = headerFormat(blockArg);
382
+ // reader.go:385-389, all formats
383
+ const size = parseNumeric(blockArg.subarray(124, 136), 'size');
384
+ const mode = parseNumeric(blockArg.subarray(100, 108), 'mode');
385
+ parseNumeric(blockArg.subarray(108, 116), 'uid');
386
+ parseNumeric(blockArg.subarray(116, 124), 'gid');
387
+ parseNumeric(blockArg.subarray(136, 148), 'mtime');
388
+ let prefix: Uint8Array = new Uint8Array(0);
389
+ if (format !== 'v7') {
390
+ // reader.go:392-397: above V7 only, so a V7 header may hold anything there
391
+ parseNumeric(blockArg.subarray(329, 337), 'devmajor');
392
+ parseNumeric(blockArg.subarray(337, 345), 'devminor');
393
+ if (format === 'ustar') {
394
+ prefix = cutAtNul(blockArg.subarray(345, 500)); // reader.go:401-404
395
+ } else if (format === 'star') {
396
+ // reader.go:417-421
397
+ prefix = cutAtNul(blockArg.subarray(345, 476));
398
+ parseNumeric(blockArg.subarray(476, 488), 'STAR atime');
399
+ parseNumeric(blockArg.subarray(488, 500), 'STAR ctime');
400
+ } else {
401
+ prefix = gnuLegacyPrefix(blockArg);
402
+ }
403
+ }
404
+ let name = decodeGoUtf8(cutAtNul(blockArg.subarray(0, 100)));
405
+ if (prefix.length > 0) {
406
+ name = `${decodeGoUtf8(prefix)}/${name}`; // reader.go:463-465
407
+ }
408
+ return {
409
+ name,
410
+ linkName: decodeGoUtf8(cutAtNul(blockArg.subarray(157, 257))),
411
+ mode,
412
+ typeflag: String.fromCharCode(blockArg[156]),
413
+ size,
414
+ format,
415
+ };
416
+ }
417
+
418
+ /**
419
+ * The old GNU format has no prefix field, but Go before 1.8 wrote one over
420
+ * atime and ctime. Go parses each of the two only when its first byte is not
421
+ * NUL, with a parser whose failure is not fatal; when either fails it takes
422
+ * bytes [345, 500) up to the first NUL as the prefix if they are ASCII
423
+ * (reader.go:422-461, `isASCII` strconv.go:21-28).
424
+ */
425
+ function gnuLegacyPrefix(blockArg: Uint8Array): Uint8Array {
426
+ const parses = (fieldArg: Uint8Array) => fieldArg[0] === 0 || goNumeric(fieldArg) !== null;
427
+ if (parses(blockArg.subarray(345, 357)) && parses(blockArg.subarray(357, 369))) {
428
+ return new Uint8Array(0);
429
+ }
430
+ const prefix = cutAtNul(blockArg.subarray(345, 500));
431
+ return prefix.every((byteArg) => byteArg < 0x80) ? prefix : new Uint8Array(0);
432
+ }
433
+
434
+ /**
435
+ * Reads one old GNU sparse array (Go `sparseArray`, format.go:304-313):
436
+ * 24-byte entries, each a 12-byte offset and length, then the isExtended
437
+ * byte. As in Go `readOldGNUSparseMap` (reader.go:496-511), the entries up to
438
+ * the first whose offset starts with NUL must parse and go into the map
439
+ * check. Returns isExtended.
440
+ */
441
+ function checkOldGnuSparseArray(arrayArg: Uint8Array, checkArg: SparseMapCheck): boolean {
442
+ const entries = Math.floor(arrayArg.length / 24);
443
+ for (let i = 0; i < entries; i++) {
444
+ const entry = arrayArg.subarray(i * 24, i * 24 + 24);
445
+ if (entry[0] === 0) {
446
+ break;
447
+ }
448
+ const offset = parseNumeric(entry.subarray(0, 12), 'GNU sparse offset');
449
+ const length = parseNumeric(entry.subarray(12, 24), 'GNU sparse length');
450
+ checkArg.add(offset, length);
451
+ }
452
+ return arrayArg[entries * 24] !== 0;
453
+ }
454
+
455
+ /**
456
+ * Go `validateSparseEntries` (common.go:255-278), fed one map entry at a
457
+ * time: no negative offset or length, no int64 overflow, no fragment past the
458
+ * real size, fragments in order without overlap; a negative real size fails
459
+ * even an empty map. Go checks the map once it has parsed all of it
460
+ * (reader.go:206-209), so callers read `valid` after the last entry. Go's
461
+ * `maxSparseFileEntries` (reader.go:643-648) is not ported: the 1 MiB caps
462
+ * keep every map below it.
463
+ */
464
+ class SparseMapCheck {
465
+ private previousEnd = 0n;
466
+ private ok: boolean;
467
+
468
+ constructor(private readonly size: bigint) {
469
+ this.ok = size >= 0n;
470
+ }
471
+
472
+ public get valid(): boolean {
473
+ return this.ok;
474
+ }
475
+
476
+ public add(offsetArg: bigint, lengthArg: bigint): void {
477
+ if (!this.ok) {
478
+ return;
479
+ }
480
+ const end = offsetArg + lengthArg;
481
+ if (offsetArg < 0n || lengthArg < 0n || end > INT64_MAX || end > this.size || this.previousEnd > offsetArg) {
482
+ this.ok = false;
483
+ return;
484
+ }
485
+ this.previousEnd = end;
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Go `readGNUSparsePAXHeaders`' version detection (reader.go:221-235): 0.0
491
+ * and 0.1 keep their map in PAX records, 1.0 at the start of the data; null
492
+ * when the records describe no PAX GNU sparse file.
493
+ */
494
+ function gnuSparseVersion(recordsArg: Map<string, string>): '0.x' | '1.0' | null {
495
+ const major = recordsArg.get('GNU.sparse.major') ?? '';
496
+ const minor = recordsArg.get('GNU.sparse.minor') ?? '';
497
+ if (major === '0' && (minor === '0' || minor === '1')) {
498
+ return '0.x';
499
+ }
500
+ if (major === '1' && minor === '0') {
501
+ return '1.0';
502
+ }
503
+ if (major !== '' || minor !== '') {
504
+ return null; // an unknown version
505
+ }
506
+ return (recordsArg.get('GNU.sparse.map') ?? '') !== '' ? '0.x' : null;
507
+ }
508
+
509
+ /**
510
+ * Go `readGNUSparseMap0x1` (reader.go:607-641): `GNU.sparse.numblocks`
511
+ * entries in `GNU.sparse.map` as comma-separated offset and length pairs
512
+ * (0.0 records arrive here already joined by `parsePax`).
513
+ */
514
+ function checkGnuSparseMap0x1(recordsArg: Map<string, string>, checkArg: SparseMapCheck): void {
515
+ const count = sparseEntryCount(recordsArg.get('GNU.sparse.numblocks') ?? '');
516
+ const map = recordsArg.get('GNU.sparse.map') ?? '';
517
+ const fields = map === '' ? [] : map.split(',');
518
+ if (BigInt(fields.length) !== 2n * count) {
519
+ throw malformed('the sparse map does not hold GNU.sparse.numblocks entries');
520
+ }
521
+ for (let i = 0; i < fields.length; i += 2) {
522
+ const offset = goParseInt(fields[i]);
523
+ const length = goParseInt(fields[i + 1]);
524
+ if (offset === null || length === null) {
525
+ throw malformed('a sparse map entry is not a decimal integer');
526
+ }
527
+ checkArg.add(offset, length);
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Go `readGNUSparseMap1x0` (reader.go:537-603): the map heads the entry's
533
+ * data as newline-terminated decimals, the entry count and then offset and
534
+ * length pairs. Go reads whole blocks until it holds the newlines it needs,
535
+ * at most 1 MiB of them (`errSparseTooLong`); the data after the last of
536
+ * those blocks is the stored fragments.
537
+ */
538
+ async function checkGnuSparseMap1x0(dataArg: LimitedReader, checkArg: SparseMapCheck): Promise<void> {
539
+ let buffer: Uint8Array = new Uint8Array(0);
540
+ let cursor = 0;
541
+ let newlines = 0;
542
+ let totalSize = 0;
543
+ const feedTokens = async (countArg: bigint) => {
544
+ const chunks = [buffer.subarray(cursor)];
545
+ while (BigInt(newlines) < countArg) {
546
+ totalSize += BLOCK;
547
+ if (totalSize > MAX_SPECIAL_FILE_SIZE) {
548
+ throw malformed('the PAX GNU sparse map is longer than 1 MiB');
549
+ }
550
+ const block = await readUpTo(dataArg, BLOCK);
551
+ if (block.length < BLOCK) {
552
+ throw malformed('the PAX GNU sparse map runs past the entry data');
553
+ }
554
+ chunks.push(block);
555
+ for (const byte of block) {
556
+ if (byte === 0x0a) {
557
+ newlines++;
558
+ }
559
+ }
560
+ }
561
+ buffer = concatBytes(chunks);
562
+ cursor = 0;
563
+ };
564
+ const nextToken = (): string => {
565
+ newlines--;
566
+ const end = buffer.indexOf(0x0a, cursor);
567
+ const token = decodeLatin1(buffer.subarray(cursor, end));
568
+ cursor = end + 1;
569
+ return token;
570
+ };
571
+ await feedTokens(1n);
572
+ const count = sparseEntryCount(nextToken());
573
+ await feedTokens(2n * count);
574
+ for (let i = 0n; i < count; i++) {
575
+ const offset = goParseInt(nextToken());
576
+ const length = goParseInt(nextToken());
577
+ if (offset === null || length === null) {
578
+ throw malformed('a sparse map entry is not a decimal integer');
579
+ }
580
+ checkArg.add(offset, length);
581
+ }
582
+ }
583
+
584
+ /** A sparse map's entry count as Go checks it (reader.go:579-582, :611-614). */
585
+ function sparseEntryCount(textArg: string): bigint {
586
+ const count = goParseInt(textArg);
587
+ if (count === null || count < 0n || 2n * count > INT64_MAX) {
588
+ throw malformed('invalid sparse map entry count');
589
+ }
590
+ return count;
591
+ }
592
+
593
+ function verifyChecksum(blockArg: Uint8Array): void {
594
+ const stored = goOctal(blockArg.subarray(148, 156));
595
+ if (stored === null) {
596
+ throw malformed('checksum field is not octal');
597
+ }
598
+ let unsigned = 0;
599
+ let signed = 0;
600
+ for (let i = 0; i < BLOCK; i++) {
601
+ const byte = i >= 148 && i < 156 ? 0x20 : blockArg[i];
602
+ unsigned += byte;
603
+ signed += byte >= 0x80 ? byte - 0x100 : byte;
604
+ }
605
+ if (stored !== BigInt(unsigned) && stored !== BigInt(signed)) {
606
+ throw malformed('header checksum mismatch');
607
+ }
608
+ }
609
+
610
+ /** Go `parser.parseNumeric` (strconv.go:96-135), failing the archive where Go sets its error. */
611
+ function parseNumeric(fieldArg: Uint8Array, whatArg: string): bigint {
612
+ const value = goNumeric(fieldArg);
613
+ if (value === null) {
614
+ throw malformed(`${whatArg} field is not a valid tar number`);
615
+ }
616
+ return value;
617
+ }
618
+
619
+ /**
620
+ * Go `parser.parseNumeric` (strconv.go:96-135): base-256 when the high bit is
621
+ * set, else octal; null where Go sets the parser's error.
622
+ */
623
+ function goNumeric(fieldArg: Uint8Array): bigint | null {
624
+ if (fieldArg.length > 0 && (fieldArg[0] & 0x80) !== 0) {
625
+ const inverse = (fieldArg[0] & 0x40) !== 0 ? 0xff : 0x00;
626
+ let value = 0n;
627
+ for (let i = 0; i < fieldArg.length; i++) {
628
+ let byte = fieldArg[i] ^ inverse;
629
+ if (i === 0) {
630
+ byte &= 0x7f;
631
+ }
632
+ if (value >> 56n > 0n) {
633
+ return null;
634
+ }
635
+ value = (value << 8n) | BigInt(byte);
636
+ }
637
+ if (value >> 63n > 0n) {
638
+ return null;
639
+ }
640
+ return inverse === 0xff ? -value - 1n : value;
641
+ }
642
+ return goOctal(fieldArg);
643
+ }
644
+
645
+ /**
646
+ * Go `parser.parseOctal` (strconv.go:158-174): trims spaces and NULs, cuts at
647
+ * a NUL, parses base 8 into 64 bits; null where Go sets the parser's error.
648
+ */
649
+ function goOctal(fieldArg: Uint8Array): bigint | null {
650
+ let start = 0;
651
+ let end = fieldArg.length;
652
+ while (start < end && (fieldArg[start] === 0x20 || fieldArg[start] === 0x00)) {
653
+ start++;
654
+ }
655
+ while (end > start && (fieldArg[end - 1] === 0x20 || fieldArg[end - 1] === 0x00)) {
656
+ end--;
657
+ }
658
+ const text = decodeLatin1(cutAtNul(fieldArg.subarray(start, end)));
659
+ if (text.length === 0) {
660
+ return 0n;
661
+ }
662
+ if (!/^[0-7]+$/.test(text)) {
663
+ return null;
664
+ }
665
+ const value = BigInt(`0o${text}`);
666
+ return value >> 64n > 0n ? null : value;
667
+ }
668
+
669
+ /**
670
+ * Go `strconv.ParseInt(s, 10, 64)`: an optional sign, decimal digits, int64
671
+ * range; null where Go fails.
672
+ */
673
+ function goParseInt(textArg: string): bigint | null {
674
+ if (!/^[+-]?[0-9]+$/.test(textArg)) {
675
+ return null;
676
+ }
677
+ const digits = textArg.replace(/^[+-]/, '').replace(/^0+/, '');
678
+ if (digits.length > 19) {
679
+ return null;
680
+ }
681
+ const value = BigInt(textArg);
682
+ return value < INT64_MIN || value > INT64_MAX ? null : value;
683
+ }
684
+
685
+ /** A PAX integer record Go parses with `strconv.ParseInt`, failing the header where Go does. */
686
+ function parsePaxInt(textArg: string, keyArg: string): bigint {
687
+ const value = goParseInt(textArg);
688
+ if (value === null) {
689
+ throw malformedPax(`${keyArg} is not a decimal integer`);
690
+ }
691
+ return value;
692
+ }
693
+
694
+ /**
695
+ * Go `mergePAX` (reader.go:261-305) for the records the codec does not keep:
696
+ * a uid, gid, atime, mtime or ctime Go cannot parse fails the header; an
697
+ * empty value keeps the USTAR field, as in Go.
698
+ */
699
+ function checkPaxNumbers(recordsArg: Map<string, string>): void {
700
+ for (const key of ['uid', 'gid']) {
701
+ const value = recordsArg.get(key) ?? '';
702
+ if (value !== '') {
703
+ parsePaxInt(value, key);
704
+ }
705
+ }
706
+ for (const key of ['atime', 'mtime', 'ctime']) {
707
+ const value = recordsArg.get(key) ?? '';
708
+ if (value !== '' && !isGoPaxTime(value)) {
709
+ throw malformedPax(`${key} is not a PAX time`);
710
+ }
711
+ }
712
+ }
713
+
714
+ /**
715
+ * Go `parsePAXTime` (strconv.go:197-231): seconds as `strconv.ParseInt`,
716
+ * then optionally a dot and any number of digits.
717
+ */
718
+ function isGoPaxTime(textArg: string): boolean {
719
+ const dot = textArg.indexOf('.');
720
+ const seconds = dot < 0 ? textArg : textArg.slice(0, dot);
721
+ const fraction = dot < 0 ? '' : textArg.slice(dot + 1);
722
+ return goParseInt(seconds) !== null && /^[0-9]*$/.test(fraction);
723
+ }
724
+
725
+ /**
726
+ * Go `parsePAX` / `parsePAXRecord` (reader.go:307-346, strconv.go:251-287):
727
+ * `"<length> <key>=<value>\n"` records; a later `x` header replaces the
728
+ * earlier records. NUL is refused in `path`, `linkpath`, `uname` and `gname`
729
+ * values and in every key. GNU sparse 0.0 offset and size records must
730
+ * alternate, offset first, without commas; Go joins them into
731
+ * `GNU.sparse.map`, replacing a `GNU.sparse.map` record.
732
+ */
733
+ function parsePax(dataArg: Uint8Array): Map<string, string> {
734
+ const records = new Map<string, string>();
735
+ const sparseMap: string[] = [];
736
+ let offset = 0;
737
+ while (offset < dataArg.length) {
738
+ const rest = dataArg.subarray(offset);
739
+ const space = rest.indexOf(0x20);
740
+ if (space < 0) {
741
+ throw malformedPax('a record has no length');
742
+ }
743
+ const lengthText = decodeLatin1(rest.subarray(0, space));
744
+ if (!/^[+-]?[0-9]+$/.test(lengthText)) {
745
+ throw malformedPax('a record length is not a decimal integer');
746
+ }
747
+ const length = BigInt(lengthText);
748
+ if (length < 5n || length > BigInt(rest.length)) {
749
+ throw malformedPax('a record length is out of range');
750
+ }
751
+ const recordLength = Number(length);
752
+ if (recordLength <= space + 1) {
753
+ throw malformedPax('a record is shorter than its length field');
754
+ }
755
+ if (rest[recordLength - 1] !== 0x0a) {
756
+ throw malformedPax('a record does not end with a newline');
757
+ }
758
+ const record = rest.subarray(space + 1, recordLength - 1);
759
+ const equals = record.indexOf(0x3d);
760
+ if (equals < 0) {
761
+ throw malformedPax('a record has no "="');
762
+ }
763
+ const keyBytes = record.subarray(0, equals);
764
+ const valueBytes = record.subarray(equals + 1);
765
+ if (keyBytes.length === 0 || keyBytes.includes(0)) {
766
+ throw malformedPax('a record key is empty or contains NUL');
767
+ }
768
+ const key = decodeGoUtf8(keyBytes);
769
+ if (['path', 'linkpath', 'uname', 'gname'].includes(key) && valueBytes.includes(0)) {
770
+ throw malformedPax(`the ${key} value contains NUL`);
771
+ }
772
+ const value = decodeGoUtf8(valueBytes);
773
+ if (key === 'GNU.sparse.offset' || key === 'GNU.sparse.numbytes') {
774
+ const expected = sparseMap.length % 2 === 0 ? 'GNU.sparse.offset' : 'GNU.sparse.numbytes';
775
+ if (key !== expected || value.includes(',')) {
776
+ throw malformedPax('GNU sparse 0.0 records are out of order or hold a comma');
777
+ }
778
+ sparseMap.push(value);
779
+ } else {
780
+ records.set(key, value);
781
+ }
782
+ offset += recordLength;
783
+ }
784
+ if (sparseMap.length > 0) {
785
+ records.set('GNU.sparse.map', sparseMap.join(','));
786
+ }
787
+ return records;
788
+ }
789
+
790
+ /**
791
+ * The data size of a PAX header or GNU long name or link. Go reads at most
792
+ * 1 MiB of it (`readSpecialFile`, reader.go:868-876, `ErrFieldTooLong`); the
793
+ * codec refuses a larger one from its declared size, before reading.
794
+ */
795
+ function specialFileSize(headerArg: IHeaderBlock, whatArg: string): number {
796
+ if (headerArg.size < 0n) {
797
+ throw malformed('negative entry size');
798
+ }
799
+ if (headerArg.size > BigInt(MAX_SPECIAL_FILE_SIZE)) {
800
+ throw malformed(`${whatArg} is longer than 1 MiB`);
801
+ }
802
+ return Number(headerArg.size);
803
+ }
804
+
805
+ function padding(sizeArg: number): number {
806
+ return (BLOCK - (sizeArg % BLOCK)) % BLOCK;
807
+ }
808
+
809
+ function isZeroBlock(blockArg: Uint8Array): boolean {
810
+ for (let i = 0; i < blockArg.length; i++) {
811
+ if (blockArg[i] !== 0) {
812
+ return false;
813
+ }
814
+ }
815
+ return true;
816
+ }
817
+
818
+ function cutAtNul(bytesArg: Uint8Array): Uint8Array {
819
+ const nul = bytesArg.indexOf(0);
820
+ return nul < 0 ? bytesArg : bytesArg.subarray(0, nul);
821
+ }
822
+
823
+ function malformed(messageArg: string): LinuxRepoError {
824
+ return new LinuxRepoError('malformed-archive', `tar: ${messageArg}`, { field: 'tar header' });
825
+ }
826
+
827
+ /** Reading a sparse entry's data: Go expands its holes, the codec does not (a known difference from Gitea). */
828
+ function sparseDataRefused(nameArg: string): LinuxRepoError {
829
+ const message = `tar: the data of sparse entry ${nameArg} is not read (holes are not expanded)`;
830
+ return new LinuxRepoError('unsupported-package-format', message, { field: 'tar entry data' });
831
+ }
832
+
833
+ function malformedPax(messageArg: string): LinuxRepoError {
834
+ return new LinuxRepoError('malformed-archive', `tar PAX header: ${messageArg}`, { field: 'tar PAX header' });
835
+ }