buninu-linux 0.8.55 → 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.
Files changed (52) hide show
  1. package/NOTICE.md +46 -46
  2. package/README.md +7 -6
  3. package/initramfs/bin/tar +34 -46
  4. package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +103 -0
  5. package/initramfs/buninu/apps/bunmsh/PORTING.md +133 -15
  6. package/initramfs/buninu/apps/bunmsh/help/awk.md +38 -0
  7. package/initramfs/buninu/apps/bunmsh/help/break.md +21 -0
  8. package/initramfs/buninu/apps/bunmsh/help/chmod.md +8 -2
  9. package/initramfs/buninu/apps/bunmsh/help/cmp.md +29 -0
  10. package/initramfs/buninu/apps/bunmsh/help/continue.md +22 -0
  11. package/initramfs/buninu/apps/bunmsh/help/cp.md +4 -0
  12. package/initramfs/buninu/apps/bunmsh/help/diff.md +30 -0
  13. package/initramfs/buninu/apps/bunmsh/help/egrep.md +26 -0
  14. package/initramfs/buninu/apps/bunmsh/help/export.md +2 -2
  15. package/initramfs/buninu/apps/bunmsh/help/expr.md +30 -0
  16. package/initramfs/buninu/apps/bunmsh/help/fgrep.md +25 -0
  17. package/initramfs/buninu/apps/bunmsh/help/grep.md +4 -0
  18. package/initramfs/buninu/apps/bunmsh/help/ls.md +4 -2
  19. package/initramfs/buninu/apps/bunmsh/help/mkdir.md +1 -1
  20. package/initramfs/buninu/apps/bunmsh/help/print.md +3 -0
  21. package/initramfs/buninu/apps/bunmsh/help/return.md +23 -0
  22. package/initramfs/buninu/apps/bunmsh/help/sed.md +8 -3
  23. package/initramfs/buninu/apps/bunmsh/help/set.md +12 -3
  24. package/initramfs/buninu/apps/bunmsh/help/tr.md +4 -0
  25. package/initramfs/buninu/apps/bunmsh/help/trap.md +22 -0
  26. package/initramfs/buninu/apps/bunmsh/help/uniq.md +27 -0
  27. package/initramfs/buninu/apps/bunmsh/package.json +1 -1
  28. package/initramfs/buninu/apps/bunmsh/src/awk.js +203 -0
  29. package/initramfs/buninu/apps/bunmsh/src/diff.js +222 -0
  30. package/initramfs/buninu/apps/bunmsh/src/fancy-ls.js +15 -4
  31. package/initramfs/buninu/apps/bunmsh/src/main.js +21 -5
  32. package/initramfs/buninu/apps/bunmsh/src/sed.js +159 -0
  33. package/initramfs/buninu/apps/bunmsh/src/shell.js +1283 -540
  34. package/initramfs/buninu/apps/bunmsh/test/awk-reference.test.js +35 -0
  35. package/initramfs/buninu/apps/bunmsh/test/awk.test.js +62 -0
  36. package/initramfs/buninu/apps/bunmsh/test/posix-bootstrap.test.js +595 -0
  37. package/initramfs/buninu/apps/bunmsh/test/utility-reference-cases.mjs +98 -0
  38. package/initramfs/buninu/apps/bunmsh/test/utility-reference.test.js +23 -0
  39. package/initramfs/buninu/apps/jsgotty/client-kitty.mjs +61 -0
  40. package/initramfs/buninu/apps/jsgotty/client.mjs +16 -0
  41. package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
  42. package/initramfs/buninu/apps/jsgotty/package.json +1 -1
  43. package/initramfs/buninu/apps/jsgotty/tests/client-kitty.test.mjs +43 -0
  44. package/initramfs/etc/resolv.conf +2 -1
  45. package/initramfs/usr/share/doc/buninu-linux/tar.md +12 -1
  46. package/package.json +1 -1
  47. package/scripts/arch.js +5 -5
  48. package/scripts/arch.sh +5 -5
  49. package/scripts/fetch.js +2 -0
  50. package/scripts/npm-packages.md +4 -3
  51. package/test/fetch-npm.test.js +1 -1
  52. 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.14",
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.14",
9
+ "version": "1.1.15",
10
10
  "dependencies": {
11
11
  "node-addon-api": "^7.1.1",
12
12
  "ws": "^8.18.3"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsgotty",
3
- "version": "1.1.14",
3
+ "version": "1.1.15",
4
4
  "description": "Share your terminal as a web application: A JavaScript rewrite of GoTTY",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -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 10.0.2.3
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
@@ -50,7 +61,7 @@ tar --zstd -xf backup.tar.zst -C /mnt/restore
50
61
  tar -xzf package.tgz -C /tmp/package --strip-components=1
51
62
 
52
63
  # Exclude browser resources; match before removing package/
53
- tar -xzf buninu-browser-0.2.2.tgz -C /tmp/browser --strip-components=1 --exclude=package/usr
64
+ tar -xzf buninu-browser-*.tgz -C /tmp/browser --strip-components=1 --exclude=package/usr
54
65
 
55
66
  # An Alpine root filesystem, symbolic links and all; then enter it
56
67
  tar -xzf alpine-minirootfs-3.24.1-x86_64.tar.gz -C /mnt/alpine
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu-linux",
3
- "version": "0.8.55",
3
+ "version": "0.9.56",
4
4
  "description": "A Linux distribution running Bun as PID 1 with the Buninu userspace.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/scripts/arch.js CHANGED
@@ -16,8 +16,8 @@ export const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
16
16
 
17
17
  const pins = {
18
18
  x86_64: {
19
- linuxVirtSha256: "a941c15fc5db26b6692fd0140fa0970da76cb12aadf3dc8306c419f21bd39c93",
20
- linuxLtsSha256: "02ba9491d4cc110707de64ed03dd7e2fa5c93fdd851979c244ceb018785a9eea",
19
+ linuxVirtSha256: "755deb045cfeb8c2bcaa2b2febe01ab2101d747921e64d4cd54d0312321e8d5d",
20
+ linuxLtsSha256: "b0d8cc4d7e031b5619221e7978920d051bf1dd1e335f6d05d03964b2285fc3b4",
21
21
  muslSha256: "573712e2f49c15bfc20a2699f204acdfc74c772722b15e7353d768057fae0e71",
22
22
  stubSha256: "8e64a5a3afee5f930e6e6716be726dc6d405530ac7f8fa5be6251dae68671ec9",
23
23
  libstdcppSha256: "14c987b556f5385a5db18376e788c75f37d85321b8dc1920d926ea7daac1d6f6",
@@ -32,8 +32,8 @@ const pins = {
32
32
  serialMajor: 4,
33
33
  },
34
34
  aarch64: {
35
- linuxVirtSha256: "c7fb892408d7fe163a18671e5c7816752976d1c67fd17794dfba794aa0d6c1ac",
36
- linuxLtsSha256: "31aeae56fa527b2fb4fdd4aefbd693cbec210a8cbfd4ea7597a1773b832e9f83",
35
+ linuxVirtSha256: "e3839bfd751ff0a4421484f712b9b29efb514024523736a0ef62521521571b12",
36
+ linuxLtsSha256: "795bcd49840be7f4ff9566f5f669201bd41202376a455b720a98eb814f0e72f8",
37
37
  muslSha256: "5e9674b7f41152fe2119093b5cb4c13eaaadb19c2d5422b2d7267913e663ee6e",
38
38
  stubSha256: "a1823d2d7082db555d528f82c1809f276c818aeb5b40f198f4565f104e055c38",
39
39
  libstdcppSha256: "2302e766d4e4926038ec166ecb85837ee884576115236ddb565e3a5fca4a11d7",
@@ -67,7 +67,7 @@ export const config = ({
67
67
  // The Alpine repository keeps only the newest build of a package, so a pin
68
68
  // stops downloading when Alpine bumps it; the hashes then fail loudly.
69
69
  const alpineBase = `${mirror}/v3.24/main/${arch}`;
70
- const kernelVersion = "6.18.55";
70
+ const kernelVersion = "6.18.56";
71
71
  const kernelRelease = `${kernelVersion}-0-${flavor}`;
72
72
  const bunVersion = "1.4.2";
73
73
  const downloadsDir = join(rootDir, "downloads", arch);
package/scripts/arch.sh CHANGED
@@ -44,7 +44,7 @@ fi
44
44
  alpine_mirror=${ALPINE_MIRROR:-https://dl-cdn.alpinelinux.org/alpine}
45
45
  alpine_base="$alpine_mirror/v3.24/main/$arch"
46
46
 
47
- kernel_version=6.18.55
47
+ kernel_version=6.18.56
48
48
  kernel_release="$kernel_version-0-$linux_flavor"
49
49
  linux_package="linux-$linux_flavor-$kernel_version-r0.apk"
50
50
  musl_package=musl-1.2.6-r2.apk
@@ -63,8 +63,8 @@ musl_libc="libc.musl-$arch.so.1"
63
63
 
64
64
  case $arch in
65
65
  x86_64)
66
- linux_virt_sha256=a941c15fc5db26b6692fd0140fa0970da76cb12aadf3dc8306c419f21bd39c93
67
- linux_lts_sha256=02ba9491d4cc110707de64ed03dd7e2fa5c93fdd851979c244ceb018785a9eea
66
+ linux_virt_sha256=755deb045cfeb8c2bcaa2b2febe01ab2101d747921e64d4cd54d0312321e8d5d
67
+ linux_lts_sha256=b0d8cc4d7e031b5619221e7978920d051bf1dd1e335f6d05d03964b2285fc3b4
68
68
  musl_sha256=573712e2f49c15bfc20a2699f204acdfc74c772722b15e7353d768057fae0e71
69
69
  stub_sha256=8e64a5a3afee5f930e6e6716be726dc6d405530ac7f8fa5be6251dae68671ec9
70
70
  libstdcpp_sha256=14c987b556f5385a5db18376e788c75f37d85321b8dc1920d926ea7daac1d6f6
@@ -82,8 +82,8 @@ case $arch in
82
82
  serial_major=4
83
83
  ;;
84
84
  aarch64)
85
- linux_virt_sha256=c7fb892408d7fe163a18671e5c7816752976d1c67fd17794dfba794aa0d6c1ac
86
- linux_lts_sha256=31aeae56fa527b2fb4fdd4aefbd693cbec210a8cbfd4ea7597a1773b832e9f83
85
+ linux_virt_sha256=e3839bfd751ff0a4421484f712b9b29efb514024523736a0ef62521521571b12
86
+ linux_lts_sha256=795bcd49840be7f4ff9566f5f669201bd41202376a455b720a98eb814f0e72f8
87
87
  musl_sha256=5e9674b7f41152fe2119093b5cb4c13eaaadb19c2d5422b2d7267913e663ee6e
88
88
  stub_sha256=a1823d2d7082db555d528f82c1809f276c818aeb5b40f198f4565f104e055c38
89
89
  libstdcpp_sha256=2302e766d4e4926038ec166ecb85837ee884576115236ddb565e3a5fca4a11d7
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}`);
@@ -1,9 +1,10 @@
1
1
  # npm packages
2
2
  # Latest versions checked on 2026-10-11; SHA-256 pins verified against registry SHA-512 integrity.
3
+ # buninu-browser updated and verified on 2026-10-12.
3
4
 
4
- "buninu-browser@0.2.2":
5
- - https://registry.npmjs.org/buninu-browser/-/buninu-browser-0.2.2.tgz
6
- - sha256:d551fec88f0fe8a6494f806cc8b4b560454796e25f25493e8a3b5b77e6f7f10b
5
+ "buninu-browser@0.2.3":
6
+ - https://registry.npmjs.org/buninu-browser/-/buninu-browser-0.2.3.tgz
7
+ - sha256:0f7c9cd9df689d7b0900fdf3172851125ebd8aba12bed555be7e091d427146e8
7
8
 
8
9
  "@drxiaozhi/casty@1.3.4":
9
10
  - https://registry.npmjs.org/@drxiaozhi/casty/-/casty-1.3.4.tgz
@@ -23,7 +23,7 @@ const url = `http://127.0.0.1:${server.port}/archive`;
23
23
 
24
24
  test("four exact registry pins share an architecture-independent cache", () => {
25
25
  expect(npmPackages.map(({ name, version }) => `${name}@${version}`)).toEqual([
26
- "buninu-browser@0.2.2", "@drxiaozhi/casty@1.3.4",
26
+ "buninu-browser@0.2.3", "@drxiaozhi/casty@1.3.4",
27
27
  "@drxiaozhi/jspulse@0.2.1", "@drxiaozhi/jspinyin@0.1.1",
28
28
  ]);
29
29
  expect(basename(npmDownloadsDir)).toBe("npm");
@@ -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
+ });