char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsnapshot: embed shared main and worker startup in the CLI688e96e

main
10.5 KiB229 linesraw
1#![cfg(unix)]
2mod common;
3
4use std::io::{BufRead, BufReader, Write};
5use std::os::unix::net::UnixStream;
6use std::process::{Command, Stdio};
7
8#[test]
9fn unix_streams_need_only_write_authority_and_support_half_close() {
10    let directory = tempfile::tempdir().unwrap();
11    let path = directory.path().join("stream.sock");
12    let label = serde_json::to_string(path.to_str().unwrap()).unwrap();
13    let output = common::run(
14        &format!(
15            r#"
16        import {{listen,connect,pledge}} from 'avril:std';
17        pledge({{net:false,read:false,write:{{allow:[{label}]}}}});
18        const listener=listen({{transport:'unix',path:{label}}}),accept=listener.accept(),client=await connect({{transport:'unix',path:{label}}}),server=await accept;
19        if(listener.addr.path!=={label}||client.remoteAddr.path!=={label}||client.localAddr.path!==null||server.remoteAddr.path!==null||server.localAddr.path!=={label})throw Error('addresses');
20        const reading=server.read(new Uint8Array(1));await client.write(new Uint8Array([7]));if(await reading!==1)throw Error('read');
21        await client.closeWrite();if(await server.read(new Uint8Array(1))!==null)throw Error('FIN');
22        await server.write(new Uint8Array([8]));const byte=new Uint8Array(1);if(await client.read(byte)!==1||byte[0]!==8)throw Error('half close');
23        await server.closeWrite();if(await client.read(byte)!==null)throw Error('EOF');
24        client.close();server.close();listener.close();console.log('unix');
25    "#
26        ),
27        &[],
28    );
29    assert_eq!(output.stdout, b"unix\n");
30    assert!(!path.exists());
31}
32
33#[test]
34fn unix_cleanup_preserves_replacements_and_existing_entries() {
35    let directory = tempfile::tempdir().unwrap();
36    let path = directory.path().join("socket");
37    let label = serde_json::to_string(path.to_str().unwrap()).unwrap();
38    let (_source, mut child) = common::spawn(&format!(
39        r#"
40        import {{listen}} from 'avril:std';
41        const listener=listen({{transport:'unix',path:{label}}});console.log('ready');const connection=await listener.accept();await connection.read(new Uint8Array(1));listener.close();connection.close();
42    "#
43    ));
44    let mut stdout = BufReader::new(child.stdout.take().unwrap());
45    let mut line = String::new();
46    stdout.read_line(&mut line).unwrap();
47    assert_eq!(line, "ready\n");
48    child.stdout = Some(stdout.into_inner());
49    let mut socket = UnixStream::connect(&path).unwrap();
50    std::fs::remove_file(&path).unwrap();
51    std::fs::write(&path, "replacement").unwrap();
52    socket.write_all(b"x").unwrap();
53    let output = common::wait(child);
54    assert!(
55        output.status.success(),
56        "{}",
57        String::from_utf8_lossy(&output.stderr)
58    );
59    assert_eq!(std::fs::read_to_string(&path).unwrap(), "replacement");
60    let output = common::run(
61        &format!(
62            r#"
63        import {{listen,errors}} from 'avril:std';
64        try{{listen({{transport:'unix',path:{label}}});throw Error('existing entry replaced');}}catch(e){{if(e.message==='existing entry replaced')throw e;}}
65        console.log('preserved');
66    "#
67        ),
68        &[],
69    );
70    assert_eq!(output.stdout, b"preserved\n");
71    assert_eq!(std::fs::read_to_string(&path).unwrap(), "replacement");
72}
73
74#[test]
75fn write_revocation_blocks_all_unix_io_but_not_cleanup() {
76    let directory = tempfile::tempdir().unwrap();
77    let path = directory.path().join("revoke.sock");
78    let label = serde_json::to_string(path.to_str().unwrap()).unwrap();
79    let output = common::run(
80        &format!(
81            r#"
82        import {{listen,connect,pledge,errors}} from 'avril:std';
83        const listener=listen({{transport:'unix',path:{label}}}),accept=listener.accept(),client=await connect({{transport:'unix',path:{label}}}),server=await accept;
84        pledge({{write:false}});
85        for(const operation of [()=>client.read(new Uint8Array(1)),()=>client.write(new Uint8Array([1])),()=>client.closeWrite(),()=>listener.accept(),()=>connect({{transport:'unix',path:{label}}})]){{
86            try{{await operation();throw Error('authority escaped');}}catch(e){{if(!(e instanceof errors.PermissionDenied))throw e;}}
87        }}
88        client.close();server.close();listener.close();console.log('revoked');
89    "#
90        ),
91        &[],
92    );
93    assert_eq!(output.stdout, b"revoked\n");
94    assert!(!path.exists());
95}
96
97#[test]
98fn unix_connect_authorizes_the_resolved_symlink_target() {
99    let directory = tempfile::tempdir().unwrap();
100    let path = directory.path().join("actual.sock");
101    let link = directory.path().join("alias.sock");
102    let label = serde_json::to_string(path.to_str().unwrap()).unwrap();
103    let alias = serde_json::to_string(link.to_str().unwrap()).unwrap();
104    let listener = std::os::unix::net::UnixListener::bind(&path).unwrap();
105    std::os::unix::fs::symlink(&path, &link).unwrap();
106    let output = common::run(
107        &format!(
108            r#"
109        import {{connect,pledge,errors}} from 'avril:std';
110        pledge({{net:false,write:{{allow:[{alias}],deny:[{label}]}}}});
111        try{{await connect({{transport:'unix',path:{alias}}});throw Error('symlink escaped');}}catch(e){{if(!(e instanceof errors.PermissionDenied))throw e;}}
112        console.log('denied');
113    "#
114        ),
115        &[],
116    );
117    assert_eq!(output.stdout, b"denied\n");
118    drop(listener);
119}
120
121#[test]
122fn relative_unix_paths_resolve_against_the_working_directory() {
123    let directory = tempfile::tempdir().unwrap();
124    let source = directory.path().join("main.js");
125    std::fs::write(&source,r#"
126        import {listen,connect} from 'avril:std';
127        const listener=listen({transport:'unix',path:'relative.sock'}),accept=listener.accept(),client=await connect({transport:'unix',path:'relative.sock'}),server=await accept;
128        if(!listener.addr.path.endsWith('/relative.sock'))throw Error('path');client.close();server.close();listener.close();console.log('relative');
129    "#).unwrap();
130    let child = Command::new(env!("CARGO_BIN_EXE_avril"))
131        .args(["run", "main.js"])
132        .current_dir(directory.path())
133        .stdout(Stdio::piped())
134        .stderr(Stdio::piped())
135        .spawn()
136        .unwrap();
137    let output = common::wait(child);
138    assert!(
139        output.status.success(),
140        "{}",
141        String::from_utf8_lossy(&output.stderr)
142    );
143    assert_eq!(output.stdout, b"relative\n");
144    assert!(!directory.path().join("relative.sock").exists());
145}
146
147#[test]
148fn closing_and_worker_termination_unlink_listeners_with_pending_accepts() {
149    let directory = tempfile::tempdir().unwrap();
150    let path = directory.path().join("worker.sock");
151    let label = serde_json::to_string(path.to_str().unwrap()).unwrap();
152    let worker = directory.path().join("worker.js");
153    std::fs::write(&worker,format!("import {{listen}} from 'avril:std';const listener=listen({{transport:'unix',path:{label}}});postMessage('ready');await listener.accept();")).unwrap();
154    let url = url::Url::from_file_path(&worker).unwrap();
155    let output = common::run(
156        &format!(
157            r#"
158        import {{listen,errors}} from 'avril:std';
159        const worker=new Worker('{url}',{{type:'module'}});await new Promise(resolve=>worker.onmessage=resolve);worker.terminate();
160        let listener;for(let i=0;i<100&&!listener;i++){{try{{listener=listen({{transport:'unix',path:{label}}});}}catch{{await new Promise(resolve=>setTimeout(resolve,5));}}}}
161        if(!listener)throw Error('worker leaked socket');const accept=listener.accept().catch(e=>e);listener.close();if(!((await accept) instanceof errors.BadResource))throw Error('pending accept');console.log('terminated');
162    "#
163        ),
164        &[],
165    );
166    assert_eq!(output.stdout, b"terminated\n");
167    assert!(!path.exists());
168}
169
170#[test]
171fn unix_socket_options_reject_ambiguous_transports() {
172    let output = common::run(
173        r#"
174        import {listen,connect} from 'avril:std';
175        for(const options of [{transport:'unix',path:'',port:0},{transport:'unix',path:'x',hostname:'localhost'},{transport:'unix'},{path:'x',port:0}]){
176            try{listen(options);throw Error('invalid Unix options');}catch(e){if(!(e instanceof TypeError))throw e;}
177        }
178        console.log('validated');
179    "#,
180        &[],
181    );
182    assert_eq!(output.stdout, b"validated\n");
183}
184
185#[test]
186fn unix_writev_writes_ordered_vectors_and_validates() {
187    let directory = tempfile::tempdir().unwrap();
188    let path = directory.path().join("vector.sock");
189    let label = serde_json::to_string(path.to_str().unwrap()).unwrap();
190    let output = common::run(
191        &format!(
192            r#"
193        import {{listen,connect,errors}} from 'avril:std';
194        const listener=listen({{transport:'unix',path:{label}}}),accept=listener.accept();
195        const client=await connect({{transport:'unix',path:{label}}}),server=await accept;
196        const full=new Uint8Array([5,6,7,8]);
197        if(await client.writev([full.subarray(0,2),new Uint8Array(0),full.subarray(2)])!==4)throw Error('Count');
198        if(await client.writev([])!==0)throw Error('Empty');
199        await client.write(new Uint8Array([9]));
200        await client.closeWrite();
201        const buf=new Uint8Array(5);let offset=0,count;
202        while(offset<5&&(count=await server.read(buf.subarray(offset)))!==null)offset+=count;
203        if(offset!==5||buf.join()!=='5,6,7,8,9')throw Error('Order '+buf.join());
204        try{{await client.writev([new Uint8Array([1])]);throw Error('FIN');}}catch(e){{if(!(e instanceof errors.BadResource))throw e;}}
205        client.close();server.close();
206        const accept2=listener.accept(),client2=await connect({{transport:'unix',path:{label}}}),server2=await accept2;
207        const wide=[];for(let i=0;i<20;i++){{const view=new Uint8Array(1000);view.fill(i+1);wide.push(view);}}
208        const gathering=(async()=>{{
209            const rest=new Uint8Array(20000);let at=0,got;
210            while(at<20000&&(got=await server2.read(rest.subarray(at)))!==null)at+=got;
211            if(at!==20000)throw Error('Wide length');
212            return rest;
213        }})();
214        if(await client2.writev(wide)!==20000)throw Error('Wide');
215        await client2.closeWrite();
216        const rest=await gathering;
217        for(let i=0;i<20;i++)for(let j=0;j<1000;j++)if(rest[i*1000+j]!==i+1)throw Error('Wide order '+i);
218        for(const bad of ['bytes',new Uint8Array([1]),[new Uint8Array([1]),'x']]){{
219            try{{await server2.writev(bad);throw Error('Accepted');}}catch(e){{if(!(e instanceof TypeError))throw e;}}
220        }}
221        client2.close();server2.close();
222        listener.close();console.log('unix-vectors');
223    "#
224        ),
225        &[],
226    );
227    assert_eq!(output.stdout, b"unix-vectors\n");
228    assert!(!path.exists());
229}