buninu-linux 0.13.56 → 0.15.58
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/LICENSES/linux-firmware/LICENCE.nvidia +131 -0
- package/LICENSES/linux-firmware/LICENSE.amdgpu +51 -0
- package/LICENSES/linux-firmware/LICENSE.amdisp +31 -0
- package/LICENSES/linux-firmware/LICENSE.i915 +39 -0
- package/LICENSES/linux-firmware/LICENSE.radeon +51 -0
- package/LICENSES/linux-firmware/LICENSE.xe +39 -0
- package/LICENSES/linux-firmware/WHENCE +10280 -0
- package/NOTICE.md +64 -46
- package/README.md +410 -96
- package/README.zh.md +2034 -0
- package/build-image.sh +10 -1
- package/build-uki.sh +3 -1
- package/fetch-alpine.sh +15 -1
- package/graphics.md +31 -0
- package/index.js +28 -11
- package/initramfs/bin/buninu-browser +4 -1
- package/initramfs/bin/buninu-land +1 -1
- package/initramfs/bin/bunterm +3 -3
- package/initramfs/bin/chroot +13 -0
- package/initramfs/bin/chvt +11 -0
- package/initramfs/bin/dhclient +8 -0
- package/initramfs/bin/mount +4 -0
- package/initramfs/bin/ping +8 -0
- package/initramfs/bin/umount +6 -0
- package/initramfs/bin/xdg-open +33 -0
- package/initramfs/buninu/CHANGELOG.md +19 -0
- package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +4 -0
- package/initramfs/buninu/apps/bunmsh/package.json +1 -1
- package/initramfs/buninu/apps/bunmsh/src/main.js +1 -1
- package/initramfs/buninu/apps/jsgotty/README.en.md +40 -0
- package/initramfs/buninu/apps/jsgotty/README.md +40 -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/static/index.html +1 -0
- package/initramfs/buninu/apps/jsgotty/static/js/gotty.js +82 -17
- package/initramfs/buninu/apps/jsgotty/static/js/kitty-keyboard.js +193 -0
- package/initramfs/buninu/apps/jsgotty/tests/android-alt.test.js +115 -0
- package/initramfs/buninu/apps/jsgotty/tests/arrow-modifiers.test.js +80 -0
- package/initramfs/buninu/apps/jsgotty/tests/kitty-keyboard-browser.test.js +53 -0
- package/initramfs/buninu/apps/jsgotty/tests/kitty-keyboard.test.js +102 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/README.md +20 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/keyboard-collector.cjs +55 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/keyboard-debug.js +74 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/kitty-keyboard.js +80 -0
- package/initramfs/buninu/package.json +1 -1
- package/initramfs/init.js +27 -10
- package/initramfs/lib/bunterm/clipboard.js +14 -0
- package/initramfs/lib/bunterm/commands.js +37 -0
- package/initramfs/lib/bunterm/fps.js +38 -0
- package/initramfs/lib/bunterm/images.js +9 -5
- package/initramfs/lib/bunterm/input.js +58 -15
- package/initramfs/lib/bunterm/keyboard.js +62 -0
- package/initramfs/lib/bunterm/keycodes.js +104 -0
- package/initramfs/lib/bunterm/kitty-keyboard.cjs +195 -0
- package/initramfs/lib/bunterm/raw-protocol.js +33 -0
- package/initramfs/lib/bunterm/renderer.js +90 -21
- package/initramfs/lib/bunterm/selection.js +60 -0
- package/initramfs/lib/bunterm/terminal.js +147 -12
- package/initramfs/lib/bunterm/vt-menu.js +181 -0
- package/initramfs/lib/bunterm/vt.js +27 -1
- package/initramfs/lib/canvas.js +99 -0
- package/initramfs/lib/chvt.js +31 -0
- package/initramfs/lib/dhclient.js +135 -0
- package/initramfs/lib/dhcp-start.js +85 -0
- package/initramfs/lib/dhcp-transport.js +44 -0
- package/initramfs/lib/dhcp.js +124 -0
- package/initramfs/lib/dlopen.js +58 -11
- package/initramfs/lib/fbdev.js +21 -3
- package/initramfs/lib/gpu.js +77 -0
- package/initramfs/lib/ping.js +110 -0
- package/initramfs/lib/raw-image.js +47 -0
- package/initramfs/lib/xdg-open.js +42 -0
- package/initramfs/usr/share/doc/buninu-linux/bunterm.md +86 -12
- package/initramfs/usr/share/doc/buninu-linux/chvt.md +34 -0
- package/initramfs/usr/share/doc/buninu-linux/dhclient.md +82 -0
- package/initramfs/usr/share/doc/buninu-linux/ping.md +91 -0
- package/initramfs/usr/share/doc/buninu-linux/xdg-open.md +43 -0
- package/package.json +2 -2
- package/scripts/arch.js +7 -5
- package/scripts/arch.sh +5 -5
- package/scripts/build.js +16 -3
- package/scripts/fetch-gpu.js +105 -0
- package/scripts/fetch.js +11 -1
- package/scripts/gpu-firmware.json +22 -0
- package/scripts/modules.js +5 -1
- package/scripts/npm-packages.md +17 -17
- package/test/bunterm-fps.test.js +17 -0
- package/test/bunterm-keyboard.test.js +116 -0
- package/test/bunterm-next-vt.test.js +9 -0
- package/test/bunterm-raw-protocol.test.js +19 -0
- package/test/bunterm-renderer.test.js +158 -0
- package/test/bunterm-selection.test.js +38 -0
- package/test/bunterm-vt-menu.test.js +178 -0
- package/test/chroot-bind.test.js +40 -2
- package/test/chvt.test.js +46 -0
- package/test/dhcp-start.test.js +67 -0
- package/test/dhcp.test.js +99 -0
- package/test/fbdev-sync.test.js +33 -0
- package/test/fetch-npm.test.js +1 -1
- package/test/gpu.test.js +155 -0
- package/test/image-size.test.js +17 -0
- package/test/libc.test.js +41 -0
- package/test/manual-bunterm-image-paths.js +31 -0
- package/test/mount-libc-lifetime.test.js +50 -0
- package/test/no-gpu.test.js +43 -0
- package/test/ping.test.js +80 -0
- package/test/readme-cli.test.js +29 -0
- package/test/stage-npm.test.js +11 -4
- package/test/xdg-open.test.js +55 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { imageBytes } from "../scripts/build.js";
|
|
3
|
+
import { layout } from "../initramfs/lib/fat.js";
|
|
4
|
+
const MiB = 1024 * 1024;
|
|
5
|
+
test("image sizing preserves small images and fits GPU firmware payloads plus FAT metadata", () => {
|
|
6
|
+
expect(imageBytes(80 * MiB)).toBe(128 * MiB);
|
|
7
|
+
// Actual no-gpu EFI: the former 16 MiB reserve incorrectly made this 192 MiB.
|
|
8
|
+
expect(imageBytes(113646592)).toBe(128 * MiB);
|
|
9
|
+
expect(imageBytes(266 * MiB)).toBe(320 * MiB);
|
|
10
|
+
for (const payload of [0, 100 * MiB, 120 * MiB, 240 * MiB, 266 * MiB, 512 * MiB, 0xffffffff]) {
|
|
11
|
+
const size = imageBytes(payload);
|
|
12
|
+
const geometry = layout(size / 512 - 34 - 2048 + 1);
|
|
13
|
+
expect((geometry.clusters - 3) * geometry.perCluster * 512).toBeGreaterThan(payload);
|
|
14
|
+
expect(size % (64 * MiB)).toBe(0);
|
|
15
|
+
}
|
|
16
|
+
expect(() => imageBytes(0x100000000)).toThrow("FAT32");
|
|
17
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {libc,libcPath,errno,strerror,SysError,withInetSocket} from '../initramfs/lib/dlopen.js';
|
|
3
|
+
import {spawnSync} from 'node:child_process';
|
|
4
|
+
test('host libc loads automatically and exposes the platform errno',()=>{
|
|
5
|
+
expect(libc.symbols.close(-1)).toBe(-1);expect(libcPath.length).toBeGreaterThan(0);expect(errno()).toBe(9);expect(strerror(9)).toContain('descriptor');expect(new SysError('close').errno).toBe(9);
|
|
6
|
+
if(process.platform==='android')expect(libcPath).toBe('libc.so');
|
|
7
|
+
});
|
|
8
|
+
test('an explicit invalid libc path fails instead of silently falling back',()=>{
|
|
9
|
+
const r=spawnSync(process.execPath,['-e',`const {libc}=await import(${JSON.stringify(new URL('../initramfs/lib/dlopen.js',import.meta.url).href)});libc.symbols.close(-1)`],{env:{...process.env,BUNINU_LIBC:'/nonexistent/buninu-libc.so'},encoding:'utf8'});
|
|
10
|
+
expect(r.status).not.toBe(0);expect(r.stderr).toContain('/nonexistent/buninu-libc.so');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('existing libc socket/ioctl helper still queries loopback without sysfs',()=>{
|
|
14
|
+
const request=Buffer.alloc(40);request.write('lo');
|
|
15
|
+
withInetSocket(ioctl=>ioctl(0x8933,request,'SIOCGIFINDEX'));
|
|
16
|
+
expect(request.readInt32LE(16)).toBeGreaterThan(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('manual library resolves only requested symbols and keeps working after a missing symbol',()=>{
|
|
20
|
+
const moduleURL=new URL('../initramfs/lib/dlopen.js',import.meta.url).href;
|
|
21
|
+
const script=`
|
|
22
|
+
import {mock} from 'bun:test';
|
|
23
|
+
const ffi=await import('bun:ffi');
|
|
24
|
+
const calls=[];
|
|
25
|
+
mock.module('bun:ffi',()=>({...ffi,dlopen(path,symbols){
|
|
26
|
+
if(path!=='/manual/partial.so')throw new Error('unexpected fallback');
|
|
27
|
+
const name=Object.keys(symbols)[0];calls.push(name);
|
|
28
|
+
if(name!=='close')throw new Error('missing '+name);
|
|
29
|
+
return {symbols:{close:()=>7},close(){}};
|
|
30
|
+
}}));
|
|
31
|
+
const {libc}=await import(${JSON.stringify(moduleURL)});
|
|
32
|
+
if(calls.length)throw new Error('eager symbol resolution');
|
|
33
|
+
if(libc.symbols.close(0)!==7)throw new Error('supported symbol failed');
|
|
34
|
+
try {libc.symbols.mount;throw new Error('missing symbol accepted');}
|
|
35
|
+
catch(e){if(!e.message.includes('missing mount'))throw e;}
|
|
36
|
+
if(libc.symbols.close(0)!==7)throw new Error('missing symbol poisoned library');
|
|
37
|
+
if(calls.filter(x=>x==='close').length!==1)throw new Error('symbol not cached');
|
|
38
|
+
`;
|
|
39
|
+
const r=spawnSync(process.execPath,['-e',script],{env:{...process.env,BUNINU_LIBC:'/manual/partial.so'},encoding:'utf8'});
|
|
40
|
+
expect(r.stderr).toBe('');expect(r.status).toBe(0);
|
|
41
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {runGraphics} from '/lib/fbdev.js';
|
|
2
|
+
import {loadCanvasKit,openCanvas} from '/lib/canvas.js';
|
|
3
|
+
import {claimVirtualConsole} from '/lib/bunterm/vt.js';
|
|
4
|
+
// Manual native Linux benchmark; takes over tty6 temporarily.
|
|
5
|
+
// bun test/manual-bunterm-image-paths.js /path/to/1920x1080.png
|
|
6
|
+
if(!process.argv[2])throw Error('Pass a 1920x1080 PNG filename');
|
|
7
|
+
const CK=await loadCanvasKit(), results=[];
|
|
8
|
+
await runGraphics(async(display,{onRelease,onAcquire})=>{
|
|
9
|
+
const screen=openCanvas(CK,display);let active=true;onRelease(()=>active=false);onAcquire(()=>active=true);
|
|
10
|
+
const bytes=await Bun.file(process.argv[2]).bytes();
|
|
11
|
+
const image=CK.MakeImageFromEncoded(bytes),w=image.width(),h=image.height();
|
|
12
|
+
if(w!==1920||h!==1080||display.width!==1920||display.height!==1080||display.bitsPerPixel!==32)throw Error("This comparison requires a 1920x1080 image and 32-bit 1920x1080 framebuffer");
|
|
13
|
+
const src=CK.LTRBRect(0,0,w,h), native=CK.LTRBRect(0,0,w,h),scaled=CK.LTRBRect(0,26,1920,1066);
|
|
14
|
+
const info={width:w,height:h,colorType:CK.ColorType.RGBA_8888,alphaType:CK.AlphaType.Unpremul,colorSpace:CK.ColorSpace.SRGB};
|
|
15
|
+
const start=performance.now(),pixels=image.readPixels(0,0,info);
|
|
16
|
+
if(!pixels)throw Error('readPixels failed');
|
|
17
|
+
if(screen.colorType===CK.ColorType.BGRA_8888)for(let i=0;i<pixels.length;i+=4){const r=pixels[i];pixels[i]=pixels[i+2];pixels[i+2]=r;}results.push({readPixelsMs:performance.now()-start,width:w,height:h,bytes:pixels?.length});
|
|
18
|
+
const actions={
|
|
19
|
+
'rows-scaled':()=>{for(let y=0;y<1080;y+=26){screen.canvas.save();screen.canvas.clipRect(CK.LTRBRect(0,y,1920,Math.min(1080,y+26)),CK.ClipOp.Intersect,false);screen.canvas.drawImageRectOptions(image,src,scaled,CK.FilterMode.Linear,CK.MipmapMode.Linear,null);screen.canvas.restore();}},
|
|
20
|
+
'single-scaled':()=>screen.canvas.drawImageRectOptions(image,src,scaled,CK.FilterMode.Linear,CK.MipmapMode.Linear,null),
|
|
21
|
+
'single-1to1':()=>screen.canvas.drawImageRectOptions(image,src,native,CK.FilterMode.Linear,CK.MipmapMode.Linear,null),
|
|
22
|
+
'drawImage-1to1':()=>screen.canvas.drawImage(image,0,0),
|
|
23
|
+
'decode-copy-1to1':()=>{const fresh=CK.MakeImageFromEncoded(bytes);try{const p=fresh.readPixels(0,0,info);if(!p)throw Error('decode failed');if(screen.colorType===CK.ColorType.BGRA_8888)for(let i=0;i<p.length;i+=4){const r=p[i];p[i]=p[i+2];p[i+2]=r;}screen.restoreRegion({x:0,y:0,height:Math.min(h,1080),rowBytes:w*4,pixels:p});}finally{fresh.delete();}},
|
|
24
|
+
'copy-1to1':()=>screen.restoreRegion({x:0,y:0,height:Math.min(h,1080),rowBytes:w*4,pixels}),
|
|
25
|
+
};
|
|
26
|
+
try {for(const [name,draw] of Object.entries(actions)){
|
|
27
|
+
screen.canvas.clear(CK.BLACK);draw();screen.flush();
|
|
28
|
+
const t=performance.now();let count=0,total=0;while(performance.now()-t<2500){if(!active)throw Error('VT switched');const t0=performance.now();draw();screen.flush();total+=performance.now()-t0;count++;await Bun.sleep(16);}
|
|
29
|
+
results.push({name,ms:total/count,count});await Bun.write('/tmp/bunterm-image-paths.json',JSON.stringify(results,null,2));
|
|
30
|
+
}}finally{image.delete();screen.close();}
|
|
31
|
+
},{console:['/dev/tty6'],prepareConsole:claimVirtualConsole});process.exit(0);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {mkdtempSync,writeFileSync,readFileSync,rmSync} from 'node:fs';
|
|
3
|
+
import {tmpdir} from 'node:os';
|
|
4
|
+
import {join} from 'node:path';
|
|
5
|
+
|
|
6
|
+
// Run the real command with a libc whose path disappears after the first
|
|
7
|
+
// successful mount change. Never mount or unmount anything on the host.
|
|
8
|
+
async function invoke(name,args){
|
|
9
|
+
const dir=mkdtempSync(join(tmpdir(),'buninu-mount-libc-'));
|
|
10
|
+
const mock=join(dir,'libc.mjs');
|
|
11
|
+
writeFileSync(mock,`
|
|
12
|
+
let hidden=false;
|
|
13
|
+
const bound=new Map();
|
|
14
|
+
const functions={mount:()=>{if(hidden)return -1;hidden=true;return 0;},
|
|
15
|
+
umount2:()=>{if(hidden)return -1;hidden=true;return 0;},
|
|
16
|
+
errno:()=>5,strerror:()=> 'mock I/O error'};
|
|
17
|
+
export const libc={symbols:new Proxy({}, {get(_target,name){
|
|
18
|
+
if(!bound.has(name)){
|
|
19
|
+
if(hidden)throw Error('Cannot load libc symbol '+name);
|
|
20
|
+
bound.set(name,functions[name]);
|
|
21
|
+
}
|
|
22
|
+
return bound.get(name);
|
|
23
|
+
}})};
|
|
24
|
+
export const ptr=x=>x,cString=x=>x,errno=()=>libc.symbols.errno(),
|
|
25
|
+
strerror=code=>libc.symbols.strerror(code),Errno={},SysError=class extends Error{};
|
|
26
|
+
`);
|
|
27
|
+
const source=readFileSync(new URL('../initramfs/bin/'+name,import.meta.url),'utf8')
|
|
28
|
+
.replace('"../lib/dlopen.js"',JSON.stringify(mock))
|
|
29
|
+
.replace('import { showDocument } from "../lib/document.js";','const showDocument=async()=>{};')
|
|
30
|
+
.replace('import { modprobe, tryModprobe, modulesDirectory, ModuleNotFound } from "../lib/modprobe.js";',
|
|
31
|
+
'const modprobe=()=>[],tryModprobe=()=>[],modulesDirectory=()=>"";class ModuleNotFound extends Error{}');
|
|
32
|
+
const entry=join(dir,'command.mjs');writeFileSync(entry,source);
|
|
33
|
+
try{
|
|
34
|
+
const child=Bun.spawn([process.execPath,entry,...args],{stdout:'pipe',stderr:'pipe'});
|
|
35
|
+
const stderr=new Response(child.stderr).text();
|
|
36
|
+
return {status:await child.exited,stderr:await stderr};
|
|
37
|
+
}finally{rmSync(dir,{recursive:true,force:true});}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test('read-only bind reports remount failure after the libc path is hidden',async()=>{
|
|
41
|
+
const r=await invoke('mount',['--bind','-o','ro','/fake-source','/fake-target']);
|
|
42
|
+
expect(r.status).toBe(32);expect(r.stderr).toContain('mount: mock I/O error');
|
|
43
|
+
expect(r.stderr).not.toContain('Cannot load libc symbol');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('later unmount failure reports strerror after an earlier target hides libc',async()=>{
|
|
47
|
+
const r=await invoke('umount',['/fake-lib','/fake-missing']);
|
|
48
|
+
expect(r.status).toBe(32);expect(r.stderr).toContain('umount: /fake-missing: mock I/O error.');
|
|
49
|
+
expect(r.stderr).not.toContain('Cannot load libc symbol');
|
|
50
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {test,expect} from "bun:test";
|
|
2
|
+
import {mkdtempSync,mkdirSync,writeFileSync,existsSync,rmSync} from "node:fs";
|
|
3
|
+
import {tmpdir} from "node:os";
|
|
4
|
+
import {join} from "node:path";
|
|
5
|
+
import {requests,select} from "../scripts/modules.js";
|
|
6
|
+
import {config} from "../scripts/arch.js";
|
|
7
|
+
import {fetchGpuFirmware,gpuReady} from "../scripts/fetch-gpu.js";
|
|
8
|
+
const additions=new Set(["virtio-gpu","vmwgfx","i915","xe","amdgpu","radeon","nouveau"]);
|
|
9
|
+
test("no-gpu only removes new explicit requests across target configurations",()=>{
|
|
10
|
+
for(const arch of ["x86_64","aarch64"])for(const flavor of ["lts","virt"])for(const real of [true,false]){
|
|
11
|
+
const cfg={arch,flavor,real};
|
|
12
|
+
expect(requests({...cfg,noGpu:true})).toEqual(requests(cfg).filter(r=>!additions.has(r.name)));
|
|
13
|
+
}
|
|
14
|
+
expect(config({noGpu:true}).noGpu).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
test("no-gpu retains a GPU-directory dependency when an original module needs it",()=>{
|
|
17
|
+
const cfg={arch:"x86_64",flavor:"lts",real:false,noGpu:true};
|
|
18
|
+
const dep=requests(cfg).map(({name})=>`kernel/test/${name}.ko.gz:${name==="snd-hda-intel"?" kernel/drivers/gpu/drm/shared.ko.gz":""}`).join("\n")+"\nkernel/drivers/gpu/drm/shared.ko.gz:";
|
|
19
|
+
expect(select({...cfg,dep,builtin:"",packageName:"test"}).selected).toContain("kernel/drivers/gpu/drm/shared.ko.gz");
|
|
20
|
+
});
|
|
21
|
+
test("no-gpu clears generated firmware and caches cannot cross GPU modes",async()=>{
|
|
22
|
+
const root=mkdtempSync(join(tmpdir(),"no-gpu-"));
|
|
23
|
+
try{
|
|
24
|
+
const cfg={...config({arch:"x86_64",flavor:"lts",real:true,noGpu:true}),nativeDir:root,modulesRoot:join(root,"modules")};
|
|
25
|
+
mkdirSync(cfg.modulesRoot,{recursive:true});
|
|
26
|
+
writeFileSync(join(cfg.modulesRoot,"modules.dep"),"");
|
|
27
|
+
writeFileSync(join(cfg.modulesRoot,"modules.builtin"),"kernel/drivers/gpu/drm/i915/i915.ko\n");
|
|
28
|
+
mkdirSync(join(root,"lib/firmware"),{recursive:true});
|
|
29
|
+
writeFileSync(join(root,"lib/firmware/stale"),"old firmware");
|
|
30
|
+
await fetchGpuFirmware(cfg);
|
|
31
|
+
expect(existsSync(join(root,"lib/firmware"))).toBe(false);
|
|
32
|
+
expect(gpuReady(cfg)).toBe(true);
|
|
33
|
+
expect(gpuReady({...cfg,noGpu:false})).toBe(false);
|
|
34
|
+
mkdirSync(join(root,"lib/firmware"));
|
|
35
|
+
expect(gpuReady(cfg)).toBe(false);
|
|
36
|
+
}finally{rmSync(root,{recursive:true,force:true});}
|
|
37
|
+
});
|
|
38
|
+
test("launcher documents no-gpu and rejects it without fetch/build",()=>{
|
|
39
|
+
const help=Bun.spawnSync([process.execPath,"index.js","--help"]);
|
|
40
|
+
expect(help.exitCode).toBe(0);expect(help.stdout.toString()).toContain("--no-gpu");
|
|
41
|
+
const invalid=Bun.spawnSync([process.execPath,"index.js","-r","--no-gpu"]);
|
|
42
|
+
expect(invalid.exitCode).not.toBe(0);expect(invalid.stderr.toString()).toContain("only make sense");
|
|
43
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {spawnSync} from 'node:child_process';
|
|
3
|
+
import {resolve} from 'node:path';
|
|
4
|
+
import {parsePing,checksum,echoPacket,isEchoReply} from '../initramfs/lib/ping.js';
|
|
5
|
+
const command=resolve(import.meta.dir,'../initramfs/bin/ping');
|
|
6
|
+
const system=Bun.which('ping');
|
|
7
|
+
const separatePing6=Bun.which('ping6');
|
|
8
|
+
const supports6=system&&spawnSync(system,['-6','-c','1','-W','1','::1'],{encoding:'utf8',timeout:2000}).status===0;
|
|
9
|
+
const run=(args,reference=false)=>{
|
|
10
|
+
const usePing6=reference&&args.includes('-6')&&!supports6&&separatePing6;
|
|
11
|
+
const executable=reference?(usePing6?separatePing6:system):process.execPath;
|
|
12
|
+
const options=reference?args.filter(a=>a!=='-4'&&(!usePing6||a!=='-6')):[command,...args];
|
|
13
|
+
return spawnSync(executable,options,{encoding:'utf8',timeout:6000,env:{...process.env,LC_ALL:'C'}});
|
|
14
|
+
};
|
|
15
|
+
const summary=s=>{const m=/(\d+) packets transmitted, (\d+) (?:packets )?received,.*?(\d+)% packet loss/.exec(s);return m?m.slice(1).map(Number):null};
|
|
16
|
+
const probe=system?run(['-4','-c','1','-W','1','127.0.0.1'],true):null;
|
|
17
|
+
const available=probe?.status===0;
|
|
18
|
+
|
|
19
|
+
test('ping parses common flags and rejects malformed arguments',()=>{
|
|
20
|
+
expect(parsePing(['-4nqD','-c3','-i','.2','-W','1','-w','5','-s','17','-t64','-I','lo','localhost'])).toMatchObject({family:4,count:3,interval:.2,timeout:1,deadline:5,size:17,ttl:64,source:'lo',host:'localhost',quiet:true,timestamp:true});
|
|
21
|
+
expect(parsePing(['localhost']).count).toBe(4);
|
|
22
|
+
expect(parsePing(['-6','--','::1']).family).toBe(6);
|
|
23
|
+
for(const args of [[],['-c'],['-c','0','localhost'],['-c','Infinity','localhost'],['-W','NaN','localhost'],['-s','65508','localhost'],['-s','-1','localhost'],['-t','256','localhost'],['-i','0','localhost'],['-w','0','localhost'],['-Z','localhost'],['a','b']])expect(()=>parsePing(args)).toThrow();
|
|
24
|
+
expect(parsePing(['--help']).help).toBe(true);
|
|
25
|
+
expect(run(['-c','0','127.0.0.1']).status).toBe(2);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('ICMP checksum, reply identity, data integrity and raw IPv4 header validation',()=>{
|
|
29
|
+
for(const size of [0,1,17,56]){
|
|
30
|
+
const request=echoPacket(4,123,7,size);expect(checksum(request)).toBe(0);
|
|
31
|
+
const reply=Buffer.from(request);reply[0]=0;reply.writeUInt16BE(0,2);reply.writeUInt16BE(checksum(reply),2);
|
|
32
|
+
const info={family:4,raw:false,id:123,seq:7,packet:request};
|
|
33
|
+
expect(isEchoReply(reply,info)).toBe(true);expect(isEchoReply(request,info)).toBe(false);
|
|
34
|
+
expect(isEchoReply(reply,{...info,id:124})).toBe(false);expect(isEchoReply(reply,{...info,seq:8})).toBe(false);
|
|
35
|
+
const ip=Buffer.alloc(24);ip[0]=0x46;ip[9]=1;
|
|
36
|
+
expect(isEchoReply(Buffer.concat([ip,reply]),{...info,raw:true})).toBe(true);
|
|
37
|
+
expect(isEchoReply(reply.subarray(0,7),info)).toBe(false);
|
|
38
|
+
if(size){reply[8]^=1;expect(isEchoReply(reply,info)).toBe(false);}
|
|
39
|
+
}
|
|
40
|
+
const packet=echoPacket(6,3,4,8),reply=Buffer.from(packet);reply[0]=129;
|
|
41
|
+
expect(isEchoReply(reply,{family:6,raw:false,id:3,seq:4,packet})).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
for(const size of [0,17,56,1400])test.skipIf(!available)(`system ping comparison: IPv4 count, payload ${size}, statistics and exit status`,()=>{
|
|
45
|
+
const args=['-4','-n','-c','3','-i','0.2','-W','1','-s',String(size),'127.0.0.1'];
|
|
46
|
+
const reference=run(args,true),actual=run(args);
|
|
47
|
+
expect(actual.status).toBe(reference.status);expect(actual.status).toBe(0);
|
|
48
|
+
expect(summary(actual.stdout)).toEqual(summary(reference.stdout));expect(summary(actual.stdout)).toEqual([3,3,0]);
|
|
49
|
+
expect(actual.stdout).toContain(`${size+8} bytes from 127.0.0.1`);expect(actual.stdout).toContain('icmp_seq=3');
|
|
50
|
+
expect(actual.stdout).toMatch(/rtt min\/avg\/max\/mdev = [\d.]+\/[\d.]+\/[\d.]+\/[\d.]+ ms/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test.skipIf(!available)('system ping comparison: hostname, source address, quiet mode and TTL',()=>{
|
|
54
|
+
const args=['-4','-n','-q','-c','2','-i','.2','-W','1','-t','32','-I','127.0.0.1','localhost'];
|
|
55
|
+
const reference=run(args,true),actual=run(args);
|
|
56
|
+
expect(actual.status).toBe(reference.status);expect(actual.status).toBe(0);expect(summary(actual.stdout)).toEqual(summary(reference.stdout));expect(actual.stdout).not.toContain('icmp_seq=');
|
|
57
|
+
});
|
|
58
|
+
const ipv6=available&&run(['-6','-c','1','-W','1','::1'],true).status===0;
|
|
59
|
+
test.skipIf(!ipv6)('system ping comparison: IPv6 loopback',()=>{
|
|
60
|
+
const args=['-6','-n','-c','3','-i','.2','-W','1','::1'],reference=run(args,true),actual=run(args);
|
|
61
|
+
expect(actual.status).toBe(reference.status);expect(actual.status).toBe(0);expect(summary(actual.stdout)).toEqual([3,3,0]);expect(summary(actual.stdout)).toEqual(summary(reference.stdout));
|
|
62
|
+
});
|
|
63
|
+
test.skipIf(!available)('system ping comparison: deadline before requested count returns failure',()=>{
|
|
64
|
+
const args=['-4','-q','-c','100','-i','.2','-w','1','127.0.0.1'],reference=run(args,true),begin=performance.now(),actual=run(args);
|
|
65
|
+
expect(actual.status).toBe(reference.status);expect(actual.status).toBe(1);expect(performance.now()-begin).toBeLessThan(2500);expect(summary(actual.stdout)[0]).toBeLessThan(100);
|
|
66
|
+
});
|
|
67
|
+
test.skipIf(!available)('Ctrl-C prints statistics and releases the socket',async()=>{
|
|
68
|
+
const child=Bun.spawn([process.execPath,command,'-i','.2','127.0.0.1'],{stdout:'pipe',stderr:'pipe'});
|
|
69
|
+
const reader=child.stdout.getReader();let output='';
|
|
70
|
+
try{
|
|
71
|
+
for(;;){const {value,done}=await reader.read();if(done)break;output+=new TextDecoder().decode(value);if(output.includes('icmp_seq=')){child.kill('SIGINT');break;}}
|
|
72
|
+
for(;;){const {value,done}=await reader.read();if(done)break;output+=new TextDecoder().decode(value);}
|
|
73
|
+
expect(await child.exited).toBe(0);expect(output).toContain('ping statistics');expect(summary(output)[1]).toBeGreaterThan(0);
|
|
74
|
+
}finally{child.kill();reader.releaseLock();}
|
|
75
|
+
},6000);
|
|
76
|
+
|
|
77
|
+
test.skipIf(!available)('sub-200ms interval and fractional timeout are supported independently of old Android ping',()=>{
|
|
78
|
+
const r=run(['-c','3','-i','.01','-W','.1','-D','127.0.0.1']);
|
|
79
|
+
expect(r.status).toBe(0);expect(summary(r.stdout)).toEqual([3,3,0]);expect(r.stdout).toMatch(/\[\d+\.\d+\] 64 bytes/);
|
|
80
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
const entry = fileURLToPath(new URL("../index.js", import.meta.url));
|
|
5
|
+
|
|
6
|
+
for (const [flag, filename] of [
|
|
7
|
+
["--readme", "README.md"],
|
|
8
|
+
["--readme-zh", "README.zh.md"],
|
|
9
|
+
["--docs", "README.zh.md"],
|
|
10
|
+
]) {
|
|
11
|
+
test(`${flag} flushes the complete document to a pipe before exiting`, async () => {
|
|
12
|
+
const markdown = await Bun.file(new URL(`../${filename}`, import.meta.url)).text();
|
|
13
|
+
const rendered = Bun.markdown.ansi(markdown, { hyperlinks: true });
|
|
14
|
+
const expected = rendered.endsWith("\n") ? rendered : `${rendered}\n`;
|
|
15
|
+
// Both real documents exceed the pipe chunk that was previously truncated.
|
|
16
|
+
expect(Buffer.byteLength(expected)).toBeGreaterThan(65536);
|
|
17
|
+
const child = Bun.spawn([process.execPath, entry, flag], {
|
|
18
|
+
stdout: "pipe", stderr: "pipe",
|
|
19
|
+
});
|
|
20
|
+
const [output, errors, status] = await Promise.all([
|
|
21
|
+
new Response(child.stdout).text(),
|
|
22
|
+
new Response(child.stderr).text(),
|
|
23
|
+
child.exited,
|
|
24
|
+
]);
|
|
25
|
+
expect(status).toBe(0);
|
|
26
|
+
expect(errors).toBe("");
|
|
27
|
+
expect(output).toBe(expected);
|
|
28
|
+
});
|
|
29
|
+
}
|
package/test/stage-npm.test.js
CHANGED
|
@@ -101,9 +101,16 @@ test("distribution launcher ensures resources then forwards to local casty witho
|
|
|
101
101
|
`);
|
|
102
102
|
writeFileSync(join(root, "usr/lib/node_modules/@drxiaozhi/casty/index.js"),
|
|
103
103
|
"console.log(JSON.stringify({args:process.argv.slice(2),engine:process.env.CASTY_BROWSER}));");
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
for (const args of [
|
|
105
|
+
["https://example.test", "--", "--chromium"],
|
|
106
|
+
["--", "--help"],
|
|
107
|
+
["--", "--version"],
|
|
108
|
+
["--", "--window-size=160,90"],
|
|
109
|
+
]) {
|
|
110
|
+
const result = spawnSync(process.execPath, [join(root, "bin/buninu-browser"), "--casty", ...args], { encoding: "utf8" });
|
|
111
|
+
expect(result.status).toBe(0);
|
|
112
|
+
expect(JSON.parse(result.stdout)).toEqual({ args, engine: join(browser, "buninu-browser.js") });
|
|
113
|
+
}
|
|
107
114
|
});
|
|
108
115
|
|
|
109
116
|
test("land launcher selects bundled casty and preserves package frontend dispatch", async () => {
|
|
@@ -130,7 +137,7 @@ test("land launcher selects bundled casty and preserves package frontend dispatc
|
|
|
130
137
|
});
|
|
131
138
|
expect(result.status).toBe(0);
|
|
132
139
|
expect(JSON.parse(result.stdout)).toEqual({
|
|
133
|
-
args: args[0] === '--casty' ? args.slice(1) : args,
|
|
140
|
+
args: ['--no-shortcuts', ...(args[0] === '--casty' ? args.slice(1) : args)],
|
|
134
141
|
engine: join(land, "compositor.js"),
|
|
135
142
|
});
|
|
136
143
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {afterAll, expect, test} from 'bun:test';
|
|
2
|
+
import {mkdtempSync,writeFileSync,symlinkSync,rmSync,chmodSync} from 'node:fs';
|
|
3
|
+
import {tmpdir} from 'node:os';
|
|
4
|
+
import {join} from 'node:path';
|
|
5
|
+
import {pathToFileURL} from 'node:url';
|
|
6
|
+
import {browserTarget,parseOpenArgs} from '../initramfs/lib/xdg-open.js';
|
|
7
|
+
const dir=mkdtempSync(join(tmpdir(),'buninu-xdg-open-'));
|
|
8
|
+
afterAll(()=>rmSync(dir,{recursive:true,force:true}));
|
|
9
|
+
const file=join(dir,'中文 # %.html');writeFileSync(file,'<p>test</p>');
|
|
10
|
+
symlinkSync(file,join(dir,'alias.html'));
|
|
11
|
+
test('HTTP and HTTPS remain browser URLs',()=>{
|
|
12
|
+
expect(browserTarget('https://buninu.org/path?q=one#two')).toBe('https://buninu.org/path?q=one#two');
|
|
13
|
+
expect(browserTarget('http://localhost:8080')).toBe('http://localhost:8080/');
|
|
14
|
+
expect(()=>browserTarget('http://')).toThrow();
|
|
15
|
+
});
|
|
16
|
+
test('local files resolve symlinks and escape URL-special characters',()=>{
|
|
17
|
+
expect(browserTarget('alias.html',dir)).toBe(pathToFileURL(file).href);
|
|
18
|
+
expect(browserTarget(pathToFileURL(file).href)).toBe(pathToFileURL(file).href);
|
|
19
|
+
expect(()=>browserTarget('missing',dir)).toThrow();
|
|
20
|
+
expect(()=>browserTarget(dir)).toThrow('regular file');
|
|
21
|
+
expect(()=>browserTarget('javascript:alert(1)')).toThrow('Unsupported');
|
|
22
|
+
expect(()=>browserTarget('mailto:test@example.com')).toThrow('Unsupported');
|
|
23
|
+
});
|
|
24
|
+
test('argument validation',()=>{
|
|
25
|
+
expect(parseOpenArgs(['--help'])).toEqual({help:true});
|
|
26
|
+
expect(()=>parseOpenArgs([])).toThrow('Usage');
|
|
27
|
+
expect(()=>parseOpenArgs(['a','b'])).toThrow('Usage');
|
|
28
|
+
});
|
|
29
|
+
test('launcher passes a single URL argument to a new bunterm and maps launch failure to status 4',async()=>{
|
|
30
|
+
const stub=join(dir,'bunterm');writeFileSync(stub,`#!${process.execPath}\nconsole.log(JSON.stringify(process.argv.slice(2)));process.exit(7);`);chmodSync(stub,0o755);
|
|
31
|
+
const browser=join(dir,'buninu-browser');writeFileSync(browser,'#!/bin/sh\nexit 0');chmodSync(browser,0o755);
|
|
32
|
+
const url='https://example.test/?q=$(touch%20bad)&x=1';
|
|
33
|
+
const p=Bun.spawn([process.execPath,new URL('../initramfs/bin/xdg-open',import.meta.url).pathname,url],{env:{...process.env,PATH:dir+':'+process.env.PATH},stdout:'pipe',stderr:'pipe'});
|
|
34
|
+
expect(await p.exited).toBe(4);
|
|
35
|
+
expect(JSON.parse(await new Response(p.stdout).text())).toEqual(['-e','buninu-browser',url]);
|
|
36
|
+
expect(await new Response(p.stderr).text()).toBe('');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('about URLs pass through; Markdown remains a browser file URL',()=>{
|
|
40
|
+
for(const url of ['about:blank','about:audio','about:custom?x=1#part'])
|
|
41
|
+
expect(browserTarget(url)).toBe(url);
|
|
42
|
+
const markdown=join(dir,'note.md');writeFileSync(markdown,'# Note');
|
|
43
|
+
expect(browserTarget(markdown)).toBe(pathToFileURL(markdown).href);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('manual aliases, version and standard failure codes work through the command',async()=>{
|
|
47
|
+
const run=async args=>{const p=Bun.spawn([process.execPath,new URL('../initramfs/bin/xdg-open',import.meta.url).pathname,...args],{env:{...process.env,PATH:'/nonexistent'},stdout:'pipe',stderr:'pipe'});return {code:await p.exited,out:await new Response(p.stdout).text()}};
|
|
48
|
+
for(const flag of ['-h','--help','--manual']){const r=await run([flag]);expect(r.code).toBe(0);expect(r.out).toContain('Exit codes');}
|
|
49
|
+
const v=await run(['--version']);expect(v.code).toBe(0);expect(v.out).toContain('xdg-open (Buninu Linux)');
|
|
50
|
+
expect((await run([])).code).toBe(1);
|
|
51
|
+
expect((await run(['http://'])).code).toBe(1);
|
|
52
|
+
expect((await run([join(dir,'absent')])).code).toBe(2);
|
|
53
|
+
expect((await run(['https://buninu.org'])).code).toBe(3);
|
|
54
|
+
expect((await run(['mailto:a@example.com'])).code).toBe(4);
|
|
55
|
+
});
|