@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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const strictUtf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Decodes bytes as Go maps them to text: valid UTF-8 unchanged (a leading BOM
|
|
5
|
+
* is kept), and every byte of an invalid sequence replaced by one U+FFFD, as
|
|
6
|
+
* Go's `utf8.DecodeRune` reports invalid input with width 1. The WHATWG
|
|
7
|
+
* decoder differs for truncated multi-byte sequences (one U+FFFD for the
|
|
8
|
+
* maximal subpart).
|
|
9
|
+
*
|
|
10
|
+
* Gitea 28.2.0 stores package metadata with `encoding/json/v2`, which refuses
|
|
11
|
+
* invalid UTF-8 (`modules/json/jsonv2.go:22-29`,
|
|
12
|
+
* `services/packages/packages.go:166`), so 28.2.0 rejects such a package.
|
|
13
|
+
* Packages stored under older Gitea are still served, so the codec
|
|
14
|
+
* deliberately accepts more than 28.2.0 to keep them importable.
|
|
15
|
+
*/
|
|
16
|
+
export function decodeGoUtf8(bytesArg: Uint8Array): string {
|
|
17
|
+
try {
|
|
18
|
+
return strictUtf8.decode(bytesArg);
|
|
19
|
+
} catch {
|
|
20
|
+
return decodeGoUtf8Slow(bytesArg);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function decodeGoUtf8Slow(bytesArg: Uint8Array): string {
|
|
25
|
+
const parts: string[] = [];
|
|
26
|
+
let i = 0;
|
|
27
|
+
while (i < bytesArg.length) {
|
|
28
|
+
const b0 = bytesArg[i];
|
|
29
|
+
if (b0 < 0x80) {
|
|
30
|
+
parts.push(String.fromCharCode(b0));
|
|
31
|
+
i += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
let need: number;
|
|
35
|
+
let codePoint: number;
|
|
36
|
+
let low = 0x80;
|
|
37
|
+
let high = 0xbf;
|
|
38
|
+
if (b0 >= 0xc2 && b0 <= 0xdf) {
|
|
39
|
+
need = 1;
|
|
40
|
+
codePoint = b0 & 0x1f;
|
|
41
|
+
} else if (b0 >= 0xe0 && b0 <= 0xef) {
|
|
42
|
+
need = 2;
|
|
43
|
+
codePoint = b0 & 0x0f;
|
|
44
|
+
if (b0 === 0xe0) {
|
|
45
|
+
low = 0xa0;
|
|
46
|
+
} else if (b0 === 0xed) {
|
|
47
|
+
high = 0x9f;
|
|
48
|
+
}
|
|
49
|
+
} else if (b0 >= 0xf0 && b0 <= 0xf4) {
|
|
50
|
+
need = 3;
|
|
51
|
+
codePoint = b0 & 0x07;
|
|
52
|
+
if (b0 === 0xf0) {
|
|
53
|
+
low = 0x90;
|
|
54
|
+
} else if (b0 === 0xf4) {
|
|
55
|
+
high = 0x8f;
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
parts.push('\uFFFD');
|
|
59
|
+
i += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
let valid = i + need < bytesArg.length;
|
|
63
|
+
for (let k = 1; valid && k <= need; k++) {
|
|
64
|
+
const b = bytesArg[i + k];
|
|
65
|
+
const min = k === 1 ? low : 0x80;
|
|
66
|
+
const max = k === 1 ? high : 0xbf;
|
|
67
|
+
if (b < min || b > max) {
|
|
68
|
+
valid = false;
|
|
69
|
+
} else {
|
|
70
|
+
codePoint = (codePoint << 6) | (b & 0x3f);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!valid) {
|
|
74
|
+
parts.push('\uFFFD');
|
|
75
|
+
i += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
parts.push(String.fromCodePoint(codePoint));
|
|
79
|
+
i += need + 1;
|
|
80
|
+
}
|
|
81
|
+
return parts.join('');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** ASCII/Latin-1 view of header bytes (magic values, numeric fields). */
|
|
85
|
+
export function decodeLatin1(bytesArg: Uint8Array): string {
|
|
86
|
+
let text = '';
|
|
87
|
+
for (let i = 0; i < bytesArg.length; i++) {
|
|
88
|
+
text += String.fromCharCode(bytesArg[i]);
|
|
89
|
+
}
|
|
90
|
+
return text;
|
|
91
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { isIndexableArchitecture } from '../shared/architecture.js';
|
|
3
|
+
import { LinuxRepoError } from '../shared/errors.js';
|
|
4
|
+
import type { ILinuxIndexFile, IRpmPackageRecord, IRpmUpdate } from '../shared/types.js';
|
|
5
|
+
import { assertTimestamp, isPathSegment } from '../shared/validation.js';
|
|
6
|
+
import { createDetachedArmoredSignature } from '../signing/openpgpframing.js';
|
|
7
|
+
import type { IOpenPgpSigner } from '../signing/signers.js';
|
|
8
|
+
import { renderFilelists, renderOther, renderPrimary, renderRepomd, rpmPackageLocation, type IRepomdData } from './repodata.js';
|
|
9
|
+
import { assertRpmUpdates, combineRpmUpdates, renderUpdateInfo } from './updates.js';
|
|
10
|
+
import { compareCodePoints } from './xml.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Builds the repodata of one RPM repository group, as Gitea 28.2.0's
|
|
14
|
+
* `BuildSpecificRepositoryFiles` does (`services/packages/rpm/repository.go:136-309`):
|
|
15
|
+
* `primary`, `filelists` and `other` (gzip), `updateinfo` only when there are
|
|
16
|
+
* updates, then `repomd.xml` and its armored detached signature
|
|
17
|
+
* `repomd.xml.asc`. Paths are relative to the RPM root: `<group>/repodata/…`,
|
|
18
|
+
* or `repodata/…` for the empty group. `repodataNaming` has no default:
|
|
19
|
+
* `'plain'` is Gitea's `repodata/<type>.xml.gz`, `'content-addressed'` is
|
|
20
|
+
* createrepo_c's default `repodata/<sha256 of the .gz>-<type>.xml.gz`
|
|
21
|
+
* (`--unique-md-filenames`, `src/cmd_parser.c:37`, `src/repomd.c:576-636`).
|
|
22
|
+
*
|
|
23
|
+
* Packages are sorted by name, version, architecture and file name; updates
|
|
24
|
+
* are combined as `combineRpmUpdates` describes. Every `repomd.xml` timestamp
|
|
25
|
+
* is `timestamp` in whole seconds. An empty entry set returns `[]` (Gitea
|
|
26
|
+
* deletes the repodata of a group without packages). An entry whose
|
|
27
|
+
* architecture fails `isIndexableArchitecture`, or whose name or version is not
|
|
28
|
+
* a path segment, is `invalid-input`, as are two entries with the same file.
|
|
29
|
+
*/
|
|
30
|
+
export async function buildRpmIndex(input: {
|
|
31
|
+
group: string;
|
|
32
|
+
entries: IRpmPackageRecord[];
|
|
33
|
+
updates: IRpmUpdate[];
|
|
34
|
+
repodataNaming: 'plain' | 'content-addressed';
|
|
35
|
+
signer: IOpenPgpSigner;
|
|
36
|
+
timestamp: Date;
|
|
37
|
+
}): Promise<ILinuxIndexFile[]> {
|
|
38
|
+
if (input === null || typeof input !== 'object') {
|
|
39
|
+
throw new LinuxRepoError('invalid-input', 'input must be an object', { field: 'input' });
|
|
40
|
+
}
|
|
41
|
+
const prefix = groupPrefix(input.group);
|
|
42
|
+
if (input.repodataNaming !== 'plain' && input.repodataNaming !== 'content-addressed') {
|
|
43
|
+
throw new LinuxRepoError('invalid-input', "repodataNaming must be 'plain' or 'content-addressed'", {
|
|
44
|
+
field: 'repodataNaming',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (
|
|
48
|
+
input.signer === null ||
|
|
49
|
+
typeof input.signer !== 'object' ||
|
|
50
|
+
typeof input.signer.signBinary !== 'function' ||
|
|
51
|
+
typeof input.signer.signText !== 'function'
|
|
52
|
+
) {
|
|
53
|
+
throw new LinuxRepoError('invalid-input', 'signer must implement IOpenPgpSigner', { field: 'signer' });
|
|
54
|
+
}
|
|
55
|
+
assertTimestamp(input.timestamp);
|
|
56
|
+
assertRpmRecords(input.entries);
|
|
57
|
+
assertRpmUpdates(input.updates, 'updates');
|
|
58
|
+
if (input.entries.length === 0) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const entries = [...input.entries].sort(
|
|
63
|
+
(a, b) =>
|
|
64
|
+
compareCodePoints(a.name, b.name) ||
|
|
65
|
+
compareCodePoints(a.version, b.version) ||
|
|
66
|
+
compareCodePoints(a.architecture, b.architecture) ||
|
|
67
|
+
compareCodePoints(a.fileName, b.fileName),
|
|
68
|
+
);
|
|
69
|
+
const seconds = Math.floor(input.timestamp.getTime() / 1000);
|
|
70
|
+
const documents: Array<[string, string]> = [
|
|
71
|
+
['primary', renderPrimary(entries)],
|
|
72
|
+
['filelists', renderFilelists(entries)],
|
|
73
|
+
['other', renderOther(entries)],
|
|
74
|
+
];
|
|
75
|
+
const updates = combineRpmUpdates(input.updates);
|
|
76
|
+
if (updates.length > 0) {
|
|
77
|
+
documents.push(['updateinfo', renderUpdateInfo(updates)]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const files: ILinuxIndexFile[] = [];
|
|
81
|
+
const data: IRepomdData[] = [];
|
|
82
|
+
for (const [type, xml] of documents) {
|
|
83
|
+
const open = new TextEncoder().encode(xml);
|
|
84
|
+
const gzipped = gzip(open);
|
|
85
|
+
const checksum = sha256(gzipped);
|
|
86
|
+
const name = input.repodataNaming === 'plain' ? `${type}.xml.gz` : `${checksum}-${type}.xml.gz`;
|
|
87
|
+
files.push(indexFile(`${prefix}repodata/${name}`, gzipped, checksum, 'application/gzip', 'content'));
|
|
88
|
+
data.push({
|
|
89
|
+
type,
|
|
90
|
+
checksum,
|
|
91
|
+
openChecksum: sha256(open),
|
|
92
|
+
href: `repodata/${name}`,
|
|
93
|
+
timestamp: seconds,
|
|
94
|
+
size: gzipped.length,
|
|
95
|
+
openSize: open.length,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const repomd = new TextEncoder().encode(renderRepomd(data));
|
|
99
|
+
const signature = new TextEncoder().encode(
|
|
100
|
+
await createDetachedArmoredSignature({ data: repomd, signer: input.signer, timestamp: input.timestamp }),
|
|
101
|
+
);
|
|
102
|
+
files.push(indexFile(`${prefix}repodata/repomd.xml`, repomd, sha256(repomd), 'application/xml', 'pointer'));
|
|
103
|
+
files.push(indexFile(`${prefix}repodata/repomd.xml.asc`, signature, sha256(signature), 'application/pgp-signature', 'pointer'));
|
|
104
|
+
return files;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `''` for the root group, else `<group>/`; a group is `/`-separated path segments. */
|
|
108
|
+
function groupPrefix(groupArg: unknown): string {
|
|
109
|
+
if (typeof groupArg !== 'string') {
|
|
110
|
+
throw new LinuxRepoError('invalid-input', 'group must be a string', { field: 'group' });
|
|
111
|
+
}
|
|
112
|
+
if (groupArg === '') {
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
for (const segment of groupArg.split('/')) {
|
|
116
|
+
if (!isPathSegment(segment)) {
|
|
117
|
+
throw new LinuxRepoError('invalid-input', 'group must be "/"-separated non-empty segments other than "." and ".."', {
|
|
118
|
+
field: 'group',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return `${groupArg}/`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function indexFile(
|
|
126
|
+
pathArg: string,
|
|
127
|
+
bytesArg: Uint8Array,
|
|
128
|
+
sha256Arg: string,
|
|
129
|
+
mediaTypeArg: string,
|
|
130
|
+
stageArg: ILinuxIndexFile['stage'],
|
|
131
|
+
): ILinuxIndexFile {
|
|
132
|
+
return { path: pathArg, bytes: bytesArg, sha256: sha256Arg, mediaType: mediaTypeArg, stage: stageArg, aliases: [] };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function gzip(bytesArg: Uint8Array): Uint8Array {
|
|
136
|
+
const compressed = plugins.zlib.gzipSync(bytesArg);
|
|
137
|
+
return new Uint8Array(compressed.buffer, compressed.byteOffset, compressed.byteLength);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sha256(bytesArg: Uint8Array): string {
|
|
141
|
+
return plugins.crypto.createHash('sha256').update(bytesArg).digest('hex');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Checks the record fields the renderers read and the layout rules: an
|
|
146
|
+
* indexable architecture, a name and version that are path segments, and no
|
|
147
|
+
* two entries with the same package location.
|
|
148
|
+
*/
|
|
149
|
+
function assertRpmRecords(entriesArg: unknown): asserts entriesArg is IRpmPackageRecord[] {
|
|
150
|
+
if (!Array.isArray(entriesArg)) {
|
|
151
|
+
throw new LinuxRepoError('invalid-input', 'entries must be an array of RPM package records', { field: 'entries' });
|
|
152
|
+
}
|
|
153
|
+
const locations = new Set<string>();
|
|
154
|
+
entriesArg.forEach((entry: unknown, i) => {
|
|
155
|
+
const at = `entries[${i}]`;
|
|
156
|
+
const record = entry as IRpmPackageRecord;
|
|
157
|
+
const fail = (whatArg: string): never => {
|
|
158
|
+
throw new LinuxRepoError('invalid-input', `${at} ${whatArg}`, { field: at });
|
|
159
|
+
};
|
|
160
|
+
if (record === null || typeof record !== 'object' || record.format !== 'rpm') {
|
|
161
|
+
fail('must be an RPM package record');
|
|
162
|
+
}
|
|
163
|
+
for (const key of ['name', 'version', 'architecture', 'fileName'] as const) {
|
|
164
|
+
if (typeof record[key] !== 'string') {
|
|
165
|
+
fail(`.${key} must be a string`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!isIndexableArchitecture(record.architecture)) {
|
|
169
|
+
fail(`(${record.fileName}) has an architecture that cannot name an index path: ${JSON.stringify(record.architecture)}`);
|
|
170
|
+
}
|
|
171
|
+
if (!isPathSegment(record.name) || !isPathSegment(record.version)) {
|
|
172
|
+
fail(`(${record.fileName}) has a name or version that cannot name a package path`);
|
|
173
|
+
}
|
|
174
|
+
const digests = record.digests;
|
|
175
|
+
if (digests === null || typeof digests !== 'object' || typeof digests.sha256 !== 'string' || !isCount(digests.size)) {
|
|
176
|
+
fail('.digests must hold sha256 and size');
|
|
177
|
+
}
|
|
178
|
+
const metadata = record.metadata;
|
|
179
|
+
if (metadata === null || typeof metadata !== 'object') {
|
|
180
|
+
fail('.metadata must be an object');
|
|
181
|
+
}
|
|
182
|
+
for (const key of [
|
|
183
|
+
'epoch',
|
|
184
|
+
'upstreamVersion',
|
|
185
|
+
'release',
|
|
186
|
+
'summary',
|
|
187
|
+
'description',
|
|
188
|
+
'license',
|
|
189
|
+
'projectUrl',
|
|
190
|
+
'vendor',
|
|
191
|
+
'group',
|
|
192
|
+
'packager',
|
|
193
|
+
'sourceRpm',
|
|
194
|
+
'buildHost',
|
|
195
|
+
] as const) {
|
|
196
|
+
if (typeof metadata[key] !== 'string') {
|
|
197
|
+
fail(`.metadata.${key} must be a string`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const key of ['buildTime', 'fileTime', 'installedSize', 'archiveSize'] as const) {
|
|
201
|
+
if (!isCount(metadata[key])) {
|
|
202
|
+
fail(`.metadata.${key} must be a non-negative safe integer`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for (const key of ['provides', 'requires', 'conflicts', 'obsoletes'] as const) {
|
|
206
|
+
const list: unknown = metadata[key];
|
|
207
|
+
if (
|
|
208
|
+
!Array.isArray(list) ||
|
|
209
|
+
!list.every(
|
|
210
|
+
(dependency) =>
|
|
211
|
+
dependency !== null &&
|
|
212
|
+
typeof dependency === 'object' &&
|
|
213
|
+
['name', 'flags', 'epoch', 'version', 'release'].every((field) => typeof dependency[field] === 'string'),
|
|
214
|
+
)
|
|
215
|
+
) {
|
|
216
|
+
fail(`.metadata.${key} must be a list of dependencies`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const files: unknown = metadata.files;
|
|
220
|
+
if (
|
|
221
|
+
!Array.isArray(files) ||
|
|
222
|
+
!files.every(
|
|
223
|
+
(file) =>
|
|
224
|
+
file !== null && typeof file === 'object' && typeof file.path === 'string' && typeof file.type === 'string' && typeof file.isExecutable === 'boolean',
|
|
225
|
+
)
|
|
226
|
+
) {
|
|
227
|
+
fail('.metadata.files must be a list of files');
|
|
228
|
+
}
|
|
229
|
+
const changelogs: unknown = metadata.changelogs;
|
|
230
|
+
if (
|
|
231
|
+
!Array.isArray(changelogs) ||
|
|
232
|
+
!changelogs.every(
|
|
233
|
+
(changelog) =>
|
|
234
|
+
changelog !== null &&
|
|
235
|
+
typeof changelog === 'object' &&
|
|
236
|
+
typeof changelog.author === 'string' &&
|
|
237
|
+
typeof changelog.text === 'string' &&
|
|
238
|
+
isCount(changelog.date),
|
|
239
|
+
)
|
|
240
|
+
) {
|
|
241
|
+
fail('.metadata.changelogs must be a list of changelog entries');
|
|
242
|
+
}
|
|
243
|
+
const location = rpmPackageLocation(record);
|
|
244
|
+
if (locations.has(location)) {
|
|
245
|
+
fail(`duplicates the file ${record.fileName}`);
|
|
246
|
+
}
|
|
247
|
+
locations.add(location);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isCount(valueArg: unknown): boolean {
|
|
252
|
+
return typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg >= 0;
|
|
253
|
+
}
|