@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.
- package/.smartconfig.json +64 -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
package/ts/rpm/header.ts
ADDED
|
@@ -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
|
+
}
|