@push.rocks/smartlinuxrepo 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/.smartconfig.json +64 -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,628 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { readExact, readUpTo, type IByteReader } from '../readers/bytes.js';
3
+ import { LinuxRepoError, limitExceeded, truncated } from '../shared/errors.js';
4
+ import type { ILinuxPackageLimits } from '../shared/limits.js';
5
+
6
+ /**
7
+ * RPM lead and header structures, read the way go-rpmutils v0.4.0 (Gitea
8
+ * 28.2.0's pin, `go.mod:85`) reads them (`header.go:75-146`,
9
+ * `rpmutils.go:97-136`), so that a package Gitea accepts is accepted and one
10
+ * Gitea refuses (or panics on) is refused:
11
+ * - an index entry of a fixed-size type covers `offset .. offset + size*count`;
12
+ * any other type is a run of `count` NUL-terminated strings; an entry that
13
+ * does not fit the data is `malformed-header`;
14
+ * - a later entry with the same tag replaces an earlier one;
15
+ * - the main header's SHA-256 (signature tag 273), else SHA-1 (269), is
16
+ * checked as lowercase hex (`digest-mismatch`).
17
+ */
18
+
19
+ export const RPM_LEAD_BYTES = 96;
20
+ const LEAD_MAGIC = [0xed, 0xab, 0xee, 0xdb];
21
+ const HEADER_MAGIC = 0x8eade801;
22
+ const HEADER_INTRO_BYTES = 16;
23
+ const INDEX_ENTRY_BYTES = 16;
24
+
25
+ /** Header data types. */
26
+ export const RPM_TYPE = {
27
+ null: 0,
28
+ char: 1,
29
+ int8: 2,
30
+ int16: 3,
31
+ int32: 4,
32
+ int64: 5,
33
+ string: 6,
34
+ bin: 7,
35
+ stringArray: 8,
36
+ i18nString: 9,
37
+ } as const;
38
+
39
+ /** Element sizes of the fixed-size types (go-rpmutils `typeSizes`); every other type is string-like. */
40
+ const TYPE_SIZES = new Map<number, number>([
41
+ [RPM_TYPE.null, 0],
42
+ [RPM_TYPE.char, 1],
43
+ [RPM_TYPE.int8, 1],
44
+ [RPM_TYPE.int16, 2],
45
+ [RPM_TYPE.int32, 4],
46
+ [RPM_TYPE.int64, 8],
47
+ [RPM_TYPE.bin, 1],
48
+ ]);
49
+
50
+ /** Data alignment go-rpmutils applies when it writes a header (`typeAlign`). */
51
+ const TYPE_ALIGNMENT = new Map<number, number>([
52
+ [RPM_TYPE.int16, 2],
53
+ [RPM_TYPE.int32, 4],
54
+ [RPM_TYPE.int64, 8],
55
+ ]);
56
+
57
+ /** Signature header tags. */
58
+ export const RPM_SIGNATURE_TAG = {
59
+ headerSignatures: 62,
60
+ dsa: 267,
61
+ rsa: 268,
62
+ sha1: 269,
63
+ longArchiveSize: 271,
64
+ sha256: 273,
65
+ size: 1000,
66
+ pgp: 1002,
67
+ md5: 1004,
68
+ gpg: 1005,
69
+ payloadSize: 1007,
70
+ reservedSpace: 1008,
71
+ } as const;
72
+
73
+ /** Region tags below this value are dropped when a header is rewritten. */
74
+ const RPM_TAG_HEADER_REGIONS = 64;
75
+
76
+ /** One index entry with the byte range of its contents in the header data. */
77
+ export interface IRpmHeaderEntry {
78
+ tag: number;
79
+ type: number;
80
+ count: number;
81
+ start: number;
82
+ end: number;
83
+ }
84
+
85
+ /** The value of an integer tag as go-rpmutils `getInts` reads it. */
86
+ export interface IRpmIntegers {
87
+ /** Element width in bytes: 1, 2, 4 or 8. */
88
+ width: 1 | 2 | 4 | 8;
89
+ values: bigint[];
90
+ }
91
+
92
+ /**
93
+ * A parsed header: its exact bytes, as read, and its index entries. The index
94
+ * stays in the header bytes; only the end of each entry's contents is kept
95
+ * beside it (4 bytes per entry), and finding the end of a string entry may
96
+ * build a NUL directory of about 3% of the data (`NulIndex`), so a header
97
+ * costs little more than its own bytes: reading a 64 MiB header peaks at
98
+ * about 1.33x its bytes with 4.19M entries and 1.12x with data of all NULs
99
+ * (Node 24.20.0). `IRpmHeaderEntry` objects are made only for the entries
100
+ * asked for.
101
+ */
102
+ export class RpmHeader {
103
+ /** The header bytes as read: intro, index and data (with the padding of a signature header). */
104
+ public readonly bytes: Uint8Array;
105
+ public readonly data: Uint8Array;
106
+ /** Number of index entries. */
107
+ public readonly entryCount: number;
108
+ private readonly view: DataView;
109
+ private readonly ends: Uint32Array;
110
+ private readonly lookup = new Map<number, IRpmHeaderEntry | null>();
111
+ private readonly maxEntries: number;
112
+
113
+ constructor(bytesArg: Uint8Array, endsArg: Uint32Array, dataStartArg: number, maxEntriesArg: number) {
114
+ this.bytes = bytesArg;
115
+ this.data = bytesArg.subarray(dataStartArg);
116
+ this.entryCount = endsArg.length;
117
+ this.view = new DataView(bytesArg.buffer, bytesArg.byteOffset, bytesArg.byteLength);
118
+ this.ends = endsArg;
119
+ this.maxEntries = maxEntriesArg;
120
+ }
121
+
122
+ /** The tag of index entry `indexArg`. */
123
+ public tagAt(indexArg: number): number {
124
+ return this.view.getInt32(HEADER_INTRO_BYTES + indexArg * INDEX_ENTRY_BYTES);
125
+ }
126
+
127
+ /** Index entry `indexArg`. */
128
+ public entryAt(indexArg: number): IRpmHeaderEntry {
129
+ const at = HEADER_INTRO_BYTES + indexArg * INDEX_ENTRY_BYTES;
130
+ return {
131
+ tag: this.view.getInt32(at),
132
+ type: this.view.getInt32(at + 4),
133
+ count: this.view.getInt32(at + 12),
134
+ start: this.view.getInt32(at + 8),
135
+ end: this.ends[indexArg],
136
+ };
137
+ }
138
+
139
+ /** The effective entry for a tag (the last one in the index), or `null`. */
140
+ public entry(tagArg: number): IRpmHeaderEntry | null {
141
+ let found = this.lookup.get(tagArg);
142
+ if (found === undefined) {
143
+ found = null;
144
+ for (let i = this.entryCount - 1; i >= 0; i--) {
145
+ if (this.tagAt(i) === tagArg) {
146
+ found = this.entryAt(i);
147
+ break;
148
+ }
149
+ }
150
+ this.lookup.set(tagArg, found);
151
+ }
152
+ return found;
153
+ }
154
+
155
+ /** The index positions of every effective entry (the last one per tag), in ascending tag order. */
156
+ public effectiveIndexes(): Uint32Array {
157
+ const order = new Uint32Array(this.entryCount);
158
+ for (let i = 0; i < order.length; i++) {
159
+ order[i] = i;
160
+ }
161
+ order.sort((a, b) => this.tagAt(a) - this.tagAt(b) || a - b);
162
+ // within one tag the positions ascend: keep the last of each run
163
+ let kept = 0;
164
+ for (let i = 0; i < order.length; i++) {
165
+ if (i + 1 === order.length || this.tagAt(order[i + 1]) !== this.tagAt(order[i])) {
166
+ order[kept++] = order[i];
167
+ }
168
+ }
169
+ return order.subarray(0, kept);
170
+ }
171
+
172
+ public contents(entryArg: IRpmHeaderEntry): Uint8Array {
173
+ return this.data.subarray(entryArg.start, entryArg.end);
174
+ }
175
+
176
+ /**
177
+ * go-rpmutils `GetStrings` on the raw bytes: `null` when the tag is absent
178
+ * or not a string type. A negative count (a Go slice panic) is
179
+ * `malformed-header`; more values than `maxEntries` is `limit-exceeded`.
180
+ */
181
+ public strings(tagArg: number): Uint8Array[] | null {
182
+ const entry = this.entry(tagArg);
183
+ if (
184
+ entry === null ||
185
+ (entry.type !== RPM_TYPE.string && entry.type !== RPM_TYPE.stringArray && entry.type !== RPM_TYPE.i18nString)
186
+ ) {
187
+ return null;
188
+ }
189
+ if (entry.count < 0) {
190
+ throw new LinuxRepoError('malformed-header', `RPM tag ${tagArg} has a negative count`, { field: `tag ${tagArg}` });
191
+ }
192
+ this.assertCount(entry);
193
+ const values: Uint8Array[] = [];
194
+ const contents = this.contents(entry);
195
+ let offset = 0;
196
+ for (let i = 0; i < entry.count; i++) {
197
+ const nul = contents.indexOf(0, offset);
198
+ values.push(contents.subarray(offset, nul));
199
+ offset = nul + 1;
200
+ }
201
+ return values;
202
+ }
203
+
204
+ /** go-rpmutils `getInts`: `null` when the tag is absent or not an integer type. */
205
+ public integers(tagArg: number): IRpmIntegers | null {
206
+ const entry = this.entry(tagArg);
207
+ if (entry === null) {
208
+ return null;
209
+ }
210
+ let width: 1 | 2 | 4 | 8;
211
+ switch (entry.type) {
212
+ case RPM_TYPE.char:
213
+ case RPM_TYPE.int8:
214
+ width = 1;
215
+ break;
216
+ case RPM_TYPE.int16:
217
+ width = 2;
218
+ break;
219
+ case RPM_TYPE.int32:
220
+ width = 4;
221
+ break;
222
+ case RPM_TYPE.int64:
223
+ width = 8;
224
+ break;
225
+ default:
226
+ return null;
227
+ }
228
+ this.assertCount(entry);
229
+ const contents = this.contents(entry);
230
+ const view = new DataView(contents.buffer, contents.byteOffset, contents.byteLength);
231
+ const values: bigint[] = [];
232
+ for (let offset = 0; offset + width <= contents.length; offset += width) {
233
+ values.push(
234
+ width === 1
235
+ ? BigInt(view.getUint8(offset))
236
+ : width === 2
237
+ ? BigInt(view.getUint16(offset))
238
+ : width === 4
239
+ ? BigInt(view.getUint32(offset))
240
+ : view.getBigUint64(offset),
241
+ );
242
+ }
243
+ return { width, values };
244
+ }
245
+
246
+ /** go-rpmutils `GetUint32s`: `null` when absent, not an integer type, or 64-bit. */
247
+ public uint32s(tagArg: number): number[] | null {
248
+ const integers = this.integers(tagArg);
249
+ if (integers === null || integers.width === 8) {
250
+ return null;
251
+ }
252
+ return integers.values.map(Number);
253
+ }
254
+
255
+ /** go-rpmutils `GetUint64s`: `null` when absent or not an integer type. */
256
+ public uint64s(tagArg: number): bigint[] | null {
257
+ return this.integers(tagArg)?.values ?? null;
258
+ }
259
+
260
+ /** go-rpmutils `GetBytes`: `null` when absent or not binary. */
261
+ public binary(tagArg: number): Uint8Array | null {
262
+ const entry = this.entry(tagArg);
263
+ return entry === null || entry.type !== RPM_TYPE.bin ? null : this.contents(entry);
264
+ }
265
+
266
+ private assertCount(entryArg: IRpmHeaderEntry): void {
267
+ if (entryArg.count > this.maxEntries) {
268
+ throw limitExceeded('maxEntries', this.maxEntries, `RPM tag ${entryArg.tag}`);
269
+ }
270
+ }
271
+ }
272
+
273
+ /** The lead, both headers and their offsets, as read from the start of a package. */
274
+ export interface IRpmPackageHeaders {
275
+ lead: Uint8Array;
276
+ /** Lead bytes 6-7 equal 1 (go-rpmutils `rpmutils.go:131`). */
277
+ isSource: boolean;
278
+ signature: RpmHeader;
279
+ main: RpmHeader;
280
+ /** Offset of the main header: 96 plus the signature header with its padding. */
281
+ mainStart: number;
282
+ }
283
+
284
+ /**
285
+ * Reads the lead, the signature header and the main header from `readerArg`
286
+ * and checks the main header digest. The signature header plus the main
287
+ * header count against `maxMetadataBytes`. The reader is left at the payload.
288
+ */
289
+ export async function readRpmHeaders(
290
+ readerArg: IByteReader,
291
+ limitsArg: Readonly<ILinuxPackageLimits>,
292
+ ): Promise<IRpmPackageHeaders> {
293
+ const lead = await readUpTo(readerArg, RPM_LEAD_BYTES);
294
+ const magicLength = Math.min(lead.length, LEAD_MAGIC.length);
295
+ for (let i = 0; i < magicLength; i++) {
296
+ if (lead[i] !== LEAD_MAGIC[i]) {
297
+ throw new LinuxRepoError('unsupported-package-format', 'the input is not an RPM package (bad lead magic)', {
298
+ field: 'lead',
299
+ });
300
+ }
301
+ }
302
+ if (lead.length < RPM_LEAD_BYTES) {
303
+ throw truncated('RPM lead');
304
+ }
305
+ const isSource = ((lead[6] << 8) | lead[7]) === 1;
306
+ const signature = await readHeader(readerArg, limitsArg, 0, true, 'RPM signature header');
307
+ const main = await readHeader(readerArg, limitsArg, signature.bytes.length, false, 'RPM main header');
308
+ checkMainHeaderDigest(signature, main);
309
+ const mainStart = RPM_LEAD_BYTES + signature.bytes.length;
310
+ return { lead, isSource, signature, main, mainStart };
311
+ }
312
+
313
+ async function readHeader(
314
+ readerArg: IByteReader,
315
+ limitsArg: Readonly<ILinuxPackageLimits>,
316
+ alreadyReadArg: number,
317
+ isSignatureArg: boolean,
318
+ whatArg: string,
319
+ ): Promise<RpmHeader> {
320
+ const intro = await readExact(readerArg, HEADER_INTRO_BYTES, whatArg);
321
+ const introView = new DataView(intro.buffer, intro.byteOffset, intro.byteLength);
322
+ if (introView.getUint32(0) !== HEADER_MAGIC) {
323
+ throw new LinuxRepoError('malformed-header', `bad magic for the ${whatArg}`, { field: whatArg });
324
+ }
325
+ const indexCount = introView.getUint32(8);
326
+ const dataLength = introView.getUint32(12);
327
+ // go-rpmutils computes both in uint32: a wrapped index size cannot hold
328
+ // `indexCount` entries, and a wrapped padded size reads that many bytes
329
+ if (indexCount * INDEX_ENTRY_BYTES > 0xffffffff) {
330
+ throw new LinuxRepoError('malformed-header', `the ${whatArg} index is too large`, { field: whatArg });
331
+ }
332
+ const readLength = isSignatureArg ? ((dataLength + 7) & 0xfffffff8) >>> 0 : dataLength;
333
+ const total = HEADER_INTRO_BYTES + indexCount * INDEX_ENTRY_BYTES + readLength;
334
+ if (alreadyReadArg + total > limitsArg.maxMetadataBytes) {
335
+ throw limitExceeded('maxMetadataBytes', limitsArg.maxMetadataBytes, 'the RPM signature and main headers');
336
+ }
337
+ // one buffer for the whole header, filled in place
338
+ const bytes = new Uint8Array(total);
339
+ bytes.set(intro, 0);
340
+ for (let filled = HEADER_INTRO_BYTES; filled < total; ) {
341
+ const chunk = await readerArg.read(total - filled);
342
+ if (chunk === null) {
343
+ throw truncated(whatArg);
344
+ }
345
+ bytes.set(chunk, filled);
346
+ filled += chunk.length;
347
+ }
348
+ const dataStart = HEADER_INTRO_BYTES + indexCount * INDEX_ENTRY_BYTES;
349
+ return new RpmHeader(bytes, parseIndex(bytes, indexCount, dataStart, whatArg), dataStart, limitsArg.maxEntries);
350
+ }
351
+
352
+ /** Checks every index entry against the data and returns the end of each entry's contents. */
353
+ function parseIndex(bytesArg: Uint8Array, indexCountArg: number, dataStartArg: number, whatArg: string): Uint32Array {
354
+ const view = new DataView(bytesArg.buffer, bytesArg.byteOffset, bytesArg.byteLength);
355
+ const data = bytesArg.subarray(dataStartArg);
356
+ const nuls = new NulIndex(data);
357
+ // an end lies inside the data, whose length is a uint32
358
+ const ends = new Uint32Array(indexCountArg);
359
+ for (let i = 0; i < indexCountArg; i++) {
360
+ const at = HEADER_INTRO_BYTES + i * INDEX_ENTRY_BYTES;
361
+ const tag = view.getInt32(at);
362
+ const type = view.getInt32(at + 4);
363
+ const offset = view.getInt32(at + 8);
364
+ const count = view.getInt32(at + 12);
365
+ const size = TYPE_SIZES.get(type);
366
+ let end: number;
367
+ if (size !== undefined) {
368
+ end = offset + size * count;
369
+ } else if (count > 0) {
370
+ // the `count` strings must each end in a NUL inside the data
371
+ end = offset < 0 || offset > data.length ? -1 : nuls.endOfStrings(offset, count);
372
+ if (end < 0) {
373
+ throw new LinuxRepoError('malformed-header', `${whatArg} tag ${tag} is truncated`, { field: `tag ${tag}` });
374
+ }
375
+ } else {
376
+ end = offset;
377
+ }
378
+ if (offset < 0 || end < offset || end > data.length) {
379
+ throw new LinuxRepoError('malformed-header', `${whatArg} tag ${tag} lies outside the header data`, {
380
+ field: `tag ${tag}`,
381
+ });
382
+ }
383
+ ends[i] = end;
384
+ }
385
+ return ends;
386
+ }
387
+
388
+ const NUL_BLOCK_BYTES = 4096;
389
+ const NUL_SUBBLOCK_BYTES = 64;
390
+ const SUBBLOCKS_PER_BLOCK = NUL_BLOCK_BYTES / NUL_SUBBLOCK_BYTES;
391
+
392
+ interface INulDirectory {
393
+ /** The NULs before each block. */
394
+ blocks: Uint32Array;
395
+ /** The NULs before each sub-block, counted from the start of its block. */
396
+ subblocks: Uint16Array;
397
+ /** The NULs in the data. */
398
+ total: number;
399
+ }
400
+
401
+ /**
402
+ * A rank directory over the NUL bytes of a header's data, built on first use,
403
+ * so that finding the end of `count` strings costs two binary searches and
404
+ * two scans of at most 64 bytes instead of a scan per entry (a crafted index
405
+ * can point many entries at one long run). It holds the NULs before every
406
+ * 4 KiB block (4 bytes each) and, relative to its block, before every 64-byte
407
+ * sub-block (2 bytes each): about 3% of the data, however many NULs it holds.
408
+ */
409
+ class NulIndex {
410
+ private directory: INulDirectory | null = null;
411
+
412
+ constructor(private readonly data: Uint8Array) {}
413
+
414
+ /** The offset just past the `countArg`-th NUL at or after `offsetArg` (`0 <= offsetArg <= data.length`), or -1. */
415
+ public endOfStrings(offsetArg: number, countArg: number): number {
416
+ if (offsetArg >= this.data.length) {
417
+ return -1;
418
+ }
419
+ if (countArg === 1) {
420
+ // a short string ends within one block; a longer one goes through the
421
+ // directory, so entries pointing into one long run never rescan it
422
+ const window = this.data.subarray(offsetArg, offsetArg + NUL_BLOCK_BYTES + 1);
423
+ const nul = window.indexOf(0);
424
+ if (nul >= 0) {
425
+ return offsetArg + nul + 1;
426
+ }
427
+ if (offsetArg + window.length === this.data.length) {
428
+ return -1;
429
+ }
430
+ }
431
+ const directory = this.build();
432
+ // the NUL wanted is the one with `rank` NULs before it
433
+ const rank = this.rank(directory, offsetArg) + countArg - 1;
434
+ return rank < directory.total ? this.select(directory, rank) + 1 : -1;
435
+ }
436
+
437
+ /** The number of NULs before `offsetArg` (`offsetArg < data.length`). */
438
+ private rank(directoryArg: INulDirectory, offsetArg: number): number {
439
+ const subblock = Math.floor(offsetArg / NUL_SUBBLOCK_BYTES);
440
+ let rank = directoryArg.blocks[Math.floor(offsetArg / NUL_BLOCK_BYTES)] + directoryArg.subblocks[subblock];
441
+ for (let i = subblock * NUL_SUBBLOCK_BYTES; i < offsetArg; i++) {
442
+ if (this.data[i] === 0) {
443
+ rank++;
444
+ }
445
+ }
446
+ return rank;
447
+ }
448
+
449
+ /** The position of the NUL with `rankArg` NULs before it (`rankArg` below the total). */
450
+ private select(directoryArg: INulDirectory, rankArg: number): number {
451
+ const { blocks, subblocks } = directoryArg;
452
+ // the last block with at most `rankArg` NULs before it
453
+ let low = 0;
454
+ let high = blocks.length - 1;
455
+ while (low < high) {
456
+ const middle = (low + high + 1) >>> 1;
457
+ if (blocks[middle] <= rankArg) {
458
+ low = middle;
459
+ } else {
460
+ high = middle - 1;
461
+ }
462
+ }
463
+ let remaining = rankArg - blocks[low];
464
+ // the last sub-block of that block with at most `remaining` NULs before it in the block
465
+ high = Math.min(subblocks.length, (low + 1) * SUBBLOCKS_PER_BLOCK) - 1;
466
+ low *= SUBBLOCKS_PER_BLOCK;
467
+ while (low < high) {
468
+ const middle = (low + high + 1) >>> 1;
469
+ if (subblocks[middle] <= remaining) {
470
+ low = middle;
471
+ } else {
472
+ high = middle - 1;
473
+ }
474
+ }
475
+ remaining -= subblocks[low];
476
+ for (let i = low * NUL_SUBBLOCK_BYTES, end = Math.min(this.data.length, i + NUL_SUBBLOCK_BYTES); i < end; i++) {
477
+ if (this.data[i] === 0 && remaining-- === 0) {
478
+ return i;
479
+ }
480
+ }
481
+ // unreachable while the directory matches the data: a codec fault, not a verdict on the package
482
+ throw new Error('the RPM header NUL directory does not match the header data');
483
+ }
484
+
485
+ private build(): INulDirectory {
486
+ if (this.directory === null) {
487
+ const data = this.data;
488
+ const blocks = new Uint32Array(Math.ceil(data.length / NUL_BLOCK_BYTES));
489
+ const subblocks = new Uint16Array(Math.ceil(data.length / NUL_SUBBLOCK_BYTES));
490
+ let total = 0;
491
+ let inBlock = 0;
492
+ for (let i = 0; i < data.length; i++) {
493
+ if (i % NUL_SUBBLOCK_BYTES === 0) {
494
+ if (i % NUL_BLOCK_BYTES === 0) {
495
+ blocks[i / NUL_BLOCK_BYTES] = total;
496
+ inBlock = 0;
497
+ }
498
+ subblocks[i / NUL_SUBBLOCK_BYTES] = inBlock;
499
+ }
500
+ if (data[i] === 0) {
501
+ total++;
502
+ inBlock++;
503
+ }
504
+ }
505
+ this.directory = { blocks, subblocks, total };
506
+ }
507
+ return this.directory;
508
+ }
509
+ }
510
+
511
+ /**
512
+ * go-rpmutils `getHashAndType` + the check in `readHeader`: the SHA-256 tag
513
+ * when it is a non-empty string, else the SHA-1 tag; a value of one character
514
+ * or less disables the check.
515
+ */
516
+ function checkMainHeaderDigest(signatureArg: RpmHeader, mainArg: RpmHeader): void {
517
+ let expected = firstString(signatureArg, RPM_SIGNATURE_TAG.sha256);
518
+ let algorithm = 'sha256';
519
+ if (expected === null || expected.length === 0) {
520
+ expected = firstString(signatureArg, RPM_SIGNATURE_TAG.sha1);
521
+ algorithm = 'sha1';
522
+ }
523
+ if (expected === null || expected.length <= 1) {
524
+ return;
525
+ }
526
+ const actual = new TextEncoder().encode(plugins.crypto.createHash(algorithm).update(mainArg.bytes).digest('hex'));
527
+ if (actual.length !== expected.length || actual.some((byte, i) => byte !== expected[i])) {
528
+ throw new LinuxRepoError('digest-mismatch', `the RPM main header does not match its ${algorithm.toUpperCase()} digest`, {
529
+ field: algorithm === 'sha256' ? 'tag 273' : 'tag 269',
530
+ });
531
+ }
532
+ }
533
+
534
+ /** go-rpmutils `getSha256`/`getSha1`: `vals[0]` panics on an empty array. */
535
+ function firstString(headerArg: RpmHeader, tagArg: number): Uint8Array | null {
536
+ const values = headerArg.strings(tagArg);
537
+ if (values === null) {
538
+ return null;
539
+ }
540
+ if (values.length === 0) {
541
+ throw new LinuxRepoError('malformed-header', `RPM signature tag ${tagArg} holds no value`, { field: `tag ${tagArg}` });
542
+ }
543
+ return values[0];
544
+ }
545
+
546
+ /** An entry written by `writeSignatureHeader` that does not come from the source header. */
547
+ export interface IRpmNewEntry {
548
+ tag: number;
549
+ type: number;
550
+ count: number;
551
+ contents: Uint8Array;
552
+ }
553
+
554
+ /**
555
+ * Serializes a signature header as go-rpmutils `WriteTo(…, 62)` does
556
+ * (`writeheader.go:66-123`): entries below tag 64 are dropped, the rest are
557
+ * written in ascending tag order with INT16/32/64 data aligned, the region
558
+ * tag 62 comes first in the index with its 16-byte trailer as the last data
559
+ * item, and the whole header is padded to 8 bytes.
560
+ *
561
+ * The entries are the effective entries of `sourceArg` whose tags are not in
562
+ * `removedArg`, copied without an object per entry, and `addedArg`, whose
563
+ * tags must be in `removedArg`.
564
+ */
565
+ export function writeSignatureHeader(sourceArg: RpmHeader, removedArg: ReadonlySet<number>, addedArg: IRpmNewEntry[]): Uint8Array {
566
+ const kept = sourceArg.effectiveIndexes();
567
+ let keptCount = 0;
568
+ for (const indexValue of kept) {
569
+ const tag = sourceArg.tagAt(indexValue);
570
+ if (tag >= RPM_TAG_HEADER_REGIONS && !removedArg.has(tag)) {
571
+ kept[keptCount++] = indexValue;
572
+ }
573
+ }
574
+ const added = addedArg.filter((entry) => entry.tag >= RPM_TAG_HEADER_REGIONS).sort((a, b) => a.tag - b.tag);
575
+ /** Calls `visitArg` for every entry in ascending tag order, with the offset of its data. */
576
+ const layout = (visitArg: (entryArg: IRpmHeaderEntry, contentsArg: Uint8Array | null, offsetArg: number) => void): number => {
577
+ let dataLength = 0;
578
+ for (let k = 0, a = 0; k < keptCount || a < added.length; ) {
579
+ let entry: IRpmHeaderEntry;
580
+ let contents: Uint8Array | null = null;
581
+ if (a < added.length && (k === keptCount || added[a].tag < sourceArg.tagAt(kept[k]))) {
582
+ const item = added[a++];
583
+ entry = { tag: item.tag, type: item.type, count: item.count, start: 0, end: item.contents.length };
584
+ contents = item.contents;
585
+ } else {
586
+ entry = sourceArg.entryAt(kept[k++]);
587
+ }
588
+ const alignment = TYPE_ALIGNMENT.get(entry.type);
589
+ if (alignment !== undefined && dataLength % alignment !== 0) {
590
+ dataLength += alignment - (dataLength % alignment);
591
+ }
592
+ visitArg(entry, contents, dataLength);
593
+ dataLength += entry.end - entry.start;
594
+ }
595
+ return dataLength;
596
+ };
597
+ const trailerOffset = layout(() => undefined);
598
+ const dataLength = trailerOffset + INDEX_ENTRY_BYTES;
599
+ const indexCount = keptCount + added.length + 1;
600
+ const unpadded = HEADER_INTRO_BYTES + indexCount * INDEX_ENTRY_BYTES + dataLength;
601
+ const bytes = new Uint8Array(unpadded + ((8 - (dataLength % 8)) % 8));
602
+ const view = new DataView(bytes.buffer);
603
+ view.setUint32(0, HEADER_MAGIC);
604
+ view.setUint32(8, indexCount);
605
+ view.setUint32(12, dataLength);
606
+ const dataStart = HEADER_INTRO_BYTES + indexCount * INDEX_ENTRY_BYTES;
607
+ const writeIndex = (indexArg: number, tagArg: number, typeArg: number, offsetArg: number, countArg: number) => {
608
+ const at = HEADER_INTRO_BYTES + indexArg * INDEX_ENTRY_BYTES;
609
+ view.setInt32(at, tagArg);
610
+ view.setInt32(at + 4, typeArg);
611
+ view.setInt32(at + 8, offsetArg);
612
+ view.setInt32(at + 12, countArg);
613
+ };
614
+ writeIndex(0, RPM_SIGNATURE_TAG.headerSignatures, RPM_TYPE.bin, trailerOffset, INDEX_ENTRY_BYTES);
615
+ let written = 1;
616
+ layout((entryArg, contentsArg, offsetArg) => {
617
+ writeIndex(written++, entryArg.tag, entryArg.type, offsetArg, entryArg.count);
618
+ if (entryArg.end > entryArg.start) {
619
+ bytes.set(contentsArg ?? sourceArg.contents(entryArg), dataStart + offsetArg);
620
+ }
621
+ });
622
+ const trailer = dataStart + trailerOffset;
623
+ view.setInt32(trailer, RPM_SIGNATURE_TAG.headerSignatures);
624
+ view.setInt32(trailer + 4, RPM_TYPE.bin);
625
+ view.setInt32(trailer + 8, -INDEX_ENTRY_BYTES * indexCount);
626
+ view.setInt32(trailer + 12, INDEX_ENTRY_BYTES);
627
+ return bytes;
628
+ }