@ecmaos/coreutils 0.2.0 → 0.3.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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +25 -0
- package/LICENSE +1 -1
- package/dist/commands/cal.js +2 -2
- package/dist/commands/cal.js.map +1 -1
- package/dist/commands/cat.js +2 -2
- package/dist/commands/cd.js +2 -2
- package/dist/commands/chmod.d.ts.map +1 -1
- package/dist/commands/chmod.js +16 -11
- package/dist/commands/chmod.js.map +1 -1
- package/dist/commands/cp.js +2 -2
- package/dist/commands/cp.js.map +1 -1
- package/dist/commands/date.js +2 -2
- package/dist/commands/date.js.map +1 -1
- package/dist/commands/echo.js +2 -2
- package/dist/commands/echo.js.map +1 -1
- package/dist/commands/false.js +2 -2
- package/dist/commands/fetch.d.ts +4 -0
- package/dist/commands/fetch.d.ts.map +1 -0
- package/dist/commands/fetch.js +210 -0
- package/dist/commands/fetch.js.map +1 -0
- package/dist/commands/format.d.ts +4 -0
- package/dist/commands/format.d.ts.map +1 -0
- package/dist/commands/format.js +178 -0
- package/dist/commands/format.js.map +1 -0
- package/dist/commands/hash.d.ts +4 -0
- package/dist/commands/hash.d.ts.map +1 -0
- package/dist/commands/hash.js +200 -0
- package/dist/commands/hash.js.map +1 -0
- package/dist/commands/head.js +2 -2
- package/dist/commands/id.js +2 -2
- package/dist/commands/id.js.map +1 -1
- package/dist/commands/less.d.ts.map +1 -1
- package/dist/commands/less.js +53 -2
- package/dist/commands/less.js.map +1 -1
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +120 -97
- package/dist/commands/ls.js.map +1 -1
- package/dist/commands/man.d.ts +4 -0
- package/dist/commands/man.d.ts.map +1 -0
- package/dist/commands/man.js +554 -0
- package/dist/commands/man.js.map +1 -0
- package/dist/commands/mkdir.js +2 -2
- package/dist/commands/mkdir.js.map +1 -1
- package/dist/commands/mktemp.d.ts +4 -0
- package/dist/commands/mktemp.d.ts.map +1 -0
- package/dist/commands/mktemp.js +229 -0
- package/dist/commands/mktemp.js.map +1 -0
- package/dist/commands/nc.js +2 -2
- package/dist/commands/nc.js.map +1 -1
- package/dist/commands/passkey.js +3 -3
- package/dist/commands/pwd.js +2 -2
- package/dist/commands/pwd.js.map +1 -1
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +57 -12
- package/dist/commands/rm.js.map +1 -1
- package/dist/commands/rmdir.js +2 -2
- package/dist/commands/rmdir.js.map +1 -1
- package/dist/commands/sockets.js +1 -1
- package/dist/commands/stat.d.ts.map +1 -1
- package/dist/commands/stat.js +37 -15
- package/dist/commands/stat.js.map +1 -1
- package/dist/commands/tail.js +2 -2
- package/dist/commands/tar.d.ts +4 -0
- package/dist/commands/tar.d.ts.map +1 -0
- package/dist/commands/tar.js +693 -0
- package/dist/commands/tar.js.map +1 -0
- package/dist/commands/touch.js +2 -2
- package/dist/commands/touch.js.map +1 -1
- package/dist/commands/true.js +2 -2
- package/dist/commands/unzip.d.ts +4 -0
- package/dist/commands/unzip.d.ts.map +1 -0
- package/dist/commands/unzip.js +443 -0
- package/dist/commands/unzip.js.map +1 -0
- package/dist/commands/user.d.ts +4 -0
- package/dist/commands/user.d.ts.map +1 -0
- package/dist/commands/user.js +427 -0
- package/dist/commands/user.js.map +1 -0
- package/dist/commands/whoami.js +2 -2
- package/dist/commands/whoami.js.map +1 -1
- package/dist/commands/zip.d.ts +4 -0
- package/dist/commands/zip.d.ts.map +1 -0
- package/dist/commands/zip.js +264 -0
- package/dist/commands/zip.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/src/commands/cal.ts +2 -2
- package/src/commands/cat.ts +2 -2
- package/src/commands/cd.ts +2 -2
- package/src/commands/chmod.ts +19 -11
- package/src/commands/cp.ts +2 -2
- package/src/commands/date.ts +2 -2
- package/src/commands/echo.ts +2 -2
- package/src/commands/false.ts +2 -2
- package/src/commands/fetch.ts +205 -0
- package/src/commands/format.ts +204 -0
- package/src/commands/hash.ts +215 -0
- package/src/commands/head.ts +2 -2
- package/src/commands/id.ts +2 -2
- package/src/commands/less.ts +50 -2
- package/src/commands/ls.ts +131 -91
- package/src/commands/man.ts +643 -0
- package/src/commands/mkdir.ts +2 -2
- package/src/commands/mktemp.ts +235 -0
- package/src/commands/nc.ts +2 -2
- package/src/commands/passkey.ts +3 -3
- package/src/commands/pwd.ts +2 -2
- package/src/commands/rm.ts +54 -12
- package/src/commands/rmdir.ts +2 -2
- package/src/commands/sockets.ts +1 -1
- package/src/commands/stat.ts +44 -16
- package/src/commands/tail.ts +2 -2
- package/src/commands/tar.ts +737 -0
- package/src/commands/touch.ts +2 -2
- package/src/commands/true.ts +2 -2
- package/src/commands/unzip.ts +517 -0
- package/src/commands/user.ts +436 -0
- package/src/commands/whoami.ts +2 -2
- package/src/commands/zip.ts +319 -0
- package/src/index.ts +28 -1
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { TerminalEvents } from '@ecmaos/types';
|
|
3
|
+
import { TerminalCommand } from '../shared/terminal-command.js';
|
|
4
|
+
import { writelnStdout, writelnStderr } from '../shared/helpers.js';
|
|
5
|
+
import { createTarPacker, createTarDecoder } from 'modern-tar';
|
|
6
|
+
function printUsage(process, terminal) {
|
|
7
|
+
const usage = `Usage: tar [OPTION]... [FILE]...
|
|
8
|
+
Create, extract, or list tar archives.
|
|
9
|
+
|
|
10
|
+
-c, --create create a new archive
|
|
11
|
+
-x, --extract extract files from an archive
|
|
12
|
+
-t, --list list the contents of an archive
|
|
13
|
+
-f, --file use archive file (required for create, optional for extract/list - uses stdin if omitted)
|
|
14
|
+
-z filter the archive through gzip
|
|
15
|
+
-v, --verbose verbosely list files processed
|
|
16
|
+
-C, --directory change to directory before extracting
|
|
17
|
+
-h, --help display this help and exit`;
|
|
18
|
+
writelnStderr(process, terminal, usage);
|
|
19
|
+
}
|
|
20
|
+
function parseArgs(argv) {
|
|
21
|
+
const options = {
|
|
22
|
+
create: false,
|
|
23
|
+
extract: false,
|
|
24
|
+
list: false,
|
|
25
|
+
file: null,
|
|
26
|
+
gzip: false,
|
|
27
|
+
verbose: false,
|
|
28
|
+
directory: null
|
|
29
|
+
};
|
|
30
|
+
const files = [];
|
|
31
|
+
let i = 0;
|
|
32
|
+
while (i < argv.length) {
|
|
33
|
+
const arg = argv[i];
|
|
34
|
+
if (!arg || typeof arg !== 'string') {
|
|
35
|
+
i++;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (arg === '--help' || arg === '-h') {
|
|
39
|
+
i++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
else if (arg === '-c' || arg === '--create') {
|
|
43
|
+
options.create = true;
|
|
44
|
+
i++;
|
|
45
|
+
}
|
|
46
|
+
else if (arg === '-x' || arg === '--extract') {
|
|
47
|
+
options.extract = true;
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
else if (arg === '-t' || arg === '--list') {
|
|
51
|
+
options.list = true;
|
|
52
|
+
i++;
|
|
53
|
+
}
|
|
54
|
+
else if (arg === '-f' || arg === '--file') {
|
|
55
|
+
if (i + 1 < argv.length) {
|
|
56
|
+
i++;
|
|
57
|
+
const nextArg = argv[i];
|
|
58
|
+
options.file = (typeof nextArg === 'string' ? nextArg : null) || null;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
options.file = null;
|
|
62
|
+
}
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
else if (arg === '-z') {
|
|
66
|
+
options.gzip = true;
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
else if (arg === '-v' || arg === '--verbose') {
|
|
70
|
+
options.verbose = true;
|
|
71
|
+
i++;
|
|
72
|
+
}
|
|
73
|
+
else if (arg === '-C' || arg === '--directory') {
|
|
74
|
+
if (i + 1 < argv.length) {
|
|
75
|
+
i++;
|
|
76
|
+
const nextArg = argv[i];
|
|
77
|
+
options.directory = (typeof nextArg === 'string' ? nextArg : null) || null;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
options.directory = null;
|
|
81
|
+
}
|
|
82
|
+
i++;
|
|
83
|
+
}
|
|
84
|
+
else if (arg.startsWith('-')) {
|
|
85
|
+
// Handle combined flags like -czf
|
|
86
|
+
const flags = arg.slice(1).split('');
|
|
87
|
+
for (const flag of flags) {
|
|
88
|
+
if (flag === 'c')
|
|
89
|
+
options.create = true;
|
|
90
|
+
else if (flag === 'x')
|
|
91
|
+
options.extract = true;
|
|
92
|
+
else if (flag === 't')
|
|
93
|
+
options.list = true;
|
|
94
|
+
else if (flag === 'f') {
|
|
95
|
+
// -f needs to be followed by filename, check if next arg exists
|
|
96
|
+
const nextArg = argv[i + 1];
|
|
97
|
+
if (i + 1 < argv.length && typeof nextArg === 'string' && !nextArg.startsWith('-')) {
|
|
98
|
+
i++;
|
|
99
|
+
options.file = nextArg;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (flag === 'z')
|
|
103
|
+
options.gzip = true;
|
|
104
|
+
else if (flag === 'v')
|
|
105
|
+
options.verbose = true;
|
|
106
|
+
else if (flag === 'C') {
|
|
107
|
+
// -C needs to be followed by directory, check if next arg exists
|
|
108
|
+
const nextArg = argv[i + 1];
|
|
109
|
+
if (i + 1 < argv.length && typeof nextArg === 'string' && !nextArg.startsWith('-')) {
|
|
110
|
+
i++;
|
|
111
|
+
options.directory = nextArg;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
files.push(arg);
|
|
119
|
+
i++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { options, files };
|
|
123
|
+
}
|
|
124
|
+
async function collectFiles(shell, filePaths, basePath = '') {
|
|
125
|
+
const result = [];
|
|
126
|
+
for (const filePath of filePaths) {
|
|
127
|
+
const fullPath = path.resolve(shell.cwd, filePath);
|
|
128
|
+
try {
|
|
129
|
+
const stat = await shell.context.fs.promises.stat(fullPath);
|
|
130
|
+
if (stat.isDirectory()) {
|
|
131
|
+
// Add directory entry
|
|
132
|
+
const relativePath = path.join(basePath, filePath);
|
|
133
|
+
result.push({
|
|
134
|
+
path: relativePath.endsWith('/') ? relativePath : relativePath + '/',
|
|
135
|
+
fullPath,
|
|
136
|
+
isDirectory: true
|
|
137
|
+
});
|
|
138
|
+
// Recursively collect directory contents
|
|
139
|
+
const entries = await shell.context.fs.promises.readdir(fullPath);
|
|
140
|
+
const subFiles = [];
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
subFiles.push(path.join(fullPath, entry));
|
|
143
|
+
}
|
|
144
|
+
const subResults = await collectFiles(shell, subFiles.map(f => path.relative(shell.cwd, f)), path.join(basePath, filePath));
|
|
145
|
+
result.push(...subResults);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
// Add file entry
|
|
149
|
+
result.push({
|
|
150
|
+
path: path.join(basePath, filePath),
|
|
151
|
+
fullPath,
|
|
152
|
+
isDirectory: false
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
// Skip files that can't be accessed
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
async function createArchive(shell, terminal, process, archivePath, filePaths, options) {
|
|
164
|
+
if (filePaths.length === 0) {
|
|
165
|
+
await writelnStderr(process, terminal, 'tar: no files specified');
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const fullArchivePath = path.resolve(shell.cwd, archivePath);
|
|
170
|
+
// Collect all files to archive
|
|
171
|
+
const filesToArchive = await collectFiles(shell, filePaths);
|
|
172
|
+
if (filesToArchive.length === 0) {
|
|
173
|
+
await writelnStderr(process, terminal, 'tar: no files to archive');
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
// Create tar packer
|
|
177
|
+
const { readable: tarStream, controller } = createTarPacker();
|
|
178
|
+
// Apply gzip compression if requested
|
|
179
|
+
let finalStream = tarStream;
|
|
180
|
+
if (options.gzip) {
|
|
181
|
+
finalStream = tarStream.pipeThrough(new CompressionStream('gzip'));
|
|
182
|
+
}
|
|
183
|
+
// Start writing the archive in the background
|
|
184
|
+
const writePromise = (async () => {
|
|
185
|
+
const archiveHandle = await shell.context.fs.promises.open(fullArchivePath, 'w');
|
|
186
|
+
const writer = archiveHandle.writableWebStream?.()?.getWriter();
|
|
187
|
+
if (!writer) {
|
|
188
|
+
// Fallback: read stream and write in chunks
|
|
189
|
+
const reader = finalStream.getReader();
|
|
190
|
+
try {
|
|
191
|
+
while (true) {
|
|
192
|
+
const { done, value } = await reader.read();
|
|
193
|
+
if (done)
|
|
194
|
+
break;
|
|
195
|
+
await archiveHandle.writeFile(value);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
reader.releaseLock();
|
|
200
|
+
await archiveHandle.close();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
try {
|
|
205
|
+
const reader = finalStream.getReader();
|
|
206
|
+
try {
|
|
207
|
+
while (true) {
|
|
208
|
+
const { done, value } = await reader.read();
|
|
209
|
+
if (done)
|
|
210
|
+
break;
|
|
211
|
+
await writer.write(value);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
reader.releaseLock();
|
|
216
|
+
}
|
|
217
|
+
await writer.close();
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
writer.releaseLock();
|
|
221
|
+
await archiveHandle.close();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
})();
|
|
225
|
+
// Add entries to the tar archive
|
|
226
|
+
for (const file of filesToArchive) {
|
|
227
|
+
if (file.isDirectory) {
|
|
228
|
+
// Add directory entry
|
|
229
|
+
const dirName = file.path.endsWith('/') ? file.path : file.path + '/';
|
|
230
|
+
controller.add({
|
|
231
|
+
name: dirName,
|
|
232
|
+
type: 'directory',
|
|
233
|
+
size: 0
|
|
234
|
+
});
|
|
235
|
+
if (options.verbose) {
|
|
236
|
+
await writelnStdout(process, terminal, dirName);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
try {
|
|
241
|
+
const handle = await shell.context.fs.promises.open(file.fullPath, 'r');
|
|
242
|
+
const stat = await shell.context.fs.promises.stat(file.fullPath);
|
|
243
|
+
// Create a readable stream from the file
|
|
244
|
+
const fileStream = new ReadableStream({
|
|
245
|
+
async start(controller) {
|
|
246
|
+
try {
|
|
247
|
+
const chunkSize = 64 * 1024; // 64KB chunks
|
|
248
|
+
let offset = 0;
|
|
249
|
+
while (offset < stat.size) {
|
|
250
|
+
const buffer = new Uint8Array(chunkSize);
|
|
251
|
+
const readSize = Math.min(chunkSize, stat.size - offset);
|
|
252
|
+
await handle.read(buffer, 0, readSize, offset);
|
|
253
|
+
const chunk = buffer.subarray(0, readSize);
|
|
254
|
+
controller.enqueue(chunk);
|
|
255
|
+
offset += readSize;
|
|
256
|
+
}
|
|
257
|
+
controller.close();
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
controller.error(error);
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
await handle.close();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
// Add file entry to tar
|
|
268
|
+
const entryStream = controller.add({
|
|
269
|
+
name: file.path,
|
|
270
|
+
type: 'file',
|
|
271
|
+
size: stat.size
|
|
272
|
+
});
|
|
273
|
+
// Copy file stream to entry stream
|
|
274
|
+
const fileReader = fileStream.getReader();
|
|
275
|
+
const entryWriter = entryStream.getWriter();
|
|
276
|
+
try {
|
|
277
|
+
while (true) {
|
|
278
|
+
const { done, value } = await fileReader.read();
|
|
279
|
+
if (done)
|
|
280
|
+
break;
|
|
281
|
+
await entryWriter.write(value);
|
|
282
|
+
}
|
|
283
|
+
await entryWriter.close();
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
fileReader.releaseLock();
|
|
287
|
+
entryWriter.releaseLock();
|
|
288
|
+
}
|
|
289
|
+
if (options.verbose) {
|
|
290
|
+
await writelnStdout(process, terminal, file.path);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
await writelnStderr(process, terminal, `tar: ${file.path}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Finalize the tar archive
|
|
299
|
+
controller.finalize();
|
|
300
|
+
// Wait for writing to complete
|
|
301
|
+
await writePromise;
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
await writelnStderr(process, terminal, `tar: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async function extractArchive(kernel, shell, terminal, process, archivePath, options) {
|
|
310
|
+
try {
|
|
311
|
+
// Create readable stream from archive file or stdin
|
|
312
|
+
let archiveStream;
|
|
313
|
+
if (archivePath) {
|
|
314
|
+
// Read from file
|
|
315
|
+
const fullArchivePath = path.resolve(shell.cwd, archivePath);
|
|
316
|
+
// Check if archive exists
|
|
317
|
+
try {
|
|
318
|
+
await shell.context.fs.promises.stat(fullArchivePath);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
await writelnStderr(process, terminal, `tar: ${archivePath}: Cannot open: No such file or directory`);
|
|
322
|
+
return 1;
|
|
323
|
+
}
|
|
324
|
+
// Read archive file
|
|
325
|
+
const archiveHandle = await shell.context.fs.promises.open(fullArchivePath, 'r');
|
|
326
|
+
const stat = await shell.context.fs.promises.stat(fullArchivePath);
|
|
327
|
+
if (archiveHandle.readableWebStream) {
|
|
328
|
+
archiveStream = archiveHandle.readableWebStream();
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
// Fallback: create stream manually
|
|
332
|
+
archiveStream = new ReadableStream({
|
|
333
|
+
async start(controller) {
|
|
334
|
+
try {
|
|
335
|
+
const chunkSize = 64 * 1024; // 64KB chunks
|
|
336
|
+
let offset = 0;
|
|
337
|
+
while (offset < stat.size) {
|
|
338
|
+
const buffer = new Uint8Array(chunkSize);
|
|
339
|
+
const readSize = Math.min(chunkSize, stat.size - offset);
|
|
340
|
+
await archiveHandle.read(buffer, 0, readSize, offset);
|
|
341
|
+
const chunk = buffer.subarray(0, readSize);
|
|
342
|
+
controller.enqueue(chunk);
|
|
343
|
+
offset += readSize;
|
|
344
|
+
}
|
|
345
|
+
controller.close();
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
controller.error(error);
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
await archiveHandle.close();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
// Read from stdin
|
|
359
|
+
if (!process || !process.stdin) {
|
|
360
|
+
await writelnStderr(process, terminal, 'tar: no input provided');
|
|
361
|
+
return 1;
|
|
362
|
+
}
|
|
363
|
+
archiveStream = process.stdin;
|
|
364
|
+
}
|
|
365
|
+
// Apply gzip decompression if requested
|
|
366
|
+
let tarStream = archiveStream;
|
|
367
|
+
if (options.gzip) {
|
|
368
|
+
tarStream = archiveStream.pipeThrough(new DecompressionStream('gzip'));
|
|
369
|
+
}
|
|
370
|
+
// Extract using modern-tar decoder
|
|
371
|
+
let hasError = false;
|
|
372
|
+
let interrupted = false;
|
|
373
|
+
const interruptHandler = () => { interrupted = true; };
|
|
374
|
+
kernel.terminal.events.on(TerminalEvents.INTERRUPT, interruptHandler);
|
|
375
|
+
try {
|
|
376
|
+
const decoder = createTarDecoder();
|
|
377
|
+
const entriesStream = tarStream.pipeThrough(decoder);
|
|
378
|
+
const entriesReader = entriesStream.getReader();
|
|
379
|
+
try {
|
|
380
|
+
while (true) {
|
|
381
|
+
if (interrupted)
|
|
382
|
+
break;
|
|
383
|
+
const { done, value: entry } = await entriesReader.read();
|
|
384
|
+
if (done)
|
|
385
|
+
break;
|
|
386
|
+
if (!entry)
|
|
387
|
+
continue;
|
|
388
|
+
if (options.verbose) {
|
|
389
|
+
await writelnStdout(process, terminal, entry.header.name);
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
// Normalize the entry name: strip leading slashes and resolve relative to extraction directory
|
|
393
|
+
let entryName = entry.header.name;
|
|
394
|
+
// Remove leading slashes to make it relative
|
|
395
|
+
while (entryName.startsWith('/')) {
|
|
396
|
+
entryName = entryName.slice(1);
|
|
397
|
+
}
|
|
398
|
+
// Skip empty entries (like just "/")
|
|
399
|
+
if (!entryName) {
|
|
400
|
+
await entry.body.cancel();
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
// Determine extraction base directory
|
|
404
|
+
const extractBase = options.directory
|
|
405
|
+
? path.resolve(shell.cwd, options.directory)
|
|
406
|
+
: shell.cwd;
|
|
407
|
+
const targetPath = path.resolve(extractBase, entryName);
|
|
408
|
+
// Security check: ensure target path is within extract base (prevent directory traversal)
|
|
409
|
+
const resolvedBase = path.resolve(extractBase);
|
|
410
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
411
|
+
if (!resolvedTarget.startsWith(resolvedBase + path.sep) && resolvedTarget !== resolvedBase) {
|
|
412
|
+
await writelnStderr(process, terminal, `tar: ${entry.header.name}: path outside extraction directory`);
|
|
413
|
+
await entry.body.cancel();
|
|
414
|
+
hasError = true;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (entry.header.type === 'directory' || entry.header.name.endsWith('/')) {
|
|
418
|
+
// Create directory
|
|
419
|
+
try {
|
|
420
|
+
await shell.context.fs.promises.mkdir(targetPath, { recursive: true });
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
// Directory might already exist, ignore
|
|
424
|
+
}
|
|
425
|
+
// Drain the body stream for directories
|
|
426
|
+
await entry.body.cancel();
|
|
427
|
+
}
|
|
428
|
+
else if (entry.header.type === 'file') {
|
|
429
|
+
// Extract file
|
|
430
|
+
const dirPath = path.dirname(targetPath);
|
|
431
|
+
try {
|
|
432
|
+
await shell.context.fs.promises.mkdir(dirPath, { recursive: true });
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
// Directory might already exist
|
|
436
|
+
}
|
|
437
|
+
// Read file content from entry stream
|
|
438
|
+
const chunks = [];
|
|
439
|
+
const reader = entry.body.getReader();
|
|
440
|
+
try {
|
|
441
|
+
while (true) {
|
|
442
|
+
const { done, value } = await reader.read();
|
|
443
|
+
if (done)
|
|
444
|
+
break;
|
|
445
|
+
if (value)
|
|
446
|
+
chunks.push(value);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
finally {
|
|
450
|
+
reader.releaseLock();
|
|
451
|
+
}
|
|
452
|
+
// Combine chunks and write file
|
|
453
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
454
|
+
const fileData = new Uint8Array(totalLength);
|
|
455
|
+
let offset = 0;
|
|
456
|
+
for (const chunk of chunks) {
|
|
457
|
+
fileData.set(chunk, offset);
|
|
458
|
+
offset += chunk.length;
|
|
459
|
+
}
|
|
460
|
+
await shell.context.fs.promises.writeFile(targetPath, fileData);
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
// For other entry types (symlinks, etc.), drain the body
|
|
464
|
+
await entry.body.cancel();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
await writelnStderr(process, terminal, `tar: ${entry.header.name}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
469
|
+
hasError = true;
|
|
470
|
+
// Drain the body stream on error
|
|
471
|
+
try {
|
|
472
|
+
await entry.body.cancel();
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
// Ignore cancel errors
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
finally {
|
|
481
|
+
entriesReader.releaseLock();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
finally {
|
|
485
|
+
kernel.terminal.events.off(TerminalEvents.INTERRUPT, interruptHandler);
|
|
486
|
+
}
|
|
487
|
+
return hasError ? 1 : 0;
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
await writelnStderr(process, terminal, `tar: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
491
|
+
return 1;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async function listArchive(kernel, shell, terminal, process, archivePath, options) {
|
|
495
|
+
try {
|
|
496
|
+
// Create readable stream from archive file or stdin
|
|
497
|
+
let archiveStream;
|
|
498
|
+
if (archivePath) {
|
|
499
|
+
// Read from file
|
|
500
|
+
const fullArchivePath = path.resolve(shell.cwd, archivePath);
|
|
501
|
+
// Check if archive exists
|
|
502
|
+
try {
|
|
503
|
+
await shell.context.fs.promises.stat(fullArchivePath);
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
await writelnStderr(process, terminal, `tar: ${archivePath}: Cannot open: No such file or directory`);
|
|
507
|
+
return 1;
|
|
508
|
+
}
|
|
509
|
+
// Read archive file
|
|
510
|
+
const archiveHandle = await shell.context.fs.promises.open(fullArchivePath, 'r');
|
|
511
|
+
const stat = await shell.context.fs.promises.stat(fullArchivePath);
|
|
512
|
+
if (archiveHandle.readableWebStream) {
|
|
513
|
+
archiveStream = archiveHandle.readableWebStream();
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
// Fallback: create stream manually
|
|
517
|
+
archiveStream = new ReadableStream({
|
|
518
|
+
async start(controller) {
|
|
519
|
+
try {
|
|
520
|
+
const chunkSize = 64 * 1024; // 64KB chunks
|
|
521
|
+
let offset = 0;
|
|
522
|
+
while (offset < stat.size) {
|
|
523
|
+
const buffer = new Uint8Array(chunkSize);
|
|
524
|
+
const readSize = Math.min(chunkSize, stat.size - offset);
|
|
525
|
+
await archiveHandle.read(buffer, 0, readSize, offset);
|
|
526
|
+
const chunk = buffer.subarray(0, readSize);
|
|
527
|
+
controller.enqueue(chunk);
|
|
528
|
+
offset += readSize;
|
|
529
|
+
}
|
|
530
|
+
controller.close();
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
controller.error(error);
|
|
534
|
+
}
|
|
535
|
+
finally {
|
|
536
|
+
await archiveHandle.close();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
// Read from stdin
|
|
544
|
+
if (!process || !process.stdin) {
|
|
545
|
+
await writelnStderr(process, terminal, 'tar: no input provided');
|
|
546
|
+
return 1;
|
|
547
|
+
}
|
|
548
|
+
archiveStream = process.stdin;
|
|
549
|
+
}
|
|
550
|
+
// Apply gzip decompression if requested
|
|
551
|
+
let tarStream = archiveStream;
|
|
552
|
+
if (options.gzip) {
|
|
553
|
+
tarStream = archiveStream.pipeThrough(new DecompressionStream('gzip'));
|
|
554
|
+
}
|
|
555
|
+
// List contents using modern-tar decoder
|
|
556
|
+
let hasError = false;
|
|
557
|
+
let interrupted = false;
|
|
558
|
+
const interruptHandler = () => { interrupted = true; };
|
|
559
|
+
kernel.terminal.events.on(TerminalEvents.INTERRUPT, interruptHandler);
|
|
560
|
+
try {
|
|
561
|
+
const decoder = createTarDecoder();
|
|
562
|
+
const entriesStream = tarStream.pipeThrough(decoder);
|
|
563
|
+
const entriesReader = entriesStream.getReader();
|
|
564
|
+
try {
|
|
565
|
+
while (true) {
|
|
566
|
+
if (interrupted)
|
|
567
|
+
break;
|
|
568
|
+
const { done, value: entry } = await entriesReader.read();
|
|
569
|
+
if (done)
|
|
570
|
+
break;
|
|
571
|
+
if (!entry)
|
|
572
|
+
continue;
|
|
573
|
+
await writelnStdout(process, terminal, entry.header.name);
|
|
574
|
+
// Drain the body stream since we're just listing
|
|
575
|
+
await entry.body.cancel();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
finally {
|
|
579
|
+
entriesReader.releaseLock();
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
await writelnStderr(process, terminal, `tar: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
584
|
+
hasError = true;
|
|
585
|
+
}
|
|
586
|
+
finally {
|
|
587
|
+
kernel.terminal.events.off(TerminalEvents.INTERRUPT, interruptHandler);
|
|
588
|
+
}
|
|
589
|
+
return hasError ? 1 : 0;
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
await writelnStderr(process, terminal, `tar: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
593
|
+
return 1;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
export function createCommand(kernel, shell, terminal) {
|
|
597
|
+
return new TerminalCommand({
|
|
598
|
+
command: 'tar',
|
|
599
|
+
description: 'Create, extract, or list tar archives',
|
|
600
|
+
kernel,
|
|
601
|
+
shell,
|
|
602
|
+
terminal,
|
|
603
|
+
run: async (pid, argv) => {
|
|
604
|
+
const process = kernel.processes.get(pid);
|
|
605
|
+
if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
|
|
606
|
+
printUsage(process, terminal);
|
|
607
|
+
return 0;
|
|
608
|
+
}
|
|
609
|
+
const { options, files } = parseArgs(argv);
|
|
610
|
+
// Validate operation mode
|
|
611
|
+
const operationCount = [options.create, options.extract, options.list].filter(Boolean).length;
|
|
612
|
+
if (operationCount === 0) {
|
|
613
|
+
await writelnStderr(process, terminal, 'tar: You must specify one of the -c, -x, or -t options');
|
|
614
|
+
await writelnStderr(process, terminal, "Try 'tar --help' for more information.");
|
|
615
|
+
return 1;
|
|
616
|
+
}
|
|
617
|
+
if (operationCount > 1) {
|
|
618
|
+
await writelnStderr(process, terminal, 'tar: You may not specify more than one of -c, -x, or -t');
|
|
619
|
+
return 1;
|
|
620
|
+
}
|
|
621
|
+
// Validate file option for create (create always needs a file)
|
|
622
|
+
if (options.create && !options.file) {
|
|
623
|
+
await writelnStderr(process, terminal, 'tar: option requires an argument -- f');
|
|
624
|
+
await writelnStderr(process, terminal, "Try 'tar --help' for more information.");
|
|
625
|
+
return 1;
|
|
626
|
+
}
|
|
627
|
+
// For extract and list, if no file is specified, use stdin
|
|
628
|
+
// Expand glob patterns in file list
|
|
629
|
+
const expandGlob = async (pattern) => {
|
|
630
|
+
if (!pattern.includes('*') && !pattern.includes('?')) {
|
|
631
|
+
return [pattern];
|
|
632
|
+
}
|
|
633
|
+
const lastSlashIndex = pattern.lastIndexOf('/');
|
|
634
|
+
const searchDir = lastSlashIndex !== -1
|
|
635
|
+
? path.resolve(shell.cwd, pattern.substring(0, lastSlashIndex + 1))
|
|
636
|
+
: shell.cwd;
|
|
637
|
+
const globPattern = lastSlashIndex !== -1
|
|
638
|
+
? pattern.substring(lastSlashIndex + 1)
|
|
639
|
+
: pattern;
|
|
640
|
+
try {
|
|
641
|
+
const entries = await shell.context.fs.promises.readdir(searchDir);
|
|
642
|
+
const regexPattern = globPattern
|
|
643
|
+
.replace(/\./g, '\\.')
|
|
644
|
+
.replace(/\*/g, '.*')
|
|
645
|
+
.replace(/\?/g, '.');
|
|
646
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
647
|
+
const matches = entries.filter(entry => regex.test(entry));
|
|
648
|
+
if (lastSlashIndex !== -1) {
|
|
649
|
+
const dirPart = pattern.substring(0, lastSlashIndex + 1);
|
|
650
|
+
return matches.map(match => dirPart + match);
|
|
651
|
+
}
|
|
652
|
+
return matches;
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
return [];
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
// Expand glob patterns in file list (shell should have already expanded them, but keep as fallback)
|
|
659
|
+
const expandedFiles = [];
|
|
660
|
+
for (const filePattern of files) {
|
|
661
|
+
if (typeof filePattern !== 'string') {
|
|
662
|
+
// Skip non-string entries (shouldn't happen, but handle gracefully)
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
// Check if this looks like a glob pattern (shell should have expanded it, but handle as fallback)
|
|
666
|
+
const expanded = await expandGlob(filePattern);
|
|
667
|
+
if (expanded.length === 0) {
|
|
668
|
+
// If glob doesn't match anything, include the pattern as-is (might be a literal path)
|
|
669
|
+
expandedFiles.push(filePattern);
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
expandedFiles.push(...expanded);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// Execute operation
|
|
676
|
+
if (options.create) {
|
|
677
|
+
if (!options.file) {
|
|
678
|
+
await writelnStderr(process, terminal, 'tar: option requires an argument -- f');
|
|
679
|
+
return 1;
|
|
680
|
+
}
|
|
681
|
+
return await createArchive(shell, terminal, process, options.file, expandedFiles, options);
|
|
682
|
+
}
|
|
683
|
+
else if (options.extract) {
|
|
684
|
+
return await extractArchive(kernel, shell, terminal, process, options.file, options);
|
|
685
|
+
}
|
|
686
|
+
else if (options.list) {
|
|
687
|
+
return await listArchive(kernel, shell, terminal, process, options.file, options);
|
|
688
|
+
}
|
|
689
|
+
return 1;
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
//# sourceMappingURL=tar.js.map
|