@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,315 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { concatBytes } from '../readers/bytes.js';
3
+ import { LinuxRepoError } from '../shared/errors.js';
4
+ import {
5
+ assertAsyncIterable,
6
+ assertBytes,
7
+ assertSegmentName,
8
+ assertString,
9
+ assertTimestamp,
10
+ } from '../shared/validation.js';
11
+
12
+ /**
13
+ * Signs Alpine indexes. Consumers may implement it; adding a required member
14
+ * later needs a major release.
15
+ */
16
+ export interface IAlpineSigner {
17
+ /**
18
+ * The key name apk looks up in `/etc/apk/keys`; the index signature entry is
19
+ * `.SIGN.RSA.<keyName>`. Gitea's form is `<owner>@<sha256 hex of the SPKI
20
+ * DER public key>.rsa.pub` (see `giteaAlpineKeyName`).
21
+ */
22
+ readonly keyName: string;
23
+ /** RSASSA-PKCS1-v1_5 over SHA-1 of `data`; returns the raw signature bytes. */
24
+ sign(data: Uint8Array): Promise<Uint8Array>;
25
+ }
26
+
27
+ /**
28
+ * Signs Debian, RPM and Arch documents. Consumers may implement it; adding a
29
+ * required member later needs a major release.
30
+ *
31
+ * Contract: each call returns exactly one binary OpenPGP v4 signature packet
32
+ * (packet header included, no armor), hashed with SHA-256, whose creation time
33
+ * is `timestamp` (whole seconds). The codec does all framing (armor, cleartext
34
+ * dash-escaping, the `Hash: SHA256` header) and treats the packet as opaque.
35
+ */
36
+ export interface IOpenPgpSigner {
37
+ /**
38
+ * A signature of type 0x00 (binary document) over the concatenation of the
39
+ * chunks of `data`. The signer reads `data` to its end exactly once.
40
+ */
41
+ signBinary(data: AsyncIterable<Uint8Array>, timestamp: Date): Promise<Uint8Array>;
42
+ /**
43
+ * A signature of type 0x01 (canonical text document) over `text`. The codec
44
+ * passes canonical text only: LF line endings and no trailing spaces or tabs
45
+ * on any line; the signature hashes it with CRLF line endings.
46
+ */
47
+ signText(text: string, timestamp: Date): Promise<Uint8Array>;
48
+ }
49
+
50
+ const ALPINE_PRIVATE_KEY_LABELS = ['RSA PRIVATE KEY', 'PRIVATE KEY'];
51
+
52
+ /**
53
+ * Builds an Alpine signer from an unencrypted RSA private key in PEM form:
54
+ * PKCS#1 (`RSA PRIVATE KEY`, Gitea's stored form) or PKCS#8 (`PRIVATE KEY`).
55
+ * Anything else is `invalid-key`. Signatures are deterministic.
56
+ */
57
+ export function createAlpineSigner(inputArg: { privateKeyPem: string; keyName: string }): IAlpineSigner {
58
+ if (inputArg === null || typeof inputArg !== 'object') {
59
+ throw new LinuxRepoError('invalid-input', 'input must be an object', { field: 'input' });
60
+ }
61
+ assertString(inputArg.privateKeyPem, 'privateKeyPem');
62
+ assertSegmentName(inputArg.keyName, 'keyName');
63
+ const label = /-----BEGIN ([A-Z0-9 ]+)-----/.exec(inputArg.privateKeyPem)?.[1];
64
+ if (label === undefined || !ALPINE_PRIVATE_KEY_LABELS.includes(label)) {
65
+ throw new LinuxRepoError('invalid-key', 'the Alpine key must be an unencrypted PKCS#1 or PKCS#8 RSA private key PEM', {
66
+ field: 'privateKeyPem',
67
+ });
68
+ }
69
+ let key: plugins.crypto.KeyObject;
70
+ try {
71
+ key = plugins.crypto.createPrivateKey({ key: inputArg.privateKeyPem, format: 'pem' });
72
+ } catch (error) {
73
+ throw new LinuxRepoError('invalid-key', 'the Alpine private key cannot be read', { field: 'privateKeyPem' }, {
74
+ cause: error,
75
+ });
76
+ }
77
+ if (key.asymmetricKeyType !== 'rsa') {
78
+ throw new LinuxRepoError('invalid-key', 'the Alpine key must be an RSA key', { field: 'privateKeyPem' });
79
+ }
80
+ const keyName = inputArg.keyName;
81
+ return {
82
+ keyName,
83
+ sign: async (data: Uint8Array): Promise<Uint8Array> => {
84
+ assertBytes(data, 'data');
85
+ try {
86
+ const signature = plugins.crypto.sign('sha1', data, {
87
+ key,
88
+ padding: plugins.crypto.constants.RSA_PKCS1_PADDING,
89
+ });
90
+ return new Uint8Array(signature.buffer, signature.byteOffset, signature.byteLength);
91
+ } catch (error) {
92
+ throw new LinuxRepoError('signing-failed', 'RSA PKCS#1 v1.5 SHA-1 signing failed', { field: 'keyName' }, {
93
+ cause: error,
94
+ });
95
+ }
96
+ },
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Builds an OpenPGP signer from an armored private key: exactly one key,
102
+ * OpenPGP v4, not encrypted, able to sign. Anything else is `invalid-key`.
103
+ * Signatures are SHA-256 and keep openpgp.js's salt notation, so their bytes
104
+ * vary per call; the produced packet is checked against the signer contract.
105
+ *
106
+ * The factory makes one trial `signBinary` and refuses the key with
107
+ * `invalid-key` when it fails, so a key that cannot meet the contract at all
108
+ * (one whose curve makes openpgp.js choose a stronger hash than SHA-256) is
109
+ * refused here. The trial signs at the latest creation time of the key, its
110
+ * subkeys and their self-signatures; a failure that depends on the signing
111
+ * time stays `signing-failed` at signing time.
112
+ */
113
+ export async function createOpenPgpSigner(inputArg: { privateKeyArmored: string }): Promise<IOpenPgpSigner> {
114
+ if (inputArg === null || typeof inputArg !== 'object') {
115
+ throw new LinuxRepoError('invalid-input', 'input must be an object', { field: 'input' });
116
+ }
117
+ assertString(inputArg.privateKeyArmored, 'privateKeyArmored');
118
+ // openpgp.js reads the first armor block and ignores anything after it
119
+ const armorBlocks = inputArg.privateKeyArmored.match(/^-----BEGIN PGP [A-Z ,/0-9]+-----\s*$/gm) ?? [];
120
+ if (armorBlocks.length > 1) {
121
+ throw new LinuxRepoError('invalid-key', `expected one armored OpenPGP private key block, got ${armorBlocks.length}`, {
122
+ field: 'privateKeyArmored',
123
+ });
124
+ }
125
+ let keys: plugins.openpgp.PrivateKey[];
126
+ try {
127
+ keys = await plugins.openpgp.readPrivateKeys({ armoredKeys: inputArg.privateKeyArmored });
128
+ } catch (error) {
129
+ throw new LinuxRepoError('invalid-key', 'the OpenPGP private key cannot be read', { field: 'privateKeyArmored' }, {
130
+ cause: error,
131
+ });
132
+ }
133
+ if (keys.length !== 1) {
134
+ throw new LinuxRepoError('invalid-key', `expected exactly one OpenPGP private key, got ${keys.length}`, {
135
+ field: 'privateKeyArmored',
136
+ });
137
+ }
138
+ const key = keys[0];
139
+ if (key.keyPacket.version !== 4) {
140
+ throw new LinuxRepoError('invalid-key', `only OpenPGP v4 keys are accepted, got v${key.keyPacket.version}`, {
141
+ field: 'privateKeyArmored',
142
+ });
143
+ }
144
+ if (!key.isDecrypted()) {
145
+ throw new LinuxRepoError('invalid-key', 'encrypted OpenPGP keys are not accepted', { field: 'privateKeyArmored' });
146
+ }
147
+ const sign = async (
148
+ produceArg: () => Promise<Uint8Array>,
149
+ timestampArg: Date,
150
+ signatureTypeArg: number,
151
+ inputFailureArg: () => { error: unknown } | undefined = () => undefined,
152
+ ): Promise<Uint8Array> => {
153
+ let packet: Uint8Array;
154
+ try {
155
+ packet = await produceArg();
156
+ } catch (error) {
157
+ // an invalid chunk or the caller's own source error, not a signing failure
158
+ const inputFailure = inputFailureArg();
159
+ if (inputFailure !== undefined) {
160
+ throw inputFailure.error;
161
+ }
162
+ throw new LinuxRepoError('signing-failed', 'OpenPGP signing failed', { field: 'signingKey' }, { cause: error });
163
+ }
164
+ await assertSignaturePacket(packet, timestampArg, signatureTypeArg);
165
+ return packet;
166
+ };
167
+ const config = { preferredHashAlgorithm: plugins.openpgp.enums.hash.sha256 };
168
+ const signer: IOpenPgpSigner = {
169
+ signBinary: async (data: AsyncIterable<Uint8Array>, timestamp: Date): Promise<Uint8Array> => {
170
+ assertAsyncIterable(data, 'data');
171
+ assertTimestamp(timestamp);
172
+ const source = new CheckedChunkSource(data);
173
+ return sign(
174
+ async () => {
175
+ const signature = await plugins.openpgp.sign({
176
+ message: await plugins.openpgp.createMessage({ binary: source.toReadableStream() }),
177
+ signingKeys: key,
178
+ detached: true,
179
+ format: 'binary',
180
+ date: timestamp,
181
+ config,
182
+ });
183
+ const packet = await readAll(signature);
184
+ if (!source.ended) {
185
+ throw new Error('openpgp.js returned a signature before reading the data to its end');
186
+ }
187
+ return packet;
188
+ },
189
+ timestamp,
190
+ plugins.openpgp.enums.signature.binary,
191
+ () => source.failure,
192
+ );
193
+ },
194
+ signText: async (text: string, timestamp: Date): Promise<Uint8Array> => {
195
+ assertString(text, 'text');
196
+ assertTimestamp(timestamp);
197
+ return sign(
198
+ async () =>
199
+ plugins.openpgp.sign({
200
+ message: await plugins.openpgp.createMessage({ text }),
201
+ signingKeys: key,
202
+ detached: true,
203
+ format: 'binary',
204
+ date: timestamp,
205
+ config,
206
+ }),
207
+ timestamp,
208
+ plugins.openpgp.enums.signature.text,
209
+ );
210
+ },
211
+ };
212
+ try {
213
+ await signer.signBinary(singleChunk(new Uint8Array(0)), latestKeyTime(key));
214
+ } catch (error) {
215
+ throw new LinuxRepoError('invalid-key', 'the OpenPGP key cannot sign a v4 SHA-256 signature', { field: 'privateKeyArmored' }, {
216
+ cause: error,
217
+ });
218
+ }
219
+ return signer;
220
+ }
221
+
222
+ /** `bytesArg` as a one-chunk source, for callers that hold the whole document. */
223
+ export async function* singleChunk(bytesArg: Uint8Array): AsyncGenerator<Uint8Array> {
224
+ yield bytesArg;
225
+ }
226
+
227
+ /**
228
+ * Pulls the caller's source with `next()` only (never `return()` or
229
+ * `throw()`, so a cancelled stream never cancels the source), refuses a chunk
230
+ * that is not a `Uint8Array` with `invalid-input`, and records the first
231
+ * error it raised or met so the signer can rethrow it unchanged.
232
+ */
233
+ class CheckedChunkSource {
234
+ public ended = false;
235
+ public failure: { error: unknown } | undefined = undefined;
236
+ private readonly source: AsyncIterable<unknown>;
237
+
238
+ constructor(sourceArg: AsyncIterable<unknown>) {
239
+ this.source = sourceArg;
240
+ }
241
+
242
+ public toReadableStream(): plugins.streamWeb.ReadableStream<Uint8Array> {
243
+ const iterator = this.source[Symbol.asyncIterator]();
244
+ const next = async (): Promise<IteratorResult<Uint8Array>> => {
245
+ let result: IteratorResult<unknown>;
246
+ try {
247
+ result = await iterator.next();
248
+ } catch (error) {
249
+ this.failure ??= { error };
250
+ throw error;
251
+ }
252
+ if (result.done === true) {
253
+ this.ended = true;
254
+ return { done: true, value: undefined };
255
+ }
256
+ if (!(result.value instanceof Uint8Array)) {
257
+ const error = new LinuxRepoError('invalid-input', 'every chunk of data must be a Uint8Array', { field: 'data' });
258
+ this.failure ??= { error };
259
+ throw error;
260
+ }
261
+ return { done: false, value: result.value };
262
+ };
263
+ return plugins.streamWeb.ReadableStream.from<Uint8Array>({ [Symbol.asyncIterator]: () => ({ next }) });
264
+ }
265
+ }
266
+
267
+ /** Reads openpgp.js's streamed output to its end. */
268
+ async function readAll(streamArg: plugins.streamWeb.ReadableStream<Uint8Array>): Promise<Uint8Array> {
269
+ const reader = streamArg.getReader();
270
+ const chunks: Uint8Array[] = [];
271
+ let total = 0;
272
+ for (let result = await reader.read(); !result.done; result = await reader.read()) {
273
+ chunks.push(result.value);
274
+ total += result.value.length;
275
+ }
276
+ return concatBytes(chunks, total);
277
+ }
278
+
279
+ /** The latest creation time of the key, its subkeys and their self-signatures: the key's current state applies. */
280
+ function latestKeyTime(keyArg: plugins.openpgp.PrivateKey): Date {
281
+ const times = [keyArg.getCreationTime().getTime()];
282
+ for (const user of keyArg.users) {
283
+ for (const signature of user.selfCertifications) {
284
+ times.push(signature.created?.getTime() ?? 0);
285
+ }
286
+ }
287
+ for (const subkey of keyArg.subkeys) {
288
+ times.push(subkey.getCreationTime().getTime());
289
+ for (const signature of subkey.bindingSignatures) {
290
+ times.push(signature.created?.getTime() ?? 0);
291
+ }
292
+ }
293
+ return new Date(Math.max(...times));
294
+ }
295
+
296
+ /** Checks that openpgp.js produced exactly the packet the signer contract promises. */
297
+ async function assertSignaturePacket(packetArg: Uint8Array, timestampArg: Date, signatureTypeArg: number): Promise<void> {
298
+ const signature = await plugins.openpgp.readSignature({ binarySignature: packetArg });
299
+ const packets = signature.packets;
300
+ const expectedSeconds = Math.floor(timestampArg.getTime() / 1000);
301
+ const ok =
302
+ packets.length === 1 &&
303
+ packets[0].version === 4 &&
304
+ packets[0].signatureType === signatureTypeArg &&
305
+ packets[0].hashAlgorithm === plugins.openpgp.enums.hash.sha256 &&
306
+ packets[0].created !== null &&
307
+ Math.floor(packets[0].created.getTime() / 1000) === expectedSeconds;
308
+ if (!ok) {
309
+ throw new LinuxRepoError(
310
+ 'signing-failed',
311
+ 'the signing key cannot produce one v4 SHA-256 signature packet at the requested time',
312
+ { field: 'signingKey' },
313
+ );
314
+ }
315
+ }
@@ -0,0 +1,256 @@
1
+ import { concatBytes } from '../readers/bytes.js';
2
+ import { LinuxRepoError } from '../shared/errors.js';
3
+ import { goPathJoin } from '../shared/gopath.js';
4
+
5
+ export interface IGoTarFile {
6
+ name: string;
7
+ mode: number;
8
+ data: Uint8Array;
9
+ }
10
+
11
+ export interface IGoTarWriteOptions {
12
+ /**
13
+ * The two zero blocks Go `Writer.Close` writes. `false` for Gitea's Alpine
14
+ * signature member (`writeGzipStream` with `addTarEnd=false`). Gitea never
15
+ * calls `Close` there, so Go also leaves the last entry unpadded; the codec
16
+ * pads every entry to the block regardless (a difference from Gitea).
17
+ */
18
+ endOfArchive: boolean;
19
+ }
20
+
21
+ const BLOCK = 512;
22
+ const NAME_SIZE = 100;
23
+ const PREFIX_SIZE = 155;
24
+ /** Go `maxSpecialFileSize`: the largest PAX record block Go writes or reads. */
25
+ const MAX_SPECIAL_FILE_SIZE = 1 << 20;
26
+ /** Go `fitsInOctal(8, x)` for the mode field: below 8^7. */
27
+ const MODE_LIMIT = 8 ** 7;
28
+ /** Go `fitsInOctal(12, x)` for the size field: below 8^11 (8 GiB). */
29
+ const SIZE_LIMIT = 8 ** 11;
30
+ const encoder = new TextEncoder();
31
+
32
+ /**
33
+ * The subset of Go 1.27 `archive/tar` `Writer` that Gitea 28.2.0 uses for its
34
+ * index archives (Arch `services/packages/arch/repository.go:381-399`, Alpine
35
+ * `services/packages/alpine/repository.go:331-351`): regular files from
36
+ * `tar.Header{Name, Mode, Size}` with every other field zero, so the zero
37
+ * `ModTime` is written as 0, uid and gid are 0 and user and group names are
38
+ * empty.
39
+ *
40
+ * Format choice follows Go's `allowedFormats` and `WriteHeader`: USTAR when the
41
+ * name is ASCII and at most 100 bytes, USTAR with a prefix split when the name
42
+ * splits, else PAX with one `path` record (refused when the records exceed
43
+ * Go's 1 MiB `maxSpecialFileSize`, as Go returns `ErrFieldTooLong`).
44
+ *
45
+ * Refused with `invalid-input` where Go would not write a regular file in USTAR
46
+ * or PAX-`path` form:
47
+ * - a name with NUL (Go fails with an invalid PAX record) or ending with `/`
48
+ * (Go refuses it for a regular file; Gitea's untyped header would become a
49
+ * directory entry instead);
50
+ * - a mode outside 7 octal digits (Go falls back to the GNU format);
51
+ * - a size of 8 GiB or more (Go adds a PAX `size` record; the codec does not);
52
+ * - a string that is not well-formed UTF-16 (Go strings never carry one).
53
+ */
54
+ export function writeGoTar(
55
+ filesArg: readonly IGoTarFile[],
56
+ optionsArg: IGoTarWriteOptions = { endOfArchive: true },
57
+ ): Uint8Array {
58
+ const parts: Uint8Array[] = [];
59
+ for (const file of filesArg) {
60
+ parts.push(goTarHeader(file.name, file.mode, file.data.length), file.data, new Uint8Array(padding(file.data.length)));
61
+ }
62
+ if (optionsArg.endOfArchive) {
63
+ parts.push(new Uint8Array(BLOCK * 2));
64
+ }
65
+ return concatBytes(parts);
66
+ }
67
+
68
+ /**
69
+ * The bytes Go `Writer.WriteHeader` writes for a regular file: the main
70
+ * header block, preceded by a PAX `x` header and its padded records when the
71
+ * name needs one.
72
+ */
73
+ export function goTarHeader(nameArg: string, modeArg: number, sizeArg: number): Uint8Array {
74
+ if (!nameArg.isWellFormed()) {
75
+ throw new LinuxRepoError('invalid-input', 'a tar entry name is not well-formed UTF-16', { field: 'name' });
76
+ }
77
+ if (nameArg.includes('\0')) {
78
+ throw new LinuxRepoError('invalid-input', 'a tar entry name contains NUL', { field: 'name' });
79
+ }
80
+ if (nameArg.endsWith('/')) {
81
+ throw new LinuxRepoError('invalid-input', 'a regular tar entry name ends with /', { field: 'name' });
82
+ }
83
+ if (!Number.isSafeInteger(modeArg) || modeArg < 0 || modeArg >= MODE_LIMIT) {
84
+ throw new LinuxRepoError('invalid-input', 'a tar entry mode does not fit the USTAR mode field', {
85
+ field: 'mode',
86
+ });
87
+ }
88
+ if (!Number.isSafeInteger(sizeArg) || sizeArg < 0 || sizeArg >= SIZE_LIMIT) {
89
+ throw new LinuxRepoError('invalid-input', 'a tar entry size does not fit the USTAR size field', {
90
+ field: 'size',
91
+ });
92
+ }
93
+ const nameBytes = encoder.encode(nameArg);
94
+ if (isAscii(nameBytes) && nameBytes.length <= NAME_SIZE) {
95
+ return header({ name: nameBytes, prefix: null, size: sizeArg, mode: modeArg, typeflag: '0' });
96
+ }
97
+ const split = splitUstarPath(nameBytes);
98
+ if (split !== null) {
99
+ return header({ name: split.name, prefix: split.prefix, size: sizeArg, mode: modeArg, typeflag: '0' });
100
+ }
101
+ return paxHeader(nameArg, modeArg, sizeArg);
102
+ }
103
+
104
+ /** Go `splitUSTARPath` (`archive/tar/writer.go:454`). */
105
+ function splitUstarPath(nameArg: Uint8Array): { prefix: Uint8Array; name: Uint8Array } | null {
106
+ let length = nameArg.length;
107
+ if (length <= NAME_SIZE || !isAscii(nameArg)) {
108
+ return null;
109
+ } else if (length > PREFIX_SIZE + 1) {
110
+ length = PREFIX_SIZE + 1;
111
+ } else if (nameArg[length - 1] === 0x2f) {
112
+ length--;
113
+ }
114
+ const i = nameArg.subarray(0, length).lastIndexOf(0x2f);
115
+ const nameLength = nameArg.length - i - 1;
116
+ const prefixLength = i;
117
+ if (i <= 0 || nameLength > NAME_SIZE || nameLength === 0 || prefixLength > PREFIX_SIZE) {
118
+ return null;
119
+ }
120
+ return { prefix: nameArg.subarray(0, i), name: nameArg.subarray(i + 1) };
121
+ }
122
+
123
+ /**
124
+ * Go `writePAXHeader` (`archive/tar/writer.go:131`) for one `path` record:
125
+ * the `x` header written by `writeRawFile`, the records, their padding, then
126
+ * the main header with the ASCII-folded name.
127
+ */
128
+ function paxHeader(nameArg: string, modeArg: number, sizeArg: number): Uint8Array {
129
+ const records = paxRecord('path', nameArg);
130
+ if (records.length > MAX_SPECIAL_FILE_SIZE) {
131
+ throw new LinuxRepoError('invalid-input', 'a tar entry name is longer than Go archive/tar writes', {
132
+ field: 'name',
133
+ });
134
+ }
135
+ // path.Split, then path.Join(dir, "PaxHeaders.0", file); writeRawFile folds
136
+ // it to ASCII, cuts it to 100 bytes and trims trailing slashes
137
+ const slash = nameArg.lastIndexOf('/');
138
+ const dir = nameArg.slice(0, slash + 1);
139
+ const file = nameArg.slice(slash + 1);
140
+ let paxName = encoder.encode(toAscii(goPathJoin([dir, 'PaxHeaders.0', file])));
141
+ if (paxName.length > NAME_SIZE) {
142
+ paxName = paxName.subarray(0, NAME_SIZE);
143
+ }
144
+ let end = paxName.length;
145
+ while (end > 0 && paxName[end - 1] === 0x2f) {
146
+ end--;
147
+ }
148
+ return concatBytes([
149
+ header({ name: paxName.subarray(0, end), prefix: null, size: records.length, mode: 0, typeflag: 'x', raw: true }),
150
+ records,
151
+ new Uint8Array(padding(records.length)),
152
+ header({ name: encoder.encode(toAscii(nameArg)), prefix: null, size: sizeArg, mode: modeArg, typeflag: '0' }),
153
+ ]);
154
+ }
155
+
156
+ /** Go `formatPAXRecord` (`archive/tar/strconv.go:291`). */
157
+ function paxRecord(keyArg: string, valueArg: string): Uint8Array {
158
+ const payload = encoder.encode(` ${keyArg}=${valueArg}\n`);
159
+ let size = payload.length;
160
+ size += String(size).length;
161
+ let record = concatBytes([encoder.encode(String(size)), payload]);
162
+ if (record.length !== size) {
163
+ size = record.length;
164
+ record = concatBytes([encoder.encode(String(size)), payload]);
165
+ }
166
+ return record;
167
+ }
168
+
169
+ interface IHeaderSpec {
170
+ name: Uint8Array;
171
+ prefix: Uint8Array | null;
172
+ size: number;
173
+ mode: number;
174
+ typeflag: '0' | 'x';
175
+ /** `writeRawFile` (`archive/tar/writer.go:353`): devmajor and devminor stay zero bytes. */
176
+ raw?: boolean;
177
+ }
178
+
179
+ /** Go `templateV7Plus` plus `setFormat` (`archive/tar/writer.go:323`, `format.go:205`). */
180
+ function header(specArg: IHeaderSpec): Uint8Array {
181
+ const block = new Uint8Array(BLOCK);
182
+ formatString(block, 0, NAME_SIZE, specArg.name);
183
+ formatOctal(block, 100, 8, specArg.mode);
184
+ formatOctal(block, 108, 8, 0); // uid
185
+ formatOctal(block, 116, 8, 0); // gid
186
+ formatOctal(block, 124, 12, specArg.size);
187
+ formatOctal(block, 136, 12, 0); // mtime: the zero time.Time is written as 0
188
+ block[156] = specArg.typeflag.charCodeAt(0);
189
+ // linkname, uname and gname stay empty
190
+ if (!specArg.raw) {
191
+ formatOctal(block, 329, 8, 0); // devmajor
192
+ formatOctal(block, 337, 8, 0); // devminor
193
+ }
194
+ if (specArg.prefix !== null) {
195
+ formatString(block, 345, PREFIX_SIZE, specArg.prefix);
196
+ }
197
+ block.set(encoder.encode('ustar\0'), 257);
198
+ block.set(encoder.encode('00'), 263);
199
+ let sum = 0;
200
+ for (let i = 0; i < BLOCK; i++) {
201
+ sum += i >= 148 && i < 156 ? 0x20 : block[i];
202
+ }
203
+ formatOctal(block, 148, 7, sum);
204
+ block[155] = 0x20;
205
+ return block;
206
+ }
207
+
208
+ /** Go `formatter.formatString` (`archive/tar/strconv.go:63`): copy, NUL-terminate when shorter, cut when longer. */
209
+ function formatString(blockArg: Uint8Array, offsetArg: number, lengthArg: number, valueArg: Uint8Array): void {
210
+ const field = blockArg.subarray(offsetArg, offsetArg + lengthArg);
211
+ field.set(valueArg.subarray(0, lengthArg));
212
+ if (valueArg.length < lengthArg) {
213
+ field[valueArg.length] = 0;
214
+ }
215
+ if (valueArg.length > lengthArg && field[lengthArg - 1] === 0x2f) {
216
+ let n = lengthArg - 1;
217
+ while (n > 0 && valueArg[n - 1] === 0x2f) {
218
+ n--;
219
+ }
220
+ field[n] = 0;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Go `formatter.formatOctal` (`archive/tar/strconv.go:176`): zero-padded octal
226
+ * leaving room for a NUL. Callers check the range first; Go would write 0.
227
+ */
228
+ function formatOctal(blockArg: Uint8Array, offsetArg: number, lengthArg: number, valueArg: number): void {
229
+ const digits = valueArg.toString(8).padStart(lengthArg - 1, '0');
230
+ formatString(blockArg, offsetArg, lengthArg, encoder.encode(digits));
231
+ }
232
+
233
+ /** Go `toASCII` (`archive/tar/strconv.go:32`): drops every non-ASCII rune (NUL is refused earlier). */
234
+ function toAscii(valueArg: string): string {
235
+ let result = '';
236
+ for (const char of valueArg) {
237
+ if (char.codePointAt(0)! < 0x80) {
238
+ result += char;
239
+ }
240
+ }
241
+ return result;
242
+ }
243
+
244
+ /** Go `isASCII` (`archive/tar/strconv.go:21`) for a name without NUL. */
245
+ function isAscii(bytesArg: Uint8Array): boolean {
246
+ for (let i = 0; i < bytesArg.length; i++) {
247
+ if (bytesArg[i] >= 0x80) {
248
+ return false;
249
+ }
250
+ }
251
+ return true;
252
+ }
253
+
254
+ function padding(sizeArg: number): number {
255
+ return (BLOCK - (sizeArg % BLOCK)) % BLOCK;
256
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.