@push.rocks/smartlinuxrepo 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.smartconfig.json +65 -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
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { isIndexableArchitecture } from '../shared/architecture.js';
|
|
3
|
+
import { LinuxRepoError } from '../shared/errors.js';
|
|
4
|
+
import type { IDebianPackageRecord, ILinuxIndexFile } from '../shared/types.js';
|
|
5
|
+
import { assertSegmentName, assertString, assertTimestamp, hasControlCharacter } from '../shared/validation.js';
|
|
6
|
+
import type { IOpenPgpSigner } from '../signing/signers.js';
|
|
7
|
+
import { createCleartextSignedMessage, createDetachedArmoredSignature } from '../signing/openpgpframing.js';
|
|
8
|
+
import { DEBIAN_SYMBOL_PATTERN } from './control.js';
|
|
9
|
+
import { compareUtf8, goTrimSpace, utf8ByteString } from './gostring.js';
|
|
10
|
+
|
|
11
|
+
/** The fields the `Packages` stanza writes itself; a control file's own copies are dropped. */
|
|
12
|
+
const INDEX_OWNED_FIELDS = new Set(['filename', 'size', 'md5sum', 'sha1', 'sha256', 'sha512']);
|
|
13
|
+
|
|
14
|
+
const PACKAGES_FILES = ['Packages', 'Packages.gz', 'Packages.xz'] as const;
|
|
15
|
+
|
|
16
|
+
const MEDIA_TYPES: Record<string, string> = {
|
|
17
|
+
Packages: 'text/plain; charset=utf-8',
|
|
18
|
+
'Packages.gz': 'application/gzip',
|
|
19
|
+
'Packages.xz': 'application/x-xz',
|
|
20
|
+
Release: 'text/plain; charset=utf-8',
|
|
21
|
+
'Release.gpg': 'application/pgp-signature',
|
|
22
|
+
InRelease: 'text/plain; charset=utf-8',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const HASH_PATTERNS = {
|
|
26
|
+
md5: /^[0-9a-f]{32}$/,
|
|
27
|
+
sha1: /^[0-9a-f]{40}$/,
|
|
28
|
+
sha256: /^[0-9a-f]{64}$/,
|
|
29
|
+
sha512: /^[0-9a-f]{128}$/,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface IDebianIndexEntry {
|
|
33
|
+
component: string;
|
|
34
|
+
record: IDebianPackageRecord;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Builds the index files of one Debian distribution as Gitea 28.2.0 does
|
|
39
|
+
* (`services/packages/debian/repository.go:147-397`):
|
|
40
|
+
*
|
|
41
|
+
* - per component and architecture with packages:
|
|
42
|
+
* `dists/<dist>/<component>/binary-<arch>/Packages`, `.gz` and `.xz`,
|
|
43
|
+
* each with `by-hash/{MD5Sum,SHA1,SHA256,SHA512}/<hex>` aliases;
|
|
44
|
+
* - `dists/<dist>/Release`, `Release.gpg` (armored detached signature) and
|
|
45
|
+
* `InRelease` (cleartext-signed `Release`), all signed at `timestamp`.
|
|
46
|
+
*
|
|
47
|
+
* An `all` package is listed only under `binary-all`, as in Gitea. Stanzas are
|
|
48
|
+
* sorted by name, version, architecture and file name (bytewise; Gitea uses
|
|
49
|
+
* database order), and the Release file lists follow component, architecture
|
|
50
|
+
* and file order. The output is the complete set of files for the
|
|
51
|
+
* distribution; an empty entry set returns `[]`.
|
|
52
|
+
*/
|
|
53
|
+
export async function buildDebianIndex(input: {
|
|
54
|
+
distribution: string;
|
|
55
|
+
entries: Array<{ component: string; record: IDebianPackageRecord }>;
|
|
56
|
+
origin: string;
|
|
57
|
+
label: string;
|
|
58
|
+
signer: IOpenPgpSigner;
|
|
59
|
+
timestamp: Date;
|
|
60
|
+
}): Promise<ILinuxIndexFile[]> {
|
|
61
|
+
if (input === null || typeof input !== 'object') {
|
|
62
|
+
throw new LinuxRepoError('invalid-input', 'input must be an object', { field: 'input' });
|
|
63
|
+
}
|
|
64
|
+
assertSymbol(input.distribution, 'distribution');
|
|
65
|
+
assertReleaseValue(input.origin, 'origin');
|
|
66
|
+
assertReleaseValue(input.label, 'label');
|
|
67
|
+
assertTimestamp(input.timestamp);
|
|
68
|
+
if (input.signer === null || typeof input.signer !== 'object') {
|
|
69
|
+
throw new LinuxRepoError('invalid-input', 'signer must be an IOpenPgpSigner', { field: 'signer' });
|
|
70
|
+
}
|
|
71
|
+
if (!Array.isArray(input.entries)) {
|
|
72
|
+
throw new LinuxRepoError('invalid-input', 'entries must be an array', { field: 'entries' });
|
|
73
|
+
}
|
|
74
|
+
if (input.entries.length === 0) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const groups = new Map<string, { component: string; architecture: string; entries: IDebianIndexEntry[] }>();
|
|
79
|
+
const poolPaths = new Set<string>();
|
|
80
|
+
for (const entry of input.entries) {
|
|
81
|
+
if (entry === null || typeof entry !== 'object') {
|
|
82
|
+
throw new LinuxRepoError('invalid-input', 'each entry must be an object', { field: 'entries' });
|
|
83
|
+
}
|
|
84
|
+
assertSymbol(entry.component, 'component');
|
|
85
|
+
assertRecord(entry.record);
|
|
86
|
+
const poolPath = `${entry.component}/${entry.record.fileName}`;
|
|
87
|
+
if (poolPaths.has(poolPath)) {
|
|
88
|
+
throw new LinuxRepoError('invalid-input', `duplicate package file pool/${input.distribution}/${poolPath}`, {
|
|
89
|
+
field: 'entries',
|
|
90
|
+
value: entry.record.fileName,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
poolPaths.add(poolPath);
|
|
94
|
+
const groupKey = `${entry.component}\n${entry.record.architecture}`;
|
|
95
|
+
let group = groups.get(groupKey);
|
|
96
|
+
if (group === undefined) {
|
|
97
|
+
group = { component: entry.component, architecture: entry.record.architecture, entries: [] };
|
|
98
|
+
groups.set(groupKey, group);
|
|
99
|
+
}
|
|
100
|
+
group.entries.push(entry);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const sortedGroups = [...groups.values()].sort(
|
|
104
|
+
(a, b) => compareUtf8(a.component, b.component) || compareUtf8(a.architecture, b.architecture),
|
|
105
|
+
);
|
|
106
|
+
const contentFiles: ILinuxIndexFile[] = [];
|
|
107
|
+
const releaseLines: Record<'md5' | 'sha1' | 'sha256' | 'sha512', string[]> = { md5: [], sha1: [], sha256: [], sha512: [] };
|
|
108
|
+
for (const group of sortedGroups) {
|
|
109
|
+
const stanzas = group.entries
|
|
110
|
+
.slice()
|
|
111
|
+
.sort((a, b) => compareRecords(a.record, b.record))
|
|
112
|
+
.map((entry) => renderDebianPackagesStanza({ distribution: input.distribution, component: entry.component, record: entry.record }));
|
|
113
|
+
// one blank line between stanzas (repository.go:197-199)
|
|
114
|
+
const packages = new TextEncoder().encode(stanzas.join('\n'));
|
|
115
|
+
const directory = `dists/${input.distribution}/${group.component}/binary-${group.architecture}`;
|
|
116
|
+
const encoded: Record<(typeof PACKAGES_FILES)[number], Uint8Array> = {
|
|
117
|
+
Packages: packages,
|
|
118
|
+
'Packages.gz': await gzip(packages),
|
|
119
|
+
'Packages.xz': new Uint8Array(await plugins.lzma.xz.compress(packages)),
|
|
120
|
+
};
|
|
121
|
+
for (const fileName of PACKAGES_FILES) {
|
|
122
|
+
const bytes = encoded[fileName];
|
|
123
|
+
const hashes = digestsOf(bytes);
|
|
124
|
+
const releasePath = `${group.component}/binary-${group.architecture}/${fileName}`;
|
|
125
|
+
for (const algorithm of ['md5', 'sha1', 'sha256', 'sha512'] as const) {
|
|
126
|
+
releaseLines[algorithm].push(` ${hashes[algorithm]} ${bytes.length} ${releasePath}`);
|
|
127
|
+
}
|
|
128
|
+
contentFiles.push({
|
|
129
|
+
path: `${directory}/${fileName}`,
|
|
130
|
+
bytes,
|
|
131
|
+
sha256: hashes.sha256,
|
|
132
|
+
mediaType: MEDIA_TYPES[fileName],
|
|
133
|
+
stage: 'content',
|
|
134
|
+
aliases: [
|
|
135
|
+
`${directory}/by-hash/MD5Sum/${hashes.md5}`,
|
|
136
|
+
`${directory}/by-hash/SHA1/${hashes.sha1}`,
|
|
137
|
+
`${directory}/by-hash/SHA256/${hashes.sha256}`,
|
|
138
|
+
`${directory}/by-hash/SHA512/${hashes.sha512}`,
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const components = distinctSorted(sortedGroups.map((group) => group.component));
|
|
145
|
+
const architectures = distinctSorted(sortedGroups.map((group) => group.architecture));
|
|
146
|
+
// repository.go:322-352
|
|
147
|
+
const release =
|
|
148
|
+
`Origin: ${input.origin}\n` +
|
|
149
|
+
`Label: ${input.label}\n` +
|
|
150
|
+
`Suite: ${input.distribution}\n` +
|
|
151
|
+
`Codename: ${input.distribution}\n` +
|
|
152
|
+
`Components: ${components.join(' ')}\n` +
|
|
153
|
+
`Architectures: ${architectures.join(' ')}\n` +
|
|
154
|
+
`Date: ${formatRfc1123Utc(input.timestamp)}\n` +
|
|
155
|
+
'Acquire-By-Hash: yes\n' +
|
|
156
|
+
`MD5Sum:\n${releaseLines.md5.map((line) => `${line}\n`).join('')}` +
|
|
157
|
+
`SHA1:\n${releaseLines.sha1.map((line) => `${line}\n`).join('')}` +
|
|
158
|
+
`SHA256:\n${releaseLines.sha256.map((line) => `${line}\n`).join('')}` +
|
|
159
|
+
`SHA512:\n${releaseLines.sha512.map((line) => `${line}\n`).join('')}`;
|
|
160
|
+
const releaseBytes = new TextEncoder().encode(release);
|
|
161
|
+
const releaseGpg = new TextEncoder().encode(
|
|
162
|
+
await createDetachedArmoredSignature({ data: releaseBytes, signer: input.signer, timestamp: input.timestamp }),
|
|
163
|
+
);
|
|
164
|
+
const inRelease = new TextEncoder().encode(
|
|
165
|
+
await createCleartextSignedMessage({ text: release, signer: input.signer, timestamp: input.timestamp }),
|
|
166
|
+
);
|
|
167
|
+
const pointerFiles: ILinuxIndexFile[] = [
|
|
168
|
+
['Release', releaseBytes],
|
|
169
|
+
['Release.gpg', releaseGpg],
|
|
170
|
+
['InRelease', inRelease],
|
|
171
|
+
].map(([fileName, bytes]) => ({
|
|
172
|
+
path: `dists/${input.distribution}/${fileName as string}`,
|
|
173
|
+
bytes: bytes as Uint8Array,
|
|
174
|
+
sha256: plugins.crypto.createHash('sha256').update(bytes as Uint8Array).digest('hex'),
|
|
175
|
+
mediaType: MEDIA_TYPES[fileName as string],
|
|
176
|
+
stage: 'pointer',
|
|
177
|
+
aliases: [],
|
|
178
|
+
}));
|
|
179
|
+
return [...contentFiles, ...pointerFiles];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* One `Packages` stanza, without the separating blank line: Gitea's trimmed
|
|
184
|
+
* control stanza without the six index-owned fields (`Filename`, `Size`,
|
|
185
|
+
* `MD5sum`, `SHA1`, `SHA256`, `SHA512`, matched case-insensitively, with their
|
|
186
|
+
* continuation lines), then those fields from the record
|
|
187
|
+
* (`repository.go:202-209`). A control file that sets them would point apt at
|
|
188
|
+
* other bytes; Gitea would write them twice. Internal: 1.0.0 exports no
|
|
189
|
+
* per-entry renderer.
|
|
190
|
+
*/
|
|
191
|
+
export function renderDebianPackagesStanza(input: {
|
|
192
|
+
distribution: string;
|
|
193
|
+
component: string;
|
|
194
|
+
record: IDebianPackageRecord;
|
|
195
|
+
}): string {
|
|
196
|
+
assertSymbol(input.distribution, 'distribution');
|
|
197
|
+
assertSymbol(input.component, 'component');
|
|
198
|
+
assertRecord(input.record);
|
|
199
|
+
const kept: string[] = [];
|
|
200
|
+
let dropping = false;
|
|
201
|
+
// Gitea trims the stored stanza again when it writes it (repository.go:202)
|
|
202
|
+
for (const line of goTrimSpaceText(input.record.metadata.control).split('\n')) {
|
|
203
|
+
if (line[0] === ' ' || line[0] === '\t') {
|
|
204
|
+
if (!dropping) {
|
|
205
|
+
kept.push(line);
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const colon = line.indexOf(':');
|
|
210
|
+
const field = colon < 0 ? null : goTrimSpaceText(line.slice(0, colon)).toLowerCase();
|
|
211
|
+
dropping = field !== null && INDEX_OWNED_FIELDS.has(field);
|
|
212
|
+
if (!dropping) {
|
|
213
|
+
kept.push(line);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const control = kept.join('\n');
|
|
217
|
+
if (control === '') {
|
|
218
|
+
throw new LinuxRepoError('invalid-input', 'the record control stanza has no fields besides the index-owned ones', {
|
|
219
|
+
field: 'record.metadata.control',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const { digests } = input.record;
|
|
223
|
+
return (
|
|
224
|
+
`${control}\n` +
|
|
225
|
+
`Filename: pool/${input.distribution}/${input.component}/${input.record.fileName}\n` +
|
|
226
|
+
`Size: ${digests.size}\n` +
|
|
227
|
+
`MD5sum: ${digests.md5}\n` +
|
|
228
|
+
`SHA1: ${digests.sha1}\n` +
|
|
229
|
+
`SHA256: ${digests.sha256}\n` +
|
|
230
|
+
`SHA512: ${digests.sha512}\n`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Refuses a record the index cannot list: wrong format, an architecture that
|
|
236
|
+
* is not a path segment, a file name that is not one, digests that are not
|
|
237
|
+
* hex of the right length, or a control text that is not one stanza (empty,
|
|
238
|
+
* or holding a blank line that would start another stanza).
|
|
239
|
+
*/
|
|
240
|
+
function assertRecord(recordArg: IDebianPackageRecord): void {
|
|
241
|
+
if (recordArg === null || typeof recordArg !== 'object' || recordArg.format !== 'debian') {
|
|
242
|
+
throw new LinuxRepoError('invalid-input', 'record must be a Debian package record', { field: 'record' });
|
|
243
|
+
}
|
|
244
|
+
if (!isIndexableArchitecture(recordArg.architecture)) {
|
|
245
|
+
throw new LinuxRepoError(
|
|
246
|
+
'invalid-input',
|
|
247
|
+
`${String(recordArg.fileName)}: architecture "${String(recordArg.architecture)}" cannot name an index path`,
|
|
248
|
+
{ field: 'record.architecture', value: String(recordArg.fileName) },
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
assertSegmentName(recordArg.fileName, 'record.fileName');
|
|
252
|
+
if (/\s/.test(recordArg.fileName)) {
|
|
253
|
+
throw new LinuxRepoError('invalid-input', 'record.fileName must not contain whitespace', {
|
|
254
|
+
field: 'record.fileName',
|
|
255
|
+
value: recordArg.fileName,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
const digests = recordArg.digests;
|
|
259
|
+
if (digests === null || typeof digests !== 'object' || !Number.isSafeInteger(digests.size) || digests.size < 0) {
|
|
260
|
+
throw new LinuxRepoError('invalid-input', 'record.digests.size must be a non-negative safe integer', {
|
|
261
|
+
field: 'record.digests',
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
for (const algorithm of ['md5', 'sha1', 'sha256', 'sha512'] as const) {
|
|
265
|
+
if (typeof digests[algorithm] !== 'string' || !HASH_PATTERNS[algorithm].test(digests[algorithm])) {
|
|
266
|
+
throw new LinuxRepoError('invalid-input', `record.digests.${algorithm} must be lowercase hex`, {
|
|
267
|
+
field: `record.digests.${algorithm}`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const metadata = recordArg.metadata;
|
|
272
|
+
if (metadata === null || typeof metadata !== 'object') {
|
|
273
|
+
throw new LinuxRepoError('invalid-input', 'record.metadata must be an object', { field: 'record.metadata' });
|
|
274
|
+
}
|
|
275
|
+
assertString(metadata.control, 'record.metadata.control');
|
|
276
|
+
if (goTrimSpaceText(metadata.control).split('\n').some((line) => goTrimSpaceText(line) === '')) {
|
|
277
|
+
throw new LinuxRepoError('invalid-input', 'record.metadata.control must be one stanza without blank lines', {
|
|
278
|
+
field: 'record.metadata.control',
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function assertSymbol(valueArg: unknown, fieldArg: string): asserts valueArg is string {
|
|
284
|
+
assertString(valueArg, fieldArg);
|
|
285
|
+
if (!DEBIAN_SYMBOL_PATTERN.test(valueArg)) {
|
|
286
|
+
throw new LinuxRepoError('invalid-input', `${fieldArg} must match ${DEBIAN_SYMBOL_PATTERN.source}`, {
|
|
287
|
+
field: fieldArg,
|
|
288
|
+
value: valueArg,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** A Release header value: one line, non-empty, no leading or trailing whitespace (canonical cleartext). */
|
|
294
|
+
function assertReleaseValue(valueArg: unknown, fieldArg: string): asserts valueArg is string {
|
|
295
|
+
assertString(valueArg, fieldArg);
|
|
296
|
+
if (valueArg === '' || hasControlCharacter(valueArg) || valueArg !== valueArg.trim()) {
|
|
297
|
+
throw new LinuxRepoError(
|
|
298
|
+
'invalid-input',
|
|
299
|
+
`${fieldArg} must be non-empty, without control characters or surrounding whitespace`,
|
|
300
|
+
{ field: fieldArg },
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Go `strings.TrimSpace` on a Unicode string. */
|
|
306
|
+
function goTrimSpaceText(valueArg: string): string {
|
|
307
|
+
return Buffer.from(goTrimSpace(utf8ByteString(valueArg)), 'latin1').toString('utf8');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function compareRecords(aArg: IDebianPackageRecord, bArg: IDebianPackageRecord): number {
|
|
311
|
+
return (
|
|
312
|
+
compareUtf8(aArg.name, bArg.name) ||
|
|
313
|
+
compareUtf8(aArg.version, bArg.version) ||
|
|
314
|
+
compareUtf8(aArg.architecture, bArg.architecture) ||
|
|
315
|
+
compareUtf8(aArg.fileName, bArg.fileName)
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function distinctSorted(valuesArg: string[]): string[] {
|
|
320
|
+
return [...new Set(valuesArg)].sort(compareUtf8);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function digestsOf(bytesArg: Uint8Array): Record<'md5' | 'sha1' | 'sha256' | 'sha512', string> {
|
|
324
|
+
return {
|
|
325
|
+
md5: plugins.crypto.createHash('md5').update(bytesArg).digest('hex'),
|
|
326
|
+
sha1: plugins.crypto.createHash('sha1').update(bytesArg).digest('hex'),
|
|
327
|
+
sha256: plugins.crypto.createHash('sha256').update(bytesArg).digest('hex'),
|
|
328
|
+
sha512: plugins.crypto.createHash('sha512').update(bytesArg).digest('hex'),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function gzip(bytesArg: Uint8Array): Promise<Uint8Array> {
|
|
333
|
+
return new Promise((resolve, reject) => {
|
|
334
|
+
plugins.zlib.gzip(bytesArg, (error, result) => {
|
|
335
|
+
if (error !== null) {
|
|
336
|
+
reject(error);
|
|
337
|
+
} else {
|
|
338
|
+
resolve(new Uint8Array(result.buffer, result.byteOffset, result.byteLength));
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
345
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
346
|
+
|
|
347
|
+
/** Go `time.Time.UTC().Format(time.RFC1123)`: `Mon, 02 Jan 2006 15:04:05 UTC`. */
|
|
348
|
+
function formatRfc1123Utc(timestampArg: Date): string {
|
|
349
|
+
const pad = (valueArg: number) => String(valueArg).padStart(2, '0');
|
|
350
|
+
return (
|
|
351
|
+
`${WEEKDAYS[timestampArg.getUTCDay()]}, ${pad(timestampArg.getUTCDate())} ${MONTHS[timestampArg.getUTCMonth()]} ` +
|
|
352
|
+
`${timestampArg.getUTCFullYear()} ${pad(timestampArg.getUTCHours())}:${pad(timestampArg.getUTCMinutes())}:` +
|
|
353
|
+
`${pad(timestampArg.getUTCSeconds())} UTC`
|
|
354
|
+
);
|
|
355
|
+
}
|