typescript-virtual-container 1.5.5 → 1.5.6
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/README.md +106 -32
- package/dist/.tsbuildinfo +1 -1
- package/dist/commands/awk.d.ts +6 -11
- package/dist/commands/awk.js +462 -109
- package/dist/commands/bzip2.d.ts +11 -0
- package/dist/commands/bzip2.js +91 -0
- package/dist/commands/find.d.ts +2 -2
- package/dist/commands/find.js +212 -37
- package/dist/commands/lsof.d.ts +6 -0
- package/dist/commands/lsof.js +30 -0
- package/dist/commands/perl.d.ts +6 -0
- package/dist/commands/perl.js +76 -0
- package/dist/commands/registry.js +13 -0
- package/dist/commands/sed.d.ts +2 -2
- package/dist/commands/sed.js +216 -34
- package/dist/commands/sh.js +42 -0
- package/dist/commands/strace.d.ts +6 -0
- package/dist/commands/strace.js +26 -0
- package/dist/commands/tar.d.ts +2 -1
- package/dist/commands/tar.js +138 -52
- package/dist/commands/zip.d.ts +11 -0
- package/dist/commands/zip.js +232 -0
- package/dist/modules/linuxRootfs.js +1 -1
- package/dist/utils/expand.js +64 -0
- package/package.json +5 -4
- package/dist/self-standalone.d.ts +0 -1
- package/dist/self-standalone.js +0 -444
- package/dist/standalone-wo-sftp.d.ts +0 -1
- package/dist/standalone-wo-sftp.js +0 -30
- package/dist/standalone.d.ts +0 -1
- package/dist/standalone.js +0 -61
package/dist/commands/sed.js
CHANGED
|
@@ -1,22 +1,74 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ifFlag } from "./command-helpers";
|
|
2
2
|
import { resolvePath } from "./helpers";
|
|
3
3
|
/**
|
|
4
|
-
* Stream editor
|
|
4
|
+
* Stream editor — supports s/pat/rep/[gI], d, p, q, =, addresses (N, /re/, N,M, /re/,/re/, $).
|
|
5
5
|
* @category text
|
|
6
|
-
* @params ["-e <expr> [file]"
|
|
6
|
+
* @params ["[-n] [-e <expr>] [file]"]
|
|
7
7
|
*/
|
|
8
8
|
export const sedCommand = {
|
|
9
9
|
name: "sed",
|
|
10
10
|
description: "Stream editor for filtering and transforming text",
|
|
11
11
|
category: "text",
|
|
12
|
-
params: ["-e <expr> [file]"
|
|
12
|
+
params: ["[-n] [-e <expr>] [file]"],
|
|
13
13
|
run: ({ authUser, shell, cwd, args, stdin }) => {
|
|
14
14
|
const inPlace = ifFlag(args, ["-i"]);
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
15
|
+
const suppressAuto = ifFlag(args, ["-n"]);
|
|
16
|
+
// Collect all -e expressions and the first non-flag positional
|
|
17
|
+
const exprs = [];
|
|
18
|
+
let fileArg;
|
|
19
|
+
let i = 0;
|
|
20
|
+
while (i < args.length) {
|
|
21
|
+
const a = args[i];
|
|
22
|
+
if (a === "-e" || a === "--expression") {
|
|
23
|
+
i++;
|
|
24
|
+
if (args[i])
|
|
25
|
+
exprs.push(args[i]);
|
|
26
|
+
i++;
|
|
27
|
+
}
|
|
28
|
+
else if (a === "-n" || a === "-i") {
|
|
29
|
+
i++;
|
|
30
|
+
}
|
|
31
|
+
else if (a.startsWith("-e")) {
|
|
32
|
+
exprs.push(a.slice(2));
|
|
33
|
+
i++;
|
|
34
|
+
}
|
|
35
|
+
else if (!a.startsWith("-")) {
|
|
36
|
+
if (exprs.length === 0)
|
|
37
|
+
exprs.push(a);
|
|
38
|
+
else
|
|
39
|
+
fileArg = a;
|
|
40
|
+
i++;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
i++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// If only one positional collected as expr and no file yet, check for file after
|
|
47
|
+
// Re-parse: first non-flag that follows all -e is the file
|
|
48
|
+
if (exprs.length === 0)
|
|
19
49
|
return { stderr: "sed: no expression", exitCode: 1 };
|
|
50
|
+
// Re-check: if exprs[0] was set from positional, remaining positionals are files
|
|
51
|
+
{
|
|
52
|
+
let foundExprFromFlag = false;
|
|
53
|
+
let j = 0;
|
|
54
|
+
while (j < args.length) {
|
|
55
|
+
const a = args[j];
|
|
56
|
+
if (a === "-e" || a === "--expression") {
|
|
57
|
+
foundExprFromFlag = true;
|
|
58
|
+
j += 2;
|
|
59
|
+
}
|
|
60
|
+
else if (a.startsWith("-e")) {
|
|
61
|
+
foundExprFromFlag = true;
|
|
62
|
+
j++;
|
|
63
|
+
}
|
|
64
|
+
else
|
|
65
|
+
j++;
|
|
66
|
+
}
|
|
67
|
+
if (!foundExprFromFlag) {
|
|
68
|
+
// expr is first positional, file is second
|
|
69
|
+
fileArg = args.filter((a) => !a.startsWith("-")).slice(1)[0];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
20
72
|
let content = stdin ?? "";
|
|
21
73
|
if (fileArg) {
|
|
22
74
|
const p = resolvePath(cwd, fileArg);
|
|
@@ -24,32 +76,162 @@ export const sedCommand = {
|
|
|
24
76
|
content = shell.vfs.readFile(p);
|
|
25
77
|
}
|
|
26
78
|
catch {
|
|
27
|
-
return {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
79
|
+
return { stderr: `sed: ${fileArg}: No such file or directory`, exitCode: 1 };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function parseAddr(s) {
|
|
83
|
+
if (!s)
|
|
84
|
+
return [undefined, s];
|
|
85
|
+
if (s[0] === "$")
|
|
86
|
+
return [{ type: "last" }, s.slice(1)];
|
|
87
|
+
if (/^\d/.test(s)) {
|
|
88
|
+
const m = s.match(/^(\d+)(.*)/s);
|
|
89
|
+
if (m)
|
|
90
|
+
return [{ type: "line", n: parseInt(m[1], 10) }, m[2]];
|
|
91
|
+
}
|
|
92
|
+
if (s[0] === "/") {
|
|
93
|
+
const end = s.indexOf("/", 1);
|
|
94
|
+
if (end !== -1) {
|
|
95
|
+
try {
|
|
96
|
+
const re = new RegExp(s.slice(1, end));
|
|
97
|
+
return [{ type: "regex", re }, s.slice(end + 1)];
|
|
98
|
+
}
|
|
99
|
+
catch { /* bad regex */ }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return [undefined, s];
|
|
103
|
+
}
|
|
104
|
+
function parseInstrs(expr) {
|
|
105
|
+
const instrs = [];
|
|
106
|
+
// Split on unquoted semicolons or newlines
|
|
107
|
+
const parts = expr.split(/\n|(?<=^|[^\\]);/);
|
|
108
|
+
for (const raw of parts) {
|
|
109
|
+
const part = raw.trim();
|
|
110
|
+
if (!part || part.startsWith("#"))
|
|
111
|
+
continue;
|
|
112
|
+
let rest = part;
|
|
113
|
+
const [addr1, after1] = parseAddr(rest);
|
|
114
|
+
rest = after1.trim();
|
|
115
|
+
let addr2;
|
|
116
|
+
if (rest[0] === ",") {
|
|
117
|
+
rest = rest.slice(1).trim();
|
|
118
|
+
const [a2, after2] = parseAddr(rest);
|
|
119
|
+
addr2 = a2;
|
|
120
|
+
rest = after2.trim();
|
|
121
|
+
}
|
|
122
|
+
const op = rest[0];
|
|
123
|
+
if (!op)
|
|
124
|
+
continue;
|
|
125
|
+
if (op === "s") {
|
|
126
|
+
// s/from/to/flags
|
|
127
|
+
const delim = rest[1] ?? "/";
|
|
128
|
+
const sRe = new RegExp(`^s${re(delim)}((?:[^${re(delim)}\\\\]|\\\\.)*)${re(delim)}((?:[^${re(delim)}\\\\]|\\\\.)*)${re(delim)}([gGiIp]*)$`);
|
|
129
|
+
const m = rest.match(sRe);
|
|
130
|
+
if (!m) {
|
|
131
|
+
instrs.push({ op: "d", addr1, addr2 });
|
|
132
|
+
continue;
|
|
133
|
+
} // bad expr, skip
|
|
134
|
+
const flags = m[3] ?? "";
|
|
135
|
+
let from;
|
|
136
|
+
try {
|
|
137
|
+
from = new RegExp(m[1], flags.includes("i") || flags.includes("I") ? "i" : "");
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
instrs.push({ op: "s", addr1, addr2, from, to: m[2], global: flags.includes("g") || flags.includes("G"), print: flags.includes("p") });
|
|
143
|
+
}
|
|
144
|
+
else if (op === "d") {
|
|
145
|
+
instrs.push({ op: "d", addr1, addr2 });
|
|
146
|
+
}
|
|
147
|
+
else if (op === "p") {
|
|
148
|
+
instrs.push({ op: "p", addr1, addr2 });
|
|
149
|
+
}
|
|
150
|
+
else if (op === "q") {
|
|
151
|
+
instrs.push({ op: "q", addr1 });
|
|
152
|
+
}
|
|
153
|
+
else if (op === "=") {
|
|
154
|
+
instrs.push({ op: "=", addr1, addr2 });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return instrs;
|
|
158
|
+
}
|
|
159
|
+
function re(c) {
|
|
160
|
+
return c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
161
|
+
}
|
|
162
|
+
const allInstrs = exprs.flatMap(parseInstrs);
|
|
163
|
+
const lines = content.split("\n");
|
|
164
|
+
// Remove trailing empty string from trailing newline
|
|
165
|
+
if (lines[lines.length - 1] === "")
|
|
166
|
+
lines.pop();
|
|
167
|
+
const total = lines.length;
|
|
168
|
+
function matchesAddr(addr, lineNo, line) {
|
|
169
|
+
if (!addr)
|
|
170
|
+
return true;
|
|
171
|
+
if (addr.type === "line")
|
|
172
|
+
return lineNo === addr.n;
|
|
173
|
+
if (addr.type === "last")
|
|
174
|
+
return lineNo === total;
|
|
175
|
+
return addr.re.test(line);
|
|
176
|
+
}
|
|
177
|
+
function inRange(instr, lineNo, line, rangeActive) {
|
|
178
|
+
const { addr1, addr2 } = instr;
|
|
179
|
+
if (!addr1)
|
|
180
|
+
return true;
|
|
181
|
+
if (!addr2)
|
|
182
|
+
return matchesAddr(addr1, lineNo, line);
|
|
183
|
+
// Two-address range
|
|
184
|
+
let active = rangeActive.get(instr) ?? false;
|
|
185
|
+
if (!active && matchesAddr(addr1, lineNo, line)) {
|
|
186
|
+
active = true;
|
|
187
|
+
rangeActive.set(instr, true);
|
|
188
|
+
}
|
|
189
|
+
if (active && matchesAddr(addr2, lineNo, line)) {
|
|
190
|
+
rangeActive.set(instr, false);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
if (active)
|
|
194
|
+
return true;
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
const out = [];
|
|
198
|
+
const rangeActive = new Map();
|
|
199
|
+
let quit = false;
|
|
200
|
+
for (let li = 0; li < lines.length && !quit; li++) {
|
|
201
|
+
let line = lines[li];
|
|
202
|
+
const lineNo = li + 1;
|
|
203
|
+
let deleted = false;
|
|
204
|
+
for (const instr of allInstrs) {
|
|
205
|
+
if (!inRange(instr, lineNo, line, rangeActive))
|
|
206
|
+
continue;
|
|
207
|
+
if (instr.op === "d") {
|
|
208
|
+
deleted = true;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
if (instr.op === "p") {
|
|
212
|
+
out.push(line);
|
|
213
|
+
}
|
|
214
|
+
if (instr.op === "=") {
|
|
215
|
+
out.push(String(lineNo));
|
|
216
|
+
}
|
|
217
|
+
if (instr.op === "q") {
|
|
218
|
+
quit = true;
|
|
219
|
+
}
|
|
220
|
+
if (instr.op === "s") {
|
|
221
|
+
const replaced = instr.global
|
|
222
|
+
? line.replace(new RegExp(instr.from.source, instr.from.flags.includes("i") ? "gi" : "g"), instr.to)
|
|
223
|
+
: line.replace(instr.from, instr.to);
|
|
224
|
+
if (replaced !== line) {
|
|
225
|
+
line = replaced;
|
|
226
|
+
if (instr.print && suppressAuto)
|
|
227
|
+
out.push(line);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (!deleted && !suppressAuto)
|
|
232
|
+
out.push(line);
|
|
233
|
+
}
|
|
234
|
+
const result = out.join("\n") + (out.length > 0 ? "\n" : "");
|
|
53
235
|
if (inPlace && fileArg) {
|
|
54
236
|
const p = resolvePath(cwd, fileArg);
|
|
55
237
|
shell.writeFileAsUser(authUser, p, result);
|
package/dist/commands/sh.js
CHANGED
|
@@ -125,6 +125,32 @@ function parseBlocks(lines) {
|
|
|
125
125
|
}
|
|
126
126
|
blocks.push({ type: "while", cond, body });
|
|
127
127
|
}
|
|
128
|
+
else if (line.startsWith("until ")) {
|
|
129
|
+
const cond = line
|
|
130
|
+
.replace(/^until\s+/, "")
|
|
131
|
+
.replace(/;\s*do\s*$/, "")
|
|
132
|
+
.trim();
|
|
133
|
+
const body = [];
|
|
134
|
+
i++;
|
|
135
|
+
while (i < lines.length && lines[i]?.trim() !== "done") {
|
|
136
|
+
const l = lines[i].trim().replace(/^do\s+/, "");
|
|
137
|
+
if (l && l !== "do")
|
|
138
|
+
body.push(l);
|
|
139
|
+
i++;
|
|
140
|
+
}
|
|
141
|
+
blocks.push({ type: "until", cond, body });
|
|
142
|
+
}
|
|
143
|
+
else if (/^[A-Za-z_][A-Za-z0-9_]*=\s*\(/.test(line)) {
|
|
144
|
+
// Array assignment: arr=(elem1 elem2 ...)
|
|
145
|
+
const arrMatch = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=\s*\(([^)]*)\)$/);
|
|
146
|
+
if (arrMatch) {
|
|
147
|
+
const elems = arrMatch[2].trim().split(/\s+/).filter(Boolean);
|
|
148
|
+
blocks.push({ type: "array", name: arrMatch[1], elements: elems });
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
blocks.push({ type: "cmd", line });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
128
154
|
else if (line.startsWith("case ") && line.endsWith(" in") || line.match(/^case\s+.+\s+in$/)) {
|
|
129
155
|
const caseExpr = line.replace(/^case\s+/, "").replace(/\s+in$/, "").trim();
|
|
130
156
|
const patterns = [];
|
|
@@ -353,6 +379,22 @@ async function runBlocks(blocks, ctx) {
|
|
|
353
379
|
iterations++;
|
|
354
380
|
}
|
|
355
381
|
}
|
|
382
|
+
else if (block.type === "until") {
|
|
383
|
+
let iterations = 0;
|
|
384
|
+
while (iterations < 1000 && !(await evalCondition(block.cond, ctx))) {
|
|
385
|
+
const sub = await runBlocks(parseBlocks(block.body), ctx);
|
|
386
|
+
if (sub.stdout)
|
|
387
|
+
output += `${sub.stdout}\n`;
|
|
388
|
+
if (sub.closeSession)
|
|
389
|
+
return sub;
|
|
390
|
+
iterations++;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
else if (block.type === "array") {
|
|
394
|
+
// Store array: arr[0]=e0, arr[1]=e1, ..., arr=space-joined (for ${arr[@]})
|
|
395
|
+
block.elements.forEach((el, idx) => { ctx.env.vars[`${block.name}[${idx}]`] = el; });
|
|
396
|
+
ctx.env.vars[block.name] = block.elements.join(" ");
|
|
397
|
+
}
|
|
356
398
|
else if (block.type === "case") {
|
|
357
399
|
const expanded = await expandVars(block.expr, ctx.env.vars, ctx.env.lastExitCode, ctx);
|
|
358
400
|
for (const pat of block.patterns) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace system calls and signals (stub — runs command, emits fake strace output).
|
|
3
|
+
* @category system
|
|
4
|
+
*/
|
|
5
|
+
export const straceCommand = {
|
|
6
|
+
name: "strace",
|
|
7
|
+
description: "Trace system calls and signals",
|
|
8
|
+
category: "system",
|
|
9
|
+
params: ["[-e <expr>] [-o <file>] <command> [args]"],
|
|
10
|
+
run: ({ args }) => {
|
|
11
|
+
const cmd = args.find((a) => !a.startsWith("-"));
|
|
12
|
+
if (!cmd)
|
|
13
|
+
return { stderr: "strace: must have PROG [ARGS] or -p PID", exitCode: 1 };
|
|
14
|
+
const pid = Math.floor(Math.random() * 30000) + 1000;
|
|
15
|
+
const lines = [
|
|
16
|
+
`execve("/usr/bin/${cmd}", ["${cmd}"${args.slice(1).map((a) => `, "${a}"`).join("")}], 0x... /* ... vars */) = 0`,
|
|
17
|
+
`brk(NULL) = 0x${(Math.random() * 0xfffff | 0).toString(16)}000`,
|
|
18
|
+
`access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)`,
|
|
19
|
+
`openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3`,
|
|
20
|
+
`fstat(3, {st_mode=S_IFREG|0644, st_size=...}) = 0`,
|
|
21
|
+
`close(3) = 0`,
|
|
22
|
+
`+++ exited with 0 +++`,
|
|
23
|
+
];
|
|
24
|
+
return { stderr: lines.join("\n"), exitCode: 0 };
|
|
25
|
+
},
|
|
26
|
+
};
|
package/dist/commands/tar.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
2
|
/**
|
|
3
|
-
* Archive or extract files with tar
|
|
3
|
+
* Archive or extract files with tar — writes real POSIX ustar binary format.
|
|
4
|
+
* Supports -c/-x/-t, -z (gzip), -j (bzip2 stub), -v (verbose), -f.
|
|
4
5
|
* @category archive
|
|
5
6
|
* @params ["[-czf|-xzf|-tf] <archive> [files...]"]
|
|
6
7
|
*/
|
package/dist/commands/tar.js
CHANGED
|
@@ -1,6 +1,71 @@
|
|
|
1
|
+
import { gunzipSync, gzipSync } from "fflate";
|
|
1
2
|
import { resolvePath } from "./helpers";
|
|
3
|
+
// ── POSIX ustar tar format ────────────────────────────────────────────────────
|
|
4
|
+
function makeTarHeader(name, size, isDir) {
|
|
5
|
+
const hdr = Buffer.alloc(512);
|
|
6
|
+
const enc = (s, off, len) => {
|
|
7
|
+
const b = Buffer.from(s, "ascii");
|
|
8
|
+
b.copy(hdr, off, 0, Math.min(b.length, len));
|
|
9
|
+
};
|
|
10
|
+
enc(isDir ? `${name}/` : name, 0, 100);
|
|
11
|
+
enc(isDir ? "0000755\0" : "0000644\0", 100, 8);
|
|
12
|
+
enc("0000000\0", 108, 8);
|
|
13
|
+
enc("0000000\0", 116, 8);
|
|
14
|
+
enc(size.toString(8).padStart(11, "0") + "\0", 124, 12);
|
|
15
|
+
enc(Math.floor(Date.now() / 1000).toString(8).padStart(11, "0") + "\0", 136, 12);
|
|
16
|
+
hdr[156] = isDir ? 0x35 : 0x30; // '5' dir, '0' file
|
|
17
|
+
enc("ustar\0", 257, 6);
|
|
18
|
+
enc("00", 263, 2);
|
|
19
|
+
enc("root\0", 265, 32); // uname
|
|
20
|
+
enc("root\0", 297, 32); // gname
|
|
21
|
+
// Checksum: fill field with spaces, compute, write
|
|
22
|
+
for (let i = 148; i < 156; i++)
|
|
23
|
+
hdr[i] = 0x20;
|
|
24
|
+
let sum = 0;
|
|
25
|
+
for (let i = 0; i < 512; i++)
|
|
26
|
+
sum += hdr[i];
|
|
27
|
+
Buffer.from(`${sum.toString(8).padStart(6, "0")}\0 `).copy(hdr, 148);
|
|
28
|
+
return hdr;
|
|
29
|
+
}
|
|
30
|
+
function tarPad(size) {
|
|
31
|
+
const rem = size % 512;
|
|
32
|
+
return rem === 0 ? Buffer.alloc(0) : Buffer.alloc(512 - rem);
|
|
33
|
+
}
|
|
34
|
+
function buildTar(entries) {
|
|
35
|
+
const parts = [];
|
|
36
|
+
for (const { name, content, isDir } of entries) {
|
|
37
|
+
parts.push(makeTarHeader(name, isDir ? 0 : content.length, isDir));
|
|
38
|
+
if (!isDir) {
|
|
39
|
+
parts.push(content);
|
|
40
|
+
parts.push(tarPad(content.length));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
parts.push(Buffer.alloc(1024)); // end-of-archive
|
|
44
|
+
return Buffer.concat(parts);
|
|
45
|
+
}
|
|
46
|
+
function parseTar(raw) {
|
|
47
|
+
const files = [];
|
|
48
|
+
let off = 0;
|
|
49
|
+
while (off + 512 <= raw.length) {
|
|
50
|
+
const hdr = raw.slice(off, off + 512);
|
|
51
|
+
if (hdr.every((b) => b === 0))
|
|
52
|
+
break;
|
|
53
|
+
const name = hdr.slice(0, 100).toString("ascii").replace(/\0.*/, "");
|
|
54
|
+
const sizeStr = hdr.slice(124, 135).toString("ascii").replace(/\0.*/, "").trim();
|
|
55
|
+
const size = parseInt(sizeStr, 8) || 0;
|
|
56
|
+
const typeflag = hdr[156];
|
|
57
|
+
off += 512;
|
|
58
|
+
if (name && typeflag !== 0x35 && typeflag !== 53) { // not a directory
|
|
59
|
+
const content = raw.slice(off, off + size);
|
|
60
|
+
files.push({ name, content });
|
|
61
|
+
}
|
|
62
|
+
off += Math.ceil(size / 512) * 512;
|
|
63
|
+
}
|
|
64
|
+
return files;
|
|
65
|
+
}
|
|
2
66
|
/**
|
|
3
|
-
* Archive or extract files with tar
|
|
67
|
+
* Archive or extract files with tar — writes real POSIX ustar binary format.
|
|
68
|
+
* Supports -c/-x/-t, -z (gzip), -j (bzip2 stub), -v (verbose), -f.
|
|
4
69
|
* @category archive
|
|
5
70
|
* @params ["[-czf|-xzf|-tf] <archive> [files...]"]
|
|
6
71
|
*/
|
|
@@ -10,17 +75,15 @@ export const tarCommand = {
|
|
|
10
75
|
category: "archive",
|
|
11
76
|
params: ["[-czf|-xzf|-tf] <archive> [files...]"],
|
|
12
77
|
run: ({ authUser, shell, cwd, args }) => {
|
|
13
|
-
// Expand combined flags: -czf
|
|
78
|
+
// Expand combined flags: -czf → ["-c", "-z", "-f"]
|
|
14
79
|
const expanded = [];
|
|
15
80
|
let foundModeStr = false;
|
|
16
81
|
for (const a of args) {
|
|
17
82
|
if (/^-[a-zA-Z]{2,}$/.test(a)) {
|
|
18
|
-
// -czf style
|
|
19
83
|
for (const ch of a.slice(1))
|
|
20
84
|
expanded.push(`-${ch}`);
|
|
21
85
|
}
|
|
22
|
-
else if (!foundModeStr && /^[cxtdru]
|
|
23
|
-
// czf bare style (first non-path arg)
|
|
86
|
+
else if (!foundModeStr && /^[cxtdru][a-zA-Z]*$/.test(a) && !a.includes("/") && !a.startsWith("-")) {
|
|
24
87
|
foundModeStr = true;
|
|
25
88
|
for (const ch of a)
|
|
26
89
|
expanded.push(`-${ch}`);
|
|
@@ -32,70 +95,93 @@ export const tarCommand = {
|
|
|
32
95
|
const create = expanded.includes("-c");
|
|
33
96
|
const extract = expanded.includes("-x");
|
|
34
97
|
const list = expanded.includes("-t");
|
|
98
|
+
const useGzip = expanded.includes("-z");
|
|
99
|
+
const verbose = expanded.includes("-v");
|
|
35
100
|
const fIdx = expanded.indexOf("-f");
|
|
36
101
|
const archiveName = fIdx !== -1
|
|
37
102
|
? expanded[fIdx + 1]
|
|
38
|
-
: expanded.find((a) => a.endsWith(".tar") || a.endsWith(".tar.gz") || a.endsWith(".tgz"));
|
|
39
|
-
if (!create && !extract && !list)
|
|
40
|
-
return { stderr: "tar: must specify -c, -x, or -t
|
|
41
|
-
}
|
|
103
|
+
: expanded.find((a) => a.endsWith(".tar") || a.endsWith(".tar.gz") || a.endsWith(".tgz") || a.endsWith(".tar.bz2"));
|
|
104
|
+
if (!create && !extract && !list)
|
|
105
|
+
return { stderr: "tar: must specify -c, -x, or -t", exitCode: 1 };
|
|
42
106
|
if (!archiveName)
|
|
43
|
-
return { stderr: "tar: no archive specified
|
|
107
|
+
return { stderr: "tar: no archive specified", exitCode: 1 };
|
|
44
108
|
const archivePath = resolvePath(cwd, archiveName);
|
|
109
|
+
const autoGzip = useGzip || archiveName.endsWith(".gz") || archiveName.endsWith(".tgz");
|
|
45
110
|
if (create) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const
|
|
111
|
+
const skipSet = new Set();
|
|
112
|
+
if (fIdx !== -1 && expanded[fIdx + 1])
|
|
113
|
+
skipSet.add(expanded[fIdx + 1]);
|
|
114
|
+
const fileArgs = expanded.filter((a) => !a.startsWith("-") && !skipSet.has(a));
|
|
115
|
+
const entries = [];
|
|
116
|
+
const verboseLines = [];
|
|
52
117
|
for (const f of fileArgs) {
|
|
53
118
|
const p = resolvePath(cwd, f);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const s = shell.vfs.stat(full);
|
|
63
|
-
if (s.type === "file")
|
|
64
|
-
entries[rel] = shell.vfs.readFile(full);
|
|
65
|
-
else
|
|
66
|
-
walk(full, rel);
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
walk(p, f);
|
|
70
|
-
}
|
|
119
|
+
if (!shell.vfs.exists(p))
|
|
120
|
+
return { stderr: `tar: ${f}: No such file or directory`, exitCode: 1 };
|
|
121
|
+
const st = shell.vfs.stat(p);
|
|
122
|
+
if (st.type === "file") {
|
|
123
|
+
const content = shell.vfs.readFileRaw(p);
|
|
124
|
+
entries.push({ name: f, content, isDir: false });
|
|
125
|
+
if (verbose)
|
|
126
|
+
verboseLines.push(f);
|
|
71
127
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
128
|
+
else {
|
|
129
|
+
entries.push({ name: f, content: Buffer.alloc(0), isDir: true });
|
|
130
|
+
if (verbose)
|
|
131
|
+
verboseLines.push(`${f}/`);
|
|
132
|
+
const walk = (dir, prefix) => {
|
|
133
|
+
for (const e of shell.vfs.list(dir)) {
|
|
134
|
+
const full = `${dir}/${e}`, rel = `${prefix}/${e}`;
|
|
135
|
+
const s = shell.vfs.stat(full);
|
|
136
|
+
if (s.type === "directory") {
|
|
137
|
+
entries.push({ name: rel, content: Buffer.alloc(0), isDir: true });
|
|
138
|
+
if (verbose)
|
|
139
|
+
verboseLines.push(`${rel}/`);
|
|
140
|
+
walk(full, rel);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const content = shell.vfs.readFileRaw(full);
|
|
144
|
+
entries.push({ name: rel, content, isDir: false });
|
|
145
|
+
if (verbose)
|
|
146
|
+
verboseLines.push(rel);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
76
149
|
};
|
|
150
|
+
walk(p, f);
|
|
77
151
|
}
|
|
78
152
|
}
|
|
79
|
-
|
|
80
|
-
|
|
153
|
+
const tarBuf = buildTar(entries);
|
|
154
|
+
const finalBuf = autoGzip ? Buffer.from(gzipSync(tarBuf)) : tarBuf;
|
|
155
|
+
shell.vfs.writeFile(archivePath, finalBuf);
|
|
156
|
+
return { stdout: verbose ? verboseLines.join("\n") : undefined, exitCode: 0 };
|
|
81
157
|
}
|
|
82
158
|
if (list || extract) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
159
|
+
const rawArchive = shell.vfs.readFileRaw(archivePath);
|
|
160
|
+
let raw;
|
|
161
|
+
if (autoGzip) {
|
|
162
|
+
try {
|
|
163
|
+
raw = Buffer.from(gunzipSync(rawArchive));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return { stderr: `tar: ${archiveName}: not a gzip file`, exitCode: 1 };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
raw = rawArchive;
|
|
86
171
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
};
|
|
172
|
+
const files = parseTar(raw);
|
|
173
|
+
if (list) {
|
|
174
|
+
const names = files.map((f) => (verbose ? `-rw-r--r-- 0/0 ${f.content.length.toString().padStart(8)} 1970-01-01 00:00 ${f.name}` : f.name));
|
|
175
|
+
return { stdout: names.join("\n"), exitCode: 0 };
|
|
92
176
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
shell.writeFileAsUser(authUser,
|
|
177
|
+
const verboseLines = [];
|
|
178
|
+
for (const { name, content } of files) {
|
|
179
|
+
const destPath = resolvePath(cwd, name);
|
|
180
|
+
shell.writeFileAsUser(authUser, destPath, content);
|
|
181
|
+
if (verbose)
|
|
182
|
+
verboseLines.push(name);
|
|
97
183
|
}
|
|
98
|
-
return { exitCode: 0 };
|
|
184
|
+
return { stdout: verbose ? verboseLines.join("\n") : undefined, exitCode: 0 };
|
|
99
185
|
}
|
|
100
186
|
return { stderr: "tar: must specify -c, -x, or -t", exitCode: 1 };
|
|
101
187
|
},
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
/**
|
|
3
|
+
* Create ZIP archives using real PKZIP format with DEFLATE compression.
|
|
4
|
+
* @category archive
|
|
5
|
+
*/
|
|
6
|
+
export declare const zipCommand: ShellModule;
|
|
7
|
+
/**
|
|
8
|
+
* Extract ZIP archives (real PKZIP DEFLATE format).
|
|
9
|
+
* @category archive
|
|
10
|
+
*/
|
|
11
|
+
export declare const unzipCommand: ShellModule;
|