@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,387 @@
1
+ /**
2
+ * Gitea 28.2.0's URL check for package metadata (`validation.IsValidURL`,
3
+ * `modules/validation/helpers.go:42-50`): Go 1.27's `url.ParseRequestURI`
4
+ * succeeds, the scheme is `http` or `https` (case-insensitive), and the host's
5
+ * port part holds only digits (`portOnly`/`validPort`,
6
+ * `modules/validation/binding.go:131-152`; no range check, an empty port
7
+ * passes). Readers keep a URL field only when this returns true.
8
+ *
9
+ * The port follows Go's `net/url` byte for byte on the UTF-8 encoding of the
10
+ * value, including its quirks: `http:x` is an opaque URL and passes,
11
+ * `http://` (empty host) passes, a query or fragment is never validated, and a
12
+ * `#` before the first `/` is part of the host and fails. Only the parts of the
13
+ * parser that can change the result for `http`/`https` are ported. Expected
14
+ * values come from Go: `test/fixtures/url/isvalidurl.gitea.json`.
15
+ *
16
+ * Internal: not exported from the package entry.
17
+ */
18
+ export function isValidUrl(valueArg: string): boolean {
19
+ if (typeof valueArg !== 'string') {
20
+ return false;
21
+ }
22
+ const raw = utf8.encode(valueArg);
23
+ // parse(): control characters anywhere, then the empty and `*` forms
24
+ if (raw.some((byte) => byte < 0x20 || byte === 0x7f) || raw.length === 0) {
25
+ return false;
26
+ }
27
+ const scheme = splitScheme(raw);
28
+ if (scheme === null) {
29
+ return false;
30
+ }
31
+ const schemeName = ascii(raw.subarray(0, scheme)).toLowerCase();
32
+ if (schemeName !== 'http' && schemeName !== 'https') {
33
+ return false;
34
+ }
35
+ let rest = raw.subarray(scheme + 1);
36
+ const questionMarks = count(rest, QUESTION);
37
+ if (questionMarks === 1 && rest[rest.length - 1] === QUESTION) {
38
+ rest = rest.subarray(0, rest.length - 1);
39
+ } else if (questionMarks > 0) {
40
+ rest = rest.subarray(0, rest.indexOf(QUESTION));
41
+ }
42
+ if (rest[0] !== SLASH) {
43
+ // a rootless path with a scheme is opaque and not validated further
44
+ return true;
45
+ }
46
+ let host: Uint8Array = new Uint8Array(0);
47
+ let path = rest;
48
+ if (rest[1] === SLASH) {
49
+ const slash = rest.indexOf(SLASH, 2);
50
+ const authority = slash < 0 ? rest.subarray(2) : rest.subarray(2, slash);
51
+ path = slash < 0 ? new Uint8Array(0) : rest.subarray(slash);
52
+ const parsedHost = parseAuthority(authority);
53
+ if (parsedHost === null) {
54
+ return false;
55
+ }
56
+ host = parsedHost;
57
+ }
58
+ if (unescape(path, 'other') === null) {
59
+ return false;
60
+ }
61
+ return isDigits(portOnly(host));
62
+ }
63
+
64
+ type TEscapeMode = 'host' | 'zone' | 'other';
65
+
66
+ const utf8 = new TextEncoder();
67
+ const SLASH = 0x2f;
68
+ const QUESTION = 0x3f;
69
+ const COLON = 0x3a;
70
+ const PERCENT = 0x25;
71
+ const AT = 0x40;
72
+ const OPEN_BRACKET = 0x5b;
73
+ const CLOSE_BRACKET = 0x5d;
74
+ const DOT = 0x2e;
75
+
76
+ /** Characters Go's `shouldEscape` leaves alone in a host besides alphanumerics and `-_.~`. */
77
+ const HOST_SAFE = new Set([..."!$&'()*+,;=:[]<>\""].map((character) => character.charCodeAt(0)));
78
+ /** Characters `validUserinfo` accepts besides alphanumerics. */
79
+ const USERINFO_SAFE = new Set([..."-._:~!$&'()*+,;=%@"].map((character) => character.charCodeAt(0)));
80
+
81
+ /** `getScheme`: the index of the scheme's colon, or `null` when there is no scheme or it is malformed. */
82
+ function splitScheme(rawArg: Uint8Array): number | null {
83
+ for (let i = 0; i < rawArg.length; i++) {
84
+ const c = rawArg[i];
85
+ if (isAlpha(c)) {
86
+ continue;
87
+ }
88
+ if (isDigit(c) || c === 0x2b || c === 0x2d || c === DOT) {
89
+ if (i === 0) {
90
+ return null;
91
+ }
92
+ continue;
93
+ }
94
+ return c === COLON && i > 0 ? i : null;
95
+ }
96
+ return null;
97
+ }
98
+
99
+ /** `parseAuthority`: the unescaped host, or `null` on a parse error. */
100
+ function parseAuthority(authorityArg: Uint8Array): Uint8Array | null {
101
+ const at = authorityArg.lastIndexOf(AT);
102
+ const host = parseHost(at < 0 ? authorityArg : authorityArg.subarray(at + 1));
103
+ if (host === null || at < 0) {
104
+ return host;
105
+ }
106
+ const userinfo = authorityArg.subarray(0, at);
107
+ // validUserinfo iterates runes: any byte >= 0x80 is a rune outside the allowed set
108
+ if (!userinfo.every((byte) => isAlpha(byte) || isDigit(byte) || USERINFO_SAFE.has(byte))) {
109
+ return null;
110
+ }
111
+ const colon = userinfo.indexOf(COLON);
112
+ const parts = colon < 0 ? [userinfo] : [userinfo.subarray(0, colon), userinfo.subarray(colon + 1)];
113
+ return parts.every((part) => unescape(part, 'other') !== null) ? host : null;
114
+ }
115
+
116
+ /** `parseHost` for `http`/`https` (strict colons, the Go 1.26+ default Gitea's go.mod selects). */
117
+ function parseHost(hostArg: Uint8Array): Uint8Array | null {
118
+ const openBracket = hostArg.lastIndexOf(OPEN_BRACKET);
119
+ if (openBracket > 0) {
120
+ return null;
121
+ }
122
+ if (openBracket === 0) {
123
+ const closeBracket = hostArg.lastIndexOf(CLOSE_BRACKET);
124
+ if (closeBracket < 0) {
125
+ return null;
126
+ }
127
+ const colonPort = hostArg.subarray(closeBracket + 1);
128
+ if (!isOptionalPort(colonPort)) {
129
+ return null;
130
+ }
131
+ const hostname = hostArg.subarray(1, closeBracket);
132
+ const zone = indexOfZone(hostname);
133
+ let unescapedHostname: Uint8Array | null;
134
+ if (zone >= 0) {
135
+ const hostPart = unescape(hostname.subarray(0, zone), 'host');
136
+ const zonePart = unescape(hostname.subarray(zone), 'zone');
137
+ unescapedHostname = hostPart === null || zonePart === null ? null : concat(hostPart, zonePart);
138
+ } else {
139
+ unescapedHostname = unescape(hostname, 'host');
140
+ }
141
+ // only an IPv6 address may sit in brackets (netip.ParseAddr, then !Is4)
142
+ if (unescapedHostname === null || !isIpv6Address(unescapedHostname)) {
143
+ return null;
144
+ }
145
+ return concat(Uint8Array.of(OPEN_BRACKET), unescapedHostname, Uint8Array.of(CLOSE_BRACKET), colonPort);
146
+ }
147
+ const colon = hostArg.indexOf(COLON);
148
+ if (colon >= 0 && !isOptionalPort(hostArg.subarray(colon))) {
149
+ return null;
150
+ }
151
+ return unescape(hostArg, 'host');
152
+ }
153
+
154
+ /** Go's `unescape` for the path, userinfo, host and zone modes; `null` on an invalid escape or host byte. */
155
+ function unescape(bytesArg: Uint8Array, modeArg: TEscapeMode): Uint8Array | null {
156
+ let escapes = 0;
157
+ for (let i = 0; i < bytesArg.length; ) {
158
+ const c = bytesArg[i];
159
+ if (c === PERCENT) {
160
+ escapes++;
161
+ if (i + 2 >= bytesArg.length || !isHex(bytesArg[i + 1]) || !isHex(bytesArg[i + 2])) {
162
+ return null;
163
+ }
164
+ const isPercentEscape = bytesArg[i + 1] === 0x32 && bytesArg[i + 2] === 0x35;
165
+ if (modeArg === 'host' && unhex(bytesArg[i + 1]) < 8 && !isPercentEscape) {
166
+ return null;
167
+ }
168
+ if (modeArg === 'zone') {
169
+ const value = (unhex(bytesArg[i + 1]) << 4) | unhex(bytesArg[i + 2]);
170
+ if (!isPercentEscape && value !== 0x20 && shouldEscapeInHost(value)) {
171
+ return null;
172
+ }
173
+ }
174
+ i += 3;
175
+ continue;
176
+ }
177
+ if (modeArg !== 'other' && c < 0x80 && shouldEscapeInHost(c)) {
178
+ return null;
179
+ }
180
+ i++;
181
+ }
182
+ if (escapes === 0) {
183
+ return bytesArg;
184
+ }
185
+ const result = new Uint8Array(bytesArg.length - 2 * escapes);
186
+ let length = 0;
187
+ for (let i = 0; i < bytesArg.length; i++) {
188
+ if (bytesArg[i] === PERCENT) {
189
+ result[length++] = (unhex(bytesArg[i + 1]) << 4) | unhex(bytesArg[i + 2]);
190
+ i += 2;
191
+ } else {
192
+ result[length++] = bytesArg[i];
193
+ }
194
+ }
195
+ return result;
196
+ }
197
+
198
+ /** Go's `shouldEscape(c, encodeHost)` (identical for `encodeZone`). */
199
+ function shouldEscapeInHost(cArg: number): boolean {
200
+ if (isAlpha(cArg) || isDigit(cArg)) {
201
+ return false;
202
+ }
203
+ return !(HOST_SAFE.has(cArg) || cArg === 0x2d || cArg === 0x5f || cArg === DOT || cArg === 0x7e);
204
+ }
205
+
206
+ /** Index of the first `%25` (the RFC 6874 zone introducer), or -1. */
207
+ function indexOfZone(bytesArg: Uint8Array): number {
208
+ for (let i = 0; i + 2 < bytesArg.length; i++) {
209
+ if (bytesArg[i] === PERCENT && bytesArg[i + 1] === 0x32 && bytesArg[i + 2] === 0x35) {
210
+ return i;
211
+ }
212
+ }
213
+ return -1;
214
+ }
215
+
216
+ /**
217
+ * Whether `netip.ParseAddr` parses the bytes as an IPv6 address (with an
218
+ * optional non-empty zone). An input whose first `.`, `:` or `%` is not `:` is
219
+ * an IPv4 address or an error, both refused in brackets.
220
+ */
221
+ function isIpv6Address(bytesArg: Uint8Array): boolean {
222
+ const first = bytesArg.findIndex((byte) => byte === DOT || byte === COLON || byte === PERCENT);
223
+ if (first < 0 || bytesArg[first] !== COLON) {
224
+ return false;
225
+ }
226
+ let s = bytesArg;
227
+ const zone = s.indexOf(PERCENT);
228
+ if (zone >= 0) {
229
+ if (zone === s.length - 1) {
230
+ return false;
231
+ }
232
+ s = s.subarray(0, zone);
233
+ }
234
+ let ellipsis = -1;
235
+ if (s.length >= 2 && s[0] === COLON && s[1] === COLON) {
236
+ ellipsis = 0;
237
+ s = s.subarray(2);
238
+ if (s.length === 0) {
239
+ return true;
240
+ }
241
+ }
242
+ let i = 0;
243
+ while (i < 16) {
244
+ let off = 0;
245
+ let acc = 0;
246
+ for (; off < s.length; off++) {
247
+ const c = s[off];
248
+ if (!isHex(c)) {
249
+ break;
250
+ }
251
+ acc = acc * 16 + unhex(c);
252
+ if (off > 3 || acc > 0xffff) {
253
+ return false;
254
+ }
255
+ }
256
+ if (off === 0) {
257
+ return false;
258
+ }
259
+ if (off < s.length && s[off] === DOT) {
260
+ if ((ellipsis < 0 && i !== 12) || i + 4 > 16 || !isIpv4Fields(s)) {
261
+ return false;
262
+ }
263
+ s = s.subarray(s.length);
264
+ i += 4;
265
+ break;
266
+ }
267
+ i += 2;
268
+ s = s.subarray(off);
269
+ if (s.length === 0) {
270
+ break;
271
+ }
272
+ if (s[0] !== COLON || s.length === 1) {
273
+ return false;
274
+ }
275
+ s = s.subarray(1);
276
+ if (s[0] === COLON) {
277
+ if (ellipsis >= 0) {
278
+ return false;
279
+ }
280
+ ellipsis = i;
281
+ s = s.subarray(1);
282
+ if (s.length === 0) {
283
+ break;
284
+ }
285
+ }
286
+ }
287
+ if (s.length !== 0) {
288
+ return false;
289
+ }
290
+ return i < 16 ? ellipsis >= 0 : ellipsis < 0;
291
+ }
292
+
293
+ /** `parseIPv4Fields`: four decimal octets without leading zeros. */
294
+ function isIpv4Fields(bytesArg: Uint8Array): boolean {
295
+ let value = 0;
296
+ let digits = 0;
297
+ let dots = 0;
298
+ for (let i = 0; i < bytesArg.length; i++) {
299
+ const c = bytesArg[i];
300
+ if (isDigit(c)) {
301
+ if (digits === 1 && value === 0) {
302
+ return false;
303
+ }
304
+ value = value * 10 + (c - 0x30);
305
+ digits++;
306
+ if (value > 255) {
307
+ return false;
308
+ }
309
+ } else if (c === DOT) {
310
+ if (i === 0 || i === bytesArg.length - 1 || bytesArg[i - 1] === DOT || dots === 3) {
311
+ return false;
312
+ }
313
+ dots++;
314
+ value = 0;
315
+ digits = 0;
316
+ } else {
317
+ return false;
318
+ }
319
+ }
320
+ return dots === 3;
321
+ }
322
+
323
+ /** `validOptionalPort`: empty, or `:` followed by digits only. */
324
+ function isOptionalPort(bytesArg: Uint8Array): boolean {
325
+ return bytesArg.length === 0 || (bytesArg[0] === COLON && isDigits(bytesArg.subarray(1)));
326
+ }
327
+
328
+ /** Gitea's `portOnly` on the parsed host. */
329
+ function portOnly(hostArg: Uint8Array): Uint8Array {
330
+ const colon = hostArg.indexOf(COLON);
331
+ if (colon < 0) {
332
+ return new Uint8Array(0);
333
+ }
334
+ for (let i = 0; i + 1 < hostArg.length; i++) {
335
+ if (hostArg[i] === CLOSE_BRACKET && hostArg[i + 1] === COLON) {
336
+ return hostArg.subarray(i + 2);
337
+ }
338
+ }
339
+ if (hostArg.includes(CLOSE_BRACKET)) {
340
+ return new Uint8Array(0);
341
+ }
342
+ return hostArg.subarray(colon + 1);
343
+ }
344
+
345
+ function isDigits(bytesArg: Uint8Array): boolean {
346
+ return bytesArg.every(isDigit);
347
+ }
348
+
349
+ function isDigit(cArg: number): boolean {
350
+ return cArg >= 0x30 && cArg <= 0x39;
351
+ }
352
+
353
+ function isAlpha(cArg: number): boolean {
354
+ return (cArg >= 0x41 && cArg <= 0x5a) || (cArg >= 0x61 && cArg <= 0x7a);
355
+ }
356
+
357
+ function isHex(cArg: number): boolean {
358
+ return isDigit(cArg) || (cArg >= 0x41 && cArg <= 0x46) || (cArg >= 0x61 && cArg <= 0x66);
359
+ }
360
+
361
+ function unhex(cArg: number): number {
362
+ return isDigit(cArg) ? cArg - 0x30 : (cArg | 0x20) - 0x61 + 10;
363
+ }
364
+
365
+ function count(bytesArg: Uint8Array, byteArg: number): number {
366
+ let total = 0;
367
+ for (const byte of bytesArg) {
368
+ if (byte === byteArg) {
369
+ total++;
370
+ }
371
+ }
372
+ return total;
373
+ }
374
+
375
+ function ascii(bytesArg: Uint8Array): string {
376
+ return String.fromCharCode(...bytesArg);
377
+ }
378
+
379
+ function concat(...partsArg: Uint8Array[]): Uint8Array {
380
+ const result = new Uint8Array(partsArg.reduce((sum, part) => sum + part.length, 0));
381
+ let offset = 0;
382
+ for (const part of partsArg) {
383
+ result.set(part, offset);
384
+ offset += part.length;
385
+ }
386
+ return result;
387
+ }
@@ -0,0 +1,71 @@
1
+ import { LinuxRepoError } from './errors.js';
2
+
3
+ /** The largest creation time an OpenPGP v4 packet can carry (seconds since the epoch). */
4
+ const MAX_OPENPGP_SECONDS = 0xffffffff;
5
+
6
+ /**
7
+ * Refuses anything but a valid `Date` whose whole seconds fit an OpenPGP v4
8
+ * time field (1970-01-01 to 2106-02-07).
9
+ */
10
+ export function assertTimestamp(valueArg: unknown, fieldArg = 'timestamp'): asserts valueArg is Date {
11
+ if (!(valueArg instanceof Date) || !Number.isFinite(valueArg.getTime())) {
12
+ throw new LinuxRepoError('invalid-input', `${fieldArg} must be a valid Date`, { field: fieldArg });
13
+ }
14
+ const seconds = Math.floor(valueArg.getTime() / 1000);
15
+ if (seconds < 0 || seconds > MAX_OPENPGP_SECONDS) {
16
+ throw new LinuxRepoError('invalid-input', `${fieldArg} is outside 1970-01-01..2106-02-07`, {
17
+ field: fieldArg,
18
+ });
19
+ }
20
+ }
21
+
22
+ export function assertBytes(valueArg: unknown, fieldArg: string): asserts valueArg is Uint8Array {
23
+ if (!(valueArg instanceof Uint8Array)) {
24
+ throw new LinuxRepoError('invalid-input', `${fieldArg} must be a Uint8Array`, { field: fieldArg });
25
+ }
26
+ }
27
+
28
+ /** Refuses anything that is not an object with a `Symbol.asyncIterator` method; the chunks are checked as they arrive. */
29
+ export function assertAsyncIterable(valueArg: unknown, fieldArg: string): asserts valueArg is AsyncIterable<unknown> {
30
+ if (
31
+ valueArg === null ||
32
+ typeof valueArg !== 'object' ||
33
+ typeof (valueArg as Partial<AsyncIterable<unknown>>)[Symbol.asyncIterator] !== 'function'
34
+ ) {
35
+ throw new LinuxRepoError('invalid-input', `${fieldArg} must be an AsyncIterable<Uint8Array>`, { field: fieldArg });
36
+ }
37
+ }
38
+
39
+ export function assertString(valueArg: unknown, fieldArg: string): asserts valueArg is string {
40
+ if (typeof valueArg !== 'string') {
41
+ throw new LinuxRepoError('invalid-input', `${fieldArg} must be a string`, { field: fieldArg });
42
+ }
43
+ }
44
+
45
+ /** True when the text contains a C0 control character, DEL or a C1 control character. */
46
+ export function hasControlCharacter(textArg: string): boolean {
47
+ return /\p{Cc}/u.test(textArg);
48
+ }
49
+
50
+ /**
51
+ * True when the text can name one path segment of a served file: non-empty,
52
+ * not `.` or `..`, no `/` and no control characters.
53
+ */
54
+ export function isPathSegment(valueArg: string): boolean {
55
+ return valueArg !== '' && valueArg !== '.' && valueArg !== '..' && !valueArg.includes('/') && !hasControlCharacter(valueArg);
56
+ }
57
+
58
+ /**
59
+ * A name that becomes one path or tar-entry segment: non-empty, no `/`, no
60
+ * control characters.
61
+ */
62
+ export function assertSegmentName(valueArg: unknown, fieldArg: string): asserts valueArg is string {
63
+ assertString(valueArg, fieldArg);
64
+ if (valueArg.length === 0 || valueArg.includes('/') || hasControlCharacter(valueArg)) {
65
+ throw new LinuxRepoError(
66
+ 'invalid-input',
67
+ `${fieldArg} must be non-empty and contain no "/" or control characters`,
68
+ { field: fieldArg },
69
+ );
70
+ }
71
+ }
@@ -0,0 +1,110 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { LinuxRepoError } from '../shared/errors.js';
3
+ import { assertSegmentName, assertString, assertTimestamp, hasControlCharacter } from '../shared/validation.js';
4
+
5
+ /** An Alpine key pair in Gitea's stored forms. */
6
+ export interface IAlpineKeyPair {
7
+ /** PKCS#1 PEM (`RSA PRIVATE KEY`). */
8
+ privateKeyPem: string;
9
+ /** PKIX/SPKI PEM (`PUBLIC KEY`). */
10
+ publicKeyPem: string;
11
+ }
12
+
13
+ /** An OpenPGP key pair, armored. */
14
+ export interface IOpenPgpKeyPair {
15
+ privateKeyArmored: string;
16
+ publicKeyArmored: string;
17
+ /** The v4 fingerprint, 40 uppercase hex digits. */
18
+ fingerprint: string;
19
+ }
20
+
21
+ const RSA_BITS = 4096;
22
+
23
+ /**
24
+ * Generates an RSA-4096 Alpine key pair in Gitea's forms (Gitea
25
+ * `util.GenerateKeyPair(4096)`: PKCS#1 private key, PKIX public key, e=65537).
26
+ * The codec stores nothing. A failure inside node:crypto propagates unchanged.
27
+ */
28
+ export async function generateAlpineKeyPair(): Promise<IAlpineKeyPair> {
29
+ const { privateKey, publicKey } = await new Promise<{ privateKey: string; publicKey: string }>((resolve, reject) => {
30
+ plugins.crypto.generateKeyPair(
31
+ 'rsa',
32
+ {
33
+ modulusLength: RSA_BITS,
34
+ publicExponent: 0x10001,
35
+ privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
36
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
37
+ },
38
+ (error, publicKeyArg, privateKeyArg) => {
39
+ if (error) {
40
+ reject(error);
41
+ } else {
42
+ resolve({ privateKey: privateKeyArg, publicKey: publicKeyArg });
43
+ }
44
+ },
45
+ );
46
+ });
47
+ return { privateKeyPem: privateKey, publicKeyPem: publicKey };
48
+ }
49
+
50
+ /**
51
+ * Generates an RSA-4096 OpenPGP v4 key pair (primary key for certification and
52
+ * signing, RSA-4096 encryption subkey, no expiry) whose creation time is
53
+ * `timestamp` and whose single user ID is exactly `userId`. Bad input is
54
+ * `invalid-input`; a failure inside openpgp.js propagates unchanged.
55
+ */
56
+ export async function generateOpenPgpKeyPair(inputArg: { userId: string; timestamp: Date }): Promise<IOpenPgpKeyPair> {
57
+ if (inputArg === null || typeof inputArg !== 'object') {
58
+ throw new LinuxRepoError('invalid-input', 'input must be an object', { field: 'input' });
59
+ }
60
+ assertString(inputArg.userId, 'userId');
61
+ if (inputArg.userId.length === 0 || hasControlCharacter(inputArg.userId)) {
62
+ throw new LinuxRepoError('invalid-input', 'userId must be non-empty and contain no control characters', {
63
+ field: 'userId',
64
+ });
65
+ }
66
+ assertTimestamp(inputArg.timestamp);
67
+ const generated = await plugins.openpgp.generateKey({
68
+ type: 'rsa',
69
+ rsaBits: RSA_BITS,
70
+ userIDs: [{ name: inputArg.userId }],
71
+ date: inputArg.timestamp,
72
+ format: 'object',
73
+ config: { v6Keys: false },
74
+ });
75
+ return {
76
+ privateKeyArmored: generated.privateKey.armor(),
77
+ publicKeyArmored: generated.publicKey.armor(),
78
+ fingerprint: generated.privateKey.getFingerprint().toUpperCase(),
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Gitea's Alpine key name: `<ownerLowerName>@<hex>.rsa.pub`, where `<hex>` is
84
+ * the lowercase hex SHA-256 of the PKIX (SPKI) DER of the RSA public key
85
+ * (Gitea `util.CreatePublicKeyFingerprint`). Pass the owner's lower-case name.
86
+ */
87
+ export function giteaAlpineKeyName(ownerLowerName: string, publicKeyPem: string): string {
88
+ assertSegmentName(ownerLowerName, 'ownerLowerName');
89
+ assertString(publicKeyPem, 'publicKeyPem');
90
+ const label = /-----BEGIN ([A-Z0-9 ]+)-----/.exec(publicKeyPem)?.[1];
91
+ if (label !== 'PUBLIC KEY') {
92
+ throw new LinuxRepoError('invalid-key', 'the Alpine public key must be a PKIX "PUBLIC KEY" PEM', {
93
+ field: 'publicKeyPem',
94
+ });
95
+ }
96
+ let key: plugins.crypto.KeyObject;
97
+ try {
98
+ key = plugins.crypto.createPublicKey({ key: publicKeyPem, format: 'pem' });
99
+ } catch (error) {
100
+ throw new LinuxRepoError('invalid-key', 'the Alpine public key cannot be read', { field: 'publicKeyPem' }, {
101
+ cause: error,
102
+ });
103
+ }
104
+ if (key.asymmetricKeyType !== 'rsa') {
105
+ throw new LinuxRepoError('invalid-key', 'the Alpine public key must be an RSA key', { field: 'publicKeyPem' });
106
+ }
107
+ const der = key.export({ type: 'spki', format: 'der' });
108
+ const hex = plugins.crypto.createHash('sha256').update(der).digest('hex');
109
+ return `${ownerLowerName}@${hex}.rsa.pub`;
110
+ }
@@ -0,0 +1,115 @@
1
+ import { LinuxRepoError } from '../shared/errors.js';
2
+ import { assertAsyncIterable, assertBytes, assertString, assertTimestamp } from '../shared/validation.js';
3
+ import { singleChunk, type IOpenPgpSigner } from './signers.js';
4
+
5
+ /**
6
+ * The OpenPGP framing the codec owns: only the bytes of the signature packet
7
+ * may vary; framing and armor are deterministic.
8
+ * Armor has no headers, 64-column base64 and a CRC-24 line.
9
+ */
10
+
11
+ const BASE64_LINE = 64;
12
+
13
+ /**
14
+ * Signs the concatenation of the chunks of `data` and returns the signer's
15
+ * binary packet unchanged (Arch `.sig`, RPM header tags). The signer reads
16
+ * `data` to its end exactly once.
17
+ */
18
+ export async function createDetachedBinarySignature(inputArg: {
19
+ data: AsyncIterable<Uint8Array>;
20
+ signer: IOpenPgpSigner;
21
+ timestamp: Date;
22
+ }): Promise<Uint8Array> {
23
+ assertAsyncIterable(inputArg.data, 'data');
24
+ assertTimestamp(inputArg.timestamp);
25
+ const packet = await inputArg.signer.signBinary(inputArg.data, inputArg.timestamp);
26
+ assertSignatureBytes(packet);
27
+ return packet;
28
+ }
29
+
30
+ /** Signs `data` and returns an armored detached signature (`Release.gpg`, `repomd.xml.asc`). */
31
+ export async function createDetachedArmoredSignature(inputArg: {
32
+ data: Uint8Array;
33
+ signer: IOpenPgpSigner;
34
+ timestamp: Date;
35
+ }): Promise<string> {
36
+ assertBytes(inputArg.data, 'data');
37
+ return armorSignature(
38
+ await createDetachedBinarySignature({
39
+ data: singleChunk(inputArg.data),
40
+ signer: inputArg.signer,
41
+ timestamp: inputArg.timestamp,
42
+ }),
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Cleartext-signs `text` (`InRelease`). The text must be canonical: no CR and
48
+ * no trailing spaces or tabs on any line. The signed text is `text` without
49
+ * its final line break; the output always ends the text with one line break
50
+ * before the signature block, as GnuPG and Go do. Lines starting with `-` are
51
+ * dash-escaped. The header is always `Hash: SHA256`.
52
+ */
53
+ export async function createCleartextSignedMessage(inputArg: {
54
+ text: string;
55
+ signer: IOpenPgpSigner;
56
+ timestamp: Date;
57
+ }): Promise<string> {
58
+ assertCanonicalText(inputArg.text);
59
+ assertTimestamp(inputArg.timestamp);
60
+ const signedText = inputArg.text.endsWith('\n') ? inputArg.text.slice(0, -1) : inputArg.text;
61
+ const packet = await inputArg.signer.signText(signedText, inputArg.timestamp);
62
+ assertSignatureBytes(packet);
63
+ const body = `${signedText}\n`.replace(/^-/gm, '- -');
64
+ return `-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\n${body}${armorSignature(packet)}`;
65
+ }
66
+
67
+ /** ASCII armor for one signature packet list. */
68
+ export function armorSignature(packetArg: Uint8Array): string {
69
+ const base64 = Buffer.from(packetArg.buffer, packetArg.byteOffset, packetArg.byteLength).toString('base64');
70
+ const lines: string[] = [];
71
+ for (let offset = 0; offset < base64.length; offset += BASE64_LINE) {
72
+ lines.push(base64.slice(offset, offset + BASE64_LINE));
73
+ }
74
+ return [
75
+ '-----BEGIN PGP SIGNATURE-----',
76
+ '',
77
+ ...lines,
78
+ `=${crc24Base64(packetArg)}`,
79
+ '-----END PGP SIGNATURE-----',
80
+ '',
81
+ ].join('\n');
82
+ }
83
+
84
+ /** Refuses text that is not canonical cleartext input. */
85
+ export function assertCanonicalText(textArg: unknown): asserts textArg is string {
86
+ assertString(textArg, 'text');
87
+ if (textArg.includes('\r')) {
88
+ throw new LinuxRepoError('invalid-input', 'cleartext must use LF line endings', { field: 'text' });
89
+ }
90
+ if (/[ \t]$/m.test(textArg)) {
91
+ throw new LinuxRepoError('invalid-input', 'cleartext lines must not end with spaces or tabs', { field: 'text' });
92
+ }
93
+ }
94
+
95
+ function assertSignatureBytes(packetArg: unknown): asserts packetArg is Uint8Array {
96
+ if (!(packetArg instanceof Uint8Array) || packetArg.length === 0) {
97
+ throw new LinuxRepoError('signing-failed', 'the signer returned no signature bytes', { field: 'signer' });
98
+ }
99
+ }
100
+
101
+ /** RFC 4880 section 6.1 CRC-24, base64-encoded. */
102
+ function crc24Base64(bytesArg: Uint8Array): string {
103
+ let crc = 0xb704ce;
104
+ for (let i = 0; i < bytesArg.length; i++) {
105
+ crc ^= bytesArg[i] << 16;
106
+ for (let bit = 0; bit < 8; bit++) {
107
+ crc <<= 1;
108
+ if (crc & 0x1000000) {
109
+ crc ^= 0x1864cfb;
110
+ }
111
+ }
112
+ }
113
+ crc &= 0xffffff;
114
+ return Buffer.from([(crc >> 16) & 0xff, (crc >> 8) & 0xff, crc & 0xff]).toString('base64');
115
+ }