mod common; use std::io::{BufRead, BufReader}; #[test] fn shared_views_keep_receive_aliasing_and_send_snapshots() { let output = common::run( r#" import {listenDatagram} from 'avril:std'; const receiver = listenDatagram({port:0}), sender = listenDatagram({port:0}); const storage = new SharedArrayBuffer(12), target = new Uint8Array(storage, 3, 4); const input = new Uint8Array(new SharedArrayBuffer(4)); input.set([1,2,3,4]); const pending = receiver.receive(target); const sending = sender.send(input, receiver.addr); input.fill(9); if (await sending !== 4) throw Error('send count'); const [message, remote] = await pending; if (message.buffer !== storage || message.byteOffset !== 3 || message.length !== 4 || message.join() !== '1,2,3,4' || target.join() !== '1,2,3,4') throw Error('shared view or snapshot'); if (remote.transport !== 'udp' || remote.port !== sender.addr.port || Object.getPrototypeOf(remote) !== null || !Object.isFrozen(remote)) throw Error('peer address'); receiver.close(); sender.close(); console.log('shared datagrams'); "#, &[], ); assert_eq!(output.stdout, b"shared datagrams\n"); } #[test] fn datagrams_preserve_boundaries_empty_messages_and_sender_addresses() { let output = common::run( r#" import {listenDatagram,DatagramConn} from 'avril:std'; const a=listenDatagram({port:0}),b=listenDatagram({port:0}); if(!(a instanceof DatagramConn)||a.addr.transport!=='udp'||!Object.isFrozen(a.addr))throw Error('socket'); const bytes=new Uint8Array([1,2,3]),sending=a.send(bytes,b.addr);bytes.fill(0);if(await sending!==3)throw Error('send'); let [packet,peer]=await b.receive();if(packet.join()!=='1,2,3'||peer.port!==a.addr.port||peer.transport!=='udp')throw Error('packet'); await a.send(new Uint8Array(),b.addr);[packet,peer]=await b.receive();if(packet.length!==0||peer.port!==a.addr.port)throw Error('empty datagram'); const large=new Uint8Array(65507);large.fill(42);if(await a.send(large,b.addr)!==large.length)throw Error('large send');[packet]=await b.receive();if(packet.length!==large.length||packet.some(byte=>byte!==42))throw Error('large datagram'); a.setBroadcast();a.setBroadcast(false);a.close();b.close();console.log('datagrams'); "#, &[], ); assert_eq!(output.stdout, b"datagrams\n"); } #[test] fn receive_buffers_truncate_only_the_current_datagram_and_empty_buffers_consume_it() { let output = common::run( r#" import {listenDatagram} from 'avril:std'; const a=listenDatagram({port:0}),b=listenDatagram({port:0}); const buffer=new Uint8Array(5);await a.send(new Uint8Array([1,2,3,4]),b.addr); const [packet]=await b.receive(buffer.subarray(1,3));if(packet.buffer!==buffer.buffer||packet.byteOffset!==1||packet.join()!=='1,2')throw Error('buffer'); await a.send(new Uint8Array([9]),b.addr);const [next]=await b.receive();if(next.join()!=='9')throw Error('truncated packet spilled'); await a.send(new Uint8Array([4]),b.addr);const [empty]=await b.receive(new Uint8Array());if(empty.length!==0)throw Error('empty buffer'); await a.send(new Uint8Array([5]),b.addr);const [last]=await b.receive();if(last.join()!=='5')throw Error('empty receive did not consume'); a.close();b.close();console.log('buffers'); "#, &[], ); assert_eq!(output.stdout, b"buffers\n"); } #[test] fn closing_cancels_receive_and_iteration_break_closes_the_socket() { let output = common::run( r#" import {listenDatagram,errors} from 'avril:std'; const socket=listenDatagram({port:0}),pending=socket.receive().catch(e=>e);socket.close();if(!((await pending) instanceof errors.BadResource))throw Error('cancellation');socket[Symbol.dispose](); const a=listenDatagram({port:0}),b=listenDatagram({port:0}); const consuming=(async()=>{for await(const [packet] of b){if(packet.join()!=='7')throw Error('iterator');break;}})(); await a.send(new Uint8Array([7]),b.addr);await consuming; if(!(await b[Symbol.asyncIterator]().next()).done)throw Error('closed iteration');a.close();console.log('closed'); "#, &[], ); assert_eq!(output.stdout, b"closed\n"); } #[test] fn send_requires_destination_authority_not_just_bind_authority() { let output = common::run( r#" import {listenDatagram,pledge,errors} from 'avril:std'; const a=listenDatagram({port:0}),b=listenDatagram({port:0}); pledge({net:{allow:[`127.0.0.1:${a.addr.port}`]}}); try{await a.send(new Uint8Array([1]),b.addr);throw Error('destination escaped');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;} const received=a.receive();await a.send(new Uint8Array([8]),a.addr);if((await received)[0][0]!==8)throw Error('authorized peer'); a.close();b.close();console.log('scoped'); "#, &[], ); assert_eq!(output.stdout, b"scoped\n"); } #[test] fn revocation_blocks_queued_sends_binding_and_broadcast_changes() { let output = common::run( r#" import {listenDatagram,pledge,errors} from 'avril:std'; const a=listenDatagram({port:0}),send=a.send(new Uint8Array([1]),a.addr);pledge({net:false}); for(const operation of [()=>send,()=>a.receive(),()=>a.setBroadcast(),()=>listenDatagram({port:0})]){ try{await operation();throw Error('authority escaped');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;} } a.close();console.log('revoked'); "#, &[], ); assert_eq!(output.stdout, b"revoked\n"); } #[test] fn receiving_rechecks_authority_after_waiting_for_a_packet() { let (_directory, mut child) = common::spawn( r#" import {listenDatagram,pledge,errors} from 'avril:std'; const a=listenDatagram({port:0});console.log(a.addr.port);const reading=a.receive();await new Promise(resolve=>setTimeout(resolve,10));pledge({net:false});console.log('narrowed'); try{await reading;throw Error('received without authority');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;} a.close();console.log('denied'); "#, ); let mut stdout = BufReader::new(child.stdout.take().unwrap()); let mut line = String::new(); stdout.read_line(&mut line).unwrap(); let port = line.trim().parse::().unwrap(); line.clear(); stdout.read_line(&mut line).unwrap(); assert_eq!(line, "narrowed\n"); let socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap(); socket.send_to(b"x", ("127.0.0.1", port)).unwrap(); line.clear(); stdout.read_line(&mut line).unwrap(); assert_eq!(line, "denied\n"); drop(stdout); let output = common::wait(child); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); } #[test] fn worker_termination_releases_udp_sockets_and_workers_cannot_escalate_authority() { let directory = tempfile::tempdir().unwrap(); let source = directory.path().join("worker.js"); std::fs::write(&source,"import {listenDatagram} from 'avril:std';try{const a=listenDatagram({port:0});postMessage(a.addr.port);await a.receive();}catch(e){postMessage(e.name);close();}").unwrap(); let url = url::Url::from_file_path(&source).unwrap(); let output = common::run( &format!( r#" import {{listenDatagram,avril}} from 'avril:std'; const denied=new Worker('{url}',{{type:'module',[avril]:{{caps:{{net:false}}}}}});if(await new Promise(resolve=>denied.onmessage=e=>resolve(e.data))!=='PermissionDenied')throw Error('worker escaped');denied.terminate(); const worker=new Worker('{url}',{{type:'module'}}),port=await new Promise(resolve=>worker.onmessage=e=>resolve(e.data));worker.terminate(); let socket;for(let i=0;i<100&&!socket;i++){{try{{socket=listenDatagram({{port}});}}catch{{await new Promise(resolve=>setTimeout(resolve,5));}}}} if(!socket)throw Error('worker leaked UDP socket');socket.close();console.log('terminated'); "# ), &[], ); assert_eq!(output.stdout, b"terminated\n"); } #[test] fn udp_uses_captured_intrinsics_and_handles_cannot_be_cloned() { let output = common::run( r#" import {listenDatagram} from 'avril:std'; const chunk=new Uint8Array([9]),then=Promise.prototype.then,PromiseCtor=Promise; function observe(p){Object.defineProperty(p,'constructor',{value:undefined});const result=Reflect.apply(then,p,[v=>v,e=>{throw e;}]);Object.defineProperty(result,'constructor',{value:PromiseCtor});return result;} const poison=()=>{throw Error('guest hook');};Object.defineProperty(Promise.prototype,'constructor',{get:poison,configurable:true});Object.defineProperty(Promise,Symbol.species,{get:poison});Promise.prototype.then=poison;Array.prototype[Symbol.iterator]=poison; const a=listenDatagram({port:0}),b=listenDatagram({port:0}),reading=b.receive(); await observe(a.send(chunk,b.addr));const packet=await observe(reading);if(packet[0][0]!==9)throw Error('packet'); try{structuredClone(a);throw Error('cloned');}catch(e){if(e.name!=='DataCloneError')throw e;} a.close();b.close();console.log('safe'); "#, &[], ); assert_eq!(output.stdout, b"safe\n"); } #[test] fn dns_selects_the_bound_address_family() { let output = common::run( r#" import {listenDatagram} from 'avril:std'; const a=listenDatagram({port:0}),b=listenDatagram({port:0}); const reading=b.receive();await a.send(new Uint8Array([6]),{hostname:'localhost',port:b.addr.port}); if((await reading)[0][0]!==6)throw Error('DNS family');a.close();b.close();console.log('dns'); "#, &[], ); assert_eq!(output.stdout, b"dns\n"); } #[test] fn ipv6_datagrams_accept_bracketed_destinations() { if std::net::UdpSocket::bind("[::1]:0").is_err() { return; } let output = common::run( r#" import {listenDatagram} from 'avril:std'; const a=listenDatagram({hostname:'::1',port:0}),b=listenDatagram({hostname:'::1',port:0}); const reading=b.receive();await a.send(new Uint8Array([6]),{hostname:'[::1]',port:b.addr.port}); const [packet,peer]=await reading;if(packet[0]!==6||peer.hostname!=='::1')throw Error('IPv6');a.close();b.close();console.log('ipv6'); "#, &[], ); assert_eq!(output.stdout, b"ipv6\n"); } #[test] fn invalid_datagram_options_and_oversized_messages_are_rejected() { let output = common::run( r#" import {listenDatagram,DatagramConn} from 'avril:std'; for(const port of [-1,65536,NaN,1.5]){try{listenDatagram({port});throw Error('invalid port');}catch(e){if(!(e instanceof RangeError))throw e;}} try{new DatagramConn();throw Error('constructed');}catch(e){if(!(e instanceof TypeError))throw e;} try{listenDatagram({transport:'tcp',port:0});throw Error('transport');}catch(e){if(!(e instanceof TypeError))throw e;} const a=listenDatagram({port:0}); try{a.send(new Uint8Array(65508),a.addr);throw Error('oversized');}catch(e){if(!(e instanceof RangeError))throw e;} try{await a.receive(new ArrayBuffer(1));throw Error('invalid buffer');}catch(e){if(!(e instanceof TypeError))throw e;} a.close();console.log('validated'); "#, &[], ); assert_eq!(output.stdout, b"validated\n"); }