@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.
Files changed (131) hide show
  1. package/.smartconfig.json +65 -0
  2. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  3. package/dist_ts/00_commitinfo_data.js +9 -0
  4. package/dist_ts/alpine/apkindex.d.ts +39 -0
  5. package/dist_ts/alpine/apkindex.js +211 -0
  6. package/dist_ts/alpine/pkginfo.d.ts +45 -0
  7. package/dist_ts/alpine/pkginfo.js +159 -0
  8. package/dist_ts/alpine/reader.d.ts +20 -0
  9. package/dist_ts/alpine/reader.js +89 -0
  10. package/dist_ts/arch/builder.d.ts +41 -0
  11. package/dist_ts/arch/builder.js +234 -0
  12. package/dist_ts/arch/reader.d.ts +16 -0
  13. package/dist_ts/arch/reader.js +271 -0
  14. package/dist_ts/arch/signer.d.ts +21 -0
  15. package/dist_ts/arch/signer.js +66 -0
  16. package/dist_ts/arch/vercmp.d.ts +12 -0
  17. package/dist_ts/arch/vercmp.js +170 -0
  18. package/dist_ts/debian/control.d.ts +22 -0
  19. package/dist_ts/debian/control.js +137 -0
  20. package/dist_ts/debian/gostring.d.ts +26 -0
  21. package/dist_ts/debian/gostring.js +145 -0
  22. package/dist_ts/debian/indexbuilder.d.ts +47 -0
  23. package/dist_ts/debian/indexbuilder.js +299 -0
  24. package/dist_ts/debian/mailaddress.d.ts +12 -0
  25. package/dist_ts/debian/mailaddress.js +736 -0
  26. package/dist_ts/debian/reader.d.ts +30 -0
  27. package/dist_ts/debian/reader.js +110 -0
  28. package/dist_ts/index.d.ts +23 -0
  29. package/dist_ts/index.js +18 -0
  30. package/dist_ts/plugins.d.ts +8 -0
  31. package/dist_ts/plugins.js +11 -0
  32. package/dist_ts/readers/ar.d.ts +31 -0
  33. package/dist_ts/readers/ar.js +87 -0
  34. package/dist_ts/readers/bytes.d.ts +58 -0
  35. package/dist_ts/readers/bytes.js +147 -0
  36. package/dist_ts/readers/bytesource.d.ts +40 -0
  37. package/dist_ts/readers/bytesource.js +154 -0
  38. package/dist_ts/readers/decompress.d.ts +43 -0
  39. package/dist_ts/readers/decompress.js +269 -0
  40. package/dist_ts/readers/gzipmember.d.ts +32 -0
  41. package/dist_ts/readers/gzipmember.js +176 -0
  42. package/dist_ts/readers/tar.d.ts +85 -0
  43. package/dist_ts/readers/tar.js +731 -0
  44. package/dist_ts/readers/text.d.ts +16 -0
  45. package/dist_ts/readers/text.js +96 -0
  46. package/dist_ts/rpm/builder.d.ts +28 -0
  47. package/dist_ts/rpm/builder.js +208 -0
  48. package/dist_ts/rpm/header.d.ts +138 -0
  49. package/dist_ts/rpm/header.js +546 -0
  50. package/dist_ts/rpm/reader.d.ts +20 -0
  51. package/dist_ts/rpm/reader.js +306 -0
  52. package/dist_ts/rpm/repoconfig.d.ts +15 -0
  53. package/dist_ts/rpm/repoconfig.js +38 -0
  54. package/dist_ts/rpm/repodata.d.ts +44 -0
  55. package/dist_ts/rpm/repodata.js +148 -0
  56. package/dist_ts/rpm/sign.d.ts +30 -0
  57. package/dist_ts/rpm/sign.js +183 -0
  58. package/dist_ts/rpm/updates.d.ts +38 -0
  59. package/dist_ts/rpm/updates.js +265 -0
  60. package/dist_ts/rpm/xml.d.ts +18 -0
  61. package/dist_ts/rpm/xml.js +90 -0
  62. package/dist_ts/shared/architecture.d.ts +8 -0
  63. package/dist_ts/shared/architecture.js +17 -0
  64. package/dist_ts/shared/errors.d.ts +38 -0
  65. package/dist_ts/shared/errors.js +22 -0
  66. package/dist_ts/shared/gopath.d.ts +13 -0
  67. package/dist_ts/shared/gopath.js +53 -0
  68. package/dist_ts/shared/gostrings.d.ts +14 -0
  69. package/dist_ts/shared/gostrings.js +29 -0
  70. package/dist_ts/shared/limits.d.ts +47 -0
  71. package/dist_ts/shared/limits.js +57 -0
  72. package/dist_ts/shared/types.d.ts +213 -0
  73. package/dist_ts/shared/types.js +13 -0
  74. package/dist_ts/shared/url.d.ts +18 -0
  75. package/dist_ts/shared/url.js +371 -0
  76. package/dist_ts/shared/validation.d.ts +21 -0
  77. package/dist_ts/shared/validation.js +58 -0
  78. package/dist_ts/signing/keys.d.ts +36 -0
  79. package/dist_ts/signing/keys.js +89 -0
  80. package/dist_ts/signing/openpgpframing.d.ts +33 -0
  81. package/dist_ts/signing/openpgpframing.js +93 -0
  82. package/dist_ts/signing/signers.d.ts +63 -0
  83. package/dist_ts/signing/signers.js +248 -0
  84. package/dist_ts/writers/gotar.d.ts +43 -0
  85. package/dist_ts/writers/gotar.js +218 -0
  86. package/license.md +19 -0
  87. package/package.json +56 -3
  88. package/readme.md +176 -0
  89. package/ts/00_commitinfo_data.ts +8 -0
  90. package/ts/alpine/apkindex.ts +241 -0
  91. package/ts/alpine/pkginfo.ts +183 -0
  92. package/ts/alpine/reader.ts +96 -0
  93. package/ts/arch/builder.ts +283 -0
  94. package/ts/arch/reader.ts +307 -0
  95. package/ts/arch/signer.ts +70 -0
  96. package/ts/arch/vercmp.ts +188 -0
  97. package/ts/debian/control.ts +150 -0
  98. package/ts/debian/gostring.ts +150 -0
  99. package/ts/debian/indexbuilder.ts +355 -0
  100. package/ts/debian/mailaddress.ts +754 -0
  101. package/ts/debian/reader.ts +126 -0
  102. package/ts/index.ts +46 -0
  103. package/ts/plugins.ts +13 -0
  104. package/ts/readers/ar.ts +102 -0
  105. package/ts/readers/bytes.ts +182 -0
  106. package/ts/readers/bytesource.ts +164 -0
  107. package/ts/readers/decompress.ts +332 -0
  108. package/ts/readers/gzipmember.ts +209 -0
  109. package/ts/readers/tar.ts +835 -0
  110. package/ts/readers/text.ts +91 -0
  111. package/ts/rpm/builder.ts +253 -0
  112. package/ts/rpm/header.ts +628 -0
  113. package/ts/rpm/reader.ts +317 -0
  114. package/ts/rpm/repoconfig.ts +45 -0
  115. package/ts/rpm/repodata.ts +219 -0
  116. package/ts/rpm/sign.ts +200 -0
  117. package/ts/rpm/updates.ts +314 -0
  118. package/ts/rpm/xml.ts +101 -0
  119. package/ts/shared/architecture.ts +16 -0
  120. package/ts/shared/errors.ts +78 -0
  121. package/ts/shared/gopath.ts +54 -0
  122. package/ts/shared/gostrings.ts +31 -0
  123. package/ts/shared/limits.ts +93 -0
  124. package/ts/shared/types.ts +251 -0
  125. package/ts/shared/url.ts +387 -0
  126. package/ts/shared/validation.ts +71 -0
  127. package/ts/signing/keys.ts +110 -0
  128. package/ts/signing/openpgpframing.ts +115 -0
  129. package/ts/signing/signers.ts +315 -0
  130. package/ts/writers/gotar.ts +256 -0
  131. package/README.md +0 -3
@@ -0,0 +1,317 @@
1
+ import { PackageByteSource } from '../readers/bytesource.js';
2
+ import { decodeGoUtf8 } from '../readers/text.js';
3
+ import { LinuxRepoError } from '../shared/errors.js';
4
+ import { resolveLinuxPackageLimits, type ILinuxPackageLimits } from '../shared/limits.js';
5
+ import type { IRpmChangelog, IRpmDependency, IRpmFile, IRpmPackageRecord } from '../shared/types.js';
6
+ import { isValidUrl } from '../shared/url.js';
7
+ import { isPathSegment } from '../shared/validation.js';
8
+ import { readRpmHeaders, RPM_SIGNATURE_TAG, type RpmHeader } from './header.js';
9
+
10
+ /** Main header tags Gitea 28.2.0 reads (go-rpmutils `tags.go`). */
11
+ const TAG = {
12
+ name: 1000,
13
+ version: 1001,
14
+ release: 1002,
15
+ epoch: 1003,
16
+ summary: 1004,
17
+ description: 1005,
18
+ buildTime: 1006,
19
+ buildHost: 1007,
20
+ size: 1009,
21
+ vendor: 1011,
22
+ license: 1014,
23
+ packager: 1015,
24
+ group: 1016,
25
+ url: 1020,
26
+ arch: 1022,
27
+ fileModes: 1030,
28
+ fileMtimes: 1034,
29
+ fileFlags: 1037,
30
+ sourceRpm: 1044,
31
+ provideName: 1047,
32
+ requireFlags: 1048,
33
+ requireName: 1049,
34
+ requireVersion: 1050,
35
+ conflictFlags: 1053,
36
+ conflictName: 1054,
37
+ conflictVersion: 1055,
38
+ changelogTime: 1080,
39
+ changelogName: 1081,
40
+ changelogText: 1082,
41
+ obsoleteName: 1090,
42
+ provideFlags: 1112,
43
+ provideVersion: 1113,
44
+ obsoleteFlags: 1114,
45
+ obsoleteVersion: 1115,
46
+ dirIndexes: 1116,
47
+ baseNames: 1117,
48
+ dirNames: 1118,
49
+ longSize: 5009,
50
+ } as const;
51
+
52
+ const RPMSENSE_LESS = 1n << 1n;
53
+ const RPMSENSE_GREATER = 1n << 2n;
54
+ const RPMSENSE_EQUAL = 1n << 3n;
55
+ const RPMFILE_GHOST = 1 << 6;
56
+ const S_IFMT = 0xf000;
57
+ const S_IFDIR = 0x4000;
58
+ const S_IXANY = 0x40 | 0x8 | 0x1;
59
+
60
+ /**
61
+ * Reads an RPM package into a record holding exactly what Gitea 28.2.0's
62
+ * `rpm.ParsePackage` derives (`modules/packages/rpm/metadata.go:102-299`),
63
+ * plus the digests of the whole file.
64
+ *
65
+ * Gitea reads only the lead and the two headers; so does the reader, which
66
+ * then drains the payload for the digests. The differences from Gitea:
67
+ * `installedSize` falls back to LONGSIZE (5009) and `archiveSize` to the
68
+ * signature tag LONGARCHIVESIZE (271) when the 32-bit tag gives no value;
69
+ * numbers above `Number.MAX_SAFE_INTEGER`, tags with more than `maxEntries`
70
+ * values, and a NAME or built version that is not a path segment (empty,
71
+ * `.`, `..`, or holding `/` or a control character) are refused. Strings are decoded with the shared
72
+ * Go-compatible decoder (each byte of an invalid UTF-8 sequence becomes
73
+ * U+FFFD); Gitea 28.2.0 refuses such packages because its JSON v2 encoder
74
+ * rejects invalid UTF-8; the codec accepts them, so it accepts a superset of
75
+ * what Gitea accepts.
76
+ */
77
+ export async function readRpmPackage(
78
+ source: AsyncIterable<Uint8Array>,
79
+ limits?: Partial<ILinuxPackageLimits>,
80
+ ): Promise<IRpmPackageRecord> {
81
+ const resolved = resolveLinuxPackageLimits(limits);
82
+ const bytes = new PackageByteSource(source, resolved.maxPackageBytes);
83
+ const headers = await readRpmHeaders(bytes, resolved);
84
+ const main = headers.main;
85
+
86
+ // GetNEVRA (go-rpmutils header.go:350-381): every part is required
87
+ const name = requiredString(main, TAG.name, 'NAME');
88
+ const epochValues = main.entry(TAG.epoch) === null ? [0n] : main.uint64s(TAG.epoch);
89
+ if (epochValues === null) {
90
+ throw new LinuxRepoError('invalid-field', 'the RPM EPOCH tag is not an integer', { field: 'EPOCH' });
91
+ }
92
+ const epoch = (epochValues[0] ?? 0n).toString();
93
+ const upstreamVersion = requiredString(main, TAG.version, 'VERSION');
94
+ const release = requiredString(main, TAG.release, 'RELEASE');
95
+ const architecture = requiredString(main, TAG.arch, 'ARCH');
96
+ const version = epoch !== '0' ? `${epoch}-${upstreamVersion}-${release}` : `${upstreamVersion}-${release}`;
97
+ // both name a segment of the package's served path (a difference from Gitea, whose upload route does not check them)
98
+ if (!isPathSegment(name)) {
99
+ throw new LinuxRepoError('invalid-field', 'the RPM NAME cannot name a package path segment', { field: 'NAME' });
100
+ }
101
+ if (!isPathSegment(version)) {
102
+ throw new LinuxRepoError('invalid-field', 'the RPM version (EPOCH, VERSION, RELEASE) cannot name a package path segment', {
103
+ field: 'VERSION',
104
+ });
105
+ }
106
+
107
+ // Gitea clears an invalid URL before it stores the metadata (metadata.go:151-153)
108
+ const urlValues = main.strings(TAG.url);
109
+ const rawUrl = urlValues === null || urlValues.length === 0 ? new Uint8Array(0) : urlValues[0];
110
+ const projectUrl = isValidUrl(decodeGoUtf8(rawUrl)) ? decodeGoUtf8(rawUrl) : '';
111
+ const record: IRpmPackageRecord = {
112
+ format: 'rpm',
113
+ name,
114
+ version,
115
+ architecture,
116
+ // routers/api/packages/rpm/rpm.go:188
117
+ fileName: `${name}-${version}.${architecture}.rpm`,
118
+ digests: { size: 0, md5: '', sha1: '', sha256: '', sha512: '' },
119
+ metadata: {
120
+ epoch,
121
+ upstreamVersion,
122
+ release,
123
+ isSourcePackage: headers.isSource,
124
+ summary: optionalString(main, TAG.summary),
125
+ description: optionalString(main, TAG.description),
126
+ license: optionalString(main, TAG.license),
127
+ projectUrl,
128
+ vendor: optionalString(main, TAG.vendor),
129
+ group: optionalString(main, TAG.group),
130
+ packager: optionalString(main, TAG.packager),
131
+ sourceRpm: optionalString(main, TAG.sourceRpm),
132
+ buildHost: optionalString(main, TAG.buildHost),
133
+ buildTime: firstNumber(main, TAG.buildTime, 'BUILDTIME') ?? 0,
134
+ fileTime: firstNumber(main, TAG.fileMtimes, 'FILEMTIMES') ?? 0,
135
+ installedSize: firstNumber(main, TAG.size, 'SIZE') ?? firstNumber(main, TAG.longSize, 'LONGSIZE') ?? 0,
136
+ archiveSize:
137
+ firstNumber(headers.signature, RPM_SIGNATURE_TAG.payloadSize, 'PAYLOADSIZE') ??
138
+ firstNumber(headers.signature, RPM_SIGNATURE_TAG.longArchiveSize, 'LONGARCHIVESIZE') ??
139
+ 0,
140
+ provides: dependencies(main, TAG.provideName, TAG.provideVersion, TAG.provideFlags),
141
+ requires: dependencies(main, TAG.requireName, TAG.requireVersion, TAG.requireFlags),
142
+ conflicts: dependencies(main, TAG.conflictName, TAG.conflictVersion, TAG.conflictFlags),
143
+ obsoletes: dependencies(main, TAG.obsoleteName, TAG.obsoleteVersion, TAG.obsoleteFlags),
144
+ files: files(main),
145
+ changelogs: changelogs(main),
146
+ },
147
+ };
148
+ await bytes.drain();
149
+ record.digests = bytes.digests();
150
+ return record;
151
+ }
152
+
153
+ /** A NEVRA part: absent, not a string, or without a value refuses the package. */
154
+ function requiredString(headerArg: RpmHeader, tagArg: number, fieldArg: string): string {
155
+ if (headerArg.entry(tagArg) === null) {
156
+ throw new LinuxRepoError('missing-field', `the RPM ${fieldArg} tag is missing`, { field: fieldArg });
157
+ }
158
+ const values = headerArg.strings(tagArg);
159
+ if (values === null) {
160
+ throw new LinuxRepoError('invalid-field', `the RPM ${fieldArg} tag is not a string`, { field: fieldArg });
161
+ }
162
+ if (values.length === 0) {
163
+ throw new LinuxRepoError('invalid-field', `the RPM ${fieldArg} tag holds no value`, { field: fieldArg });
164
+ }
165
+ return decodeGoUtf8(values[0]);
166
+ }
167
+
168
+ /** Gitea's `getString`: the first value, or `''`. */
169
+ function optionalString(headerArg: RpmHeader, tagArg: number): string {
170
+ const values = headerArg.strings(tagArg);
171
+ return values === null || values.length === 0 ? '' : decodeGoUtf8(values[0]);
172
+ }
173
+
174
+ /** Gitea's `getUInt64`: the first value, or `null` where Gitea records 0. */
175
+ function firstNumber(headerArg: RpmHeader, tagArg: number, fieldArg: string): number | null {
176
+ const values = headerArg.uint64s(tagArg);
177
+ if (values === null || values.length === 0) {
178
+ return null;
179
+ }
180
+ return safeNumber(values[0], fieldArg);
181
+ }
182
+
183
+ function safeNumber(valueArg: bigint, fieldArg: string): number {
184
+ if (valueArg > BigInt(Number.MAX_SAFE_INTEGER)) {
185
+ throw new LinuxRepoError('invalid-field', `the RPM ${fieldArg} value exceeds the largest safe integer`, {
186
+ field: fieldArg,
187
+ });
188
+ }
189
+ return Number(valueArg);
190
+ }
191
+
192
+ /** Gitea's `getEntries` (metadata.go:174-231), including its early returns. */
193
+ function dependencies(headerArg: RpmHeader, namesTagArg: number, versionsTagArg: number, flagsTagArg: number): IRpmDependency[] {
194
+ const names = headerArg.strings(namesTagArg);
195
+ if (names === null || names.length === 0) {
196
+ return [];
197
+ }
198
+ const flags = headerArg.uint64s(flagsTagArg);
199
+ if (flags === null || flags.length === 0) {
200
+ return [];
201
+ }
202
+ const versions = headerArg.strings(versionsTagArg);
203
+ if (versions === null || versions.length === 0) {
204
+ return [];
205
+ }
206
+ if (names.length !== flags.length || names.length !== versions.length) {
207
+ return [];
208
+ }
209
+ return names.map((nameBytes, i): IRpmDependency => {
210
+ const flag = flags[i];
211
+ let flagName: IRpmDependency['flags'] = '';
212
+ if ((flag & RPMSENSE_GREATER) !== 0n && (flag & RPMSENSE_EQUAL) !== 0n) {
213
+ flagName = 'GE';
214
+ } else if ((flag & RPMSENSE_LESS) !== 0n && (flag & RPMSENSE_EQUAL) !== 0n) {
215
+ flagName = 'LE';
216
+ } else if ((flag & RPMSENSE_GREATER) !== 0n) {
217
+ flagName = 'GT';
218
+ } else if ((flag & RPMSENSE_LESS) !== 0n) {
219
+ flagName = 'LT';
220
+ } else if ((flag & RPMSENSE_EQUAL) !== 0n) {
221
+ flagName = 'EQ';
222
+ }
223
+ const entry: IRpmDependency = { name: decodeGoUtf8(nameBytes), flags: flagName, epoch: '', version: '', release: '' };
224
+ const full = versions[i];
225
+ if (full.length !== 0) {
226
+ // strings.Split on '-' and then ':' over the raw bytes, exactly as Gitea does
227
+ const parts = splitBytes(full, HYPHEN);
228
+ const versionParts = splitBytes(parts[0], COLON);
229
+ if (versionParts.length === 2) {
230
+ entry.version = decodeGoUtf8(versionParts[1]);
231
+ entry.epoch = decodeGoUtf8(versionParts[0]);
232
+ } else {
233
+ entry.version = decodeGoUtf8(versionParts[0]);
234
+ entry.epoch = '0';
235
+ }
236
+ if (parts.length > 1) {
237
+ entry.release = decodeGoUtf8(parts[1]);
238
+ }
239
+ }
240
+ return entry;
241
+ });
242
+ }
243
+
244
+ /** Gitea's `getFiles` (metadata.go:233-271). The path joins the raw bytes before decoding. */
245
+ function files(headerArg: RpmHeader): IRpmFile[] {
246
+ const baseNames = headerArg.strings(TAG.baseNames) ?? [];
247
+ const dirNames = headerArg.strings(TAG.dirNames) ?? [];
248
+ const dirIndexes = headerArg.uint32s(TAG.dirIndexes) ?? [];
249
+ const fileFlags = headerArg.uint32s(TAG.fileFlags) ?? [];
250
+ const fileModes = headerArg.uint32s(TAG.fileModes) ?? [];
251
+ const result: IRpmFile[] = [];
252
+ for (let i = 0; i < baseNames.length; i++) {
253
+ if (dirIndexes.length <= i) {
254
+ continue;
255
+ }
256
+ const dirIndex = dirIndexes[i];
257
+ if (dirNames.length <= dirIndex) {
258
+ continue;
259
+ }
260
+ let type: IRpmFile['type'] = '';
261
+ let isExecutable = false;
262
+ if (i < fileFlags.length && (fileFlags[i] & RPMFILE_GHOST) !== 0) {
263
+ type = 'ghost';
264
+ } else if (i < fileModes.length) {
265
+ if ((fileModes[i] & S_IFMT) === S_IFDIR) {
266
+ type = 'dir';
267
+ } else {
268
+ isExecutable = (fileModes[i] & S_IXANY) !== 0;
269
+ }
270
+ }
271
+ const directory = dirNames[dirIndex];
272
+ const path = new Uint8Array(directory.length + baseNames[i].length);
273
+ path.set(directory, 0);
274
+ path.set(baseNames[i], directory.length);
275
+ result.push({ path: decodeGoUtf8(path), type, isExecutable });
276
+ }
277
+ return result;
278
+ }
279
+
280
+ /** Gitea's `getChangelogs` (metadata.go:273-299). */
281
+ function changelogs(headerArg: RpmHeader): IRpmChangelog[] {
282
+ const texts = headerArg.strings(TAG.changelogText);
283
+ if (texts === null || texts.length === 0) {
284
+ return [];
285
+ }
286
+ const authors = headerArg.strings(TAG.changelogName);
287
+ if (authors === null || authors.length === 0) {
288
+ return [];
289
+ }
290
+ const times = headerArg.uint32s(TAG.changelogTime);
291
+ if (times === null || times.length === 0) {
292
+ return [];
293
+ }
294
+ if (texts.length !== authors.length || texts.length !== times.length) {
295
+ return [];
296
+ }
297
+ return texts.map((text, i) => ({
298
+ author: decodeGoUtf8(authors[i]),
299
+ date: times[i],
300
+ text: decodeGoUtf8(text),
301
+ }));
302
+ }
303
+
304
+ const HYPHEN = 0x2d;
305
+ const COLON = 0x3a;
306
+
307
+ /** Go's `bytes.Split` for one separator byte. */
308
+ function splitBytes(bytesArg: Uint8Array, separatorArg: number): Uint8Array[] {
309
+ const parts: Uint8Array[] = [];
310
+ let start = 0;
311
+ for (let i = bytesArg.indexOf(separatorArg); i >= 0; i = bytesArg.indexOf(separatorArg, start)) {
312
+ parts.push(bytesArg.subarray(start, i));
313
+ start = i + 1;
314
+ }
315
+ parts.push(bytesArg.subarray(start));
316
+ return parts;
317
+ }
@@ -0,0 +1,45 @@
1
+ import { LinuxRepoError } from '../shared/errors.js';
2
+ import { hasControlCharacter } from '../shared/validation.js';
3
+
4
+ /**
5
+ * The `.repo` text for dnf. Without `repoGpgcheck` it is exactly Gitea
6
+ * 28.2.0's text (`routers/api/packages/rpm/rpm.go:46-51`): `[id]`, `name=`,
7
+ * `baseurl=`, `enabled=1`, `gpgcheck=`, `gpgkey=`, with no trailing line
8
+ * break. `repoGpgcheck` adds a `repo_gpgcheck=` line after `gpgcheck=`.
9
+ * Values with a line break or another control character are `invalid-input`.
10
+ */
11
+ export function renderRpmRepoConfig(input: {
12
+ id: string;
13
+ name: string;
14
+ baseUrl: string;
15
+ keyUrl: string;
16
+ gpgcheck: boolean;
17
+ repoGpgcheck?: boolean;
18
+ }): string {
19
+ if (input === null || typeof input !== 'object') {
20
+ throw new LinuxRepoError('invalid-input', 'input must be an object', { field: 'input' });
21
+ }
22
+ for (const key of ['id', 'name', 'baseUrl', 'keyUrl'] as const) {
23
+ const value: unknown = input[key];
24
+ if (typeof value !== 'string' || hasControlCharacter(value) || (key === 'id' && (value === '' || /[\[\]]/.test(value)))) {
25
+ throw new LinuxRepoError('invalid-input', `${key} must be a single-line string${key === 'id' ? ' without brackets' : ''}`, {
26
+ field: key,
27
+ });
28
+ }
29
+ }
30
+ if (typeof input.gpgcheck !== 'boolean' || (input.repoGpgcheck !== undefined && typeof input.repoGpgcheck !== 'boolean')) {
31
+ throw new LinuxRepoError('invalid-input', 'gpgcheck and repoGpgcheck must be booleans', { field: 'gpgcheck' });
32
+ }
33
+ const lines = [
34
+ `[${input.id}]`,
35
+ `name=${input.name}`,
36
+ `baseurl=${input.baseUrl}`,
37
+ 'enabled=1',
38
+ `gpgcheck=${input.gpgcheck ? 1 : 0}`,
39
+ ];
40
+ if (input.repoGpgcheck !== undefined) {
41
+ lines.push(`repo_gpgcheck=${input.repoGpgcheck ? 1 : 0}`);
42
+ }
43
+ lines.push(`gpgkey=${input.keyUrl}`);
44
+ return lines.join('\n');
45
+ }
@@ -0,0 +1,219 @@
1
+ import type { IRpmDependency, IRpmFile, IRpmPackageRecord } from '../shared/types.js';
2
+ import { element, textElement, XML_HEADER } from './xml.js';
3
+
4
+ /**
5
+ * The repodata documents Gitea 28.2.0 builds (`services/packages/rpm/repository.go`),
6
+ * written the way Go's `encoding/xml` writes Gitea's structs, with two
7
+ * corrections:
8
+ * - `filelists.xml` uses the namespace `http://linux.duke.edu/metadata/filelists`
9
+ * (Gitea writes `…/other`, `repository.go:494`);
10
+ * - `primary.xml` lists a file when it is executable (Gitea's rule,
11
+ * `repository.go:386-391`) or matches createrepo_c's `cr_is_primary`, once,
12
+ * in header order.
13
+ * And one more change: every document lists a source RPM's
14
+ * architecture as `src` (`rpmListedArchitecture`), where Gitea writes its ARCH
15
+ * tag (`repository.go:395`, `:483`, `:530`). One solvable keeps one identity
16
+ * across primary, filelists and other, so dnf never chooses a source RPM for
17
+ * installation and still finds its files and changelog. The location href
18
+ * keeps Gitea's path with the ARCH tag (`rpmPackageLocation`).
19
+ * Entries arrive sorted; each `<package>` renders from its record alone.
20
+ */
21
+
22
+ /**
23
+ * createrepo_c 1.2.6 `cr_is_primary` (`src/misc.h:111-119`): paths under
24
+ * `/etc/`, exactly `/usr/lib/sendmail`, or containing `bin/` anywhere. It is
25
+ * applied to every file entry, directories and ghosts included
26
+ * (`src/xml_dump.c:206`).
27
+ */
28
+ export function isPrimaryPath(pathArg: string): boolean {
29
+ return pathArg.startsWith('/etc/') || pathArg === '/usr/lib/sendmail' || pathArg.includes('bin/');
30
+ }
31
+
32
+ /** The architecture every document lists: `src` for a source RPM, else the ARCH tag. */
33
+ export function rpmListedArchitecture(recordArg: IRpmPackageRecord): string {
34
+ return recordArg.metadata.isSourcePackage ? 'src' : recordArg.architecture;
35
+ }
36
+
37
+ /** Gitea's package location below the repository root (`repository.go:420`), with the ARCH tag. */
38
+ export function rpmPackageLocation(recordArg: IRpmPackageRecord): string {
39
+ const { name, version, architecture } = recordArg;
40
+ return `package/${name}/${version}/${architecture}/${name}-${version}.${architecture}.rpm`;
41
+ }
42
+
43
+ export function renderPrimary(entriesArg: IRpmPackageRecord[]): string {
44
+ return document(
45
+ 'metadata',
46
+ [
47
+ ['xmlns', 'http://linux.duke.edu/metadata/common'],
48
+ ['xmlns:rpm', 'http://linux.duke.edu/metadata/rpm'],
49
+ ['packages', entriesArg.length],
50
+ ],
51
+ entriesArg.map(renderPrimaryPackage),
52
+ );
53
+ }
54
+
55
+ export function renderFilelists(entriesArg: IRpmPackageRecord[]): string {
56
+ return document(
57
+ 'filelists',
58
+ [
59
+ ['xmlns', 'http://linux.duke.edu/metadata/filelists'],
60
+ ['packages', entriesArg.length],
61
+ ],
62
+ entriesArg.map((record) =>
63
+ element('package', packageIdentity(record), versionElement(record) + record.metadata.files.map(fileElement).join('')),
64
+ ),
65
+ );
66
+ }
67
+
68
+ export function renderOther(entriesArg: IRpmPackageRecord[]): string {
69
+ return document(
70
+ 'otherdata',
71
+ [
72
+ ['xmlns', 'http://linux.duke.edu/metadata/other'],
73
+ ['packages', entriesArg.length],
74
+ ],
75
+ entriesArg.map((record) =>
76
+ element(
77
+ 'package',
78
+ packageIdentity(record),
79
+ versionElement(record) +
80
+ record.metadata.changelogs
81
+ .map((changelog) =>
82
+ textElement('changelog', changelog.text, [
83
+ ['author', changelog.author],
84
+ ['date', changelog.date],
85
+ ]),
86
+ )
87
+ .join(''),
88
+ ),
89
+ ),
90
+ );
91
+ }
92
+
93
+ /** One `repomd.xml` `<data>` record. */
94
+ export interface IRepomdData {
95
+ type: string;
96
+ checksum: string;
97
+ openChecksum: string;
98
+ href: string;
99
+ timestamp: number;
100
+ size: number;
101
+ openSize: number;
102
+ }
103
+
104
+ /** `repomd.xml` (`repository.go:239-255`). */
105
+ export function renderRepomd(dataArg: IRepomdData[]): string {
106
+ return document(
107
+ 'repomd',
108
+ [
109
+ ['xmlns', 'http://linux.duke.edu/metadata/repo'],
110
+ ['xmlns:rpm', 'http://linux.duke.edu/metadata/rpm'],
111
+ ],
112
+ dataArg.map((data) =>
113
+ element(
114
+ 'data',
115
+ [['type', data.type]],
116
+ [
117
+ textElement('checksum', data.checksum, [['type', 'sha256']]),
118
+ textElement('open-checksum', data.openChecksum, [['type', 'sha256']]),
119
+ element('location', [['href', data.href]]),
120
+ textElement('timestamp', String(data.timestamp)),
121
+ textElement('size', String(data.size)),
122
+ textElement('open-size', String(data.openSize)),
123
+ ].join(''),
124
+ ),
125
+ ),
126
+ );
127
+ }
128
+
129
+ function renderPrimaryPackage(recordArg: IRpmPackageRecord): string {
130
+ const metadata = recordArg.metadata;
131
+ const files = metadata.files.filter((file) => file.isExecutable || isPrimaryPath(file.path));
132
+ return element(
133
+ 'package',
134
+ [['type', 'rpm']],
135
+ [
136
+ textElement('name', recordArg.name),
137
+ textElement('arch', rpmListedArchitecture(recordArg)),
138
+ versionElement(recordArg),
139
+ textElement('checksum', recordArg.digests.sha256, [
140
+ ['type', 'sha256'],
141
+ ['pkgid', 'YES'],
142
+ ]),
143
+ textElement('summary', metadata.summary),
144
+ textElement('description', metadata.description),
145
+ textElement('packager', metadata.packager),
146
+ textElement('url', metadata.projectUrl),
147
+ element('time', [
148
+ ['file', metadata.fileTime],
149
+ ['build', metadata.buildTime],
150
+ ]),
151
+ element('size', [
152
+ ['package', recordArg.digests.size],
153
+ ['installed', metadata.installedSize],
154
+ ['archive', metadata.archiveSize],
155
+ ]),
156
+ element('location', [['href', rpmPackageLocation(recordArg)]]),
157
+ element(
158
+ 'format',
159
+ [],
160
+ [
161
+ textElement('rpm:license', metadata.license),
162
+ textElement('rpm:vendor', metadata.vendor),
163
+ textElement('rpm:group', metadata.group),
164
+ textElement('rpm:buildhost', metadata.buildHost),
165
+ textElement('rpm:sourcerpm', metadata.sourceRpm),
166
+ dependencyList('rpm:provides', metadata.provides),
167
+ dependencyList('rpm:requires', metadata.requires),
168
+ dependencyList('rpm:conflicts', metadata.conflicts),
169
+ dependencyList('rpm:obsoletes', metadata.obsoletes),
170
+ files.map(fileElement).join(''),
171
+ ].join(''),
172
+ ),
173
+ ].join(''),
174
+ );
175
+ }
176
+
177
+ function document(nameArg: string, attributesArg: Array<[string, string | number]>, packagesArg: string[]): string {
178
+ return `${XML_HEADER}${element(nameArg, attributesArg, packagesArg.join(''))}`;
179
+ }
180
+
181
+ function packageIdentity(recordArg: IRpmPackageRecord): Array<[string, string]> {
182
+ return [
183
+ ['pkgid', recordArg.digests.sha256],
184
+ ['name', recordArg.name],
185
+ ['arch', rpmListedArchitecture(recordArg)],
186
+ ];
187
+ }
188
+
189
+ function versionElement(recordArg: IRpmPackageRecord): string {
190
+ return element('version', [
191
+ ['epoch', recordArg.metadata.epoch],
192
+ ['ver', recordArg.metadata.upstreamVersion],
193
+ ['rel', recordArg.metadata.release],
194
+ ]);
195
+ }
196
+
197
+ /** Gitea's `Entry` (metadata.go:81-87): `flags`, `ver`, `epoch` and `rel` are `omitempty`. */
198
+ function dependencyList(nameArg: string, dependenciesArg: IRpmDependency[]): string {
199
+ return element(
200
+ nameArg,
201
+ [],
202
+ dependenciesArg
203
+ .map((dependency) =>
204
+ element('rpm:entry', [
205
+ ['name', dependency.name],
206
+ ['flags', dependency.flags || undefined],
207
+ ['ver', dependency.version || undefined],
208
+ ['epoch', dependency.epoch || undefined],
209
+ ['rel', dependency.release || undefined],
210
+ ]),
211
+ )
212
+ .join(''),
213
+ );
214
+ }
215
+
216
+ /** Gitea's `File` (metadata.go:89-93): `type` is `omitempty`. */
217
+ function fileElement(fileArg: IRpmFile): string {
218
+ return textElement('file', fileArg.path, [['type', fileArg.type || undefined]]);
219
+ }