@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/alpine/apkindexreader.d.ts +87 -0
- package/dist_ts/alpine/apkindexreader.js +193 -0
- package/dist_ts/arch/indexreader.d.ts +35 -0
- package/dist_ts/arch/indexreader.js +45 -0
- package/dist_ts/arch/reader.js +1 -1
- package/dist_ts/debian/indexbuilder.js +8 -23
- package/dist_ts/debian/indexreader.d.ts +107 -0
- package/dist_ts/debian/indexreader.js +274 -0
- package/dist_ts/debian/reader.js +1 -1
- package/dist_ts/debian/rules.d.ts +18 -0
- package/dist_ts/debian/rules.js +81 -2
- package/dist_ts/index.d.ts +14 -4
- package/dist_ts/index.js +9 -3
- package/dist_ts/readers/decompress.d.ts +1 -2
- package/dist_ts/readers/decompress.js +1 -1
- package/dist_ts/readers/document.d.ts +36 -0
- package/dist_ts/readers/document.js +110 -0
- package/dist_ts/rpm/repodata.d.ts +3 -2
- package/dist_ts/rpm/repodata.js +5 -4
- package/dist_ts/rpm/repodatareader.d.ts +173 -0
- package/dist_ts/rpm/repodatareader.js +414 -0
- package/dist_ts/rpm/xmlreader.d.ts +70 -0
- package/dist_ts/rpm/xmlreader.js +576 -0
- package/dist_ts/shared/errors.d.ts +12 -4
- package/dist_ts/shared/errors.js +1 -1
- package/dist_ts/shared/limits.d.ts +8 -1
- package/dist_ts/shared/limits.js +5 -2
- package/dist_ts/shared/types.d.ts +2 -0
- package/dist_ts/signing/verifiers.d.ts +43 -0
- package/dist_ts/signing/verifiers.js +81 -2
- package/package.json +1 -1
- package/readme.md +53 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/alpine/apkindexreader.ts +265 -0
- package/ts/arch/indexreader.ts +66 -0
- package/ts/arch/reader.ts +2 -1
- package/ts/debian/indexbuilder.ts +13 -23
- package/ts/debian/indexreader.ts +372 -0
- package/ts/debian/reader.ts +2 -1
- package/ts/debian/rules.ts +83 -1
- package/ts/index.ts +40 -2
- package/ts/readers/decompress.ts +1 -3
- package/ts/readers/document.ts +136 -0
- package/ts/rpm/repodata.ts +4 -3
- package/ts/rpm/repodatareader.ts +582 -0
- package/ts/rpm/xmlreader.ts +680 -0
- package/ts/shared/errors.ts +13 -4
- package/ts/shared/limits.ts +9 -1
- package/ts/shared/types.ts +3 -0
- package/ts/signing/verifiers.ts +113 -1
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { LinuxRepoError, truncated } from '../shared/errors.js';
|
|
2
|
+
import { resolveLinuxPackageLimits, type ILinuxPackageLimits } from '../shared/limits.js';
|
|
3
|
+
import { BufferReader } from '../readers/bytes.js';
|
|
4
|
+
import {
|
|
5
|
+
assertReaderBytes,
|
|
6
|
+
assertReaderInput,
|
|
7
|
+
checkUnitBytes,
|
|
8
|
+
countUnit,
|
|
9
|
+
lines,
|
|
10
|
+
readIndexDocument,
|
|
11
|
+
} from '../readers/document.js';
|
|
12
|
+
import { readGzipMember } from '../readers/gzipmember.js';
|
|
13
|
+
import { readTarEntryData, TarReader, type ITarEntry } from '../readers/tar.js';
|
|
14
|
+
import { decodeGoUtf8 } from '../readers/text.js';
|
|
15
|
+
|
|
16
|
+
/** One `.SIGN.<type>.<keyName>` entry of an index's signature member. */
|
|
17
|
+
export interface IAlpineIndexSignatureEntry {
|
|
18
|
+
/** The text between `.SIGN.` and the next `.`: `'RSA'` for the `.SIGN.RSA.<keyName>` that Gitea and the codec write. */
|
|
19
|
+
readonly type: string;
|
|
20
|
+
/** The rest of the entry name: the key name apk looks the public key up by. */
|
|
21
|
+
readonly keyName: string;
|
|
22
|
+
/** The entry's data, padding excluded. */
|
|
23
|
+
readonly signature: Uint8Array;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** One regular file of the signed stream's tar. */
|
|
27
|
+
export interface IAlpineIndexMember {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly bytes: Uint8Array;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** One block of the `APKINDEX` member. */
|
|
33
|
+
export interface IAlpineIndexBlock {
|
|
34
|
+
/** The block's lines and the empty line that ends it (a block cut by the end of the text has none). */
|
|
35
|
+
readonly bytes: Uint8Array;
|
|
36
|
+
/** The block's offset in the `APKINDEX` member's bytes. */
|
|
37
|
+
readonly offset: number;
|
|
38
|
+
/** The `P:`, `V:` and `A:` values; `''` when absent. */
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly version: string;
|
|
41
|
+
readonly architecture: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** An `APKINDEX.tar.gz` read into its signature entries, signed bytes, members and blocks. */
|
|
45
|
+
export interface IAlpineIndex {
|
|
46
|
+
/** The `.SIGN.*` entries of the first gzip member; `[]` when the index is unsigned. */
|
|
47
|
+
readonly signatures: readonly IAlpineIndexSignatureEntry[];
|
|
48
|
+
/** The input after the signature member (all of it when unsigned): the bytes the signature covers. */
|
|
49
|
+
readonly signedBytes: Uint8Array;
|
|
50
|
+
/** The regular files of the signed stream's tar in order (`APKINDEX`, `DESCRIPTION`, ...). */
|
|
51
|
+
readonly members: readonly IAlpineIndexMember[];
|
|
52
|
+
/** The blocks of the one `APKINDEX` member, in order, duplicates included. */
|
|
53
|
+
readonly blocks: readonly IAlpineIndexBlock[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The signature part of an index: its `.SIGN.*` entries and where the signed bytes start. */
|
|
57
|
+
export interface IAlpineIndexSignatureSplit {
|
|
58
|
+
signatures: IAlpineIndexSignatureEntry[];
|
|
59
|
+
/** Offset of the signed bytes in the input: the signature member's length, or 0 when unsigned. */
|
|
60
|
+
signedOffset: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const SIGN_PREFIX = '.SIGN.';
|
|
64
|
+
const APKINDEX = 'APKINDEX';
|
|
65
|
+
const COLON = 0x3a;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Splits an `APKINDEX.tar.gz` into its signature entries and the signed bytes,
|
|
69
|
+
* as apk decides it: the first gzip member is read with its exact length
|
|
70
|
+
* (`readGzipMember`, bounded as `readAlpinePackage` bounds the members before
|
|
71
|
+
* the control segment: `maxMetadataBytes` for the member, and
|
|
72
|
+
* `maxDecompressedMetadataBytes` for its tar), and when its tar's first entry
|
|
73
|
+
* is named `.SIGN.*` that member is the signature member: every entry in it
|
|
74
|
+
* must be a regular `.SIGN.<type>.<keyName>` file with a non-empty type and
|
|
75
|
+
* key name (`malformed-signature`). Otherwise the index is unsigned and the
|
|
76
|
+
* signed bytes are the whole input. Nothing after the first member is read
|
|
77
|
+
* here, so the signed bytes are never decoded. Empty input, or a signature
|
|
78
|
+
* member with nothing after it, is `truncated`; apk v3 (ADB) input is
|
|
79
|
+
* `unsupported-package-format`. Internal: shared by `readAlpineIndex` and the
|
|
80
|
+
* Alpine verifier.
|
|
81
|
+
*/
|
|
82
|
+
export async function splitAlpineIndexSignature(
|
|
83
|
+
bytesArg: Uint8Array,
|
|
84
|
+
limitsArg: Readonly<ILinuxPackageLimits>,
|
|
85
|
+
): Promise<IAlpineIndexSignatureSplit> {
|
|
86
|
+
if (bytesArg.length === 0) {
|
|
87
|
+
throw truncated('apk index');
|
|
88
|
+
}
|
|
89
|
+
if (bytesArg.length >= 3 && bytesArg[0] === 0x41 && bytesArg[1] === 0x44 && bytesArg[2] === 0x42) {
|
|
90
|
+
throw new LinuxRepoError('unsupported-package-format', 'apk v3 (ADB) indexes are not supported', {
|
|
91
|
+
field: 'apk index',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const member = await readGzipMember(new BufferReader(bytesArg), {
|
|
95
|
+
maxMemberBytes: limitsArg.maxMetadataBytes,
|
|
96
|
+
memberLimit: 'maxMetadataBytes',
|
|
97
|
+
maxOutputBytes: limitsArg.maxDecompressedMetadataBytes,
|
|
98
|
+
outputLimit: 'maxDecompressedMetadataBytes',
|
|
99
|
+
maxHeaderBytes: limitsArg.maxHeaderBytes,
|
|
100
|
+
});
|
|
101
|
+
if (member === null) {
|
|
102
|
+
throw truncated('apk index');
|
|
103
|
+
}
|
|
104
|
+
const tar = new TarReader(new BufferReader(member.output), {
|
|
105
|
+
maxEntries: limitsArg.maxEntries,
|
|
106
|
+
maxHeaderBytes: limitsArg.maxHeaderBytes,
|
|
107
|
+
});
|
|
108
|
+
// the first entry's name decides, as in apk
|
|
109
|
+
const first = await tar.next();
|
|
110
|
+
if (first === null || !first.name.startsWith(SIGN_PREFIX)) {
|
|
111
|
+
return { signatures: [], signedOffset: 0 };
|
|
112
|
+
}
|
|
113
|
+
const signatures: IAlpineIndexSignatureEntry[] = [];
|
|
114
|
+
for (let entry: ITarEntry | null = first; entry !== null; entry = await tar.next()) {
|
|
115
|
+
const parsed = parseSignatureEntryName(entry);
|
|
116
|
+
signatures.push({
|
|
117
|
+
...parsed,
|
|
118
|
+
signature: await readTarEntryData(entry, limitsArg.maxDecompressedMetadataBytes, 'maxDecompressedMetadataBytes'),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (member.memberBytes === bytesArg.length) {
|
|
122
|
+
throw truncated('apk index signed stream');
|
|
123
|
+
}
|
|
124
|
+
return { signatures, signedOffset: member.memberBytes };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseSignatureEntryName(entryArg: ITarEntry): { type: string; keyName: string } {
|
|
128
|
+
const refuse = (whatArg: string): never => {
|
|
129
|
+
throw new LinuxRepoError('malformed-signature', `the index signature member ${whatArg}`, {
|
|
130
|
+
field: 'apk index signature',
|
|
131
|
+
value: entryArg.name,
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
if (entryArg.typeflag !== '0' || entryArg.sparse) {
|
|
135
|
+
refuse('holds an entry that is not a regular file');
|
|
136
|
+
}
|
|
137
|
+
if (!entryArg.name.startsWith(SIGN_PREFIX)) {
|
|
138
|
+
refuse('holds an entry not named .SIGN.*');
|
|
139
|
+
}
|
|
140
|
+
const rest = entryArg.name.slice(SIGN_PREFIX.length);
|
|
141
|
+
const dot = rest.indexOf('.');
|
|
142
|
+
const type = dot < 0 ? rest : rest.slice(0, dot);
|
|
143
|
+
const keyName = dot < 0 ? '' : rest.slice(dot + 1);
|
|
144
|
+
if (type === '' || keyName === '') {
|
|
145
|
+
refuse('holds a .SIGN.* entry without a type or key name');
|
|
146
|
+
}
|
|
147
|
+
return { type, keyName };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Reads an `APKINDEX.tar.gz` into its signature entries, the signed bytes,
|
|
152
|
+
* the members of the signed stream and the blocks of its `APKINDEX` member.
|
|
153
|
+
*
|
|
154
|
+
* - The signature split is apk's first-entry rule: the first gzip member is
|
|
155
|
+
* the signature member when its tar's first entry is named `.SIGN.*`; it is
|
|
156
|
+
* bounded by `maxMetadataBytes` and `maxDecompressedMetadataBytes`, which
|
|
157
|
+
* an unsigned index's first member must meet too. An unsigned index has
|
|
158
|
+
* `signatures: []`. Verify a signature with `createAlpineVerifier`.
|
|
159
|
+
* - The signed bytes are decoded as a gzip stream (bounded by
|
|
160
|
+
* `maxIndexBytes`) and read as a tar: regular files become `members`, PAX
|
|
161
|
+
* global headers are skipped, a sparse entry is
|
|
162
|
+
* `unsupported-package-format` and any other type `malformed-index`.
|
|
163
|
+
* Exactly one `APKINDEX` member is required: none is `missing-field`
|
|
164
|
+
* (`APKINDEX`), two are `malformed-index`. Tar headers are not returned.
|
|
165
|
+
* - `APKINDEX` lines end in LF, the last one too, and every non-empty line is
|
|
166
|
+
* one byte other than `:`, then `:`, then the value (`malformed-index`). A
|
|
167
|
+
* block ends at an empty line, extra empty lines are skipped, and a block
|
|
168
|
+
* with two `P:`, `V:` or `A:` lines is `malformed-index`.
|
|
169
|
+
*
|
|
170
|
+
* Bounds: `maxEntries` for members and blocks, `maxMetadataBytes` for a
|
|
171
|
+
* member's data and one block, `maxHeaderBytes` for a line. Every returned
|
|
172
|
+
* array is a view into buffers the reader allocated, never the caller's input.
|
|
173
|
+
*/
|
|
174
|
+
export async function readAlpineIndex(inputArg: {
|
|
175
|
+
bytes: Uint8Array;
|
|
176
|
+
limits?: Partial<ILinuxPackageLimits>;
|
|
177
|
+
}): Promise<IAlpineIndex> {
|
|
178
|
+
assertReaderInput(inputArg);
|
|
179
|
+
assertReaderBytes(inputArg.bytes);
|
|
180
|
+
const limits = resolveLinuxPackageLimits(inputArg.limits);
|
|
181
|
+
const split = await splitAlpineIndexSignature(inputArg.bytes, limits);
|
|
182
|
+
const signedBytes = inputArg.bytes.slice(split.signedOffset);
|
|
183
|
+
const decoded = await readIndexDocument(signedBytes, 'gzip', limits);
|
|
184
|
+
|
|
185
|
+
const members: IAlpineIndexMember[] = [];
|
|
186
|
+
const tar = new TarReader(new BufferReader(decoded), { maxEntries: limits.maxEntries, maxHeaderBytes: limits.maxHeaderBytes });
|
|
187
|
+
for (let entry = await tar.next(); entry !== null; entry = await tar.next()) {
|
|
188
|
+
if (entry.sparse) {
|
|
189
|
+
await readTarEntryData(entry, limits.maxMetadataBytes, 'maxMetadataBytes'); // refused: sparse data
|
|
190
|
+
}
|
|
191
|
+
if (entry.typeflag === 'g') {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (entry.typeflag !== '0') {
|
|
195
|
+
throw new LinuxRepoError('malformed-index', `the APKINDEX tar holds an entry of type ${JSON.stringify(entry.typeflag)}`, {
|
|
196
|
+
field: 'APKINDEX tar',
|
|
197
|
+
value: entry.name,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
members.push({ name: entry.name, bytes: await readTarEntryData(entry, limits.maxMetadataBytes, 'maxMetadataBytes') });
|
|
201
|
+
}
|
|
202
|
+
const indexMembers = members.filter((member) => member.name === APKINDEX);
|
|
203
|
+
if (indexMembers.length === 0) {
|
|
204
|
+
throw new LinuxRepoError('missing-field', 'the index has no APKINDEX member', { field: APKINDEX });
|
|
205
|
+
}
|
|
206
|
+
if (indexMembers.length > 1) {
|
|
207
|
+
throw new LinuxRepoError('malformed-index', 'the index holds more than one APKINDEX member', { field: APKINDEX });
|
|
208
|
+
}
|
|
209
|
+
return { signatures: split.signatures, signedBytes, members, blocks: readBlocks(indexMembers[0].bytes, limits) };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
interface IBlockState {
|
|
213
|
+
start: number;
|
|
214
|
+
name: string | null;
|
|
215
|
+
version: string | null;
|
|
216
|
+
architecture: string | null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function readBlocks(textArg: Uint8Array, limitsArg: Readonly<ILinuxPackageLimits>): IAlpineIndexBlock[] {
|
|
220
|
+
const blocks: IAlpineIndexBlock[] = [];
|
|
221
|
+
let block: IBlockState | null = null;
|
|
222
|
+
const finish = (endArg: number) => {
|
|
223
|
+
if (block !== null) {
|
|
224
|
+
const current: IBlockState = block;
|
|
225
|
+
block = null;
|
|
226
|
+
blocks.push({
|
|
227
|
+
bytes: textArg.subarray(current.start, endArg),
|
|
228
|
+
offset: current.start,
|
|
229
|
+
name: current.name ?? '',
|
|
230
|
+
version: current.version ?? '',
|
|
231
|
+
architecture: current.architecture ?? '',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
for (const line of lines(textArg, limitsArg.maxHeaderBytes, 'APKINDEX')) {
|
|
236
|
+
if (line.end === line.start) {
|
|
237
|
+
finish(line.end + 1);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (line.end - line.start < 2 || textArg[line.start] === COLON || textArg[line.start + 1] !== COLON) {
|
|
241
|
+
throw new LinuxRepoError('malformed-index', 'an APKINDEX line is not "<letter>:<value>"', {
|
|
242
|
+
field: APKINDEX,
|
|
243
|
+
value: line.start,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (block === null) {
|
|
247
|
+
countUnit(blocks.length + 1, limitsArg, 'APKINDEX block');
|
|
248
|
+
block = { start: line.start, name: null, version: null, architecture: null };
|
|
249
|
+
}
|
|
250
|
+
checkUnitBytes(line.end + 1 - block.start, limitsArg, 'APKINDEX block');
|
|
251
|
+
const key = textArg[line.start];
|
|
252
|
+
const slot = key === 0x50 ? 'name' : key === 0x56 ? 'version' : key === 0x41 ? 'architecture' : null;
|
|
253
|
+
if (slot !== null) {
|
|
254
|
+
if (block[slot] !== null) {
|
|
255
|
+
throw new LinuxRepoError('malformed-index', `an APKINDEX block holds two ${String.fromCharCode(key)}: lines`, {
|
|
256
|
+
field: APKINDEX,
|
|
257
|
+
value: line.start,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
block[slot] = decodeGoUtf8(textArg.subarray(line.start + 2, line.end));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
finish(textArg.length);
|
|
264
|
+
return blocks;
|
|
265
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { LinuxRepoError } from '../shared/errors.js';
|
|
2
|
+
import { resolveLinuxPackageLimits, type ILinuxPackageLimits } from '../shared/limits.js';
|
|
3
|
+
import { BufferReader } from '../readers/bytes.js';
|
|
4
|
+
import type { TLinuxCompression } from '../shared/types.js';
|
|
5
|
+
import { assertLinuxCompression, assertReaderBytes, assertReaderInput, readIndexDocument } from '../readers/document.js';
|
|
6
|
+
import { readTarEntryData, TarReader } from '../readers/tar.js';
|
|
7
|
+
|
|
8
|
+
/** One regular file of a `packages.db` (`<name>-<version>/desc` or `/files`). */
|
|
9
|
+
export interface IArchIndexMember {
|
|
10
|
+
/** The tar entry name. */
|
|
11
|
+
readonly name: string;
|
|
12
|
+
/** The entry's data, without header or padding (a view into `decoded`). */
|
|
13
|
+
readonly bytes: Uint8Array;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A `packages.db` read into its members. */
|
|
17
|
+
export interface IArchIndex {
|
|
18
|
+
/** The decompressed tar (a copy for `'none'`). */
|
|
19
|
+
readonly decoded: Uint8Array;
|
|
20
|
+
/** Every regular file in archive order, duplicates included. */
|
|
21
|
+
readonly members: readonly IArchIndexMember[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Reads an Arch repository database (`packages.db`, `<repo>.db.tar.gz`,
|
|
26
|
+
* `.files`; the caller names `compression`) into its members. The tar is
|
|
27
|
+
* read with Go `archive/tar` semantics: regular files become `members`,
|
|
28
|
+
* directory entries (pacman's `repo-add` writes them) and PAX global headers
|
|
29
|
+
* are skipped, a sparse entry is `unsupported-package-format`, and any other
|
|
30
|
+
* entry type (links, devices, FIFOs) is `malformed-index`. The member data is
|
|
31
|
+
* returned raw; `desc` and `files` are not parsed. Verify `packages.db.sig`
|
|
32
|
+
* with `createOpenPgpVerifier().verifyDetachedSignature` over the compressed
|
|
33
|
+
* file's bytes.
|
|
34
|
+
*
|
|
35
|
+
* Bounds: the decoded tar by `maxIndexBytes`, entries by `maxEntries`, one
|
|
36
|
+
* member's data by `maxMetadataBytes`, one header chain by `maxHeaderBytes`.
|
|
37
|
+
*/
|
|
38
|
+
export async function readArchIndex(inputArg: {
|
|
39
|
+
bytes: Uint8Array;
|
|
40
|
+
compression: TLinuxCompression;
|
|
41
|
+
limits?: Partial<ILinuxPackageLimits>;
|
|
42
|
+
}): Promise<IArchIndex> {
|
|
43
|
+
assertReaderInput(inputArg);
|
|
44
|
+
assertReaderBytes(inputArg.bytes);
|
|
45
|
+
assertLinuxCompression(inputArg.compression);
|
|
46
|
+
const limits = resolveLinuxPackageLimits(inputArg.limits);
|
|
47
|
+
const decoded = await readIndexDocument(inputArg.bytes, inputArg.compression, limits);
|
|
48
|
+
const members: IArchIndexMember[] = [];
|
|
49
|
+
const tar = new TarReader(new BufferReader(decoded), { maxEntries: limits.maxEntries, maxHeaderBytes: limits.maxHeaderBytes });
|
|
50
|
+
for (let entry = await tar.next(); entry !== null; entry = await tar.next()) {
|
|
51
|
+
if (entry.sparse) {
|
|
52
|
+
await readTarEntryData(entry, limits.maxMetadataBytes, 'maxMetadataBytes'); // refused: sparse data
|
|
53
|
+
}
|
|
54
|
+
if (entry.typeflag === '5' || entry.typeflag === 'g') {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (entry.typeflag !== '0') {
|
|
58
|
+
throw new LinuxRepoError('malformed-index', `the Arch database holds an entry of type ${JSON.stringify(entry.typeflag)}`, {
|
|
59
|
+
field: 'Arch database',
|
|
60
|
+
value: entry.name,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
members.push({ name: entry.name, bytes: await readTarEntryData(entry, limits.maxMetadataBytes, 'maxMetadataBytes') });
|
|
64
|
+
}
|
|
65
|
+
return { decoded, members };
|
|
66
|
+
}
|
package/ts/arch/reader.ts
CHANGED
|
@@ -5,7 +5,8 @@ import type { IArchPackageMetadata, IArchPackageRecord } from '../shared/types.j
|
|
|
5
5
|
import { isValidUrl } from '../shared/url.js';
|
|
6
6
|
import { readUpTo, skipToEnd } from '../readers/bytes.js';
|
|
7
7
|
import { PackageByteSource } from '../readers/bytesource.js';
|
|
8
|
-
import { withDecompressingReader
|
|
8
|
+
import { withDecompressingReader } from '../readers/decompress.js';
|
|
9
|
+
import type { TLinuxCompression } from '../shared/types.js';
|
|
9
10
|
import { readTarEntryData, TarReader, type ITarEntry } from '../readers/tar.js';
|
|
10
11
|
import { decodeGoUtf8 } from '../readers/text.js';
|
|
11
12
|
import { goParseInt64, goTrimSpace } from '../shared/gostrings.js';
|
|
@@ -6,10 +6,13 @@ import { assertString, assertTimestamp } from '../shared/validation.js';
|
|
|
6
6
|
import type { IOpenPgpSigner } from '../signing/signers.js';
|
|
7
7
|
import { createCleartextSignedMessage, createDetachedArmoredSignature } from '../signing/openpgpframing.js';
|
|
8
8
|
import { compareUtf8, goTrimSpace, utf8ByteString } from './gostring.js';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
import {
|
|
10
|
+
DEBIAN_SYMBOL_PATTERN,
|
|
11
|
+
hasPoolFileNameRefusedCharacter,
|
|
12
|
+
isDebianReleaseValue,
|
|
13
|
+
isDebianSymbol,
|
|
14
|
+
stripDebianIndexOwnedFields,
|
|
15
|
+
} from './rules.js';
|
|
13
16
|
|
|
14
17
|
const PACKAGES_FILES = ['Packages', 'Packages.gz', 'Packages.xz'] as const;
|
|
15
18
|
|
|
@@ -197,24 +200,11 @@ export function renderDebianPackagesStanza(input: {
|
|
|
197
200
|
assertSymbol(input.distribution, 'distribution');
|
|
198
201
|
assertSymbol(input.component, 'component');
|
|
199
202
|
assertRecord(input.record);
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (!dropping) {
|
|
206
|
-
kept.push(line);
|
|
207
|
-
}
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
const colon = line.indexOf(':');
|
|
211
|
-
const field = colon < 0 ? null : goTrimSpaceText(line.slice(0, colon)).toLowerCase();
|
|
212
|
-
dropping = field !== null && INDEX_OWNED_FIELDS.has(field);
|
|
213
|
-
if (!dropping) {
|
|
214
|
-
kept.push(line);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
const control = kept.join('\n');
|
|
203
|
+
// Gitea trims the stored stanza again when it writes it (repository.go:202);
|
|
204
|
+
// the trimmed text plus its final LF is one stanza, which the index-owned
|
|
205
|
+
// field rule walks (`stripDebianIndexOwnedFields`)
|
|
206
|
+
const stanza = new TextEncoder().encode(`${goTrimSpaceText(input.record.metadata.control)}\n`);
|
|
207
|
+
const control = new TextDecoder().decode(stripDebianIndexOwnedFields(stanza));
|
|
218
208
|
if (control === '') {
|
|
219
209
|
throw new LinuxRepoError('invalid-input', 'the record control stanza has no fields besides the index-owned ones', {
|
|
220
210
|
field: 'record.metadata.control',
|
|
@@ -222,7 +212,7 @@ export function renderDebianPackagesStanza(input: {
|
|
|
222
212
|
}
|
|
223
213
|
const { digests } = input.record;
|
|
224
214
|
return (
|
|
225
|
-
|
|
215
|
+
control +
|
|
226
216
|
`Filename: pool/${input.distribution}/${input.component}/${input.record.fileName}\n` +
|
|
227
217
|
`Size: ${digests.size}\n` +
|
|
228
218
|
`MD5sum: ${digests.md5}\n` +
|