@mosetta/tar 0.0.0-stage → 0.0.10

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mosetta contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,4 @@
1
- # Temporary Holding Version
1
+ # @mosetta/tar
2
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.
3
+ A small tar reader and writer (ustar) with no dependencies — how builds and releases travel between
4
+ a publisher, a platform's API and its build container.
package/package.json CHANGED
@@ -1,6 +1,16 @@
1
1
  {
2
2
  "name": "@mosetta/tar",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.10",
4
+ "license": "MIT",
5
+ "description": "Portable tar and gzip with no dependencies: a ustar/pax/GNU-longname reader, a deterministic ustar writer, and gzip over Web streams. Runs unchanged in node, workerd and the browser.",
6
+ "type": "module",
7
+ "files": [
8
+ "src"
9
+ ],
10
+ "exports": {
11
+ ".": "./src/tar.mjs"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ }
6
16
  }
package/src/tar.d.mts ADDED
@@ -0,0 +1,18 @@
1
+ // Types for the tar module: this package ships plain JS on purpose (it runs untranspiled in
2
+ // isolates and in node), so its types live beside it — a consumer must not have to declare
3
+ // them again. They used to be an ambient shim inside @mosetta/server, invisible to everyone else.
4
+
5
+ export interface ITarEntry {
6
+ name: string;
7
+ data: Uint8Array;
8
+ }
9
+
10
+ export function parseTar(bytes: Uint8Array): ITarEntry[];
11
+
12
+ // The producers allocate their own buffer — the narrow `ArrayBuffer` parameter keeps the result
13
+ // usable everywhere a BodyInit is expected (a plain `Uint8Array` is `ArrayBufferLike` and is not).
14
+ export function writeTar(entries: ITarEntry[]): Uint8Array<ArrayBuffer>;
15
+
16
+ export function gzip(bytes: Uint8Array): Promise<Uint8Array<ArrayBuffer>>;
17
+
18
+ export function gunzip(bytes: Uint8Array): Promise<Uint8Array<ArrayBuffer>>;
package/src/tar.mjs ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Tar with no dependencies and no node (build-action): exactly the slice of the format the
3
+ * build body needs — read GitHub/registry archives and assemble the outer container.
4
+ *
5
+ * Reader: ustar (including the prefix field), pax `path=` headers (how GitHub and modern tar
6
+ * write long names) and GNU longname 'L'. Directories and special files are skipped — the tree
7
+ * is derived from file names. Writer: ustar with long-name splitting into prefix and a ZERO
8
+ * mtime — the archive is deterministic by content.
9
+ *
10
+ * Compression — standard Web streams (`CompressionStream`/`DecompressionStream`, present in
11
+ * both workerd and node): no runtime compilation and no native modules by construction.
12
+ */
13
+
14
+ const BLOCK = 512;
15
+ const td = new TextDecoder();
16
+ const te = new TextEncoder();
17
+
18
+ function str(bytes, off, len) {
19
+ let end = off;
20
+ const max = off + len;
21
+ while (end < max && bytes[end] !== 0) end++;
22
+ return td.decode(bytes.subarray(off, end));
23
+ }
24
+
25
+ function octal(bytes, off, len) {
26
+ const s = str(bytes, off, len).trim();
27
+ return s ? parseInt(s, 8) : 0;
28
+ }
29
+
30
+ /** File entries of the archive: [{ name, data: Uint8Array }]. */
31
+ export function parseTar(bytes) {
32
+ const entries = [];
33
+ let pos = 0;
34
+ let longName = null; // name from a pax/'L' entry — applies to the NEXT entry
35
+ while (pos + BLOCK <= bytes.length) {
36
+ const block = bytes.subarray(pos, pos + BLOCK);
37
+ if (block.every((b) => b === 0)) break;
38
+ const size = octal(bytes, pos + 124, 12);
39
+ const type = String.fromCharCode(bytes[pos + 156] || 0x30);
40
+ let name = str(bytes, pos, 100);
41
+ const prefix = str(bytes, pos + 345, 155);
42
+ if (prefix) name = `${prefix}/${name}`;
43
+ const data = bytes.subarray(pos + BLOCK, pos + BLOCK + size);
44
+ pos += BLOCK + Math.ceil(size / BLOCK) * BLOCK;
45
+ if (type === 'L') {
46
+ longName = td.decode(data).replace(/\0+$/, '');
47
+ continue;
48
+ }
49
+ if (type === 'x' || type === 'g') {
50
+ const m = /(?:^|\n)\d+ path=([^\n]*)\n/.exec(td.decode(data));
51
+ if (type === 'x' && m) longName = m[1];
52
+ continue;
53
+ }
54
+ if (longName) {
55
+ name = longName;
56
+ longName = null;
57
+ }
58
+ if (type === '0') entries.push({ name, data });
59
+ }
60
+ return entries;
61
+ }
62
+
63
+ function headerFor(name, size) {
64
+ const h = new Uint8Array(BLOCK);
65
+ let base = name;
66
+ let prefix = '';
67
+ if (te.encode(name).length > 100) {
68
+ // ustar split: the last slash where the tail fits into 100 and the head into 155
69
+ const parts = name.split('/');
70
+ for (let i = 1; i < parts.length; i++) {
71
+ const head = parts.slice(0, i).join('/');
72
+ const tail = parts.slice(i).join('/');
73
+ if (te.encode(tail).length <= 100 && te.encode(head).length <= 155) {
74
+ prefix = head;
75
+ base = tail;
76
+ }
77
+ }
78
+ if (!prefix) throw new Error(`name does not fit into ustar: ${name}`);
79
+ }
80
+ const put = (s, off) => h.set(te.encode(s), off);
81
+ put(base, 0);
82
+ put('0000644\0', 100);
83
+ put('0000000\0', 108);
84
+ put('0000000\0', 116);
85
+ put(`${size.toString(8).padStart(11, '0')}\0`, 124);
86
+ put('00000000000\0', 136); // mtime = 0: archive determinism
87
+ put(' ', 148); // the checksum is computed with spaces in its place
88
+ put('0', 156);
89
+ put('ustar\0' + '00', 257);
90
+ put(prefix, 345);
91
+ let sum = 0;
92
+ for (const b of h) sum += b;
93
+ put(`${sum.toString(8).padStart(6, '0')}\0 `, 148);
94
+ return h;
95
+ }
96
+
97
+ /** Assemble an archive from [{ name, data: Uint8Array }]. */
98
+ export function writeTar(entries) {
99
+ const parts = [];
100
+ for (const { name, data } of entries) {
101
+ parts.push(headerFor(name, data.length), data);
102
+ const pad = (BLOCK - (data.length % BLOCK)) % BLOCK;
103
+ if (pad) parts.push(new Uint8Array(pad));
104
+ }
105
+ parts.push(new Uint8Array(BLOCK * 2));
106
+ const total = parts.reduce((n, p) => n + p.length, 0);
107
+ const out = new Uint8Array(total);
108
+ let pos = 0;
109
+ for (const p of parts) {
110
+ out.set(p, pos);
111
+ pos += p.length;
112
+ }
113
+ return out;
114
+ }
115
+
116
+ async function pipe(bytes, stream) {
117
+ const res = new Response(new Blob([bytes]).stream().pipeThrough(stream));
118
+ return new Uint8Array(await res.arrayBuffer());
119
+ }
120
+
121
+ export function gunzip(bytes) {
122
+ return pipe(bytes, new DecompressionStream('gzip'));
123
+ }
124
+
125
+ export function gzip(bytes) {
126
+ return pipe(bytes, new CompressionStream('gzip'));
127
+ }