@gjsify/tar 0.3.12 → 0.3.14

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.
@@ -1,102 +1,105 @@
1
+ import { parseTar } from "./parser.js";
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
3
- import { parseTar } from "./parser.js";
4
+
5
+ //#region src/extract.ts
6
+ /** Extract a `.tar` or `.tar.gz` buffer into `destDir`, creating dirs as needed. */
4
7
  async function extractTarball(input, destDir, opts = {}) {
5
- const buf = input instanceof Uint8Array ? input : new Uint8Array(input);
6
- const isGz = opts.gzip ?? (buf.length >= 2 && buf[0] === 31 && buf[1] === 139);
7
- const tarBytes = isGz ? await gunzip(buf) : buf;
8
- const entries = parseTar(tarBytes);
9
- fs.mkdirSync(destDir, { recursive: true });
10
- const strip = opts.strip ?? 1;
11
- const preventEscape = opts.preventEscape ?? true;
12
- const result = { files: [], directories: [], symlinks: [], skipped: 0 };
13
- for (const entry of entries) {
14
- const stripped = stripComponents(entry.name, strip);
15
- if (stripped === null || stripped === "") {
16
- result.skipped++;
17
- continue;
18
- }
19
- const resolved = path.resolve(destDir, stripped);
20
- if (preventEscape && !isInside(resolved, destDir)) {
21
- throw new Error(
22
- `tar: refusing to extract ${entry.name} outside ${destDir} (resolved=${resolved})`
23
- );
24
- }
25
- if (opts.filter && !opts.filter(entry, resolved)) {
26
- result.skipped++;
27
- continue;
28
- }
29
- if (entry.type === "directory") {
30
- fs.mkdirSync(resolved, { recursive: true });
31
- result.directories.push(resolved);
32
- continue;
33
- }
34
- if (entry.type === "file") {
35
- fs.mkdirSync(path.dirname(resolved), { recursive: true });
36
- fs.writeFileSync(resolved, entry.body);
37
- const overrideMode = opts.chmod?.(entry, resolved);
38
- const finalMode = overrideMode ?? entry.mode & 511;
39
- if (finalMode > 0) {
40
- try {
41
- fs.chmodSync(resolved, finalMode);
42
- } catch {
43
- }
44
- }
45
- result.files.push(resolved);
46
- continue;
47
- }
48
- if (entry.type === "symlink") {
49
- fs.mkdirSync(path.dirname(resolved), { recursive: true });
50
- try {
51
- fs.symlinkSync(entry.linkname, resolved);
52
- result.symlinks.push(resolved);
53
- } catch {
54
- fs.writeFileSync(resolved, entry.linkname);
55
- result.files.push(resolved);
56
- }
57
- continue;
58
- }
59
- result.skipped++;
60
- }
61
- return result;
8
+ const buf = input instanceof Uint8Array ? input : new Uint8Array(input);
9
+ const isGz = opts.gzip ?? (buf.length >= 2 && buf[0] === 31 && buf[1] === 139);
10
+ const tarBytes = isGz ? await gunzip(buf) : buf;
11
+ const entries = parseTar(tarBytes);
12
+ fs.mkdirSync(destDir, { recursive: true });
13
+ const strip = opts.strip ?? 1;
14
+ const preventEscape = opts.preventEscape ?? true;
15
+ const result = {
16
+ files: [],
17
+ directories: [],
18
+ symlinks: [],
19
+ skipped: 0
20
+ };
21
+ for (const entry of entries) {
22
+ const stripped = stripComponents(entry.name, strip);
23
+ if (stripped === null || stripped === "") {
24
+ result.skipped++;
25
+ continue;
26
+ }
27
+ const resolved = path.resolve(destDir, stripped);
28
+ if (preventEscape && !isInside(resolved, destDir)) {
29
+ throw new Error(`tar: refusing to extract ${entry.name} outside ${destDir} (resolved=${resolved})`);
30
+ }
31
+ if (opts.filter && !opts.filter(entry, resolved)) {
32
+ result.skipped++;
33
+ continue;
34
+ }
35
+ if (entry.type === "directory") {
36
+ fs.mkdirSync(resolved, { recursive: true });
37
+ result.directories.push(resolved);
38
+ continue;
39
+ }
40
+ if (entry.type === "file") {
41
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
42
+ fs.writeFileSync(resolved, entry.body);
43
+ const overrideMode = opts.chmod?.(entry, resolved);
44
+ const finalMode = overrideMode ?? entry.mode & 511;
45
+ if (finalMode > 0) {
46
+ try {
47
+ fs.chmodSync(resolved, finalMode);
48
+ } catch {}
49
+ }
50
+ result.files.push(resolved);
51
+ continue;
52
+ }
53
+ if (entry.type === "symlink") {
54
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
55
+ try {
56
+ fs.symlinkSync(entry.linkname, resolved);
57
+ result.symlinks.push(resolved);
58
+ } catch {
59
+ fs.writeFileSync(resolved, entry.linkname);
60
+ result.files.push(resolved);
61
+ }
62
+ continue;
63
+ }
64
+ result.skipped++;
65
+ }
66
+ return result;
62
67
  }
68
+ /** Decompress a gzip buffer using Web DecompressionStream (cross-platform). */
63
69
  async function gunzip(input) {
64
- const Decomp = globalThis.DecompressionStream;
65
- if (typeof Decomp !== "function") {
66
- throw new Error(
67
- "@gjsify/tar: globalThis.DecompressionStream is not available \u2014 import '@gjsify/compression-streams/register' on GJS to register it"
68
- );
69
- }
70
- const stream = new Blob([new Uint8Array(input)]).stream().pipeThrough(new Decomp("gzip"));
71
- const chunks = [];
72
- let total = 0;
73
- const reader = stream.getReader();
74
- for (; ; ) {
75
- const { value, done } = await reader.read();
76
- if (done) break;
77
- const chunk = value instanceof Uint8Array ? value : new Uint8Array(value);
78
- chunks.push(chunk);
79
- total += chunk.length;
80
- }
81
- const out = new Uint8Array(total);
82
- let pos = 0;
83
- for (const c of chunks) {
84
- out.set(c, pos);
85
- pos += c.length;
86
- }
87
- return out;
70
+ const Decomp = globalThis.DecompressionStream;
71
+ if (typeof Decomp !== "function") {
72
+ throw new Error("@gjsify/tar: globalThis.DecompressionStream is not available — " + "import '@gjsify/compression-streams/register' on GJS to register it");
73
+ }
74
+ const stream = new Blob([new Uint8Array(input)]).stream().pipeThrough(new Decomp("gzip"));
75
+ const chunks = [];
76
+ let total = 0;
77
+ const reader = stream.getReader();
78
+ for (;;) {
79
+ const { value, done } = await reader.read();
80
+ if (done) break;
81
+ const chunk = value instanceof Uint8Array ? value : new Uint8Array(value);
82
+ chunks.push(chunk);
83
+ total += chunk.length;
84
+ }
85
+ const out = new Uint8Array(total);
86
+ let pos = 0;
87
+ for (const c of chunks) {
88
+ out.set(c, pos);
89
+ pos += c.length;
90
+ }
91
+ return out;
88
92
  }
89
93
  function stripComponents(name, n) {
90
- if (n <= 0) return name;
91
- const parts = name.split("/").filter((s) => s !== "");
92
- if (parts.length <= n) return null;
93
- return parts.slice(n).join("/");
94
+ if (n <= 0) return name;
95
+ const parts = name.split("/").filter((s) => s !== "");
96
+ if (parts.length <= n) return null;
97
+ return parts.slice(n).join("/");
94
98
  }
95
99
  function isInside(child, parent) {
96
- const rel = path.relative(parent, child);
97
- return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
100
+ const rel = path.relative(parent, child);
101
+ return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
98
102
  }
99
- export {
100
- extractTarball,
101
- gunzip
102
- };
103
+
104
+ //#endregion
105
+ export { extractTarball, gunzip };
package/lib/esm/index.js CHANGED
@@ -1,13 +1,4 @@
1
- import {
2
- BLOCK_SIZE,
3
- parseTar,
4
- TarParseError
5
- } from "./parser.js";
1
+ import { BLOCK_SIZE, TarParseError, parseTar } from "./parser.js";
6
2
  import { extractTarball, gunzip } from "./extract.js";
7
- export {
8
- BLOCK_SIZE,
9
- TarParseError,
10
- extractTarball,
11
- gunzip,
12
- parseTar
13
- };
3
+
4
+ export { BLOCK_SIZE, TarParseError, extractTarball, gunzip, parseTar };
package/lib/esm/parser.js CHANGED
@@ -1,199 +1,188 @@
1
+ //#region src/parser.ts
1
2
  const BLOCK_SIZE = 512;
3
+ /** Parse a tar archive (already-uncompressed) into entries. */
2
4
  function parseTar(buf) {
3
- const out = [];
4
- let pendingPaxHeader = null;
5
- let pendingLongName = null;
6
- let pendingLongLink = null;
7
- let offset = 0;
8
- while (offset + BLOCK_SIZE <= buf.length) {
9
- const header = buf.subarray(offset, offset + BLOCK_SIZE);
10
- if (allZeros(header)) {
11
- const next = buf.subarray(offset + BLOCK_SIZE, offset + 2 * BLOCK_SIZE);
12
- if (next.length === BLOCK_SIZE && allZeros(next)) break;
13
- offset += BLOCK_SIZE;
14
- continue;
15
- }
16
- if (!validateChecksum(header)) {
17
- throw new TarParseError(
18
- `Bad header checksum at offset ${offset} \u2014 file is not a valid tar archive`
19
- );
20
- }
21
- const rawName = readString(header, 0, 100);
22
- const mode = parseOctal(header, 100, 8);
23
- const size = parseOctal(header, 124, 12);
24
- const mtime = parseOctal(header, 136, 12);
25
- const typeflag = String.fromCharCode(header[156] || 0);
26
- const linkname = readString(header, 157, 100);
27
- const magic = readString(header, 257, 6);
28
- const prefix = readString(header, 345, 155);
29
- const uname = readString(header, 265, 32);
30
- const gname = readString(header, 297, 32);
31
- offset += BLOCK_SIZE;
32
- const body = buf.subarray(offset, offset + size);
33
- offset += alignToBlock(size);
34
- if (typeflag === "x") {
35
- pendingPaxHeader = parsePaxRecords(body);
36
- continue;
37
- }
38
- if (typeflag === "g") {
39
- continue;
40
- }
41
- if (typeflag === "L") {
42
- pendingLongName = bytesToString(body).replace(/\0+$/, "");
43
- continue;
44
- }
45
- if (typeflag === "K") {
46
- pendingLongLink = bytesToString(body).replace(/\0+$/, "");
47
- continue;
48
- }
49
- let name = rawName;
50
- if (magic === "ustar" && prefix !== "") {
51
- name = `${prefix}/${rawName}`;
52
- }
53
- if (pendingLongName !== null) {
54
- name = pendingLongName;
55
- pendingLongName = null;
56
- }
57
- let resolvedLink = linkname;
58
- if (pendingLongLink !== null) {
59
- resolvedLink = pendingLongLink;
60
- pendingLongLink = null;
61
- }
62
- if (pendingPaxHeader !== null) {
63
- const paxName = pendingPaxHeader.get("path");
64
- if (paxName !== void 0) name = paxName;
65
- const paxLink = pendingPaxHeader.get("linkpath");
66
- if (paxLink !== void 0) resolvedLink = paxLink;
67
- const paxSize = pendingPaxHeader.get("size");
68
- if (paxSize !== void 0) {
69
- const overrideSize = Number(paxSize);
70
- if (Number.isFinite(overrideSize)) {
71
- const realStart = offset - alignToBlock(size);
72
- const sliced = buf.subarray(realStart, realStart + overrideSize);
73
- offset = realStart + alignToBlock(overrideSize);
74
- pendingPaxHeader = null;
75
- out.push(buildEntry(name, resolvedLink, typeflag, mode, mtime, uname, gname, sliced));
76
- continue;
77
- }
78
- }
79
- pendingPaxHeader = null;
80
- }
81
- out.push(buildEntry(name, resolvedLink, typeflag, mode, mtime, uname, gname, body));
82
- }
83
- return out;
5
+ const out = [];
6
+ let pendingPaxHeader = null;
7
+ let pendingLongName = null;
8
+ let pendingLongLink = null;
9
+ let offset = 0;
10
+ while (offset + 512 <= buf.length) {
11
+ const header = buf.subarray(offset, offset + 512);
12
+ if (allZeros(header)) {
13
+ const next = buf.subarray(offset + 512, offset + 2 * 512);
14
+ if (next.length === 512 && allZeros(next)) break;
15
+ offset += 512;
16
+ continue;
17
+ }
18
+ if (!validateChecksum(header)) {
19
+ throw new TarParseError(`Bad header checksum at offset ${offset} — file is not a valid tar archive`);
20
+ }
21
+ const rawName = readString(header, 0, 100);
22
+ const mode = parseOctal(header, 100, 8);
23
+ const size = parseOctal(header, 124, 12);
24
+ const mtime = parseOctal(header, 136, 12);
25
+ const typeflag = String.fromCharCode(header[156] || 0);
26
+ const linkname = readString(header, 157, 100);
27
+ const magic = readString(header, 257, 6);
28
+ const prefix = readString(header, 345, 155);
29
+ const uname = readString(header, 265, 32);
30
+ const gname = readString(header, 297, 32);
31
+ offset += 512;
32
+ const body = buf.subarray(offset, offset + size);
33
+ offset += alignToBlock(size);
34
+ if (typeflag === "x") {
35
+ pendingPaxHeader = parsePaxRecords(body);
36
+ continue;
37
+ }
38
+ if (typeflag === "g") {
39
+ continue;
40
+ }
41
+ if (typeflag === "L") {
42
+ pendingLongName = bytesToString(body).replace(/\0+$/, "");
43
+ continue;
44
+ }
45
+ if (typeflag === "K") {
46
+ pendingLongLink = bytesToString(body).replace(/\0+$/, "");
47
+ continue;
48
+ }
49
+ let name = rawName;
50
+ if (magic === "ustar" && prefix !== "") {
51
+ name = `${prefix}/${rawName}`;
52
+ }
53
+ if (pendingLongName !== null) {
54
+ name = pendingLongName;
55
+ pendingLongName = null;
56
+ }
57
+ let resolvedLink = linkname;
58
+ if (pendingLongLink !== null) {
59
+ resolvedLink = pendingLongLink;
60
+ pendingLongLink = null;
61
+ }
62
+ if (pendingPaxHeader !== null) {
63
+ const paxName = pendingPaxHeader.get("path");
64
+ if (paxName !== undefined) name = paxName;
65
+ const paxLink = pendingPaxHeader.get("linkpath");
66
+ if (paxLink !== undefined) resolvedLink = paxLink;
67
+ const paxSize = pendingPaxHeader.get("size");
68
+ if (paxSize !== undefined) {
69
+ const overrideSize = Number(paxSize);
70
+ if (Number.isFinite(overrideSize)) {
71
+ const realStart = offset - alignToBlock(size);
72
+ const sliced = buf.subarray(realStart, realStart + overrideSize);
73
+ offset = realStart + alignToBlock(overrideSize);
74
+ pendingPaxHeader = null;
75
+ out.push(buildEntry(name, resolvedLink, typeflag, mode, mtime, uname, gname, sliced));
76
+ continue;
77
+ }
78
+ }
79
+ pendingPaxHeader = null;
80
+ }
81
+ out.push(buildEntry(name, resolvedLink, typeflag, mode, mtime, uname, gname, body));
82
+ }
83
+ return out;
84
84
  }
85
85
  function buildEntry(name, linkname, typeflag, mode, mtime, uname, gname, body) {
86
- return {
87
- name,
88
- linkname,
89
- type: typeflagToType(typeflag, name),
90
- mode,
91
- mtime,
92
- body,
93
- uname,
94
- gname
95
- };
86
+ return {
87
+ name,
88
+ linkname,
89
+ type: typeflagToType(typeflag, name),
90
+ mode,
91
+ mtime,
92
+ body,
93
+ uname,
94
+ gname
95
+ };
96
96
  }
97
97
  function typeflagToType(typeflag, name) {
98
- switch (typeflag) {
99
- case "0":
100
- case "\0":
101
- case "":
102
- return name.endsWith("/") ? "directory" : "file";
103
- case "1":
104
- return "hardlink";
105
- case "2":
106
- return "symlink";
107
- case "5":
108
- return "directory";
109
- case "x":
110
- return "pax-header";
111
- case "g":
112
- return "pax-global";
113
- case "L":
114
- return "gnu-longname";
115
- case "K":
116
- return "gnu-longlink";
117
- default:
118
- return "unknown";
119
- }
98
+ switch (typeflag) {
99
+ case "0":
100
+ case "\0":
101
+ case "": return name.endsWith("/") ? "directory" : "file";
102
+ case "1": return "hardlink";
103
+ case "2": return "symlink";
104
+ case "5": return "directory";
105
+ case "x": return "pax-header";
106
+ case "g": return "pax-global";
107
+ case "L": return "gnu-longname";
108
+ case "K": return "gnu-longlink";
109
+ default: return "unknown";
110
+ }
120
111
  }
121
112
  function parsePaxRecords(body) {
122
- const out = /* @__PURE__ */ new Map();
123
- let i = 0;
124
- while (i < body.length) {
125
- let space = i;
126
- while (space < body.length && body[space] !== 32) space++;
127
- if (space >= body.length) break;
128
- const lenStr = bytesToString(body.subarray(i, space));
129
- const len = Number(lenStr);
130
- if (!Number.isFinite(len) || len <= 0) break;
131
- const recordEnd = i + len;
132
- if (recordEnd > body.length) break;
133
- const recBytes = body.subarray(space + 1, recordEnd - 1);
134
- const recText = bytesToString(recBytes);
135
- const eq = recText.indexOf("=");
136
- if (eq > 0) {
137
- const key = recText.slice(0, eq);
138
- const value = recText.slice(eq + 1);
139
- out.set(key, value);
140
- }
141
- i = recordEnd;
142
- }
143
- return out;
113
+ const out = new Map();
114
+ let i = 0;
115
+ while (i < body.length) {
116
+ let space = i;
117
+ while (space < body.length && body[space] !== 32) space++;
118
+ if (space >= body.length) break;
119
+ const lenStr = bytesToString(body.subarray(i, space));
120
+ const len = Number(lenStr);
121
+ if (!Number.isFinite(len) || len <= 0) break;
122
+ const recordEnd = i + len;
123
+ if (recordEnd > body.length) break;
124
+ const recBytes = body.subarray(space + 1, recordEnd - 1);
125
+ const recText = bytesToString(recBytes);
126
+ const eq = recText.indexOf("=");
127
+ if (eq > 0) {
128
+ const key = recText.slice(0, eq);
129
+ const value = recText.slice(eq + 1);
130
+ out.set(key, value);
131
+ }
132
+ i = recordEnd;
133
+ }
134
+ return out;
144
135
  }
145
136
  function readString(buf, start, len) {
146
- let end = start;
147
- const limit = start + len;
148
- while (end < limit && buf[end] !== 0) end++;
149
- return bytesToString(buf.subarray(start, end));
137
+ let end = start;
138
+ const limit = start + len;
139
+ while (end < limit && buf[end] !== 0) end++;
140
+ return bytesToString(buf.subarray(start, end));
150
141
  }
151
142
  function bytesToString(buf) {
152
- return new TextDecoder("utf-8", { fatal: false }).decode(buf);
143
+ return new TextDecoder("utf-8", { fatal: false }).decode(buf);
153
144
  }
154
145
  function parseOctal(buf, start, len) {
155
- if (len > 0 && (buf[start] & 128) !== 0) {
156
- let n = buf[start] & 127;
157
- for (let i = 1; i < len; i++) n = n * 256 + buf[start + i];
158
- return n;
159
- }
160
- let s = "";
161
- for (let i = 0; i < len; i++) {
162
- const c = buf[start + i];
163
- if (c === 0 || c === 32) continue;
164
- s += String.fromCharCode(c);
165
- }
166
- if (s === "") return 0;
167
- return parseInt(s, 8);
146
+ if (len > 0 && (buf[start] & 128) !== 0) {
147
+ let n = buf[start] & 127;
148
+ for (let i = 1; i < len; i++) n = n * 256 + buf[start + i];
149
+ return n;
150
+ }
151
+ let s = "";
152
+ for (let i = 0; i < len; i++) {
153
+ const c = buf[start + i];
154
+ if (c === 0 || c === 32) continue;
155
+ s += String.fromCharCode(c);
156
+ }
157
+ if (s === "") return 0;
158
+ return parseInt(s, 8);
168
159
  }
169
160
  function alignToBlock(n) {
170
- return Math.ceil(n / BLOCK_SIZE) * BLOCK_SIZE;
161
+ return Math.ceil(n / 512) * 512;
171
162
  }
172
163
  function allZeros(buf) {
173
- for (let i = 0; i < buf.length; i++) {
174
- if (buf[i] !== 0) return false;
175
- }
176
- return true;
164
+ for (let i = 0; i < buf.length; i++) {
165
+ if (buf[i] !== 0) return false;
166
+ }
167
+ return true;
177
168
  }
178
169
  function validateChecksum(header) {
179
- const stored = parseOctal(header, 148, 8);
180
- let unsigned = 0;
181
- let signed = 0;
182
- for (let i = 0; i < BLOCK_SIZE; i++) {
183
- const byte = i >= 148 && i < 156 ? 32 : header[i];
184
- unsigned += byte;
185
- signed += byte > 127 ? byte - 256 : byte;
186
- }
187
- return stored === unsigned || stored === signed;
170
+ const stored = parseOctal(header, 148, 8);
171
+ let unsigned = 0;
172
+ let signed = 0;
173
+ for (let i = 0; i < 512; i++) {
174
+ const byte = i >= 148 && i < 156 ? 32 : header[i];
175
+ unsigned += byte;
176
+ signed += byte > 127 ? byte - 256 : byte;
177
+ }
178
+ return stored === unsigned || stored === signed;
188
179
  }
189
- class TarParseError extends Error {
190
- constructor(msg) {
191
- super(msg);
192
- this.name = "TarParseError";
193
- }
194
- }
195
- export {
196
- BLOCK_SIZE,
197
- TarParseError,
198
- parseTar
180
+ var TarParseError = class extends Error {
181
+ constructor(msg) {
182
+ super(msg);
183
+ this.name = "TarParseError";
184
+ }
199
185
  };
186
+
187
+ //#endregion
188
+ export { BLOCK_SIZE, TarParseError, parseTar };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/tar",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "description": "Streaming .tar / .tar.gz reader for the gjsify install backend (Node + GJS)",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -34,8 +34,8 @@
34
34
  ],
35
35
  "license": "MIT",
36
36
  "devDependencies": {
37
- "@gjsify/cli": "^0.3.12",
38
- "@gjsify/unit": "^0.3.12",
37
+ "@gjsify/cli": "^0.3.14",
38
+ "@gjsify/unit": "^0.3.14",
39
39
  "typescript": "^6.0.3"
40
40
  }
41
41
  }