@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 +21 -0
- package/README.md +3 -2
- package/package.json +13 -3
- package/src/tar.d.mts +18 -0
- package/src/tar.mjs +127 -0
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
|
-
#
|
|
1
|
+
# @mosetta/tar
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
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
|
+
}
|