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,124 @@
|
|
|
1
|
+
// DHCPv4 codec and lease state machine. Network/configuration adapters are
|
|
2
|
+
// separate so protocol tests never change the host's addresses or resolver.
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
export const TYPE = { DISCOVER:1, OFFER:2, REQUEST:3, DECLINE:4, ACK:5, NAK:6, RELEASE:7 };
|
|
5
|
+
export const ipv4 = text => {
|
|
6
|
+
const parts=String(text).split('.');
|
|
7
|
+
if(parts.length!==4 || parts.some(p=>!/^\d{1,3}$/.test(p)||Number(p)>255))throw new Error(`invalid IPv4 address: ${text}`);
|
|
8
|
+
return Buffer.from(parts.map(Number));
|
|
9
|
+
};
|
|
10
|
+
const address=b=>[...b].join('.');
|
|
11
|
+
export function checksum(b) {let n=0;for(let i=0;i<b.length;i+=2)n+=(b[i]<<8)|(b[i+1]??0);while(n>>>16)n=(n&65535)+(n>>>16);return (~n)&65535;}
|
|
12
|
+
export function encode({type,xid,mac,requested,server,ciaddr='0.0.0.0',hostname,secs=0,broadcast=true}) {
|
|
13
|
+
const b=Buffer.alloc(240);b[0]=1;b[1]=1;b[2]=6;b.writeUInt32BE(xid,4);b.writeUInt16BE(Math.min(65535,secs),8);
|
|
14
|
+
b.writeUInt16BE(broadcast?0x8000:0,10);ipv4(ciaddr).copy(b,12);Buffer.from(mac).copy(b,28,0,6);b.writeUInt32BE(0x63825363,236);
|
|
15
|
+
const options=[];
|
|
16
|
+
const option=(code,value)=>{value=Buffer.from(value);if(value.length>255)throw new Error('DHCP option too long');options.push(Buffer.from([code,value.length]),value);};
|
|
17
|
+
option(53,[type]);option(61,[1,...mac]);
|
|
18
|
+
if(requested)option(50,ipv4(requested));if(server)option(54,ipv4(server));
|
|
19
|
+
if(hostname)option(12,Buffer.from(hostname));
|
|
20
|
+
if(type!==TYPE.RELEASE){option(55,[1,3,6,15,28,51,58,59]);option(57,[5,220]);}
|
|
21
|
+
const packet=Buffer.concat([b,...options,Buffer.from([255])]);
|
|
22
|
+
return packet.length<300?Buffer.concat([packet,Buffer.alloc(300-packet.length)]):packet;
|
|
23
|
+
}
|
|
24
|
+
export function decode(b,{xid,mac}={}) {
|
|
25
|
+
if(b.length<240||b[0]!==2||b[1]!==1||b[2]!==6||b.readUInt32BE(236)!==0x63825363)return null;
|
|
26
|
+
if(xid!==undefined&&b.readUInt32BE(4)!==xid)return null;
|
|
27
|
+
if(mac&&!b.subarray(28,34).equals(Buffer.from(mac)))return null;
|
|
28
|
+
const options=new Map();
|
|
29
|
+
function parse(start,end){for(let i=start;i<end;){const code=b[i++];if(code===255)return true;if(!code)continue;if(i>=end)return false;const len=b[i++];if(i+len>end)return false;options.set(code,Buffer.concat([options.get(code)??Buffer.alloc(0),b.subarray(i,i+len)]));i+=len;}return true;}
|
|
30
|
+
if(!parse(240,b.length))return null;
|
|
31
|
+
const overload=options.get(52);if(overload&&(overload.length!==1||overload[0]>3))return null;
|
|
32
|
+
if(overload?.[0]&1)if(!parse(108,236))return null;
|
|
33
|
+
if(overload?.[0]&2)if(!parse(44,108))return null;
|
|
34
|
+
if(options.get(53)?.length!==1)return null;
|
|
35
|
+
for(const code of [1,28,51,54,58,59])if(options.has(code)&&options.get(code).length!==4)return null;
|
|
36
|
+
for(const code of [3,6])if(options.has(code)&&(!options.get(code).length||options.get(code).length%4))return null;
|
|
37
|
+
const ip=code=>options.has(code)?address(options.get(code)):undefined;
|
|
38
|
+
const ips=code=>{const v=options.get(code),out=[];if(v)for(let i=0;i<v.length;i+=4)out.push(address(v.subarray(i,i+4)));return out;};
|
|
39
|
+
const number=code=>options.get(code)?.readUInt32BE(0);
|
|
40
|
+
return {type:options.get(53)[0],xid:b.readUInt32BE(4),address:address(b.subarray(16,20)),server:ip(54),mask:ip(1),routers:options.has(3)?ips(3):undefined,dns:options.has(6)?ips(6):undefined,lease:number(51),t1:number(58),t2:number(59)};
|
|
41
|
+
}
|
|
42
|
+
export function prefixOf(mask) {
|
|
43
|
+
const n=ipv4(mask).readUInt32BE(),inverse=(~n)>>>0;
|
|
44
|
+
if((inverse&(inverse+1))!==0)throw new Error('DHCP supplied a non-contiguous subnet mask');
|
|
45
|
+
return 32-Math.log2(inverse+1);
|
|
46
|
+
}
|
|
47
|
+
export function leaseFrom(reply,previous,now) {
|
|
48
|
+
const ip=reply.address==='0.0.0.0'?previous?.address:reply.address;
|
|
49
|
+
const server=reply.server,mask=reply.mask??previous?.mask;
|
|
50
|
+
if(!ip||ip==='0.0.0.0'||!server||!mask||!reply.lease)throw new Error('DHCP ACK is missing address, server, mask or lease duration');
|
|
51
|
+
const prefix=prefixOf(mask),seconds=reply.lease;
|
|
52
|
+
let t1=reply.t1??seconds*.5,t2=reply.t2??seconds*.875;
|
|
53
|
+
if(!(t1>0&&t1<t2&&t2<seconds)){t1=seconds*.5;t2=seconds*.875;}
|
|
54
|
+
return {address:ip,server,mask,prefix,routers:reply.routers??previous?.routers??[],dns:reply.dns??previous?.dns??[],seconds,
|
|
55
|
+
acquiredAt:now,renewAt:now+t1*1000,rebindAt:now+t2*1000,expiresAt:now+seconds*1000};
|
|
56
|
+
}
|
|
57
|
+
// SOCK_DGRAM AF_PACKET supplies/consumes the Ethernet header itself.
|
|
58
|
+
export function wrapIPv4(payload,source='0.0.0.0',destination='255.255.255.255') {
|
|
59
|
+
const b=Buffer.alloc(28+payload.length);b[0]=0x45;b.writeUInt16BE(b.length,2);b[8]=64;b[9]=17;
|
|
60
|
+
ipv4(source).copy(b,12);ipv4(destination).copy(b,16);b.writeUInt16BE(checksum(b.subarray(0,20)),10);
|
|
61
|
+
b.writeUInt16BE(68,20);b.writeUInt16BE(67,22);b.writeUInt16BE(8+payload.length,24);payload.copy(b,28);
|
|
62
|
+
// IPv4 allows zero UDP checksum; the IPv4 header is checksummed above.
|
|
63
|
+
return b;
|
|
64
|
+
}
|
|
65
|
+
export function unwrapIPv4(b) {
|
|
66
|
+
if(b.length<28||b[0]>>>4!==4||b[9]!==17)return null;
|
|
67
|
+
const h=(b[0]&15)*4,n=b.readUInt16BE(2);
|
|
68
|
+
if(h<20||n>b.length||n<h+8||(b.readUInt16BE(6)&0x3fff)||checksum(b.subarray(0,h)))return null;
|
|
69
|
+
const len=b.readUInt16BE(h+4);
|
|
70
|
+
if(b.readUInt16BE(h)!==67||b.readUInt16BE(h+2)!==68||len<8||h+len>n)return null;
|
|
71
|
+
if(b.readUInt16BE(h+6)){
|
|
72
|
+
const pseudo=Buffer.alloc(12);b.copy(pseudo,0,12,20);pseudo[9]=17;pseudo.writeUInt16BE(len,10);
|
|
73
|
+
if(checksum(Buffer.concat([pseudo,b.subarray(h,h+len)])))return null;
|
|
74
|
+
}
|
|
75
|
+
return b.subarray(h+8,h+len);
|
|
76
|
+
}
|
|
77
|
+
export class NakError extends Error {}
|
|
78
|
+
export class DhcpClient {
|
|
79
|
+
constructor({transport,mac,hostname,timeout=60,signal,log=()=>{},now=Date.now,sleep=ms=>Bun.sleep(ms),random=Math.random}) {
|
|
80
|
+
Object.assign(this,{transport,mac,hostname,timeout,signal,log,now,sleep,random});
|
|
81
|
+
}
|
|
82
|
+
get stopped(){return this.signal?.aborted;}
|
|
83
|
+
async exchange(fields,types,deadline,server) {
|
|
84
|
+
const xid=randomBytes(4).readUInt32BE(),start=this.now();let retry=0,next=start;
|
|
85
|
+
while(!this.stopped&&this.now()<deadline){
|
|
86
|
+
if(this.now()>=next){
|
|
87
|
+
const packet=encode({...fields,xid,mac:this.mac,hostname:this.hostname,secs:Math.floor((this.now()-start)/1000)});
|
|
88
|
+
this.transport.send(packet,fields.broadcast===false?server:undefined);
|
|
89
|
+
next=this.now()+Math.min(64000,4000*2**Math.min(retry++,4))*(.75+.5*this.random());
|
|
90
|
+
}
|
|
91
|
+
// Bound the work per iteration so unrelated traffic cannot starve timers.
|
|
92
|
+
for(let i=0;i<64;i++){
|
|
93
|
+
const packet=this.transport.receive();if(!packet)break;
|
|
94
|
+
const reply=decode(packet,{xid,mac:this.mac});
|
|
95
|
+
if(!reply||!types.includes(reply.type)||!reply.server||(server&&reply.server!==server))continue;
|
|
96
|
+
if(reply.type===TYPE.NAK)throw new NakError('DHCP server rejected the lease');
|
|
97
|
+
return {reply,sentAt:start};
|
|
98
|
+
}
|
|
99
|
+
await this.sleep(Math.min(20,Math.max(1,deadline-this.now())));
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
async acquire() {
|
|
104
|
+
const deadline=this.now()+this.timeout*1000;
|
|
105
|
+
while(!this.stopped&&this.now()<deadline){
|
|
106
|
+
this.log('DHCPDISCOVER');
|
|
107
|
+
const offer=await this.exchange({type:TYPE.DISCOVER},[TYPE.OFFER],deadline);
|
|
108
|
+
if(!offer)return null;
|
|
109
|
+
if(offer.reply.address==='0.0.0.0')continue;
|
|
110
|
+
this.log(`DHCPOFFER ${offer.reply.address} from ${offer.reply.server}`);
|
|
111
|
+
try {
|
|
112
|
+
const result=await this.exchange({type:TYPE.REQUEST,requested:offer.reply.address,server:offer.reply.server},[TYPE.ACK,TYPE.NAK],Math.min(deadline,this.now()+16000),offer.reply.server);
|
|
113
|
+
if(result){if(result.reply.address!==offer.reply.address)continue;return leaseFrom(result.reply,offer.reply,result.sentAt);}
|
|
114
|
+
}catch(error){if(!(error instanceof NakError))throw error;await this.sleep(1000);}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
async renew(lease,rebind=false) {
|
|
119
|
+
this.log(rebind?'DHCPREQUEST broadcast (rebinding)':'DHCPREQUEST unicast (renewing)');
|
|
120
|
+
const result=await this.exchange({type:TYPE.REQUEST,ciaddr:lease.address,broadcast:rebind},[TYPE.ACK,TYPE.NAK],rebind?lease.expiresAt:lease.rebindAt,rebind?undefined:lease.server);
|
|
121
|
+
return result?leaseFrom(result.reply,lease,result.sentAt):null;
|
|
122
|
+
}
|
|
123
|
+
release(lease){this.transport.send(encode({type:TYPE.RELEASE,xid:randomBytes(4).readUInt32BE(),mac:this.mac,ciaddr:lease.address,server:lease.server,broadcast:false}),lease.server);}
|
|
124
|
+
}
|
package/initramfs/lib/dlopen.js
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
// init.js opens musl on its own because it runs before /proc exists and must
|
|
4
4
|
// not import anything. Every other command imports this module instead, so
|
|
5
5
|
// the symbol table, the errno plumbing and the error messages live in one
|
|
6
|
-
// place.
|
|
7
|
-
// which also lets the commands run from a checkout for testing.
|
|
6
|
+
// place. Automatic lookup follows the runtime ABI; BUNINU_LIBC overrides it.
|
|
8
7
|
|
|
9
8
|
import { dlopen, FFIType, ptr, read, CString, toArrayBuffer } from "bun:ffi";
|
|
10
9
|
|
|
@@ -20,11 +19,13 @@ const architectures = {
|
|
|
20
19
|
export const arch = architectures[process.arch]
|
|
21
20
|
?? (() => { throw new Error(`unsupported CPU architecture ${process.arch}`); })();
|
|
22
21
|
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
export const
|
|
22
|
+
// Automatic lookup must not mix bionic, glibc and musl runtime ABIs.
|
|
23
|
+
// An explicit override is authoritative: report its error instead of hiding it.
|
|
24
|
+
const android = process.platform === "android";
|
|
25
|
+
const glibc = !android && !!process.report?.getReport?.().header?.glibcVersionRuntime;
|
|
26
|
+
export const libcFamily = android ? "bionic" : glibc ? "glibc" : "musl";
|
|
27
|
+
const errnoSymbol = android ? "__errno" : "__errno_location";
|
|
28
|
+
const definitions = {
|
|
28
29
|
mount: {
|
|
29
30
|
args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.u64, FFIType.ptr],
|
|
30
31
|
returns: FFIType.i32,
|
|
@@ -41,7 +42,7 @@ export const libc = dlopen(libcPath, {
|
|
|
41
42
|
args: [],
|
|
42
43
|
returns: FFIType.void,
|
|
43
44
|
},
|
|
44
|
-
|
|
45
|
+
[errnoSymbol]: {
|
|
45
46
|
args: [],
|
|
46
47
|
returns: FFIType.ptr,
|
|
47
48
|
},
|
|
@@ -77,6 +78,14 @@ export const libc = dlopen(libcPath, {
|
|
|
77
78
|
args: [FFIType.i32, FFIType.i32, FFIType.i32],
|
|
78
79
|
returns: FFIType.i32,
|
|
79
80
|
},
|
|
81
|
+
bind: { args: [FFIType.i32, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
|
|
82
|
+
inet_pton: { args: [FFIType.i32, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
|
|
83
|
+
connect: { args: [FFIType.i32, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
|
|
84
|
+
getsockname: { args: [FFIType.i32, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
|
|
85
|
+
setsockopt: { args: [FFIType.i32, FFIType.i32, FFIType.i32, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
|
|
86
|
+
send: { args: [FFIType.i32, FFIType.ptr, FFIType.u64, FFIType.i32], returns: FFIType.i64 },
|
|
87
|
+
sendto: { args: [FFIType.i32, FFIType.ptr, FFIType.u64, FFIType.i32, FFIType.ptr, FFIType.u32], returns: FFIType.i64 },
|
|
88
|
+
recv: { args: [FFIType.i32, FFIType.ptr, FFIType.u64, FFIType.i32], returns: FFIType.i64 },
|
|
80
89
|
syscall: {
|
|
81
90
|
args: [FFIType.i64, FFIType.i64, FFIType.ptr, FFIType.i64],
|
|
82
91
|
returns: FFIType.i32,
|
|
@@ -93,13 +102,51 @@ export const libc = dlopen(libcPath, {
|
|
|
93
102
|
args: [FFIType.ptr],
|
|
94
103
|
returns: FFIType.i32,
|
|
95
104
|
},
|
|
96
|
-
}
|
|
105
|
+
};
|
|
106
|
+
const override = process.env.BUNINU_LIBC;
|
|
107
|
+
const candidates = override !== undefined ? [override]
|
|
108
|
+
: android ? ["libc.so", "/apex/com.android.runtime/lib64/bionic/libc.so", "/system/lib64/libc.so"]
|
|
109
|
+
: glibc ? ["libc.so.6", `/lib/${process.arch === "x64" ? "x86_64" : "aarch64"}-linux-gnu/libc.so.6`, `/usr/lib/${process.arch === "x64" ? "x86_64" : "aarch64"}-linux-gnu/libc.so.6`]
|
|
110
|
+
: [`${import.meta.dir}/${arch.libc}`, arch.libc, `/lib/${arch.libc}`, `/lib/ld-musl-${process.arch === "x64" ? "x86_64" : "aarch64"}.so.1`, "libc.so"];
|
|
111
|
+
// Bun requires at least one symbol per dlopen call. Defer opening until a
|
|
112
|
+
// symbol is requested; unrelated missing symbols never prevent an import.
|
|
113
|
+
export let libcPath;
|
|
114
|
+
const handles = [], bound = new Map();
|
|
115
|
+
let closed = false;
|
|
116
|
+
function resolve(name) {
|
|
117
|
+
if (bound.has(name)) return bound.get(name);
|
|
118
|
+
const failures = [];
|
|
119
|
+
for (const path of libcPath === undefined ? candidates : [libcPath]) {
|
|
120
|
+
try {
|
|
121
|
+
const handle = dlopen(path, { [name]: definitions[name] });
|
|
122
|
+
libcPath = path; handles.push(handle); bound.set(name, handle.symbols[name]);
|
|
123
|
+
return handle.symbols[name];
|
|
124
|
+
} catch (error) { failures.push(`${path}: ${error.message}`); }
|
|
125
|
+
}
|
|
126
|
+
throw new Error(`Cannot load libc symbol ${name}:\n${failures.join("\n")}`);
|
|
127
|
+
}
|
|
128
|
+
export const libc = {
|
|
129
|
+
symbols: new Proxy({}, {
|
|
130
|
+
get(_target, name) {
|
|
131
|
+
if (closed) throw new Error("libc is closed");
|
|
132
|
+
if (!Object.hasOwn(definitions, name)) return undefined;
|
|
133
|
+
const symbol = resolve(name);
|
|
134
|
+
// Compile the errno accessor before the native call, so first-use FFI
|
|
135
|
+
// compilation cannot clobber the errno that the caller wants to inspect.
|
|
136
|
+
// An absent accessor only fails when errno() is actually requested.
|
|
137
|
+
if (name !== errnoSymbol && !bound.has(errnoSymbol)) {
|
|
138
|
+
try { resolve(errnoSymbol); } catch {}
|
|
139
|
+
}
|
|
140
|
+
return symbol;
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
close() { if (closed) return; closed = true; for (const handle of handles) handle.close(); },
|
|
144
|
+
};
|
|
97
145
|
|
|
98
146
|
export const cString = (value) => new TextEncoder().encode(`${value}\0`);
|
|
99
147
|
|
|
100
148
|
// musl reports failure as -1 plus errno; the raw syscall() wrapper does too.
|
|
101
|
-
const
|
|
102
|
-
export const errno = () => read.i32(errnoLocation);
|
|
149
|
+
export const errno = () => read.i32(libc.symbols[errnoSymbol]());
|
|
103
150
|
export const strerror = (code) => new CString(libc.symbols.strerror(code)).toString();
|
|
104
151
|
|
|
105
152
|
export const Errno = {
|
package/initramfs/lib/fbdev.js
CHANGED
|
@@ -261,6 +261,26 @@ export const clear = (display, color = { r: 0, g: 0, b: 0, a: 255 }) => {
|
|
|
261
261
|
}
|
|
262
262
|
};
|
|
263
263
|
|
|
264
|
+
// msync is a file writeback operation, not a universal fbdev present API.
|
|
265
|
+
// DRM mappings may have no backing fsync operation (vfs_fsync_range then
|
|
266
|
+
// returns EINVAL). Keep working drivers' deferred-I/O sync, but probe unsupported
|
|
267
|
+
// mappings only once. The address is the original mmap base, and flags are fixed.
|
|
268
|
+
export const syncFramebuffer = (display, native = {
|
|
269
|
+
msync: (...args) => libc.symbols.msync(...args), errno, strerror,
|
|
270
|
+
}) => {
|
|
271
|
+
if (display.msyncUnsupported) return;
|
|
272
|
+
for (;;) {
|
|
273
|
+
if (native.msync(display.address, display.memoryLength, MS_SYNC) !== -1) return;
|
|
274
|
+
const code = native.errno();
|
|
275
|
+
if (code === 4) continue; // EINTR
|
|
276
|
+
if (code === 22 || code === 95) { // EINVAL / EOPNOTSUPP on this device mapping
|
|
277
|
+
display.msyncUnsupported = true;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
throw new Error(`msync(${display.path}): ${native.strerror(code)}`);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
264
284
|
export const flush = (display) => {
|
|
265
285
|
const visibleBytes = display.width * display.bytesPerPixel;
|
|
266
286
|
for (let y = 0; y < display.height; y++) {
|
|
@@ -269,9 +289,7 @@ export const flush = (display) => {
|
|
|
269
289
|
+ display.xOffset * display.bytesPerPixel;
|
|
270
290
|
display.memory.set(row, position);
|
|
271
291
|
}
|
|
272
|
-
|
|
273
|
-
fail(`msync(${display.path})`);
|
|
274
|
-
}
|
|
292
|
+
syncFramebuffer(display);
|
|
275
293
|
};
|
|
276
294
|
|
|
277
295
|
// Copies an RGBA ImageData (HTML canvas layout, 4 bytes per pixel, rows of
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Explicit one-shot GPU coldplug. Importing this module never loads a driver.
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { join, basename } from "node:path";
|
|
4
|
+
|
|
5
|
+
const drivers = new Set(["i915", "xe", "amdgpu", "radeon", "nouveau", "virtio_gpu", "vmwgfx"]);
|
|
6
|
+
export const isGpuPath = (_module, path) => path.startsWith("kernel/drivers/gpu/");
|
|
7
|
+
export const isGpuDriver = (module, path) => isGpuPath(module, path) && drivers.has(module.replaceAll("-", "_"));
|
|
8
|
+
|
|
9
|
+
export const framebufferUsers = ({ root = "/", io = fs } = {}) => {
|
|
10
|
+
const users = [];
|
|
11
|
+
for (const pid of io.readdirSync(join(root, "proc")).filter(name => /^\d+$/.test(name))) {
|
|
12
|
+
try {
|
|
13
|
+
const dir = join(root, "proc", pid, "fd");
|
|
14
|
+
let busy = false;
|
|
15
|
+
for (const fd of io.readdirSync(dir)) {
|
|
16
|
+
try { if (/^\/dev\/fb\d+(?: \(deleted\))?$/.test(io.readlinkSync(join(dir, fd)))) busy = true; }
|
|
17
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
18
|
+
}
|
|
19
|
+
// An mmap can outlive its fd.
|
|
20
|
+
if (/\/dev\/fb\d+(?:\s|$)/m.test(io.readFileSync(join(root, "proc", pid, "maps"), "utf8"))) busy = true;
|
|
21
|
+
if (busy) users.push(pid);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error.code !== "ENOENT" && error.code !== "ESRCH") throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return users;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const configureGpu = ({ autoload, tryModprobe, root = "/", io = fs, env = process.env, log = console.log } = {}) => {
|
|
30
|
+
const result = { devices: [], renderNodes: [], framebuffers: [], errors: [], blocked: false };
|
|
31
|
+
const say = text => { try { log(`[gpu] ${text}`); } catch {} };
|
|
32
|
+
try {
|
|
33
|
+
const gid = env.BUNINU_RENDER_GID === undefined ? 0 : Number(env.BUNINU_RENDER_GID);
|
|
34
|
+
if (!Number.isInteger(gid) || gid < 0 || gid >= 0xffffffff || env.BUNINU_RENDER_GID === "")
|
|
35
|
+
throw Error("BUNINU_RENDER_GID must be a numeric group ID");
|
|
36
|
+
const users = framebufferUsers({ root, io });
|
|
37
|
+
if (users.length) {
|
|
38
|
+
result.blocked = true;
|
|
39
|
+
say(`not loaded: framebuffer in use by PID ${users.join(", ")}; exit bunterm/fbdev and run cfg.gpu before reopening it`);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
// A virtio GPU may not appear on the virtio bus until its PCI transport loads.
|
|
43
|
+
try { tryModprobe("virtio_pci"); } catch (error) { result.errors.push(`virtio_pci: ${error?.message ?? error}`); }
|
|
44
|
+
result.devices = autoload({ buses: ["pci", "virtio"], acceptModule: isGpuDriver });
|
|
45
|
+
for (const device of result.devices) {
|
|
46
|
+
if (device.error) result.errors.push(`${device.bus}/${device.device}: ${device.error}`);
|
|
47
|
+
say(`${device.bus}/${device.device} ${device.modules.join(" ")}: ${device.error ?? (device.loaded.length ? "loaded " + device.loaded.join(" ") : "already loaded/probed")}`);
|
|
48
|
+
}
|
|
49
|
+
const list = path => { try { return io.readdirSync(join(root, path)); } catch (error) { if (error.code === "ENOENT") return []; throw error; } };
|
|
50
|
+
for (const name of list("dev/dri").filter(name => /^renderD\d+$/.test(name)).sort()) {
|
|
51
|
+
const path = join(root, "dev/dri", name);
|
|
52
|
+
try {
|
|
53
|
+
if (!io.statSync(path).isCharacterDevice()) continue;
|
|
54
|
+
io.chownSync(path, 0, gid);
|
|
55
|
+
io.chmodSync(path, 0o660);
|
|
56
|
+
let driver = "unknown";
|
|
57
|
+
try {
|
|
58
|
+
const device = join(root, "sys/class/drm", name, "device");
|
|
59
|
+
driver = basename(io.readlinkSync(join(device, "driver")));
|
|
60
|
+
// virtio DRM nodes can point at the parent PCI transport device.
|
|
61
|
+
if (driver === "virtio-pci") for (const child of io.readdirSync(device).filter(n => /^virtio\d+$/.test(n))) {
|
|
62
|
+
const candidate = basename(io.readlinkSync(join(device, child, "driver")));
|
|
63
|
+
if (candidate === "virtio_gpu" || candidate === "virtio-gpu") { driver = candidate; break; }
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
result.renderNodes.push({ path: `/dev/dri/${name}`, driver, gid });
|
|
67
|
+
say(`/dev/dri/${name}: driver=${driver}, root:${gid} mode=0660`);
|
|
68
|
+
} catch (error) { result.errors.push(`${name}: ${error?.message ?? error}`); }
|
|
69
|
+
}
|
|
70
|
+
result.framebuffers = list("dev").filter(name => /^fb\d+$/.test(name)).sort().map(name => `/dev/${name}`);
|
|
71
|
+
say(`framebuffer: ${result.framebuffers.join(", ") || "none"}; reopen framebuffer clients after GPU handoff`);
|
|
72
|
+
if (!result.renderNodes.length) say("no accessible render node yet; check kernel firmware/driver messages and GPU support, then retry cfg.gpu after probing finishes");
|
|
73
|
+
say("clients still need their own Mesa/Vulkan driver; non-root clients need the render node's numeric group");
|
|
74
|
+
} catch (error) { result.errors.push(String(error?.message ?? error)); }
|
|
75
|
+
for (const error of result.errors) say(`failed: ${error}; init continues`);
|
|
76
|
+
return result;
|
|
77
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { isIP } from 'node:net';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
export function parsePing(args) {
|
|
5
|
+
const o={count:4,interval:1,timeout:2,deadline:Infinity,size:56,ttl:64,family:0,quiet:false,timestamp:false};
|
|
6
|
+
const values={c:'count',i:'interval',W:'timeout',w:'deadline',s:'size',t:'ttl',I:'source'};
|
|
7
|
+
let positional=false;
|
|
8
|
+
for(let i=0;i<args.length;i++){
|
|
9
|
+
const arg=args[i];
|
|
10
|
+
if(!positional&&arg==='--'){positional=true;continue;}
|
|
11
|
+
if(!positional&&(arg==='--help'||arg==='-h')){o.help=true;continue;}
|
|
12
|
+
if(!positional&&arg.startsWith('-')&&arg!=='-'){
|
|
13
|
+
for(let j=1;j<arg.length;j++){
|
|
14
|
+
const flag=arg[j];
|
|
15
|
+
if(values[flag]){const value=arg.slice(j+1)||args[++i];if(value===undefined||value==='')throw Error(`-${flag} requires a value`);if(flag!=='I'&&(!Number.isFinite(Number(value))||!value.trim()))throw Error(`invalid ${values[flag]}`);o[values[flag]]=flag==='I'?value:Number(value);break;}
|
|
16
|
+
if(flag==='4'||flag==='6')o.family=Number(flag);
|
|
17
|
+
else if(flag==='q')o.quiet=true;
|
|
18
|
+
else if(flag==='D')o.timestamp=true;
|
|
19
|
+
else if(flag!=='n')throw Error(`unknown option -${flag}`);
|
|
20
|
+
}
|
|
21
|
+
}else{if(o.host)throw Error('only one destination is supported');o.host=arg;}
|
|
22
|
+
}
|
|
23
|
+
if(o.help)return o;
|
|
24
|
+
if(!o.host)throw Error('destination required (see ping --help)');
|
|
25
|
+
for(const key of ['count','size','ttl'])if((key!=='count'||o.count!==Infinity)&&(!Number.isInteger(o[key])||o[key]<(key==='size'?0:1)))throw Error(`invalid ${key}`);
|
|
26
|
+
if(o.size>65507||o.ttl>255||o.count>Number.MAX_SAFE_INTEGER&&o.count!==Infinity)throw Error('count, packet size or TTL out of range');
|
|
27
|
+
for(const key of ['interval','timeout','deadline'])if(!(o[key]>0)||(!Number.isFinite(o[key])&&!(key==='deadline'&&o[key]===Infinity)))throw Error(`invalid ${key}`);
|
|
28
|
+
if(o.interval<.002)throw Error('minimum interval is 0.002 seconds');
|
|
29
|
+
return o;
|
|
30
|
+
}
|
|
31
|
+
export function checksum(bytes) {
|
|
32
|
+
let sum=0;for(let i=0;i<bytes.length;i+=2)sum+=(bytes[i]<<8)|(bytes[i+1]??0);
|
|
33
|
+
while(sum>>>16)sum=(sum&65535)+(sum>>>16);return (~sum)&65535;
|
|
34
|
+
}
|
|
35
|
+
export function echoPacket(family,id,seq,size) {
|
|
36
|
+
const b=Buffer.alloc(size+8);b[0]=family===6?128:8;b.writeUInt16BE(id,4);b.writeUInt16BE(seq,6);randomBytes(size).copy(b,8);
|
|
37
|
+
if(family===4)b.writeUInt16BE(checksum(b),2);return b;
|
|
38
|
+
}
|
|
39
|
+
export function isEchoReply(bytes,{family,raw,id,seq,packet}) {
|
|
40
|
+
let offset=0;
|
|
41
|
+
if(raw&&family===4){if(bytes.length<20||bytes[0]>>>4!==4||bytes[9]!==1)return false;offset=(bytes[0]&15)*4;if(offset<20||offset>bytes.length-8)return false;}
|
|
42
|
+
const b=bytes.subarray(offset);
|
|
43
|
+
return b.length===packet.length&&b[0]===(family===6?129:0)&&b[1]===0&&b.readUInt16BE(4)===id&&b.readUInt16BE(6)===seq&&b.subarray(8).equals(packet.subarray(8))&&(family===6||checksum(b)===0);
|
|
44
|
+
}
|
|
45
|
+
export async function runPing(o,{log=console.log,error=console.error,forceRaw=false}={}) {
|
|
46
|
+
if(!['linux','android'].includes(process.platform))throw Error('ping currently supports Linux and Android; Windows needs an ICMP API backend');
|
|
47
|
+
const {libc,ptr,cString,errno,SysError,check}=await import('./dlopen.js');
|
|
48
|
+
const c=libc.symbols;
|
|
49
|
+
let family=o.family||isIP(o.host)||4,address=o.host;
|
|
50
|
+
if(isIP(address)&&isIP(address)!==family)throw Error('destination does not match selected address family');
|
|
51
|
+
if(!isIP(address)){
|
|
52
|
+
const records=await Bun.dns.lookup(address,{family});address=records.find(r=>r.family===family)?.address;
|
|
53
|
+
if(!address)throw Error(`no IPv${family} address for ${o.host}`);
|
|
54
|
+
}
|
|
55
|
+
const af=family===6?10:2;
|
|
56
|
+
const sockaddr=value=>{
|
|
57
|
+
const b=Buffer.alloc(family===6?28:16);b.writeUInt16LE(af,0);
|
|
58
|
+
if(c.inet_pton(af,ptr(cString(value)),ptr(b.subarray(family===6?8:4)))!==1)throw Error(`invalid IPv${family} address: ${value}`);
|
|
59
|
+
return b;
|
|
60
|
+
};
|
|
61
|
+
let raw=forceRaw,fd=c.socket(af,(raw?3:2)|0x80800,family===6?58:1);
|
|
62
|
+
if(fd<0&&!raw){const code=errno();if(![1,13,93,97].includes(code))throw new SysError('ICMP socket',code);raw=true;fd=c.socket(af,3|0x80800,family===6?58:1);}
|
|
63
|
+
if(fd<0)throw new SysError('ICMP socket (allow ping_group_range or CAP_NET_RAW)');
|
|
64
|
+
let stop=false;const interrupt=()=>{stop=true};let transmitted=0,received=0,min=Infinity,max=0,sum=0,sumSquares=0;
|
|
65
|
+
const started=performance.now(),pending=new Map();let next=started;
|
|
66
|
+
const prefix=()=>o.timestamp?`[${(Date.now()/1000).toFixed(6)}] `:'';
|
|
67
|
+
try{
|
|
68
|
+
if(o.source){
|
|
69
|
+
if(isIP(o.source)){const src=sockaddr(o.source);check('bind source address',c.bind(fd,ptr(src),src.length));}
|
|
70
|
+
else{const name=cString(o.source);check('bind interface',c.setsockopt(fd,1,25,ptr(name),name.length));}
|
|
71
|
+
}
|
|
72
|
+
const ttl=Buffer.alloc(4);ttl.writeInt32LE(o.ttl);check('set TTL',c.setsockopt(fd,family===6?41:0,family===6?16:2,ptr(ttl),4));
|
|
73
|
+
const remote=sockaddr(address);check('connect',c.connect(fd,ptr(remote),remote.length));
|
|
74
|
+
const local=Buffer.alloc(28),length=Buffer.alloc(4);length.writeUInt32LE(local.length);check('getsockname',c.getsockname(fd,ptr(local),ptr(length)));
|
|
75
|
+
const id=raw?randomBytes(2).readUInt16BE():local.readUInt16BE(2);
|
|
76
|
+
log(`PING ${o.host} (${address}) ${o.size} data bytes`);
|
|
77
|
+
process.on('SIGINT',interrupt);process.on('SIGTERM',interrupt);
|
|
78
|
+
const buffer=Buffer.alloc(65536);
|
|
79
|
+
while(!stop&&performance.now()-started<o.deadline*1000){
|
|
80
|
+
let now=performance.now();
|
|
81
|
+
if(transmitted<o.count&&now>=next){
|
|
82
|
+
const seq=(transmitted+1)&65535;
|
|
83
|
+
// Avoid confusing a sequence that wrapped while an old probe is pending.
|
|
84
|
+
if(!pending.has(seq)){
|
|
85
|
+
const packet=echoPacket(family,id,seq,o.size);const sent=Number(c.send(fd,ptr(packet),packet.length,0));
|
|
86
|
+
if(sent!==packet.length)throw new SysError('send');
|
|
87
|
+
transmitted++;pending.set(seq,{packet,start:now});
|
|
88
|
+
}
|
|
89
|
+
next=now+o.interval*1000;
|
|
90
|
+
}
|
|
91
|
+
for(let n=0;n<64;n++){
|
|
92
|
+
const size=Number(c.recv(fd,ptr(buffer),buffer.length,0));
|
|
93
|
+
if(size<0){const code=errno();if(code===11)break;if(code===4)continue;if([101,111,113].includes(code)){if(!o.quiet)error(`${prefix()}ping: ${new SysError('receive',code).message}`);break;}throw new SysError('receive',code);}
|
|
94
|
+
const b=buffer.subarray(0,size),offset=raw&&family===4?(b[0]&15)*4:0;
|
|
95
|
+
if(size<offset+8)continue;const seq=b.readUInt16BE(offset+6),p=pending.get(seq);
|
|
96
|
+
if(!p||!isEchoReply(b,{family,raw,id,seq,packet:p.packet}))continue;
|
|
97
|
+
const ms=performance.now()-p.start;pending.delete(seq);received++;min=Math.min(min,ms);max=Math.max(max,ms);sum+=ms;sumSquares+=ms*ms;
|
|
98
|
+
if(!o.quiet)log(`${prefix()}${p.packet.length} bytes from ${address}: icmp_seq=${seq}${raw&&family===4?` ttl=${b[8]}`:''} time=${ms.toFixed(3)} ms`);
|
|
99
|
+
}
|
|
100
|
+
now=performance.now();for(const [seq,p] of pending)if(now-p.start>=o.timeout*1000){pending.delete(seq);if(!o.quiet)log(`${prefix()}Request timeout for icmp_seq=${seq}`);}
|
|
101
|
+
if(transmitted>=o.count&&!pending.size)break;
|
|
102
|
+
const sendDelay=transmitted<o.count?next-now:10;
|
|
103
|
+
await Bun.sleep(Math.max(1,Math.min(10,sendDelay,started+o.deadline*1000-now)));
|
|
104
|
+
}
|
|
105
|
+
}finally{process.removeListener('SIGINT',interrupt);process.removeListener('SIGTERM',interrupt);c.close(fd);}
|
|
106
|
+
log(`\n--- ${o.host} ping statistics ---`);
|
|
107
|
+
log(`${transmitted} packets transmitted, ${received} received, ${transmitted?((transmitted-received)/transmitted*100).toFixed(0):0}% packet loss, time ${Math.round(performance.now()-started)}ms`);
|
|
108
|
+
if(received)log(`rtt min/avg/max/mdev = ${min.toFixed(3)}/${(sum/received).toFixed(3)}/${max.toFixed(3)}/${Math.sqrt(Math.max(0,sumSquares/received-(sum/received)**2)).toFixed(3)} ms`);
|
|
109
|
+
return received>0&&!(Number.isFinite(o.deadline)&&Number.isFinite(o.count)&&received<o.count)?0:1;
|
|
110
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Retain immutable raw pixels until an operation actually needs a Skia Image.
|
|
2
|
+
// The native image store owns these handles; encoded images remain ordinary Skia images.
|
|
3
|
+
const rawImages = new WeakMap();
|
|
4
|
+
export const rawImageData = image => rawImages.get(image);
|
|
5
|
+
export const skiaImage = image => rawImages.get(image)?.materialize() ?? image;
|
|
6
|
+
|
|
7
|
+
export function createRawImage(CanvasKit, pixels, width, height, colorType) {
|
|
8
|
+
let decoded = null, closed = false;
|
|
9
|
+
const data = {
|
|
10
|
+
pixels, width, height, colorType,
|
|
11
|
+
opaque: undefined,
|
|
12
|
+
materialize() {
|
|
13
|
+
if (closed) throw Error('raw image is closed');
|
|
14
|
+
if (!decoded) {
|
|
15
|
+
decoded = CanvasKit.MakeImage({width, height, colorType,
|
|
16
|
+
alphaType: CanvasKit.AlphaType.Unpremul, colorSpace: CanvasKit.ColorSpace.SRGB}, pixels, width * 4);
|
|
17
|
+
if (!decoded) throw Error('raw image rejected');
|
|
18
|
+
}
|
|
19
|
+
return decoded;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const image = {
|
|
23
|
+
width: () => width,
|
|
24
|
+
height: () => height,
|
|
25
|
+
delete() {
|
|
26
|
+
if (closed) return;
|
|
27
|
+
closed = true;
|
|
28
|
+
decoded?.delete(); decoded = null;
|
|
29
|
+
data.pixels = pixels = null;
|
|
30
|
+
rawImages.delete(image);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
rawImages.set(image, data);
|
|
34
|
+
return image;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check actual bytes once; raw protocol support does not imply opacity.
|
|
38
|
+
export function rawImageOpaque(data) {
|
|
39
|
+
if (data.opaque === undefined) {
|
|
40
|
+
const pixels = data.pixels;
|
|
41
|
+
data.opaque = true;
|
|
42
|
+
for (let i = 3; i < pixels.length; i += 4) {
|
|
43
|
+
if (pixels[i] !== 255) { data.opaque = false; break; }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return data.opaque;
|
|
47
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
export function browserTarget(target, cwd = process.cwd()) {
|
|
6
|
+
if (/^about:/i.test(target)) return target;
|
|
7
|
+
if (/^https?:\/\//i.test(target)) {
|
|
8
|
+
// Reject malformed URLs before starting a new terminal.
|
|
9
|
+
try { return new URL(target).href; }
|
|
10
|
+
catch { throw openError("Invalid HTTP/HTTPS URL", 1); }
|
|
11
|
+
}
|
|
12
|
+
const fileUrl = /^file:/i.test(target);
|
|
13
|
+
if (!fileUrl && /^[a-z][a-z0-9+.-]*:/i.test(target)) {
|
|
14
|
+
throw openError("Unsupported URL scheme; use http://, https://, about: or a local file", 4);
|
|
15
|
+
}
|
|
16
|
+
const path = realpathSync(fileUrl ? fileURLToPath(target) : resolve(cwd, target));
|
|
17
|
+
if (!statSync(path).isFile()) throw openError("Expected a regular file", 4);
|
|
18
|
+
// Encode spaces, #, %, and non-ASCII characters as URL path components.
|
|
19
|
+
return pathToFileURL(path).href;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseOpenArgs(args) {
|
|
23
|
+
if (args.length === 1 && ["-h", "--help", "--manual"].includes(args[0])) return { help: true };
|
|
24
|
+
if (args.length === 1 && args[0] === "--version") return { version: true };
|
|
25
|
+
if (args.length !== 1 || !args[0]) throw openError("Usage: xdg-open URL | FILE (see --help)", 1);
|
|
26
|
+
if (args[0].startsWith("-")) throw openError("Use ./ before a filename beginning with '-'", 1);
|
|
27
|
+
return { target: args[0] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function openError(message, exitCode) {
|
|
31
|
+
return Object.assign(new Error(message), { exitCode });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function openExitCode(error) {
|
|
35
|
+
return error.exitCode ?? (["ENOENT", "ENOTDIR"].includes(error.code) ? 2 : 4);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function releaseVersion(text) {
|
|
39
|
+
const value = /^VERSION_ID=(.*)$/m.exec(text)?.[1]?.trim();
|
|
40
|
+
if (!value) throw new Error("VERSION_ID missing from os-release");
|
|
41
|
+
return value.replace(/^["']|["']$/g, "");
|
|
42
|
+
}
|