@push.rocks/smartlinuxrepo 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/alpine/apkindexreader.d.ts +87 -0
  3. package/dist_ts/alpine/apkindexreader.js +193 -0
  4. package/dist_ts/arch/indexreader.d.ts +35 -0
  5. package/dist_ts/arch/indexreader.js +45 -0
  6. package/dist_ts/arch/reader.js +1 -1
  7. package/dist_ts/debian/indexbuilder.js +8 -23
  8. package/dist_ts/debian/indexreader.d.ts +107 -0
  9. package/dist_ts/debian/indexreader.js +274 -0
  10. package/dist_ts/debian/reader.js +1 -1
  11. package/dist_ts/debian/rules.d.ts +18 -0
  12. package/dist_ts/debian/rules.js +81 -2
  13. package/dist_ts/index.d.ts +14 -4
  14. package/dist_ts/index.js +9 -3
  15. package/dist_ts/readers/decompress.d.ts +1 -2
  16. package/dist_ts/readers/decompress.js +1 -1
  17. package/dist_ts/readers/document.d.ts +36 -0
  18. package/dist_ts/readers/document.js +110 -0
  19. package/dist_ts/rpm/repodata.d.ts +3 -2
  20. package/dist_ts/rpm/repodata.js +5 -4
  21. package/dist_ts/rpm/repodatareader.d.ts +173 -0
  22. package/dist_ts/rpm/repodatareader.js +414 -0
  23. package/dist_ts/rpm/xmlreader.d.ts +70 -0
  24. package/dist_ts/rpm/xmlreader.js +576 -0
  25. package/dist_ts/shared/errors.d.ts +12 -4
  26. package/dist_ts/shared/errors.js +1 -1
  27. package/dist_ts/shared/limits.d.ts +8 -1
  28. package/dist_ts/shared/limits.js +5 -2
  29. package/dist_ts/shared/types.d.ts +2 -0
  30. package/dist_ts/signing/verifiers.d.ts +43 -0
  31. package/dist_ts/signing/verifiers.js +81 -2
  32. package/package.json +1 -1
  33. package/readme.md +53 -2
  34. package/ts/00_commitinfo_data.ts +1 -1
  35. package/ts/alpine/apkindexreader.ts +265 -0
  36. package/ts/arch/indexreader.ts +66 -0
  37. package/ts/arch/reader.ts +2 -1
  38. package/ts/debian/indexbuilder.ts +13 -23
  39. package/ts/debian/indexreader.ts +372 -0
  40. package/ts/debian/reader.ts +2 -1
  41. package/ts/debian/rules.ts +83 -1
  42. package/ts/index.ts +40 -2
  43. package/ts/readers/decompress.ts +1 -3
  44. package/ts/readers/document.ts +136 -0
  45. package/ts/rpm/repodata.ts +4 -3
  46. package/ts/rpm/repodatareader.ts +582 -0
  47. package/ts/rpm/xmlreader.ts +680 -0
  48. package/ts/shared/errors.ts +13 -4
  49. package/ts/shared/limits.ts +9 -1
  50. package/ts/shared/types.ts +3 -0
  51. package/ts/signing/verifiers.ts +113 -1
@@ -0,0 +1,372 @@
1
+ import { LinuxRepoError } from '../shared/errors.js';
2
+ import { resolveLinuxPackageLimits, type ILinuxPackageLimits } from '../shared/limits.js';
3
+ import type { TLinuxCompression } from '../shared/types.js';
4
+ import {
5
+ assertLinuxCompression,
6
+ assertReaderBytes,
7
+ assertReaderInput,
8
+ checkUnitBytes,
9
+ countUnit,
10
+ isBlankLine,
11
+ lines,
12
+ readIndexDocument,
13
+ type ILine,
14
+ } from '../readers/document.js';
15
+ import { decodeGoUtf8, decodeLatin1 } from '../readers/text.js';
16
+ import { fromByteString, goTrimSpace } from './gostring.js';
17
+
18
+ /** One stanza of a `Packages` index. */
19
+ export interface IDebianPackagesStanza {
20
+ /** The stanza's lines, first line through the last LF, without a separating empty line (a view into `decoded`). */
21
+ readonly bytes: Uint8Array;
22
+ /** The stanza's offset in `decoded`. */
23
+ readonly offset: number;
24
+ /** The first-line value of the stanza's last `Filename` field: the unit key. */
25
+ readonly filename: string;
26
+ /** The first-line values of the last `Package`, `Version` and `Architecture` fields; `''` when absent. */
27
+ readonly package: string;
28
+ readonly version: string;
29
+ readonly architecture: string;
30
+ }
31
+
32
+ /** A `Packages` index read into its stanzas. */
33
+ export interface IDebianPackagesIndex {
34
+ /** The decompressed document (a copy for `'none'`); every `offset` indexes into it. */
35
+ readonly decoded: Uint8Array;
36
+ /** Every stanza in document order, duplicates included. */
37
+ readonly stanzas: readonly IDebianPackagesStanza[];
38
+ }
39
+
40
+ /** One field of a `Release` file. */
41
+ export interface IDebianReleaseField {
42
+ /** The field name as written. */
43
+ readonly name: string;
44
+ /**
45
+ * The first-line value without surrounding spaces and tabs, then `"\n"`
46
+ * plus each continuation line verbatim.
47
+ */
48
+ readonly value: string;
49
+ /** The field's line(s), each with its LF. */
50
+ readonly bytes: Uint8Array;
51
+ }
52
+
53
+ /** One file of a `Release` checksum list. */
54
+ export interface IDebianReleaseFile {
55
+ readonly path: string;
56
+ readonly size: number;
57
+ /** As written: hex of the algorithm's length. */
58
+ readonly digest: string;
59
+ }
60
+
61
+ /** A `Release` file read into its fields and checksum lists. */
62
+ export interface IDebianRelease {
63
+ /** Every field in order, the checksum fields included. */
64
+ readonly fields: readonly IDebianReleaseField[];
65
+ /** The `MD5Sum`, `SHA1`, `SHA256` and `SHA512` lists; `[]` when the field is absent. */
66
+ readonly files: {
67
+ readonly md5Sum: readonly IDebianReleaseFile[];
68
+ readonly sha1: readonly IDebianReleaseFile[];
69
+ readonly sha256: readonly IDebianReleaseFile[];
70
+ readonly sha512: readonly IDebianReleaseFile[];
71
+ };
72
+ }
73
+
74
+ const SPACE = 0x20;
75
+ const TAB = 0x09;
76
+ const COLON = 0x3a;
77
+
78
+ /** The four checksum fields of a Release, lower-cased, with their list key and hex length. */
79
+ const RELEASE_CHECKSUM_FIELDS: ReadonlyMap<string, { key: keyof IDebianRelease['files']; hexLength: number }> = new Map([
80
+ ['md5sum', { key: 'md5Sum', hexLength: 32 }],
81
+ ['sha1', { key: 'sha1', hexLength: 40 }],
82
+ ['sha256', { key: 'sha256', hexLength: 64 }],
83
+ ['sha512', { key: 'sha512', hexLength: 128 }],
84
+ ] as const);
85
+
86
+ /**
87
+ * Reads a `Packages` index (`dists/<d>/<c>/binary-<a>/Packages`, `.gz`,
88
+ * `.xz`; the caller names `compression` from the file name) into its stanzas.
89
+ *
90
+ * - Lines end in LF, the last one too; a line longer than `maxHeaderBytes`
91
+ * is `limit-exceeded`.
92
+ * - A stanza is a maximal run of non-empty lines; one or more empty lines
93
+ * separate stanzas, and leading or trailing empty lines are accepted.
94
+ * - A line holding only spaces or tabs is `malformed-index` (Gitea and the
95
+ * codec cannot write one; Debian Policy and the strip rule would read it
96
+ * differently). A stanza starting with a continuation line is
97
+ * `malformed-index`. A line without `:` is accepted (Gitea can write one).
98
+ * - Field names are the Go `TrimSpace` of the bytes before the first `:`,
99
+ * matched ASCII case-insensitively; values are the Go `TrimSpace` of the
100
+ * rest of the field's first line, decoded as Go does (U+FFFD for invalid
101
+ * UTF-8), from the field's last occurrence.
102
+ * - A stanza without `Filename` is `missing-field` (`Filename`, `value` the
103
+ * stanza offset).
104
+ *
105
+ * Bounds: the decoded document by `maxIndexBytes`, stanzas by `maxEntries`,
106
+ * one stanza by `maxMetadataBytes`. Every stanza is returned in document
107
+ * order, duplicates included; `bytes` are views into `decoded`.
108
+ */
109
+ export async function readDebianPackagesIndex(inputArg: {
110
+ bytes: Uint8Array;
111
+ compression: TLinuxCompression;
112
+ limits?: Partial<ILinuxPackageLimits>;
113
+ }): Promise<IDebianPackagesIndex> {
114
+ assertReaderInput(inputArg);
115
+ assertReaderBytes(inputArg.bytes);
116
+ assertLinuxCompression(inputArg.compression);
117
+ const limits = resolveLinuxPackageLimits(inputArg.limits);
118
+ const decoded = await readIndexDocument(inputArg.bytes, inputArg.compression, limits);
119
+
120
+ const stanzas: IDebianPackagesStanza[] = [];
121
+ let stanza: IStanzaState | null = null;
122
+ const finish = (endArg: number) => {
123
+ if (stanza === null) {
124
+ return;
125
+ }
126
+ const current: IStanzaState = stanza;
127
+ stanza = null;
128
+ if (current.filename === null) {
129
+ throw new LinuxRepoError('missing-field', 'a Packages stanza has no Filename field', {
130
+ field: 'Filename',
131
+ value: current.start,
132
+ });
133
+ }
134
+ stanzas.push({
135
+ bytes: decoded.subarray(current.start, endArg),
136
+ offset: current.start,
137
+ filename: current.filename,
138
+ package: current.package,
139
+ version: current.version,
140
+ architecture: current.architecture,
141
+ });
142
+ };
143
+ for (const line of lines(decoded, limits.maxHeaderBytes, 'Packages')) {
144
+ if (line.end === line.start) {
145
+ finish(line.start);
146
+ continue;
147
+ }
148
+ if (isBlankLine(decoded, line)) {
149
+ throw new LinuxRepoError('malformed-index', 'a Packages line holds only spaces or tabs', {
150
+ field: 'Packages',
151
+ value: line.start,
152
+ });
153
+ }
154
+ const first = decoded[line.start];
155
+ if (stanza === null) {
156
+ if (first === SPACE || first === TAB) {
157
+ throw new LinuxRepoError('malformed-index', 'a Packages stanza starts with a continuation line', {
158
+ field: 'Packages',
159
+ value: line.start,
160
+ });
161
+ }
162
+ countUnit(stanzas.length + 1, limits, 'Packages stanza');
163
+ stanza = { start: line.start, filename: null, package: '', version: '', architecture: '' };
164
+ }
165
+ checkUnitBytes(line.end + 1 - stanza.start, limits, 'Packages stanza');
166
+ if (first === SPACE || first === TAB) {
167
+ continue;
168
+ }
169
+ readPackagesField(decoded, line, stanza);
170
+ }
171
+ finish(decoded.length);
172
+ return { decoded, stanzas };
173
+ }
174
+
175
+ interface IStanzaState {
176
+ start: number;
177
+ filename: string | null;
178
+ package: string;
179
+ version: string;
180
+ architecture: string;
181
+ }
182
+
183
+ /** Takes the key fields' first-line values; a later occurrence replaces an earlier one. */
184
+ function readPackagesField(bytesArg: Uint8Array, lineArg: ILine, stanzaArg: IStanzaState): void {
185
+ const line = bytesArg.subarray(lineArg.start, lineArg.end);
186
+ const colon = line.indexOf(COLON);
187
+ if (colon < 0) {
188
+ return;
189
+ }
190
+ const name = asciiLower(goTrimSpace(decodeLatin1(line.subarray(0, colon))));
191
+ if (name !== 'filename' && name !== 'package' && name !== 'version' && name !== 'architecture') {
192
+ return;
193
+ }
194
+ const value = fromByteString(goTrimSpace(decodeLatin1(line.subarray(colon + 1))));
195
+ if (name === 'filename') {
196
+ stanzaArg.filename = value;
197
+ } else if (name === 'package') {
198
+ stanzaArg.package = value;
199
+ } else if (name === 'version') {
200
+ stanzaArg.version = value;
201
+ } else {
202
+ stanzaArg.architecture = value;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Reads a `Release` file (the plain `dists/<d>/Release`) into its fields and
208
+ * checksum lists. For an `InRelease`, verify it with `createOpenPgpVerifier`
209
+ * and pass the verified `signedText` here (plus a final LF when it lacks one).
210
+ *
211
+ * - Lines end in LF, the last one too; a line longer than `maxHeaderBytes`
212
+ * is `limit-exceeded`.
213
+ * - Exactly one stanza: an empty line, a line holding only spaces or tabs, a
214
+ * continuation line before the first field, a field line without `:`, a
215
+ * field name that is empty or holds a space or tab, a second checksum field
216
+ * of one algorithm, or an empty document is `malformed-index`.
217
+ * - `MD5Sum`, `SHA1`, `SHA256` and `SHA512` (ASCII case-insensitive) hold an
218
+ * empty first-line value and continuation lines of leading whitespace, then
219
+ * exactly three tokens separated by one or more spaces or tabs: the digest
220
+ * (hex of the algorithm's length), the size (decimal digits within a safe
221
+ * integer) and the path. Anything else is `invalid-field` (`field` the field
222
+ * name as written, `value` the line's offset).
223
+ *
224
+ * Bounds: the document by `maxIndexBytes`, fields and the files per list by
225
+ * `maxEntries`. Field names and values are decoded as Go does (U+FFFD for
226
+ * invalid UTF-8); `bytes` are views into a copy of the input.
227
+ */
228
+ export async function readDebianRelease(inputArg: {
229
+ bytes: Uint8Array;
230
+ limits?: Partial<ILinuxPackageLimits>;
231
+ }): Promise<IDebianRelease> {
232
+ assertReaderInput(inputArg);
233
+ assertReaderBytes(inputArg.bytes);
234
+ const limits = resolveLinuxPackageLimits(inputArg.limits);
235
+ const decoded = await readIndexDocument(inputArg.bytes, 'none', limits);
236
+
237
+ const malformed = (messageArg: string, offsetArg: number) =>
238
+ new LinuxRepoError('malformed-index', messageArg, { field: 'Release', value: offsetArg });
239
+ const fields: IDebianReleaseField[] = [];
240
+ const files: { md5Sum: IDebianReleaseFile[]; sha1: IDebianReleaseFile[]; sha256: IDebianReleaseFile[]; sha512: IDebianReleaseFile[] } = {
241
+ md5Sum: [],
242
+ sha1: [],
243
+ sha256: [],
244
+ sha512: [],
245
+ };
246
+ const seenChecksumFields = new Set<string>();
247
+ let field: IReleaseFieldState | null = null;
248
+ const finishField = (endArg: number) => {
249
+ if (field !== null) {
250
+ const value = [field.firstValue, ...field.continuations].join('\n');
251
+ fields.push({ name: field.name, value, bytes: decoded.subarray(field.start, endArg) });
252
+ field = null;
253
+ }
254
+ };
255
+ for (const line of lines(decoded, limits.maxHeaderBytes, 'Release')) {
256
+ if (line.end === line.start) {
257
+ throw malformed('a Release file holds an empty line', line.start);
258
+ }
259
+ if (isBlankLine(decoded, line)) {
260
+ throw malformed('a Release line holds only spaces or tabs', line.start);
261
+ }
262
+ const first = decoded[line.start];
263
+ if (first === SPACE || first === TAB) {
264
+ if (field === null) {
265
+ throw malformed('a Release file starts with a continuation line', line.start);
266
+ }
267
+ const current: IReleaseFieldState = field;
268
+ if (current.checksum !== null) {
269
+ const list = files[current.checksum.key];
270
+ countUnit(list.length + 1, limits, `Release ${current.name} file`);
271
+ list.push(readChecksumLine(decoded, line, current.name, current.checksum.hexLength));
272
+ }
273
+ current.continuations.push(decodeGoUtf8(decoded.subarray(line.start, line.end)));
274
+ continue;
275
+ }
276
+ finishField(line.start);
277
+ const colon = decoded.subarray(line.start, line.end).indexOf(COLON);
278
+ if (colon < 0) {
279
+ throw malformed('a Release field line has no ":"', line.start);
280
+ }
281
+ const nameBytes = decoded.subarray(line.start, line.start + colon);
282
+ if (nameBytes.length === 0 || nameBytes.includes(SPACE) || nameBytes.includes(TAB)) {
283
+ throw malformed('a Release field name is empty or holds a space or tab', line.start);
284
+ }
285
+ const name = decodeGoUtf8(nameBytes);
286
+ const lower = asciiLower(decodeLatin1(nameBytes));
287
+ const checksum = RELEASE_CHECKSUM_FIELDS.get(lower) ?? null;
288
+ const firstValue = decodeGoUtf8(trimSpaceTab(decoded.subarray(line.start + colon + 1, line.end)));
289
+ if (checksum !== null) {
290
+ if (seenChecksumFields.has(lower)) {
291
+ throw malformed(`a Release file holds a second ${name} field`, line.start);
292
+ }
293
+ seenChecksumFields.add(lower);
294
+ if (firstValue !== '') {
295
+ throw new LinuxRepoError('invalid-field', `the ${name} field has a value on its first line`, {
296
+ field: name,
297
+ value: line.start,
298
+ });
299
+ }
300
+ }
301
+ countUnit(fields.length + 1, limits, 'Release field');
302
+ field = { name, start: line.start, firstValue, continuations: [], checksum };
303
+ }
304
+ finishField(decoded.length);
305
+ if (fields.length === 0) {
306
+ throw malformed('a Release file holds no field', 0);
307
+ }
308
+ return { fields, files };
309
+ }
310
+
311
+ interface IReleaseFieldState {
312
+ name: string;
313
+ start: number;
314
+ firstValue: string;
315
+ continuations: string[];
316
+ checksum: { key: keyof IDebianRelease['files']; hexLength: number } | null;
317
+ }
318
+
319
+ /** One checksum line: leading whitespace, then digest, size and path separated by spaces or tabs. */
320
+ function readChecksumLine(bytesArg: Uint8Array, lineArg: ILine, fieldArg: string, hexLengthArg: number): IDebianReleaseFile {
321
+ const tokens: Array<[number, number]> = [];
322
+ let i = lineArg.start;
323
+ while (i < lineArg.end) {
324
+ while (i < lineArg.end && (bytesArg[i] === SPACE || bytesArg[i] === TAB)) {
325
+ i += 1;
326
+ }
327
+ if (i === lineArg.end) {
328
+ break;
329
+ }
330
+ const start = i;
331
+ while (i < lineArg.end && bytesArg[i] !== SPACE && bytesArg[i] !== TAB) {
332
+ i += 1;
333
+ }
334
+ tokens.push([start, i]);
335
+ if (tokens.length > 3) {
336
+ break;
337
+ }
338
+ }
339
+ const last = bytesArg[lineArg.end - 1];
340
+ const invalid = (whatArg: string) =>
341
+ new LinuxRepoError('invalid-field', `a ${fieldArg} line ${whatArg}`, { field: fieldArg, value: lineArg.start });
342
+ if (tokens.length !== 3 || last === SPACE || last === TAB) {
343
+ throw invalid('is not "<digest> <size> <path>"');
344
+ }
345
+ const digest = decodeLatin1(bytesArg.subarray(tokens[0][0], tokens[0][1]));
346
+ if (digest.length !== hexLengthArg || !/^[0-9a-fA-F]+$/.test(digest)) {
347
+ throw invalid(`has a digest that is not ${hexLengthArg} hex digits`);
348
+ }
349
+ const sizeText = decodeLatin1(bytesArg.subarray(tokens[1][0], tokens[1][1]));
350
+ const size = /^[0-9]+$/.test(sizeText) ? Number(sizeText) : Number.NaN;
351
+ if (!Number.isSafeInteger(size)) {
352
+ throw invalid('has a size that is not decimal digits within a safe integer');
353
+ }
354
+ return { path: decodeGoUtf8(bytesArg.subarray(tokens[2][0], tokens[2][1])), size, digest };
355
+ }
356
+
357
+ function trimSpaceTab(bytesArg: Uint8Array): Uint8Array {
358
+ let start = 0;
359
+ let end = bytesArg.length;
360
+ while (start < end && (bytesArg[start] === SPACE || bytesArg[start] === TAB)) {
361
+ start += 1;
362
+ }
363
+ while (end > start && (bytesArg[end - 1] === SPACE || bytesArg[end - 1] === TAB)) {
364
+ end -= 1;
365
+ }
366
+ return bytesArg.subarray(start, end);
367
+ }
368
+
369
+ /** ASCII-only lowercasing of a byte string. */
370
+ function asciiLower(valueArg: string): string {
371
+ return valueArg.replace(/[A-Z]+/g, (matchArg) => matchArg.toLowerCase());
372
+ }
@@ -5,7 +5,8 @@ import type { IDebianPackageRecord } from '../shared/types.js';
5
5
  import { ArReader } from '../readers/ar.js';
6
6
  import { skipToEnd } from '../readers/bytes.js';
7
7
  import { PackageByteSource } from '../readers/bytesource.js';
8
- import { withDecompressingReader, type TLinuxCompression } from '../readers/decompress.js';
8
+ import { withDecompressingReader } from '../readers/decompress.js';
9
+ import type { TLinuxCompression } from '../shared/types.js';
9
10
  import { readTarEntryData, TarReader } from '../readers/tar.js';
10
11
  import { parseDebianControlFile, type IDebianControl } from './control.js';
11
12
 
@@ -1,4 +1,6 @@
1
- import { hasControlCharacter } from '../shared/validation.js';
1
+ import { decodeLatin1 } from '../readers/text.js';
2
+ import { assertBytes, hasControlCharacter } from '../shared/validation.js';
3
+ import { goTrimSpace } from './gostring.js';
2
4
 
3
5
  /** Gitea's distribution and component rule (`modules/packages/debian/metadata.go:67`, applied at upload). */
4
6
  export const DEBIAN_SYMBOL_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9.~+_-]*$/;
@@ -42,3 +44,83 @@ const POOL_FILE_NAME_REFUSED_PATTERN = /[/#?\s\p{Cc}]/u;
42
44
  export function hasPoolFileNameRefusedCharacter(textArg: string): boolean {
43
45
  return POOL_FILE_NAME_REFUSED_PATTERN.test(textArg);
44
46
  }
47
+
48
+ /**
49
+ * The fields a `Packages` stanza writes itself (`renderDebianPackagesStanza`;
50
+ * Gitea `services/packages/debian/repository.go:202-209`): `Filename`,
51
+ * `Size`, `MD5sum`, `SHA1`, `SHA256`, `SHA512`.
52
+ */
53
+ const INDEX_OWNED_FIELDS: ReadonlySet<string> = new Set(['filename', 'size', 'md5sum', 'sha1', 'sha256', 'sha512']);
54
+
55
+ const LF = 0x0a;
56
+
57
+ /**
58
+ * Whether the bytes before a line's first `:` name an index-owned field: Go
59
+ * `strings.TrimSpace` of them, matched ASCII case-insensitively. Every other
60
+ * byte is compared as is, so no non-ASCII text matches (for these six names
61
+ * this equals `toLowerCase()` on the decoded name, the renderer's former rule).
62
+ */
63
+ function isIndexOwnedFieldName(nameArg: Uint8Array): boolean {
64
+ const name = goTrimSpace(decodeLatin1(nameArg));
65
+ if (name.length < 4 || name.length > 8) {
66
+ return false;
67
+ }
68
+ let lower = '';
69
+ for (let i = 0; i < name.length; i++) {
70
+ const code = name.charCodeAt(i);
71
+ lower += String.fromCharCode(code >= 0x41 && code <= 0x5a ? code + 0x20 : code);
72
+ }
73
+ return INDEX_OWNED_FIELDS.has(lower);
74
+ }
75
+
76
+ /**
77
+ * The stanza without its index-owned fields: `Filename`, `Size`, `MD5sum`,
78
+ * `SHA1`, `SHA256` and `SHA512`, matched ASCII case-insensitively on the Go
79
+ * `TrimSpace` of the bytes before the line's first `:`, each with its
80
+ * continuation lines. The one line walk `renderDebianPackagesStanza` applies
81
+ * to a record's control text, on bytes:
82
+ * - a line starting with a space or tab follows the dropping state of its field;
83
+ * - otherwise the field is dropped exactly when its name is one of the six;
84
+ * - a line without a `:` is kept and ends dropping.
85
+ *
86
+ * Lines end at LF and keep it; a final line without LF is a line too. The
87
+ * result is a new array, the kept lines in order. Applied to a stanza that
88
+ * `readDebianPackagesIndex` returns, from Gitea or from the codec for the same
89
+ * control file, it yields the same bytes: the trimmed control text without
90
+ * the index-owned fields, plus a final LF. A non-`Uint8Array` is
91
+ * `invalid-input`.
92
+ */
93
+ export function stripDebianIndexOwnedFields(stanzaArg: Uint8Array): Uint8Array {
94
+ assertBytes(stanzaArg, 'stanza');
95
+ const kept: Uint8Array[] = [];
96
+ let total = 0;
97
+ let dropping = false;
98
+ let start = 0;
99
+ while (start < stanzaArg.length) {
100
+ const lineFeed = stanzaArg.indexOf(LF, start);
101
+ const end = lineFeed < 0 ? stanzaArg.length : lineFeed + 1;
102
+ const first = stanzaArg[start];
103
+ if (first === 0x20 || first === 0x09) {
104
+ if (!dropping) {
105
+ kept.push(stanzaArg.subarray(start, end));
106
+ total += end - start;
107
+ }
108
+ } else {
109
+ const lineEnd = lineFeed < 0 ? stanzaArg.length : lineFeed;
110
+ const colon = stanzaArg.subarray(start, lineEnd).indexOf(0x3a);
111
+ dropping = colon >= 0 && isIndexOwnedFieldName(stanzaArg.subarray(start, start + colon));
112
+ if (!dropping) {
113
+ kept.push(stanzaArg.subarray(start, end));
114
+ total += end - start;
115
+ }
116
+ }
117
+ start = end;
118
+ }
119
+ const result = new Uint8Array(total);
120
+ let offset = 0;
121
+ for (const line of kept) {
122
+ result.set(line, offset);
123
+ offset += line.length;
124
+ }
125
+ return result;
126
+ }
package/ts/index.ts CHANGED
@@ -21,6 +21,7 @@ export type {
21
21
  TLinuxPackageRecord,
22
22
  TLinuxIndexFileStage,
23
23
  ILinuxIndexFile,
24
+ TLinuxCompression,
24
25
  } from './shared/types.js';
25
26
  export { DEFAULT_LINUX_PACKAGE_LIMITS } from './shared/limits.js';
26
27
  export type { ILinuxPackageLimits } from './shared/limits.js';
@@ -29,8 +30,10 @@ export type { TLinuxRepoErrorCode, ILinuxRepoErrorDetail } from './shared/errors
29
30
  export { isIndexableArchitecture } from './shared/architecture.js';
30
31
  export { createAlpineSigner, createOpenPgpSigner } from './signing/signers.js';
31
32
  export type { IAlpineSigner, IOpenPgpSigner } from './signing/signers.js';
32
- export { createOpenPgpVerifier } from './signing/verifiers.js';
33
+ export { createAlpineVerifier, createOpenPgpVerifier } from './signing/verifiers.js';
33
34
  export type {
35
+ IAlpineSignatureVerification,
36
+ IAlpineVerifier,
34
37
  IOpenPgpCleartextVerification,
35
38
  IOpenPgpSignatureVerification,
36
39
  IOpenPgpVerifier,
@@ -41,15 +44,50 @@ export type { IAlpineKeyPair, IOpenPgpKeyPair } from './signing/keys.js';
41
44
  export { readDebianPackage } from './debian/reader.js';
42
45
  export { buildDebianIndex } from './debian/indexbuilder.js';
43
46
  export type { IDebianIndexEntry } from './debian/indexbuilder.js';
44
- export { isDebianReleaseValue, isDebianSymbol } from './debian/rules.js';
47
+ export { isDebianReleaseValue, isDebianSymbol, stripDebianIndexOwnedFields } from './debian/rules.js';
48
+ export { readDebianPackagesIndex, readDebianRelease } from './debian/indexreader.js';
49
+ export type {
50
+ IDebianPackagesIndex,
51
+ IDebianPackagesStanza,
52
+ IDebianRelease,
53
+ IDebianReleaseField,
54
+ IDebianReleaseFile,
55
+ } from './debian/indexreader.js';
45
56
  export { readRpmPackage } from './rpm/reader.js';
46
57
  export { signRpmPackage } from './rpm/sign.js';
47
58
  export { buildRpmIndex } from './rpm/builder.js';
48
59
  export { mergeRpmUpdates } from './rpm/updates.js';
49
60
  export { renderRpmRepoConfig } from './rpm/repoconfig.js';
61
+ export { isRpmPrimaryPath } from './rpm/repodata.js';
62
+ export { readRpmRepodata, readRpmRepomd, readRpmUpdateInfo } from './rpm/repodatareader.js';
63
+ export type {
64
+ IRpmRepodata,
65
+ IRpmRepodataFile,
66
+ IRpmRepodataPackage,
67
+ IRpmRepomd,
68
+ IRpmRepomdChecksum,
69
+ IRpmRepomdData,
70
+ IRpmRepomdElement,
71
+ IRpmUpdateInfo,
72
+ IRpmUpdateInfoPackage,
73
+ IRpmUpdateInfoUpdate,
74
+ IRpmXmlAttribute,
75
+ IRpmXmlFrame,
76
+ TRpmRepodataDocument,
77
+ } from './rpm/repodatareader.js';
50
78
  export { readArchPackage } from './arch/reader.js';
51
79
  export { buildArchIndex } from './arch/builder.js';
52
80
  export type { IArchIndexEntry } from './arch/builder.js';
53
81
  export { signArchPackage } from './arch/signer.js';
82
+ export { compareArchVersions } from './arch/vercmp.js';
83
+ export { readArchIndex } from './arch/indexreader.js';
84
+ export type { IArchIndex, IArchIndexMember } from './arch/indexreader.js';
54
85
  export { readAlpinePackage } from './alpine/reader.js';
55
86
  export { buildAlpineIndex } from './alpine/apkindex.js';
87
+ export { readAlpineIndex } from './alpine/apkindexreader.js';
88
+ export type {
89
+ IAlpineIndex,
90
+ IAlpineIndexBlock,
91
+ IAlpineIndexMember,
92
+ IAlpineIndexSignatureEntry,
93
+ } from './alpine/apkindexreader.js';
@@ -1,11 +1,9 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import { LinuxRepoError, limitExceeded, truncated } from '../shared/errors.js';
3
3
  import type { ILinuxPackageLimits } from '../shared/limits.js';
4
+ import type { TLinuxCompression } from '../shared/types.js';
4
5
  import { READ_CHUNK_BYTES, takeFront, type IByteReader } from './bytes.js';
5
6
 
6
- /** The stream compressions the readers decode. */
7
- export type TLinuxCompression = 'none' | 'gzip' | 'xz' | 'zstd';
8
-
9
7
  export interface IDecompressOptions {
10
8
  /** Bound on the decompressed bytes delivered. */
11
9
  maxOutputBytes: number;