buninu-linux 0.8.56 → 0.9.56
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/initramfs/bin/tar +34 -46
- package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +103 -0
- package/initramfs/buninu/apps/bunmsh/PORTING.md +133 -15
- package/initramfs/buninu/apps/bunmsh/help/awk.md +38 -0
- package/initramfs/buninu/apps/bunmsh/help/break.md +21 -0
- package/initramfs/buninu/apps/bunmsh/help/chmod.md +8 -2
- package/initramfs/buninu/apps/bunmsh/help/cmp.md +29 -0
- package/initramfs/buninu/apps/bunmsh/help/continue.md +22 -0
- package/initramfs/buninu/apps/bunmsh/help/cp.md +4 -0
- package/initramfs/buninu/apps/bunmsh/help/diff.md +30 -0
- package/initramfs/buninu/apps/bunmsh/help/egrep.md +26 -0
- package/initramfs/buninu/apps/bunmsh/help/export.md +2 -2
- package/initramfs/buninu/apps/bunmsh/help/expr.md +30 -0
- package/initramfs/buninu/apps/bunmsh/help/fgrep.md +25 -0
- package/initramfs/buninu/apps/bunmsh/help/grep.md +4 -0
- package/initramfs/buninu/apps/bunmsh/help/ls.md +4 -2
- package/initramfs/buninu/apps/bunmsh/help/mkdir.md +1 -1
- package/initramfs/buninu/apps/bunmsh/help/print.md +3 -0
- package/initramfs/buninu/apps/bunmsh/help/return.md +23 -0
- package/initramfs/buninu/apps/bunmsh/help/sed.md +8 -3
- package/initramfs/buninu/apps/bunmsh/help/set.md +12 -3
- package/initramfs/buninu/apps/bunmsh/help/tr.md +4 -0
- package/initramfs/buninu/apps/bunmsh/help/trap.md +22 -0
- package/initramfs/buninu/apps/bunmsh/help/uniq.md +27 -0
- package/initramfs/buninu/apps/bunmsh/package.json +1 -1
- package/initramfs/buninu/apps/bunmsh/src/awk.js +203 -0
- package/initramfs/buninu/apps/bunmsh/src/diff.js +222 -0
- package/initramfs/buninu/apps/bunmsh/src/fancy-ls.js +15 -4
- package/initramfs/buninu/apps/bunmsh/src/main.js +21 -5
- package/initramfs/buninu/apps/bunmsh/src/sed.js +159 -0
- package/initramfs/buninu/apps/bunmsh/src/shell.js +1283 -540
- package/initramfs/buninu/apps/bunmsh/test/awk-reference.test.js +35 -0
- package/initramfs/buninu/apps/bunmsh/test/awk.test.js +62 -0
- package/initramfs/buninu/apps/bunmsh/test/posix-bootstrap.test.js +595 -0
- package/initramfs/buninu/apps/bunmsh/test/utility-reference-cases.mjs +98 -0
- package/initramfs/buninu/apps/bunmsh/test/utility-reference.test.js +23 -0
- package/initramfs/buninu/apps/jsgotty/client-kitty.mjs +61 -0
- package/initramfs/buninu/apps/jsgotty/client.mjs +16 -0
- package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
- package/initramfs/buninu/apps/jsgotty/package.json +1 -1
- package/initramfs/buninu/apps/jsgotty/tests/client-kitty.test.mjs +43 -0
- package/initramfs/etc/resolv.conf +2 -1
- package/initramfs/usr/share/doc/buninu-linux/tar.md +11 -0
- package/package.json +1 -1
- package/scripts/fetch.js +2 -0
- package/test/tar-strip.test.js +36 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Record system-tool results separately from the isolated bootstrap runtime:
|
|
2
|
+
// node test/utility-reference-cases.mjs OUTPUT.json
|
|
3
|
+
// BUNMSH_UTILITY_REFERENCE=/guest/OUTPUT.json bun test test/utility-reference.test.js
|
|
4
|
+
import {mkdtempSync, mkdirSync, writeFileSync, rmSync, chmodSync, statSync} from 'node:fs';
|
|
5
|
+
import {tmpdir} from 'node:os';
|
|
6
|
+
import {join, resolve, dirname} from 'node:path';
|
|
7
|
+
import {fileURLToPath} from 'node:url';
|
|
8
|
+
import {spawnSync} from 'node:child_process';
|
|
9
|
+
const diff = (name, before, after, args=[]) => ({name, tool:'diff', files:{before,after}, args:[...args,'before','after']});
|
|
10
|
+
const sed = (name, script, input, args=[]) => ({name, tool:'sed', files:{script}, args:[...args,'-f','script'], input});
|
|
11
|
+
export const cases = [
|
|
12
|
+
...[
|
|
13
|
+
['adjacent only', [], 'a\na\nb\na\n'],
|
|
14
|
+
['count', ['-c'], 'a\na\nb\n'],
|
|
15
|
+
['repeated', ['-d'], 'a\na\nb\na\n'],
|
|
16
|
+
['single', ['-u'], 'a\na\nb\na\n'],
|
|
17
|
+
['count repeated', ['-cd'], 'a\na\nb\n'],
|
|
18
|
+
['no repeated singles', ['-du'], 'a\na\nb\n'],
|
|
19
|
+
['empty input', [], ''],
|
|
20
|
+
['blank lines', [], '\n\nfirst\n\n'],
|
|
21
|
+
['unterminated duplicate', [], 'a\na'],
|
|
22
|
+
['unterminated distinct', [], 'a\nb'],
|
|
23
|
+
['binary NUL', [], 'a\0b\na\0b\na\0c\n'],
|
|
24
|
+
['file operand', ['--','input'], null],
|
|
25
|
+
].map(([name,args,input])=>({name,tool:'uniq',args,files:input===null?{input:'a\na\nb\n'}:{},input:input??''})),
|
|
26
|
+
...[
|
|
27
|
+
['remove all write bits',0o666,'a-w'],
|
|
28
|
+
['add user read and write',0o004,'u+rw'],
|
|
29
|
+
['assign group and other permissions',0o770,'go=rx'],
|
|
30
|
+
['sequential classes',0o777,'u=rw,g=r,o='],
|
|
31
|
+
['conditional execute on plain file',0o644,'a+X'],
|
|
32
|
+
['conditional execute on executable',0o744,'a+X'],
|
|
33
|
+
['conditional execute after removal',0o755,'a-x,a+X'],
|
|
34
|
+
['assignment clears file special bits',0o4766,'a=rw'],
|
|
35
|
+
['multiple operations for one class',0o777,'u+rw-x'],
|
|
36
|
+
].map(([name,mode,spec])=>({name,tool:'chmod',files:{file:'content'},modes:{file:mode},args:[spec,'--','file']})),
|
|
37
|
+
...[
|
|
38
|
+
['identical bytes', 'abc\n', 'abc\n', []],
|
|
39
|
+
['first differing byte', 'same\nabc\n', 'same\nabX\n', []],
|
|
40
|
+
['quiet difference', 'abc', 'ab', ['-s']],
|
|
41
|
+
['shorter file', 'abc', 'abcd', []],
|
|
42
|
+
['empty file', '', 'abc', []],
|
|
43
|
+
['binary difference', 'a\0b', 'a\0c', []],
|
|
44
|
+
].map(([name,before,after,args])=>({name,tool:'cmp',files:{before,after},args:[...args,'--','before','after']})),
|
|
45
|
+
{name:'squeeze spaces without translating s',tool:'tr',files:{},args:['-s',' '],input:'size_t inbytesleft\n'},
|
|
46
|
+
{name:'translate then squeeze',tool:'tr',files:{},args:['-s','ab','x'],input:'aabbbcaa\n'},
|
|
47
|
+
{name:'delete then squeeze',tool:'tr',files:{},args:['-ds','x','a'],input:'axaxa bbb\n'},
|
|
48
|
+
{name:'delete only',tool:'tr',files:{},args:['-d','x'],input:'axxbbb\n'},
|
|
49
|
+
{name:'squeeze repeated newlines',tool:'tr',files:{},args:['-s','\\n'],input:'a\n\n\nb\n'},
|
|
50
|
+
{name:'literal hyphen set',tool:'tr',files:{},args:['--','-s','XY'],input:'-s-size_t\n'},
|
|
51
|
+
sed("make object extension","s=$[\\(\\{]OBJEXT[\\)\\}]=o=g\n","file.$(OBJEXT) file.${OBJEXT}\n"),
|
|
52
|
+
sed("BRE interior anchors","s/a^b/X/;s/c$d/Y/;s/^z$/Z/\n","a^b c$d\nz\n"),
|
|
53
|
+
{name:'read file after address',tool:'sed',files:{extra:'inserted\n'},args:['/mark/r extra'],input:'before\nmark\nafter\n'},
|
|
54
|
+
{name:'read missing file',tool:'sed',files:{},args:['r missing'],input:'kept\n'},
|
|
55
|
+
{name:'read file with deleted pattern',tool:'sed',files:{extra:'inserted\n'},args:['-n','-e','r extra','-e','d'],input:'one\ntwo\n'},
|
|
56
|
+
diff('identical','a\nb\n','a\nb\n'),
|
|
57
|
+
diff('middle insertion','a\nb\nc\n','a\nx\nb\nc\n'),
|
|
58
|
+
diff('separated insertions','a\nb\nc\nd\n','a\nx\nb\nc\ny\nd\n'),
|
|
59
|
+
diff('separated deletions','a\nx\nb\nc\ny\nd\n','a\nb\nc\nd\n'),
|
|
60
|
+
diff('insertion then replacement','a\nb\nc\nd\ne\n','a\nx\nb\nc\nD\ne\n'),
|
|
61
|
+
diff('repeated lines','a\nsame\nsame\nz\n','a\nsame\nnew\nsame\nz\n'),
|
|
62
|
+
diff('empty left','','added\n'), diff('empty right','deleted\n',''),
|
|
63
|
+
diff('missing final newline','a\n','a'), diff('both missing final newline','old','new'),
|
|
64
|
+
diff('Chinese','前\n後\n','前\n中\n後\n'),
|
|
65
|
+
diff('CRLF','a\r\nb\r\n','a\r\nx\r\nb\r\n'),
|
|
66
|
+
diff('brief difference','a\n','b\n',['-q']),
|
|
67
|
+
sed('file script and branch','s/a/b/\nt done\n:done\np\n','a\n',['-n']),
|
|
68
|
+
sed('address range','2,3p\n','one\ntwo\nthree\nfour\n',['-n']),
|
|
69
|
+
sed('inverted address','/skip/!p\n','keep\nskip\nkeep again\n',['-n']),
|
|
70
|
+
sed('hold exchange','1h\n2{x;p;}\n','first\nsecond\n',['-n']),
|
|
71
|
+
sed('hold append','1h\n2{H;g;p;}\n','first\nsecond\n',['-n']),
|
|
72
|
+
sed('multiline N','N;s/\n/:/;p\n'.replace('s/\n','s/\\n'),'first\nsecond\n',['-n']),
|
|
73
|
+
sed('branch loop',':again\ns/aa/a/\nt again\np\n','aaaa\n',['-n']),
|
|
74
|
+
sed('translation','y/abc/ABC/\n','abc cab\n'),
|
|
75
|
+
sed('escaped delimiter','s|a\\|b|X|g\n','a|b a|b\n'),
|
|
76
|
+
sed('backreference','s/\\(ab\\)/[\\1]/g\n','abab\n'),
|
|
77
|
+
sed('missing input newline','s/a/b/\n','a'),
|
|
78
|
+
{name:'attached -f',tool:'sed',files:{script:'s/a/b/\n'},args:['-fscript'],input:'a\n'},
|
|
79
|
+
];
|
|
80
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
81
|
+
if (!process.argv[2]) throw new Error('Output JSON path required');
|
|
82
|
+
const root=mkdtempSync(join(tmpdir(),'bunmsh-system-reference-'));
|
|
83
|
+
try {
|
|
84
|
+
const results=cases.map((c,index) => {
|
|
85
|
+
const cwd=join(root,String(index)); mkdirSync(cwd);
|
|
86
|
+
for (const [name,content] of Object.entries(c.files)) {
|
|
87
|
+
mkdirSync(dirname(join(cwd,name)),{recursive:true});writeFileSync(join(cwd,name),content);
|
|
88
|
+
}
|
|
89
|
+
for (const [name,mode] of Object.entries(c.modes??{})) chmodSync(join(cwd,name),mode);
|
|
90
|
+
const r=spawnSync(`/usr/bin/${c.tool}`,c.args,{cwd,input:c.input??'',encoding:'utf8',env:{...process.env,LC_ALL:'C'}});
|
|
91
|
+
if (r.error || r.signal || r.status===null) throw r.error??new Error(`Reference failed: ${c.name}`);
|
|
92
|
+
return {...c,expected:{status:r.status,stdout:r.stdout,stderr:r.stderr,...(c.modes?{modes:Object.fromEntries(Object.keys(c.modes).map(name=>[name,statSync(join(cwd,name)).mode&0o7777]))}:{})}};
|
|
93
|
+
});
|
|
94
|
+
const versions=Object.fromEntries(['diff','sed','tr','cmp','chmod','uniq'].map(tool=>[tool,spawnSync(`/usr/bin/${tool}`,['--version'],{encoding:'utf8'}).stdout.split('\n')[0]]));
|
|
95
|
+
writeFileSync(process.argv[2],JSON.stringify({versions,cases:results},null,2)+'\n');
|
|
96
|
+
console.log(JSON.stringify({versions,cases:results.length}));
|
|
97
|
+
} finally {rmSync(root,{recursive:true,force:true});}
|
|
98
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {mkdtempSync,mkdirSync,readFileSync,writeFileSync,rmSync,chmodSync,statSync} from 'node:fs';
|
|
3
|
+
import {tmpdir} from 'node:os';
|
|
4
|
+
import {join,dirname} from 'node:path';
|
|
5
|
+
import {createState,execute,decode} from '../src/shell.js';
|
|
6
|
+
|
|
7
|
+
// Only data crosses into the guest. No host executables are mounted or spawned.
|
|
8
|
+
const path=process.env.BUNMSH_UTILITY_REFERENCE;
|
|
9
|
+
const fixtures=path ? JSON.parse(readFileSync(path,'utf8')).cases : [];
|
|
10
|
+
if (!path) test.skip('system diff/sed reference (set BUNMSH_UTILITY_REFERENCE)',()=>{});
|
|
11
|
+
for (const c of fixtures) test(`system ${c.tool} reference: ${c.name}`,async()=>{
|
|
12
|
+
const cwd=mkdtempSync(join(tmpdir(),'bunmsh-utility-reference-'));
|
|
13
|
+
try {
|
|
14
|
+
for (const [name,content] of Object.entries(c.files)) {
|
|
15
|
+
mkdirSync(dirname(join(cwd,name)),{recursive:true});writeFileSync(join(cwd,name),content);
|
|
16
|
+
}
|
|
17
|
+
for (const [name,mode] of Object.entries(c.modes??{})) chmodSync(join(cwd,name),mode);
|
|
18
|
+
const state=createState({cwd,env:{LC_ALL:'C'},shellOptions:{posix:true}});
|
|
19
|
+
const quote = value => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
20
|
+
const r=await execute(['builtin',c.tool,...c.args].map(quote).join(' '),state,{capture:true,input:new Response(c.input??'').body});
|
|
21
|
+
expect({status:r.status,stdout:decode(r.stdout),stderr:decode(r.stderr),...(c.modes?{modes:Object.fromEntries(Object.keys(c.modes).map(name=>[name,statSync(join(cwd,name)).mode&0o7777]))}:{})}).toEqual(c.expected);
|
|
22
|
+
} finally {rmSync(cwd,{recursive:true,force:true});}
|
|
23
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const KITTY_CHUNK_SIZE = 4096;
|
|
2
|
+
const KITTY_TRANSPORT_KEYS = new Set(["m", "o", "O", "S", "t", "U", "mime"]);
|
|
3
|
+
|
|
4
|
+
function kittyPacket(control, payload = "") {
|
|
5
|
+
const entries = Object.entries(control)
|
|
6
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== false)
|
|
7
|
+
.map(([key, value]) => value === true ? key : `${key}=${value}`);
|
|
8
|
+
return `\u001b_G${entries.join(",")};${payload}\u001b\\`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Convert the server's browser-neutral Kitty message back to native APCs. */
|
|
12
|
+
export function encodeKittyGraphics(message) {
|
|
13
|
+
if (!message || typeof message !== "object") return "";
|
|
14
|
+
if (message.kind === "delete") {
|
|
15
|
+
const deletion = message.delete || {};
|
|
16
|
+
return kittyPacket({
|
|
17
|
+
a: "d",
|
|
18
|
+
d: deletion.scope || "a",
|
|
19
|
+
i: deletion.imageId || undefined,
|
|
20
|
+
p: deletion.placementId || undefined,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
if (message.kind !== "placement" || !message.image?.data) return "";
|
|
24
|
+
|
|
25
|
+
const original = { ...(message.control || {}) };
|
|
26
|
+
const action = original.a || "t";
|
|
27
|
+
const placementOnly = action === "p";
|
|
28
|
+
const transfer = {};
|
|
29
|
+
for (const [key, value] of Object.entries(original)) {
|
|
30
|
+
if (!KITTY_TRANSPORT_KEYS.has(key)) transfer[key] = value;
|
|
31
|
+
}
|
|
32
|
+
transfer.a = placementOnly ? "t" : action;
|
|
33
|
+
transfer.t = "d";
|
|
34
|
+
transfer.f = message.image.format || transfer.f || 32;
|
|
35
|
+
if (message.image.width > 0) transfer.s = message.image.width;
|
|
36
|
+
if (message.image.height > 0) transfer.v = message.image.height;
|
|
37
|
+
if (!transfer.i && /^\d+$/.test(String(message.image.id || ""))) transfer.i = message.image.id;
|
|
38
|
+
|
|
39
|
+
const payload = String(message.image.data);
|
|
40
|
+
let output = "";
|
|
41
|
+
for (let offset = 0; offset < payload.length; offset += KITTY_CHUNK_SIZE) {
|
|
42
|
+
const more = offset + KITTY_CHUNK_SIZE < payload.length;
|
|
43
|
+
const control = offset === 0 ? { ...transfer, m: more ? 1 : 0 } : { m: more ? 1 : 0 };
|
|
44
|
+
output += kittyPacket(control, payload.slice(offset, offset + KITTY_CHUNK_SIZE));
|
|
45
|
+
}
|
|
46
|
+
if (placementOnly) output += kittyPacket(original);
|
|
47
|
+
return output;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Cursor motion the server adds for browser overlays; native Kitty does it itself. */
|
|
51
|
+
export function syntheticKittyCursorMotion(message) {
|
|
52
|
+
if (message?.kind !== "placement") return "";
|
|
53
|
+
const control = message.control || {};
|
|
54
|
+
if (control.C === "1") return "";
|
|
55
|
+
const cols = Math.max(0, Number.parseInt(control.c || "0", 10) || 0);
|
|
56
|
+
const rows = Math.max(0, Number.parseInt(control.r || "0", 10) || 0);
|
|
57
|
+
const cursorCol = Math.max(1, Number.parseInt(message.cursor?.col || "1", 10) || 1);
|
|
58
|
+
const targetCol = Math.max(1, cursorCol + cols);
|
|
59
|
+
if (rows > 0) return "\r\n".repeat(rows) + `\u001b[${targetCol}G`;
|
|
60
|
+
return cols > 0 ? `\u001b[${targetCol}G` : "";
|
|
61
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { Buffer } from "node:buffer";
|
|
5
5
|
import WebSocket from "ws";
|
|
6
|
+
import { encodeKittyGraphics, syntheticKittyCursorMotion } from "./client-kitty.mjs";
|
|
6
7
|
|
|
7
8
|
const MSG_INPUT = "1";
|
|
8
9
|
const MSG_PING = "2";
|
|
@@ -15,6 +16,7 @@ const MSG_SET_WINDOW_TITLE = "3";
|
|
|
15
16
|
const MSG_SET_PREFERENCES = "4";
|
|
16
17
|
const MSG_SET_RECONNECT = "5";
|
|
17
18
|
const MSG_SET_BUFFER_SIZE = "6";
|
|
19
|
+
const MSG_KITTY_GRAPHICS = "7";
|
|
18
20
|
const MSG_SET_ROLE = "a";
|
|
19
21
|
const MSG_CONTROL_RESULT = "b";
|
|
20
22
|
|
|
@@ -420,6 +422,7 @@ async function main() {
|
|
|
420
422
|
let fallbackOutputTail = "";
|
|
421
423
|
let viewerInputActive = false;
|
|
422
424
|
let sessionPid = "(unknown)";
|
|
425
|
+
let pendingKittyCursorMotion = "";
|
|
423
426
|
|
|
424
427
|
const normalizeEcho = (value) => String(value)
|
|
425
428
|
.replaceAll("\r", "")
|
|
@@ -603,6 +606,11 @@ pid:
|
|
|
603
606
|
case MSG_OUTPUT:
|
|
604
607
|
{
|
|
605
608
|
const output = Buffer.from(payload, "base64");
|
|
609
|
+
if (pendingKittyCursorMotion && output.equals(Buffer.from(pendingKittyCursorMotion, "utf8"))) {
|
|
610
|
+
pendingKittyCursorMotion = "";
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
pendingKittyCursorMotion = "";
|
|
606
614
|
process.stdout.write(output);
|
|
607
615
|
if (fallbackProbe && accessRole === "") {
|
|
608
616
|
fallbackOutputTail = normalizeEcho(
|
|
@@ -625,6 +633,14 @@ pid:
|
|
|
625
633
|
process.stdout.write(`\u001b]0;${payload}\u0007`);
|
|
626
634
|
}
|
|
627
635
|
break;
|
|
636
|
+
case MSG_KITTY_GRAPHICS:
|
|
637
|
+
try {
|
|
638
|
+
const message = JSON.parse(payload);
|
|
639
|
+
const encoded = encodeKittyGraphics(message);
|
|
640
|
+
pendingKittyCursorMotion = syntheticKittyCursorMotion(message);
|
|
641
|
+
if (encoded) process.stdout.write(encoded);
|
|
642
|
+
} catch {}
|
|
643
|
+
break;
|
|
628
644
|
case MSG_SET_RECONNECT: {
|
|
629
645
|
try {
|
|
630
646
|
const reconnect = JSON.parse(payload);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsgotty",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.15",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "jsgotty",
|
|
9
|
-
"version": "1.1.
|
|
9
|
+
"version": "1.1.15",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"node-addon-api": "^7.1.1",
|
|
12
12
|
"ws": "^8.18.3"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { encodeKittyGraphics, syntheticKittyCursorMotion } from "../client-kitty.mjs";
|
|
3
|
+
|
|
4
|
+
test("CLI converts a server Kitty placement to native direct-transfer APC", () => {
|
|
5
|
+
const encoded = encodeKittyGraphics({
|
|
6
|
+
kind: "placement",
|
|
7
|
+
control: { a: "T", t: "f", f: "100", i: "7", p: "9", c: "4", r: "2", o: "z" },
|
|
8
|
+
image: { id: "7", format: 100, width: 30, height: 20, data: "AA==" },
|
|
9
|
+
});
|
|
10
|
+
expect(encoded).toBe("\x1b_Ga=T,f=100,i=7,p=9,c=4,r=2,t=d,s=30,v=20,m=0;AA==\x1b\\");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("CLI retransmits cached data before a placement-only Kitty command", () => {
|
|
14
|
+
const data = "A".repeat(4100);
|
|
15
|
+
const encoded = encodeKittyGraphics({
|
|
16
|
+
kind: "placement",
|
|
17
|
+
control: { a: "p", i: "8", p: "2", C: "1" },
|
|
18
|
+
image: { id: "8", format: 100, width: 1, height: 1, data },
|
|
19
|
+
});
|
|
20
|
+
expect(encoded).toContain("\x1b_Ga=t,i=8,p=2,C=1,t=d,f=100,s=1,v=1,m=1;");
|
|
21
|
+
expect(encoded).toContain("\x1b_Gm=0;AAAA\x1b\\");
|
|
22
|
+
expect(encoded.endsWith("\x1b_Ga=p,i=8,p=2,C=1;\x1b\\")).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("CLI converts Kitty deletion messages", () => {
|
|
26
|
+
expect(encodeKittyGraphics({
|
|
27
|
+
kind: "delete",
|
|
28
|
+
delete: { scope: "I", imageId: "7", placementId: "3" },
|
|
29
|
+
})).toBe("\x1b_Ga=d,d=I,i=7,p=3;\x1b\\");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("CLI identifies browser-only cursor motion after native Kitty placement", () => {
|
|
33
|
+
expect(syntheticKittyCursorMotion({
|
|
34
|
+
kind: "placement",
|
|
35
|
+
cursor: { row: 3, col: 4 },
|
|
36
|
+
control: { c: "6", r: "2" },
|
|
37
|
+
})).toBe("\r\n\r\n\x1b[10G");
|
|
38
|
+
expect(syntheticKittyCursorMotion({
|
|
39
|
+
kind: "placement",
|
|
40
|
+
cursor: { row: 3, col: 4 },
|
|
41
|
+
control: { c: "6", r: "2", C: "1" },
|
|
42
|
+
})).toBe("");
|
|
43
|
+
});
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
nameserver
|
|
1
|
+
nameserver 1.1.1.1
|
|
2
|
+
nameserver 8.8.8.8
|
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
`/bin/tar` is a small Bun script over `Bun.Archive`. It handles uncompressed
|
|
4
4
|
tar archives plus gzip and zstd without invoking another program.
|
|
5
5
|
|
|
6
|
+
Alpine APK v2 archives are supported directly, including their concatenated
|
|
7
|
+
gzip streams and tar sections. For example:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
tar -tf musl-dev-1.2.6-r2.apk
|
|
11
|
+
tar -xf musl-dev-1.2.6-r2.apk -C ./musl-dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
This extracts archive contents, including package metadata; it does not
|
|
15
|
+
install the package or execute its package scripts.
|
|
16
|
+
|
|
6
17
|
## Synopsis
|
|
7
18
|
|
|
8
19
|
```sh
|
package/package.json
CHANGED
package/scripts/fetch.js
CHANGED
|
@@ -98,6 +98,8 @@ export const readTar = (path) => {
|
|
|
98
98
|
if (type === "0" || type === "\0") files.set(name.replace(/^\.\//, ""), body);
|
|
99
99
|
}
|
|
100
100
|
return {
|
|
101
|
+
// Enumerate regular files without exposing the Map's mutation methods.
|
|
102
|
+
entries() { return files.entries(); },
|
|
101
103
|
get(name) {
|
|
102
104
|
const bytes = files.get(name);
|
|
103
105
|
if (!bytes) throw new Error(`${path}: no ${name}`);
|
package/test/tar-strip.test.js
CHANGED
|
@@ -216,3 +216,39 @@ test("does not follow a pre-existing symlink parent or an archive symlink parent
|
|
|
216
216
|
expect(result.status).toBe(2);
|
|
217
217
|
expect(existsSync(join(outside, "nested"))).toBe(false);
|
|
218
218
|
});
|
|
219
|
+
|
|
220
|
+
test("APK concatenated gzip/tar streams list and extract every section", () => {
|
|
221
|
+
const signature = tar([{ name: ".SIGN.test", data: "signature" }]);
|
|
222
|
+
const control = tar([{ name: ".PKGINFO", data: "pkgname = sample\n" }]);
|
|
223
|
+
const data = tar([
|
|
224
|
+
{ name: "usr/include/stdio.h", data: "header\n" },
|
|
225
|
+
{ name: "usr/lib/crt1.o", data: "object\n" },
|
|
226
|
+
{ name: "usr/lib/link", type: "2", target: "crt1.o" },
|
|
227
|
+
]);
|
|
228
|
+
// APK signature/control streams can omit end markers; also cover complete
|
|
229
|
+
// concatenated tar archives, and a single gzip stream containing them all.
|
|
230
|
+
for (const raw of [false, true]) {
|
|
231
|
+
const parts = [raw ? signature.subarray(0, -1024) : signature,
|
|
232
|
+
raw ? control.subarray(0, -1024) : control, data];
|
|
233
|
+
for (const bytes of [Buffer.concat(parts.map(p => Bun.gzipSync(p))), Bun.gzipSync(Buffer.concat(parts))]) {
|
|
234
|
+
const result = extract(bytes, [], true);
|
|
235
|
+
expect(result.status).toBe(0);
|
|
236
|
+
expect(readFileSync(join(result.dir, ".PKGINFO"), "utf8")).toBe("pkgname = sample\n");
|
|
237
|
+
expect(readFileSync(join(result.dir, "usr/include/stdio.h"), "utf8")).toBe("header\n");
|
|
238
|
+
expect(readlinkSync(join(result.dir, "usr/lib/link"))).toBe("crt1.o");
|
|
239
|
+
const listed = run(["-tf", "-"], bytes);
|
|
240
|
+
expect(listed.status).toBe(0);
|
|
241
|
+
expect(listed.stdout.trim().split("\n")).toEqual([
|
|
242
|
+
".SIGN.test", ".PKGINFO", "usr/include/stdio.h", "usr/lib/crt1.o", "usr/lib/link",
|
|
243
|
+
]);
|
|
244
|
+
const selected = extract(bytes, ["usr/include"], true);
|
|
245
|
+
expect(selected.status).toBe(0);
|
|
246
|
+
expect(existsSync(join(selected.dir, ".PKGINFO"))).toBe(false);
|
|
247
|
+
expect(readFileSync(join(selected.dir, "usr/include/stdio.h"), "utf8")).toBe("header\n");
|
|
248
|
+
const stripped = extract(bytes, ["--strip-components=2", "--exclude=usr/lib", "usr"], true);
|
|
249
|
+
expect(stripped.status).toBe(0);
|
|
250
|
+
expect(readFileSync(join(stripped.dir, "stdio.h"), "utf8")).toBe("header\n");
|
|
251
|
+
expect(existsSync(join(stripped.dir, "crt1.o"))).toBe(false);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|