char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
dea05b4
main
1mod common; 2 3use std:: fs; 4use std:: process::{ Command , Stdio }; 5 6fn run ( source : & str , flags : & [ & str ]) ->( tempfile:: TempDir , std:: process:: Output ) { 7let dir = tempfile:: tempdir (). unwrap (); 8 fs:: write ( dir. path (). join ( "main.js" ), source). unwrap (); 9 fs:: write ( dir. path (). join ( "data" ), b"abcdef" ). unwrap (); 10let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 11. arg ( "run" ) 12. current_dir ( dir. path ()) 13. args ( flags) 14. arg ( "main.js" ) 15. stdin ( Stdio :: null ()) 16. stdout ( Stdio :: piped ()) 17. stderr ( Stdio :: piped ()) 18. spawn () 19. unwrap (); 20let output = common:: wait ( child); 21assert! ( 22 output. status . success (), 23"{}" , 24String :: from_utf8_lossy ( & output. stderr ) 25); 26( dir, output) 27} 28 29# [ test ] 30fn error_constructors_preserve_reflected_construction () { 31let ( _, output) =run ( 32r#" 33import {errors} from 'avril:std'; 34const Constructor = errors.NotFound; 35Object.setPrototypeOf(Constructor, function poisoned() {throw Error('Guest superclass');}); 36class Derived extends Constructor {} 37const error = new Derived('missing'); 38if (!(error instanceof Derived) || !(error instanceof Error) || 39error.name !== 'NotFound' || error.message !== 'missing') throw Error('Error identity'); 40if (Constructor.length !== 0 || Constructor.name !== 'NotFound') throw Error('Error constructor metadata'); 41if (!Object.isFrozen(errors)) throw Error('Mutable error namespace'); 42const name = Object.getOwnPropertyDescriptor(error, 'name'); 43if (!name.writable || !name.configurable || name.enumerable) throw Error('Error descriptor'); 44console.log('reflected errors'); 45"# , 46& [], 47); 48assert_eq! ( output. stdout , b"reflected errors\n" ); 49} 50 51# [ test ] 52fn handles_read_seek_write_truncate_and_report_metadata () { 53let ( dir, output) =run ( 54r#" 55import {open,create,stat,FileHandle,SeekMode} from 'avril:std'; 56const file=await open('data',{read:true,write:true}); 57if(!(file instanceof FileHandle))throw Error('Handle'); 58const bytes=new Uint8Array(3); 59if(await file.read(bytes)!==3 || bytes.join()!=='97,98,99')throw Error('Read'); 60if(await file.seek(-1,SeekMode.Current)!==2 || await file.write(new Uint8Array([88,89]))!==2)throw Error('Cursor'); 61await file.seek(-1,SeekMode.End); 62if(await file.read(bytes)!==1 || bytes[0]!==102 || await file.read(bytes)!==null || await file.read(new Uint8Array())!==0)throw Error('EOF'); 63const a=await file.stat(),b=await stat(new URL('./data',import.meta.url)); 64if(!a.isFile || a.isDirectory || a.isSymlink || a.size!==6 || b.size!==6 || !(a.mtime instanceof Date))throw Error('Metadata'); 65await file.truncate(4);await file.syncData();await file.sync();file.close(); 66const created=await create('new');await created.write(new Uint8Array([1,2]));created.close(); 67const overwritten=await create('new');if((await overwritten.stat()).size!==0)throw Error('Truncate shorthand');overwritten[Symbol.dispose](); 68console.log('file IO'); 69"# , 70& [], 71); 72assert_eq! ( output. stdout , b"file IO\n" ); 73assert_eq! ( fs:: read ( dir. path (). join ( "data" )). unwrap (), b"abXY" ); 74assert_eq! ( fs:: read ( dir. path (). join ( "new" )). unwrap (), b"" ); 75} 76 77# [ test ] 78fn options_errors_modes_and_lifecycle_are_consistent () { 79let ( dir, output) =run ( 80r#" 81import {open,create,stat,FileHandle,errors} from 'avril:std'; 82async function fails(call,C){try{await call();throw Error('Accepted invalid operation');}catch(e){if(!(e instanceof C))throw e;}} 83await fails(()=>open('missing'),errors.NotFound); 84await fails(()=>open('data',{}),errors.InvalidInput); 85await fails(()=>open('data',{read:true,truncate:true}),errors.InvalidInput); 86await fails(()=>open('data',{write:true,createNew:true}),errors.AlreadyExists); 87await fails(()=>open('data',{read:'yes'}),TypeError); 88await fails(()=>open('data',{write:true,mode:8192}),RangeError); 89await fails(()=>open(new URL('https://example.test')),errors.InvalidInput); 90await fails(()=>stat(''),errors.NotFound); 91await fails(()=>open(),TypeError); 92try{new FileHandle();throw Error('Constructed handle');}catch(e){if(!(e instanceof TypeError))throw e;} 93const file=await open('data');await fails(()=>file.write(new Uint8Array([0])),errors.BadResource); 94await fails(()=>file.read(new DataView(new ArrayBuffer(1))),TypeError); 95await fails(()=>file.seek(2**53,0),RangeError); 96await file.seek(Number.MAX_SAFE_INTEGER,0); 97await fails(()=>file.seek(1,1),errors.InvalidInput); 98if(await file.seek(0,1)!==Number.MAX_SAFE_INTEGER)throw Error('Failed seek moved cursor'); 99file.close();file[Symbol.dispose]();file[Symbol.dispose](); 100await fails(()=>file.stat(),errors.BadResource);await fails(()=>file.read(new Uint8Array(1)),errors.BadResource); 101try{file.close();throw Error('Closed twice');}catch(e){if(!(e instanceof errors.BadResource))throw e;} 102const append=await open('data',{append:true});await append.seek(0,0);await append.write(new Uint8Array([33]));append.close(); 103if(!Object.isFrozen(errors))throw Error('Errors registry'); 104console.log('options'); 105"# , 106& [], 107); 108assert_eq! ( output. stdout , b"options\n" ); 109assert_eq! ( fs:: read ( dir. path (). join ( "data" )). unwrap (), b"abcdef!" ); 110} 111 112# [ test ] 113fn direct_writes_snapshot_input_and_stream_writes_finish_all_partial_chunks () { 114let ( dir, output) =run ( 115r#" 116import {create,open,errors} from 'avril:std'; 117const file=await create('out'), bytes=new Uint8Array(200003);bytes.fill(7); 118const pending=file.write(bytes);bytes.fill(9); 119const count=await pending;if(count<=0 || count>bytes.length)throw Error('Partial count'); 120const remaining=new Uint8Array(bytes.length-count);remaining.fill(7);await file.writable.getWriter().write(remaining);file.close(); 121const check=await open('out');const all=new Uint8Array(bytes.length);let offset=0,n; 122while((n=await check.read(all.subarray(offset)))!==null && n!==0)offset+=n; 123if(offset!==bytes.length || all.some(x=>x!==7))throw Error('Snapshot/partial write');check.close(); 124const shared=new Uint8Array(new SharedArrayBuffer(3));shared.set([1,2,3]); 125const f=await create('shared');if(await f.write(shared)!==3)throw Error('Shared write');f.close(); 126console.log('snapshots'); 127"# , 128& [], 129); 130assert_eq! ( output. stdout , b"snapshots\n" ); 131assert_eq! ( fs:: read ( dir. path (). join ( "shared" )). unwrap (), [ 1 , 2 , 3 ]); 132} 133 134# [ test ] 135fn streams_support_byob_eof_pipe_backpressure_and_automatic_close () { 136let ( dir, output) =run ( 137r#" 138import {open,create,errors} from 'avril:std'; 139const file=await open('data'); 140if(file.readable!==file.readable)throw Error('Readable identity'); 141const reader=file.readable.getReader({mode:'byob'});let text=''; 142while(true){const step=await reader.read(new Uint8Array(2));if(step.done)break;text+=new TextDecoder().decode(step.value);} 143if(text!=='abcdef')throw Error('BYOB');reader.releaseLock(); 144try{await file.stat();throw Error('EOF did not close');}catch(e){if(!(e instanceof errors.BadResource))throw e;} 145const source=await open('data'),target=await create('copy'); 146if(target.writable!==target.writable)throw Error('Writable identity'); 147await source.readable.pipeTo(target.writable); 148for(const f of [source,target])try{await f.stat();throw Error('Pipe did not close');}catch(e){if(!(e instanceof errors.BadResource))throw e;} 149console.log('streams'); 150"# , 151& [], 152); 153assert_eq! ( output. stdout , b"streams\n" ); 154assert_eq! ( fs:: read ( dir. path (). join ( "copy" )). unwrap (), b"abcdef" ); 155} 156 157# [ test ] 158fn cancellation_abort_and_explicit_close_invalidate_both_streams () { 159let ( _, output) =run ( 160r#" 161import {open,create,errors} from 'avril:std'; 162const a=await open('data');await a.readable.cancel(); 163const b=await create('out');await b.writable.abort('stop'); 164const c=await open('data',{read:true,write:true}),reader=c.readable.getReader(),writer=c.writable.getWriter(); 165const closedRead=reader.closed.catch(e=>e),closedWrite=writer.closed.catch(e=>e);c.close(); 166if(!(await closedRead instanceof errors.BadResource) || !(await closedWrite instanceof errors.BadResource))throw Error('Stream close'); 167for(const file of [a,b,c])try{await file.stat();throw Error('Unclosed handle');}catch(e){if(!(e instanceof errors.BadResource))throw e;} 168console.log('lifecycle'); 169"# , 170& [], 171); 172assert_eq! ( output. stdout , b"lifecycle\n" ); 173} 174 175# [ test ] 176fn concurrent_direct_operations_share_a_serialized_cursor () { 177let ( dir, output) =run ( 178r#" 179import {open} from 'avril:std'; 180const file=await open('data',{read:true,write:true}); 181const a=new Uint8Array(2),b=new Uint8Array(2),c=new Uint8Array(2); 182await Promise.all([file.read(a),file.read(b),file.read(c)]); 183if(new TextDecoder().decode(a)!=='ab'||new TextDecoder().decode(b)!=='cd'||new TextDecoder().decode(c)!=='ef')throw Error('Read order'); 184const seek=file.seek(0,0),write=file.write(new Uint8Array([90])),stat=file.stat(); 185if(await seek!==0 || await write!==1 || (await stat).size!==6)throw Error('Operation order'); 186file.close();console.log('serialized'); 187"# , 188& [], 189); 190assert_eq! ( output. stdout , b"serialized\n" ); 191assert_eq! ( fs:: read ( dir. path (). join ( "data" )). unwrap (), b"Zbcdef" ); 192} 193 194# [ test ] 195fn permissions_apply_to_open_and_live_operations_without_affecting_stdio () { 196let ( dir, output) =run ( 197r#" 198import {open,create,stat,pledge,errors} from 'avril:std'; 199const file=await open('data',{read:true,write:true}); 200for(const name of ['forbidden','allowed/child']){ 201try{await create(name);throw Error('Created denied file');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;} 202} 203const allowed=await create('allowed');allowed.close(); 204pledge({read:false,write:false}); 205for(const call of [()=>file.read(new Uint8Array(1)),()=>file.write(new Uint8Array([1])),()=>file.truncate(),()=>file.stat(),()=>stat('data'),()=>create('allowed')]){ 206try{await call();throw Error('Retained authority');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;} 207} 208file.close();console.log('live caps'); 209"# , 210& [ 211"-0" , 212"--allow-read=data" , 213"--allow-read=allowed" , 214"--allow-write=data" , 215"--allow-write=allowed" , 216], 217); 218assert_eq! ( output. stdout , b"live caps\n" ); 219assert! ( !dir. path (). join ( "forbidden" ). exists ()); 220assert_eq! ( fs:: read ( dir. path (). join ( "data" )). unwrap (), b"abcdef" ); 221} 222 223# [ test ] 224fn handles_are_private_branded_and_not_cloneable () { 225let output = common:: run ( 226r#" 227import {open,FileHandle} from 'avril:std'; 228const file=await open(new URL(import.meta.url)),read=FileHandle.prototype.read,close=FileHandle.prototype.close; 229Object.setPrototypeOf(file,null); 230try{structuredClone(file);throw Error('Cloned file handle');}catch(e){if(e.name!=='DataCloneError')throw e;} 231if(await read.call(file,new Uint8Array(1))!==1)throw Error('Private brand'); 232close.call(file); 233for(const name of ['ext:file/main.js','ext:file/errors.js']){ 234try{await import(name);throw Error('Private import');}catch(e){if(!e.message.includes('private'))throw e;} 235} 236if(globalThis.FileHandle!==undefined || Symbol.for('__avril.base') in globalThis)throw Error('Global exposure'); 237console.log('private handles'); 238"# , 239& [], 240); 241assert_eq! ( output. stdout , b"private handles\n" ); 242} 243 244# [ cfg ( unix )] 245# [ test ] 246fn stat_follows_links_lstat_preserves_entries_and_dangling_links_cannot_create_outside () { 247let dir = tempfile:: tempdir (). unwrap (); 248 fs:: write ( dir. path (). join ( "data" ), b"x" ). unwrap (); 249 std:: os:: unix:: fs:: symlink ( "data" , dir. path (). join ( "alias" )). unwrap (); 250let outside = tempfile:: tempdir (). unwrap (); 251 std:: os:: unix:: fs:: symlink ( outside. path (). join ( "new" ), dir. path (). join ( "dangling" )). unwrap (); 252 fs:: write ( dir. path (). join ( "main.js" ), r#" 253import {stat,lstat,create,open,errors} from 'avril:std'; 254if(!(await stat('alias')).isFile || !(await lstat('alias')).isSymlink || (await lstat('alias')).isFile)throw Error('Link metadata'); 255const file=await open('alias');file.close(); 256try{await create('dangling');throw Error('Followed dangling link');}catch(e){if(!(e instanceof errors.PermissionDenied)||e.message!=='read dangling')throw e;} 257console.log('links'); 258"# ). unwrap (); 259let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 260. arg ( "run" ) 261. current_dir ( dir. path ()) 262. args ([ "-0" , "--allow-read=./" , "--allow-write=./" , "main.js" ]) 263. stdin ( Stdio :: null ()) 264. stdout ( Stdio :: piped ()) 265. stderr ( Stdio :: piped ()) 266. spawn () 267. unwrap (); 268let output = common:: wait ( child); 269assert! ( 270 output. status . success (), 271"{}" , 272String :: from_utf8_lossy ( & output. stderr ) 273); 274assert_eq! ( output. stdout , b"links\n" ); 275assert! ( !outside. path (). join ( "new" ). exists ()); 276} 277 278# [ test ] 279fn large_concurrent_batches_do_not_fail_due_to_queue_admission () { 280let ( dir, output) =run ( 281r#" 282import {create,stat} from 'avril:std'; 283const checks=[];for(let i=0;i<600;i++)checks.push(stat('data')); 284if((await Promise.all(checks)).some(x=>x.size!==6))throw Error('Stat batch'); 285const file=await create('out'),writes=[]; 286for(let i=0;i<600;i++)writes.push(file.write(new Uint8Array([7]))); 287if((await Promise.all(writes)).some(x=>x!==1) || (await file.stat()).size!==600)throw Error('Write batch'); 288file.close();console.log('batches'); 289"# , 290& [], 291); 292assert_eq! ( output. stdout , b"batches\n" ); 293assert_eq! ( fs:: read ( dir. path (). join ( "out" )). unwrap (), vec! [ 7 ; 600 ]); 294} 295 296# [ cfg ( unix )] 297# [ test ] 298fn denied_file_requests_do_not_disclose_targets_or_resolution_errors () { 299let dir = tempfile:: tempdir (). unwrap (); 300 fs:: create_dir ( dir. path (). join ( "secret" )). unwrap (); 301 fs:: write ( dir. path (). join ( "secret/file" ), b"private" ). unwrap (); 302 std:: os:: unix:: fs:: symlink ( "secret/file" , dir. path (). join ( "alias" )). unwrap (); 303 std:: os:: unix:: fs:: symlink ( "loop" , dir. path (). join ( "loop" )). unwrap (); 304 fs:: write ( dir. path (). join ( "main.js" ), r#" 305import {open,stat,lstat,errors} from 'avril:std'; 306for(const name of ['alias','secret/file/x','secret/missing','loop','missing/../secret/file']){ 307for(const call of [()=>open(name),()=>stat(name),()=>lstat(name)]){ 308try{await call();throw Error('Granted read');}catch(e){if(!(e instanceof errors.PermissionDenied)||e.message!=='read '+name)throw e;} 309} 310try{await open(name,{write:true,create:true});throw Error('Granted write');}catch(e){if(!(e instanceof errors.PermissionDenied)||e.message!=='write '+name)throw e;} 311} 312console.log('no oracle'); 313"# ). unwrap (); 314let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 315. arg ( "run" ) 316. current_dir ( dir. path ()) 317. args ([ "-0" , "main.js" ]) 318. stdin ( Stdio :: null ()) 319. stdout ( Stdio :: piped ()) 320. stderr ( Stdio :: piped ()) 321. spawn () 322. unwrap (); 323let output = common:: wait ( child); 324assert! ( 325 output. status . success (), 326"{}" , 327String :: from_utf8_lossy ( & output. stderr ) 328); 329assert_eq! ( output. stdout , b"no oracle\n" ); 330} 331 332# [ cfg ( unix )] 333# [ test ] 334fn dangling_link_metadata_reports_missing_targets_and_dot_tails_follow_directories () { 335let dir = tempfile:: tempdir (). unwrap (); 336 fs:: create_dir ( dir. path (). join ( "folder" )). unwrap (); 337 std:: os:: unix:: fs:: symlink ( "missing" , dir. path (). join ( "dangling" )). unwrap (); 338 std:: os:: unix:: fs:: symlink ( "folder" , dir. path (). join ( "alias" )). unwrap (); 339 fs:: write ( dir. path (). join ( "main.js" ), r#" 340import {stat,lstat,errors} from 'avril:std'; 341if(!(await lstat('dangling')).isSymlink || !(await lstat('alias/.')).isDirectory || (await lstat('alias/.')).isSymlink)throw Error('Entry metadata'); 342for(const call of [()=>stat('dangling'),()=>stat('dangling/file'),()=>lstat('dangling/file')]){ 343try{await call();throw Error('Found missing target');}catch(e){if(!(e instanceof errors.NotFound))throw e;} 344} 345console.log('dangling metadata'); 346"# ). unwrap (); 347let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 348. arg ( "run" ) 349. current_dir ( dir. path ()) 350. arg ( "main.js" ) 351. stdin ( Stdio :: null ()) 352. stdout ( Stdio :: piped ()) 353. stderr ( Stdio :: piped ()) 354. spawn () 355. unwrap (); 356let output = common:: wait ( child); 357assert! ( 358 output. status . success (), 359"{}" , 360String :: from_utf8_lossy ( & output. stderr ) 361); 362assert_eq! ( output. stdout , b"dangling metadata\n" ); 363} 364 365# [ test ] 366fn file_io_does_not_use_typed_array_species_or_expose_write_snapshots () { 367let ( dir, output) =run ( 368r#" 369import {create} from 'avril:std'; 370const file=await create('out'),input=new Uint8Array(200000);input.fill(5); 371Object.defineProperty(Uint8Array.prototype,'constructor',{get(){throw Error('I/O consulted species');},configurable:true}); 372if(await file.write(input)!==65536)throw Error('Partial write'); 373const writer=file.writable.getWriter();await writer.write(input);await writer.close(); 374console.log('no species'); 375"# , 376& [], 377); 378assert_eq! ( output. stdout , b"no species\n" ); 379assert_eq! ( fs:: read ( dir. path (). join ( "out" )). unwrap (), vec! [ 5 ; 265536 ]); 380} 381 382# [ test ] 383fn reads_reject_detached_and_shrunken_targets_after_pending_io () { 384let ( _, output) =run ( 385r#" 386import {open} from 'avril:std'; 387const file=await open('data'),detached=new Uint8Array(2),first=file.read(detached); 388structuredClone(detached.buffer,{transfer:[detached.buffer]}); 389try{await first;throw Error('Read detached buffer');}catch(e){if(!(e instanceof TypeError))throw e;} 390if(await file.seek(0,1)!==2)throw Error('Detached read cursor'); 391const buffer=new ArrayBuffer(2,{maxByteLength:4}),second=file.read(new Uint8Array(buffer));buffer.resize(0); 392try{await second;throw Error('Read shrunken buffer');}catch(e){if(!(e instanceof RangeError))throw e;} 393if(await file.seek(0,1)!==4)throw Error('Shrunken read cursor'); 394file.close();console.log('buffer lifetimes'); 395"# , 396& [], 397); 398assert_eq! ( output. stdout , b"buffer lifetimes\n" ); 399} 400 401# [ test ] 402fn path_primitives_enumerate_copy_rename_and_remove () { 403let ( dir, output) =run ( 404r#" 405import {mkdir,readDir,copyFile,rename,remove,stat,errors} from 'avril:std'; 406await mkdir('folder',{mode:0o700}); 407await copyFile('data',new URL('./folder/copied',import.meta.url)); 408const entries=[];for await(const entry of readDir('folder'))entries.push(entry); 409if(entries.length!==1 || entries[0].name!=='copied' || !entries[0].isFile || entries[0].isDirectory || entries[0].isSymlink)throw Error('Entries'); 410await rename('folder/copied','folder/renamed'); 411if((await stat('folder/renamed')).size!==6)throw Error('Rename'); 412try{await remove('folder');throw Error('Removed populated directory');}catch(e){if(!(e instanceof errors.NotEmpty))throw e;} 413await remove('folder/renamed');await remove('folder'); 414await mkdir('tree');await mkdir('tree/sub');await copyFile('data','tree/sub/data');await remove('tree',{recursive:true}); 415for(const name of ['folder','tree'])try{await stat(name);throw Error('Remaining path');}catch(e){if(!(e instanceof errors.NotFound))throw e;} 416console.log('path ops'); 417"# , 418& [], 419); 420assert_eq! ( output. stdout , b"path ops\n" ); 421assert! ( !dir. path (). join ( "folder" ). exists ()); 422assert! ( !dir. path (). join ( "tree" ). exists ()); 423} 424 425# [ test ] 426fn denied_descendants_block_directory_mutations_before_side_effects () { 427let ( dir, output) =run ( 428r#" 429import {mkdir,copyFile,rename,remove,stat,errors} from 'avril:std'; 430await mkdir('tree');await copyFile('data','tree/data'); 431await mkdir('source');await copyFile('data','source/data'); 432for(const call of [()=>remove('tree',{recursive:true}),()=>rename('tree','moved'),()=>rename('source','tree')]){ 433try{await call();throw Error('Moved denied subtree');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;} 434} 435if((await stat('tree/data')).size!==6 || (await stat('source/data')).size!==6)throw Error('Side effects'); 436console.log('tree denies'); 437"# , 438& [ "--deny-write=tree/protected" ], 439); 440assert_eq! ( output. stdout , b"tree denies\n" ); 441assert! ( !dir. path (). join ( "moved" ). exists ()); 442} 443 444# [ test ] 445fn listing_filters_denied_children_and_rename_cannot_launder_unread_data () { 446let ( dir, output) =run ( 447r#" 448import {mkdir,open,readDir,rename,errors,pledge} from 'avril:std'; 449await mkdir('tree'); 450const secret=await open('tree/secret',{write:true,create:true});await secret.write(new Uint8Array([42]));secret.close(); 451pledge({read:{deny:['tree/public/future']}}); 452const publicFile=await open('tree/public',{write:true,create:true});publicFile.close(); 453const names=[];for await(const entry of readDir('tree'))names.push(entry.name); 454if(names.join()!=='public')throw Error('Listed secret entry'); 455for(const call of [()=>rename('tree','publicTree'),()=>rename('tree/secret','visible')]){ 456try{await call();throw Error('Laundered read authority');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;} 457} 458try{await rename('tree/public','visible');throw Error('Ignored future read deny');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;} 459console.log('listing privacy'); 460"# , 461& [ "--deny-read=tree/secret" ], 462); 463assert_eq! ( output. stdout , b"listing privacy\n" ); 464assert_eq! ( fs:: read ( dir. path (). join ( "tree/secret" )). unwrap (), [ 42 ]); 465} 466 467# [ test ] 468fn exact_file_grants_copy_but_cannot_rename_potential_directory_trees () { 469let ( dir, output) =run ( 470r#" 471import {copyFile,rename,errors} from 'avril:std'; 472await copyFile('data','copy'); 473try{await rename('data','copy');throw Error('Renamed potential tree with exact grants');}catch(e){if(!(e instanceof errors.PermissionDenied)||!e.message.includes('tree'))throw e;} 474console.log('exact grants'); 475"# , 476& [ 477"-0" , 478"--allow-read=data" , 479"--allow-write=data" , 480"--allow-write=copy" , 481], 482); 483assert_eq! ( output. stdout , b"exact grants\n" ); 484assert_eq! ( fs:: read ( dir. path (). join ( "data" )). unwrap (), b"abcdef" ); 485assert_eq! ( fs:: read ( dir. path (). join ( "copy" )). unwrap (), b"abcdef" ); 486} 487 488# [ test ] 489fn directory_shaped_grants_support_whole_tree_rename_and_live_limits () { 490let ( dir, output) =run ( 491r#" 492import {mkdir,create,rename,remove,pledge,errors} from 'avril:std'; 493await mkdir('source');const file=await create('source/data');await file.write(new Uint8Array([1]));file.close(); 494await rename('source','destination'); 495pledge({write:{deny:['destination/future']}}); 496try{await remove('destination',{recursive:true});throw Error('Ignored pledged descendant');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;} 497console.log('tree grants'); 498"# , 499& [ 500"-0" , 501"--allow-read=source/" , 502"--allow-write=source/" , 503"--allow-write=destination/" , 504], 505); 506assert_eq! ( output. stdout , b"tree grants\n" ); 507assert_eq! ( fs:: read ( dir. path (). join ( "destination/data" )). unwrap (), [ 1 ]); 508} 509 510# [ test ] 511fn new_path_operations_preserve_privacy_and_validate_arguments () { 512let ( _, output) =run ( 513r#" 514import {mkdir,remove,rename,copyFile,readDir,errors} from 'avril:std'; 515for(const call of [()=>mkdir('data/x'),()=>remove('data/x'),()=>rename('data/x','other'),()=>copyFile('data/x','other'),async()=>{for await(const _ of readDir('data/x')){}}]){ 516try{await call();throw Error('Disclosed path');}catch(e){if(!(e instanceof errors.PermissionDenied)||!e.message.includes('data/x'))throw e;} 517} 518for(const call of [()=>mkdir('out',{mode:2**40}),()=>remove('data',{recursive:'yes'}),()=>rename('data'),()=>copyFile('data')]){ 519try{await call();throw Error('Accepted invalid arguments');}catch(e){if(!(e instanceof RangeError)&&!(e instanceof TypeError))throw e;} 520} 521console.log('path privacy'); 522"# , 523& [ "-0" ], 524); 525assert_eq! ( output. stdout , b"path privacy\n" ); 526} 527 528# [ cfg ( unix )] 529# [ test ] 530fn copied_hard_links_are_not_truncated_and_link_mutations_do_not_follow_targets () { 531let dir = tempfile:: tempdir (). unwrap (); 532 fs:: write ( dir. path (). join ( "data" ), b"preserved" ). unwrap (); 533 fs:: hard_link ( dir. path (). join ( "data" ), dir. path (). join ( "hard" )). unwrap (); 534let outside = tempfile:: tempdir (). unwrap (); 535 fs:: write ( outside. path (). join ( "protected" ), b"outside" ). unwrap (); 536 std:: os:: unix:: fs:: symlink ( outside. path (), dir. path (). join ( "link" )). unwrap (); 537 fs:: write ( dir. path (). join ( "main.js" ), r#" 538import {copyFile,rename,remove,mkdir,readDir,errors} from 'avril:std'; 539for(const name of ['data','hard'])try{await copyFile('data',name);throw Error('Copied same inode');}catch(e){if(!(e instanceof errors.InvalidInput))throw e;} 540const entries=[];for await(const e of readDir('.'))entries.push(e); 541if(!entries.find(e=>e.name==='link')?.isSymlink)throw Error('Link entry type'); 542await rename('link','renamedLink');await remove('renamedLink',{recursive:true}); 543await mkdir('folder',{mode:0o700}); 544console.log('inode identity'); 545"# ). unwrap (); 546let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 547. arg ( "run" ) 548. current_dir ( dir. path ()) 549. arg ( "main.js" ) 550. stdin ( Stdio :: null ()) 551. stdout ( Stdio :: piped ()) 552. stderr ( Stdio :: piped ()) 553. spawn () 554. unwrap (); 555let output = common:: wait ( child); 556assert! ( 557 output. status . success (), 558"{}" , 559String :: from_utf8_lossy ( & output. stderr ) 560); 561assert_eq! ( output. stdout , b"inode identity\n" ); 562assert_eq! ( fs:: read ( dir. path (). join ( "data" )). unwrap (), b"preserved" ); 563assert_eq! ( 564 fs:: read ( outside. path (). join ( "protected" )). unwrap (), 565b"outside" 566); 567use std:: os:: unix:: fs:: PermissionsExt ; 568assert_eq! ( 569 fs:: metadata ( dir. path (). join ( "folder" )) 570. unwrap () 571. permissions () 572. mode () 573& 0o777 , 5740o700 575); 576} 577 578# [ test ] 579fn fs_conveniences_read_write_text_bytes_options_and_snapshots () { 580let ( dir, output) =run ( 581r#" 582import {readFile,readTextFile,writeFile,writeTextFile,exists} from 'avril:fs'; 583import {errors} from 'avril:std'; 584if(await readTextFile('data')!=='abcdef' || await exists('missing') || await exists('missing/../data'))throw Error('Read/exists'); 585const bytes=new Uint8Array(200003);bytes.fill(7); 586const pending=writeFile(new URL('./out',import.meta.url),bytes);bytes.fill(9);await pending; 587const result=await readFile('out');if(result.length!==200003||result.some(x=>x!==7))throw Error('Snapshot'); 588await writeTextFile('text','héllo 🌍');await writeTextFile('text','!',{append:true}); 589if(await readTextFile('text')!=='héllo 🌍!')throw Error('Text/append'); 590await writeFile('out',new Uint8Array());if((await readFile('out')).length!==0)throw Error('Empty truncate'); 591for(const call of [()=>writeTextFile('text','x',{createNew:true}),()=>writeFile('missing',bytes,{create:false}),()=>writeFile('text',new DataView(new ArrayBuffer(2))),()=>writeTextFile('text',null)]){ 592try{await call();throw Error('Invalid write accepted');}catch(e){if(!(e instanceof errors.AlreadyExists)&&!(e instanceof errors.NotFound)&&!(e instanceof TypeError))throw e;} 593} 594console.log('fs IO'); 595"# , 596& [], 597); 598assert_eq! ( output. stdout , b"fs IO\n" ); 599assert_eq! ( 600 fs:: read ( dir. path (). join ( "text" )). unwrap (), 601"héllo 🌍!" . as_bytes () 602); 603} 604 605# [ test ] 606fn mkdirs_copy_and_move_support_nested_trees_and_missing_parent_directories () { 607let ( dir, output) =run ( 608r#" 609import {mkdirs,writeTextFile,copy,move,readTextFile,exists} from 'avril:fs'; 610await mkdirs('source/sub/deep');await mkdirs('source/sub/deep'); 611await writeTextFile('source/sub/deep/data','nested'); 612await copy('source','destination'); 613if(await readTextFile('destination/sub/deep/data')!=='nested')throw Error('Tree copy'); 614await copy('data','other/parents/file'); 615if(await readTextFile('other/parents/file')!=='abcdef')throw Error('File copy parents'); 616await move('destination','moved'); 617if(await exists('destination')||await readTextFile('moved/sub/deep/data')!=='nested')throw Error('Tree move'); 618console.log('fs workflows'); 619"# , 620& [], 621); 622assert_eq! ( output. stdout , b"fs workflows\n" ); 623assert! ( dir. path (). join ( "source/sub/deep/data" ). exists ()); 624assert! ( !dir. path (). join ( "destination" ). exists ()); 625} 626 627# [ test ] 628fn make_temp_dir_and_file_create_unique_namespaced_paths () { 629let ( dir, output) =run ( 630r#" 631import {makeTempDir,makeTempFile,exists,readTextFile,writeTextFile} from 'avril:fs'; 632import {mkdir,stat,remove,errors} from 'avril:std'; 633const seen=new Set(); 634const plain=await makeTempDir(); 635if(!/^[0-9a-f]{8,16}$/.test(plain.split('/').pop()))throw Error('Hex name'); 636const named=await makeTempDir({prefix:'work-',suffix:'-tmp'}); 637if(!named.split('/').pop().startsWith('work-')||!named.endsWith('-tmp'))throw Error('Affixes'); 638const file=await makeTempFile({prefix:'out-',suffix:'.bin'}); 639if((await stat(file)).size!==0||!(await stat(file)).isFile)throw Error('Empty file'); 640await writeTextFile(file,'temp'); 641if(await readTextFile(file)!=='temp')throw Error('Writable temp file'); 642const inner=await makeTempDir({dir:plain,prefix:'inner-'}); 643if(!inner.startsWith(plain+'/'))throw Error('Custom dir spelling'); 644const viaUrl=await makeTempFile({dir:new URL(plain+'/','file://')}); 645if(!viaUrl.startsWith(plain+'/'))throw Error('URL dir'); 646for(let i=0;i<10;i++){const candidate=await makeTempFile({dir:plain});if(seen.has(candidate))throw Error('Collision');seen.add(candidate);} 647for(const call of [()=>makeTempDir({prefix:'a/b'}),()=>makeTempFile({suffix:'x*y'}),()=>makeTempDir({prefix:'a\0b'}),()=>makeTempDir({prefix:3}),()=>makeTempDir({dir:plain+'/missing/nested'})]){ 648try{await call();throw Error('Invalid temp accepted');}catch(e){if(!(e instanceof errors.InvalidInput)&&!(e instanceof errors.NotFound)&&!(e instanceof TypeError))throw e;} 649} 650const dirInfo=await stat(named); 651if(!dirInfo.isDirectory)throw Error('Directory kind'); 652if(dirInfo.mode!==null&&(dirInfo.mode&0o777)!==0o700)throw Error('Directory mode'); 653const fileInfo=await stat(file); 654if(fileInfo.mode!==null&&(fileInfo.mode&0o777)!==0o600)throw Error('File mode'); 655await mkdir('scoped'); 656const scoped=await makeTempDir({dir:'scoped'}); 657if(!scoped.startsWith('scoped/'))throw Error('Relative dir'); 658await remove(plain,{recursive:true});await remove(named,{recursive:true});await remove(scoped,{recursive:true});await remove('scoped',{recursive:true});await remove(file); 659if(await exists(plain)||await exists(named)||await exists(file))throw Error('Cleanup'); 660console.log('temp IO'); 661"# , 662& [], 663); 664assert_eq! ( output. stdout , b"temp IO\n" ); 665assert! ( !dir. path (). join ( "scoped" ). exists ()); 666} 667 668# [ test ] 669fn fs_write_only_directory_and_exact_file_grants_work_without_reading_destinations () { 670let ( dir, output) =run ( 671r#" 672import {mkdirs,writeTextFile,copy,readTextFile,exists} from 'avril:fs'; 673import {errors} from 'avril:std'; 674await mkdirs('out/sub');await mkdirs('out/sub'); 675await writeTextFile('out/sub/data','written');await copy('data','exact'); 676for(const call of [()=>readTextFile('out/sub/data'),()=>exists('out/sub/data'),()=>exists('ungranted/missing')]){ 677try{await call();throw Error('Read without grant');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;} 678} 679console.log('write-only fs'); 680"# , 681& [ 682"-0" , 683"--allow-read=data" , 684"--allow-write=out/" , 685"--allow-write=exact" , 686], 687); 688assert_eq! ( output. stdout , b"write-only fs\n" ); 689assert_eq! ( fs:: read ( dir. path (). join ( "exact" )). unwrap (), b"abcdef" ); 690assert_eq! ( 691 fs:: read ( dir. path (). join ( "out/sub/data" )). unwrap (), 692b"written" 693); 694} 695 696# [ test ] 697fn whole_tree_copy_authorization_prevents_silent_skipping_and_partial_denied_writes () { 698let ( dir, output) =run ( 699r#" 700import {copy,mkdirs,writeTextFile} from 'avril:fs'; 701import {errors} from 'avril:std'; 702await mkdirs('source');await writeTextFile('source/data','payload'); 703for(const call of [()=>copy('source','readDeniedCopy'),()=>copy('source','destination')]){ 704try{await call();throw Error('Copied forbidden tree');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;} 705} 706console.log('copy ceilings'); 707"# , 708& [ 709"--deny-read=source/future" , 710"--deny-write=destination/future" , 711], 712); 713assert_eq! ( output. stdout , b"copy ceilings\n" ); 714assert! ( !dir. path (). join ( "readDeniedCopy" ). exists ()); 715assert! ( !dir. path (). join ( "destination" ). exists ()); 716} 717 718# [ test ] 719fn copying_overlapping_trees_is_rejected_before_creating_destinations () { 720let ( dir, output) =run ( 721r#" 722import {copy,mkdirs,writeTextFile} from 'avril:fs'; 723import {errors} from 'avril:std'; 724await mkdirs('source/inner');await writeTextFile('source/data','payload'); 725for(const [from,to] of [['source','source'],['source','source/inner/new'],['source/inner','source']]){ 726try{await copy(from,to);throw Error('Copied overlapping trees');}catch(e){if(!(e instanceof errors.InvalidInput))throw e;} 727} 728console.log('overlap guards'); 729"# , 730& [], 731); 732assert_eq! ( output. stdout , b"overlap guards\n" ); 733assert! ( !dir. path (). join ( "source/inner/new" ). exists ()); 734} 735 736# [ test ] 737fn utility_intrinsics_and_error_classification_resist_public_prototype_tampering () { 738let ( _, output) =run ( 739r#" 740import {writeFile,readFile,exists,writeTextFile,readTextFile} from 'avril:fs'; 741import {FileHandle,errors} from 'avril:std'; 742FileHandle.prototype.read=FileHandle.prototype.write=FileHandle.prototype[Symbol.dispose]=()=>{throw Error('Tampered method');}; 743TextEncoder.prototype.encode=TextDecoder.prototype.decode=()=>{throw Error('Tampered codec');}; 744Object.defineProperty(Uint8Array.prototype,'constructor',{get(){throw Error('Species');}}); 745Object.defineProperty(errors.NotFound,Symbol.hasInstance,{value(){return true;}}); 746await writeTextFile('text','safe');if(await readTextFile('text')!=='safe')throw Error('Codec'); 747const data=new Uint8Array(140000);data.fill(1);await writeFile('out',data); 748if((await readFile('out')).length!==140000)throw Error('Methods'); 749if(await exists('missing'))throw Error('Missing'); 750try{await exists('denied');throw Error('Swallowed permission error');}catch(e){if(e.name!=='PermissionDenied')throw e;} 751console.log('captured utilities'); 752"# , 753& [ "--deny-read=denied" ], 754); 755assert_eq! ( output. stdout , b"captured utilities\n" ); 756} 757 758# [ test ] 759fn fs_public_module_is_frozen_and_private_sources_stay_private () { 760let output = common:: run ( 761r#" 762import * as fs from 'avril:fs'; 763import * as std from 'avril:std'; 764for(const name of ['readFile','readTextFile','writeFile','writeTextFile','mkdirs','copy','move','exists']) 765if(typeof fs[name]!=='function'||std[name]!==undefined)throw Error('Module split'); 766try{fs.readFile=()=>{};throw Error('FS module');}catch(e){if(!(e instanceof TypeError))throw e;} 767if(Symbol.for('__avril.fs') in globalThis)throw Error('FS registry'); 768for(const source of ['ext:file/fs.js','ext:file/workflows.js']){ 769try{await import(source);throw Error('Private source imported');}catch(e){if(!e.message.includes('private'))throw e;} 770} 771console.log('fs module'); 772"# , 773& [], 774); 775assert! ( 776 output. status . success (), 777"{}" , 778String :: from_utf8_lossy ( & output. stderr ) 779); 780assert_eq! ( output. stdout , b"fs module\n" ); 781} 782 783# [ cfg ( unix )] 784# [ test ] 785fn directory_copy_preserves_symlink_children_and_rejects_canonical_destination_overlap () { 786let dir = tempfile:: tempdir (). unwrap (); 787 fs:: create_dir ( dir. path (). join ( "source" )). unwrap (); 788 fs:: write ( dir. path (). join ( "source/data" ), b"safe" ). unwrap (); 789 std:: os:: unix:: fs:: symlink ( "source" , dir. path (). join ( "alias" )). unwrap (); 790 std:: os:: unix:: fs:: symlink ( "../data" , dir. path (). join ( "source/link" )). unwrap (); 791 fs:: write ( dir. path (). join ( "main.js" ), r#" 792import {copy} from 'avril:fs'; 793import {errors,readLink,lstat} from 'avril:std'; 794try{await copy('source','alias/nested');throw Error('Canonical overlap');}catch(e){if(!(e instanceof errors.InvalidInput))throw e;} 795await copy('alias','copiedAlias');await copy('source','copiedTree'); 796if(!(await lstat('copiedAlias')).isSymlink||await readLink('copiedAlias')!=='source'||await readLink('copiedTree/link')!=='../data')throw Error('Link preservation'); 797console.log('symlink workflows'); 798"# ). unwrap (); 799let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 800. arg ( "run" ) 801. current_dir ( dir. path ()) 802. arg ( "main.js" ) 803. stdin ( Stdio :: null ()) 804. stdout ( Stdio :: piped ()) 805. stderr ( Stdio :: piped ()) 806. spawn () 807. unwrap (); 808let output = common:: wait ( child); 809assert! ( 810 output. status . success (), 811"{}" , 812String :: from_utf8_lossy ( & output. stderr ) 813); 814assert_eq! ( output. stdout , b"symlink workflows\n" ); 815assert! ( !dir. path (). join ( "source/nested" ). exists ()); 816assert_eq! ( 817 fs:: read_link ( dir. path (). join ( "copiedAlias" )). unwrap (), 818 std:: path:: Path :: new ( "source" ) 819); 820use std:: os:: unix:: fs:: PermissionsExt ; 821assert_eq! ( 822 fs:: metadata ( dir. path (). join ( "copiedTree" )) 823. unwrap () 824. permissions () 825. mode () 826& 0o777 , 827 fs:: metadata ( dir. path (). join ( "source" )) 828. unwrap () 829. permissions () 830. mode () 831& 0o777 832); 833assert! ( fs:: symlink_metadata ( dir. path (). join ( "source/link" )). is_ok ()); 834} 835 836# [ cfg ( target_os = "linux" )] 837# [ test ] 838fn cross_device_moves_copy_then_remove_and_keep_sources_on_failed_copy () { 839use std:: os:: unix:: fs:: MetadataExt ; 840let dir = tempfile:: tempdir (). unwrap (); 841let Ok ( other) = tempfile:: tempdir_in ( "/dev/shm" ) else { 842return ; 843}; 844if fs:: metadata ( dir. path ()). unwrap (). dev () == fs:: metadata ( other. path ()). unwrap (). dev () { 845return ; 846} 847 fs:: write ( dir. path (). join ( "file" ), b"cross-device" ). unwrap (); 848 fs:: create_dir ( dir. path (). join ( "tree" )). unwrap (); 849 fs:: write ( dir. path (). join ( "tree/data" ), b"tree" ). unwrap (); 850 fs:: create_dir ( dir. path (). join ( "unsupported" )). unwrap (); 851 std:: os:: unix:: fs:: symlink ( "data" , dir. path (). join ( "tree/link" )). unwrap (); 852 std:: os:: unix:: fs:: symlink ( "file" , dir. path (). join ( "link" )). unwrap (); 853let _socket = 854 std:: os:: unix:: net:: UnixListener :: bind ( dir. path (). join ( "unsupported/socket" )). unwrap (); 855let source =format! ( "{:?}" , dir. path (). to_str (). unwrap ()); 856let destination =format! ( "{:?}" , other. path (). to_str (). unwrap ()); 857 fs:: write ( dir. path (). join ( "main.js" ), format! ( r#" 858import {{move,readTextFile,exists}} from 'avril:fs'; 859const source={source},destination={destination}; 860await move(source+'/file',destination+'/file'); 861await move(source+'/link',destination+'/link'); 862await move(source+'/tree',destination+'/tree'); 863if(await exists(source+'/file')||await exists(source+'/tree')||await readTextFile(destination+'/file')!=='cross-device'||await readTextFile(destination+'/tree/data')!=='tree')throw Error('Cross-device move'); 864try{{await move(source+'/unsupported',destination+'/unsupported');throw Error('Moved unsupported tree');}}catch(e){{if(!e.message.includes('not yet implemented'))throw e;}} 865console.log('cross-device'); 866"# )). unwrap (); 867let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 868. arg ( "run" ) 869. arg ( dir. path (). join ( "main.js" )) 870. stdin ( Stdio :: null ()) 871. stdout ( Stdio :: piped ()) 872. stderr ( Stdio :: piped ()) 873. spawn () 874. unwrap (); 875let output = common:: wait ( child); 876assert! ( 877 output. status . success (), 878"{}" , 879String :: from_utf8_lossy ( & output. stderr ) 880); 881assert_eq! ( output. stdout , b"cross-device\n" ); 882assert! ( fs:: symlink_metadata ( dir. path (). join ( "unsupported/socket" )). is_ok ()); 883assert_eq! ( 884 fs:: read_link ( other. path (). join ( "tree/link" )). unwrap (), 885 std:: path:: Path :: new ( "data" ) 886); 887assert_eq! ( 888 fs:: read_link ( other. path (). join ( "link" )). unwrap (), 889 std:: path:: Path :: new ( "file" ) 890); 891assert! ( fs:: symlink_metadata ( dir. path (). join ( "link" )). is_err ()); 892assert! ( other. path (). join ( "tree/data" ). exists ()); 893} 894 895# [ cfg ( unix )] 896# [ test ] 897fn copying_read_only_directory_trees_preserves_modes_after_private_staging () { 898use std:: os:: unix:: fs:: PermissionsExt ; 899let dir = tempfile:: tempdir (). unwrap (); 900 fs:: create_dir ( dir. path (). join ( "source" )). unwrap (); 901 fs:: write ( dir. path (). join ( "source/data" ), b"data" ). unwrap (); 902 fs:: set_permissions ( dir. path (). join ( "source" ), fs:: Permissions :: from_mode ( 0o555 )). unwrap (); 903 fs:: create_dir ( dir. path (). join ( "existing" )). unwrap (); 904 fs:: set_permissions ( 905 dir. path (). join ( "existing" ), 906 fs:: Permissions :: from_mode ( 0o777 ), 907) 908. unwrap (); 909 fs:: write ( dir. path (). join ( "main.js" ), r#" 910import {copy,readTextFile} from 'avril:fs'; 911await copy('source','new');await copy('source','existing'); 912if(await readTextFile('new/data')!=='data'||await readTextFile('existing/data')!=='data')throw Error('Readonly copy'); 913console.log('copied modes'); 914"# ). unwrap (); 915let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 916. arg ( "run" ) 917. current_dir ( dir. path ()) 918. arg ( "main.js" ) 919. stdin ( Stdio :: null ()) 920. stdout ( Stdio :: piped ()) 921. stderr ( Stdio :: piped ()) 922. spawn () 923. unwrap (); 924let output = common:: wait ( child); 925assert! ( 926 output. status . success (), 927"{}" , 928String :: from_utf8_lossy ( & output. stderr ) 929); 930assert_eq! ( output. stdout , b"copied modes\n" ); 931for namein [ "source" , "new" , "existing" ] { 932let path = dir. path (). join ( name); 933assert_eq! ( 934 fs:: metadata ( & path). unwrap (). permissions (). mode () & 0o777 , 9350o555 936); 937 fs:: set_permissions ( path, fs:: Permissions :: from_mode ( 0o700 )). unwrap (); 938} 939} 940 941# [ cfg ( unix )] 942# [ test ] 943fn symbolic_link_primitives_preserve_text_resolve_paths_and_report_dangling_links () { 944let ( dir, output) =run ( 945r#" 946import {symlink,readLink,realPath,lstat,stat,mkdir,errors} from 'avril:std'; 947import {readTextFile,exists} from 'avril:fs'; 948await mkdir('links'); 949await symlink('../data','links/relative'); 950if(await readLink(new URL('./links/relative',import.meta.url))!=='../data'||await readTextFile('links/relative')!=='abcdef')throw Error('Relative link base'); 951const target=new URL('./data',import.meta.url);await symlink(target,'absolute',{type:'file'}); 952if(await readLink('absolute')!==await realPath('data')||await realPath('links/relative')!==await realPath('data'))throw Error('Absolute canonical path'); 953if(!(await lstat('links/relative')).isSymlink||!(await stat('links/relative')).isFile)throw Error('Metadata'); 954await symlink('missing','dangling'); 955if(await readLink('dangling')!=='missing'||await exists('dangling'))throw Error('Dangling text'); 956for(const call of [()=>realPath('dangling'),()=>realPath('missing')]){ 957try{await call();throw Error('Resolved missing target');}catch(e){if(!(e instanceof errors.NotFound))throw e;} 958} 959await symlink('loop','loop'); 960try{await realPath('loop');throw Error('Resolved loop');}catch(e){if(!(e instanceof errors.IoError))throw e;} 961console.log('symbolic links'); 962"# , 963& [], 964); 965assert_eq! ( output. stdout , b"symbolic links\n" ); 966assert_eq! ( 967 fs:: read_link ( dir. path (). join ( "links/relative" )). unwrap (), 968 std:: path:: Path :: new ( "../data" ) 969); 970} 971 972# [ cfg ( unix )] 973# [ test ] 974fn symlink_creation_requires_target_tree_rights_and_does_not_overwrite_entries () { 975let ( dir, output) =run ( 976r#" 977import {mkdir,symlink,readLink,realPath,errors} from 'avril:std'; 978await mkdir('out'); 979await symlink('../data','out/allowed'); 980if(await readLink('out/allowed')!=='../data')throw Error('Stored text'); 981for(const [target,name] of [['../secret/file','out/readDenied'],['../readonly/file','out/writeDenied'],['../exact','out/exactDenied']]){ 982try{await symlink(target,name);throw Error('Planted unauthorized link');}catch(e){if(!(e instanceof errors.PermissionDenied)||!e.message.includes(name))throw e;} 983} 984try{await symlink('replacement','out/allowed');throw Error('Overwrote link');}catch(e){if(!(e instanceof errors.AlreadyExists)&&!(e instanceof errors.PermissionDenied))throw e;} 985console.log('target permissions'); 986"# , 987& [ 988"--deny-read=secret/" , 989"--deny-write=readonly/" , 990"--deny-write=exact/future" , 991], 992); 993assert_eq! ( output. stdout , b"target permissions\n" ); 994for namein [ "readDenied" , "writeDenied" , "exactDenied" ] { 995assert! ( fs:: symlink_metadata ( dir. path (). join ( "out" ). join ( name)). is_err ()); 996} 997assert_eq! ( 998 fs:: read_link ( dir. path (). join ( "out/allowed" )). unwrap (), 999 std:: path:: Path :: new ( "../data" ) 1000); 1001} 1002 1003# [ cfg ( unix )] 1004# [ test ] 1005fn scoped_symlink_targets_may_be_missing_but_must_not_hide_unresolved_aliases () { 1006let dir = tempfile:: tempdir (). unwrap (); 1007 fs:: create_dir ( dir. path (). join ( "out" )). unwrap (); 1008let outside = tempfile:: tempdir (). unwrap (); 1009 std:: os:: unix:: fs:: symlink ( 1010 outside. path (). join ( "missing" ), 1011 dir. path (). join ( "out/hiddenAlias" ), 1012) 1013. unwrap (); 1014 fs:: write ( dir. path (). join ( "main.js" ), r#" 1015import {symlink,readLink,realPath,errors,pledge} from 'avril:std'; 1016await symlink('future','out/allowed'); 1017if(await readLink('out/allowed')!=='future')throw Error('Allowed future target'); 1018for(const target of ['hiddenAlias','hiddenAlias/file','../../outside']){ 1019try{await symlink(target,'out/denied');throw Error('Authorized unresolved target');}catch(e){if(!(e instanceof errors.PermissionDenied)||e.message!=='symlink target for out/denied')throw e;} 1020} 1021pledge({write:false}); 1022try{await symlink('future','out/revoked');throw Error('Retained write');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;} 1023console.log('scoped links'); 1024"# ). unwrap (); 1025let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 1026. arg ( "run" ) 1027. current_dir ( dir. path ()) 1028. args ([ "-0" , "--allow-read=out/" , "--allow-write=out/" , "main.js" ]) 1029. stdin ( Stdio :: null ()) 1030. stdout ( Stdio :: piped ()) 1031. stderr ( Stdio :: piped ()) 1032. spawn () 1033. unwrap (); 1034let output = common:: wait ( child); 1035assert! ( 1036 output. status . success (), 1037"{}" , 1038String :: from_utf8_lossy ( & output. stderr ) 1039); 1040assert_eq! ( output. stdout , b"scoped links\n" ); 1041assert! ( fs:: symlink_metadata ( dir. path (). join ( "out/denied" )). is_err ()); 1042assert! ( !outside. path (). join ( "missing" ). exists ()); 1043} 1044 1045# [ cfg ( unix )] 1046# [ test ] 1047fn read_link_reads_entry_data_but_real_path_requires_the_referent_and_hides_denials () { 1048let dir = tempfile:: tempdir (). unwrap (); 1049 fs:: create_dir ( dir. path (). join ( "out" )). unwrap (); 1050 fs:: create_dir ( dir. path (). join ( "secret" )). unwrap (); 1051 fs:: write ( dir. path (). join ( "secret/file" ), b"private" ). unwrap (); 1052 std:: os:: unix:: fs:: symlink ( "../secret/file" , dir. path (). join ( "out/link" )). unwrap (); 1053 fs:: write ( dir. path (). join ( "main.js" ), r#" 1054import {readLink,realPath,errors} from 'avril:std'; 1055if(await readLink('out/link')!=='../secret/file')throw Error('Entry read'); 1056for(const name of ['out/link','secret/file/x','secret/missing']){ 1057try{await realPath(name);throw Error('Disclosed target');}catch(e){if(!(e instanceof errors.PermissionDenied)||e.message!=='read '+name)throw e;} 1058} 1059try{await readLink('secret/missing');throw Error('Read unauthorized link');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;} 1060console.log('link privacy'); 1061"# ). unwrap (); 1062let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 1063. arg ( "run" ) 1064. current_dir ( dir. path ()) 1065. args ([ "-0" , "--allow-read=out/" , "main.js" ]) 1066. stdin ( Stdio :: null ()) 1067. stdout ( Stdio :: piped ()) 1068. stderr ( Stdio :: piped ()) 1069. spawn () 1070. unwrap (); 1071let output = common:: wait ( child); 1072assert! ( 1073 output. status . success (), 1074"{}" , 1075String :: from_utf8_lossy ( & output. stderr ) 1076); 1077assert_eq! ( output. stdout , b"link privacy\n" ); 1078} 1079 1080# [ cfg ( unix )] 1081# [ test ] 1082fn renaming_relative_links_rechecks_targets_and_scoped_directory_rebasing_is_explicitly_deferred () { 1083let ( dir, output) =run ( 1084r#" 1085import {mkdir,symlink,rename,readLink,errors} from 'avril:std'; 1086await mkdir('safe');await mkdir('safe/deep');await mkdir('dest'); 1087await symlink('../data','safe/link'); 1088try{await rename('safe/link','safe/deep/link');throw Error('Retargeted forbidden link');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;} 1089await mkdir('pack');await symlink('data','pack/link'); 1090try{await rename('pack','dest/tree');throw Error('Rebased scoped directory');}catch(e){if(!(e instanceof errors.NotSupported)||!e.message.includes('not yet implemented'))throw e;} 1091if(await readLink('safe/link')!=='../data')throw Error('Failed rename modified link'); 1092console.log('link rebasing'); 1093"# , 1094& [ "--deny-write=safe/data" ], 1095); 1096assert_eq! ( output. stdout , b"link rebasing\n" ); 1097assert! ( !dir. path (). join ( "dest/tree" ). exists ()); 1098} 1099 1100# [ cfg ( unix )] 1101# [ test ] 1102fn link_primitive_arguments_errors_and_private_exports_are_consistent () { 1103let ( _, output) =run ( 1104r#" 1105import {symlink,readLink,realPath,errors} from 'avril:std'; 1106for(const call of [()=>symlink('data'),()=>symlink('data','new',{type:'junction'}),()=>readLink(),()=>realPath()]){ 1107try{await call();throw Error('Accepted invalid arguments');}catch(e){if(!(e instanceof TypeError))throw e;} 1108} 1109for(const call of [()=>symlink('','new'),()=>symlink('a\0b','new'),()=>readLink('data')]){ 1110try{await call();throw Error('Accepted invalid link');}catch(e){if(!(e instanceof errors.InvalidInput))throw e;} 1111} 1112await symlink('missing','dangling'); 1113try{await symlink('other','dangling');throw Error('Replaced dangling link');}catch(e){if(!(e instanceof errors.AlreadyExists))throw e;} 1114console.log('link errors'); 1115"# , 1116& [], 1117); 1118assert_eq! ( output. stdout , b"link errors\n" ); 1119} 1120 1121# [ cfg ( unix )] 1122# [ test ] 1123fn copying_links_preserves_raw_targets_or_explicitly_dereferences_them () { 1124let ( dir, output) =run ( 1125r#" 1126import {symlink,mkdir,readLink,lstat} from 'avril:std'; 1127import {copy,readTextFile,writeTextFile} from 'avril:fs'; 1128await symlink('data','relative');await symlink(new URL('./data',import.meta.url),'absolute');await symlink('missing','dangling'); 1129for(const name of ['relative','absolute','dangling']){ 1130await copy(name,name+'Copy'); 1131if(!(await lstat(name+'Copy')).isSymlink||await readLink(name+'Copy')!==await readLink(name))throw Error('Raw preservation'); 1132} 1133await copy('relative','fileCopy',{dereference:true}); 1134if(!(await lstat('fileCopy')).isFile||await readTextFile('fileCopy')!=='abcdef')throw Error('File dereference'); 1135await mkdir('folder');await writeTextFile('folder/data','nested');await symlink('folder','dirLink'); 1136await copy('dirLink','dirCopy',{dereference:true}); 1137if(!(await lstat('dirCopy')).isDirectory||await readTextFile('dirCopy/data')!=='nested')throw Error('Directory dereference'); 1138console.log('copy links'); 1139"# , 1140& [], 1141); 1142assert_eq! ( output. stdout , b"copy links\n" ); 1143assert_eq! ( 1144 fs:: read_link ( dir. path (). join ( "danglingCopy" )). unwrap (), 1145 std:: path:: Path :: new ( "missing" ) 1146); 1147} 1148 1149# [ cfg ( unix )] 1150# [ test ] 1151fn following_directory_links_detects_cycles_but_allows_shared_subtrees () { 1152let ( dir, output) =run ( 1153r#" 1154import {symlink,mkdir,errors,lstat} from 'avril:std'; 1155import {copy,writeTextFile,readTextFile} from 'avril:fs'; 1156await mkdir('shared');await writeTextFile('shared/data','shared'); 1157await mkdir('source');await symlink('../shared','source/a');await symlink('../shared','source/b'); 1158await copy('source','destination',{dereference:true}); 1159if(await readTextFile('destination/a/data')!=='shared'||await readTextFile('destination/b/data')!=='shared')throw Error('DAG alias skipped'); 1160await symlink('.','source/cycle'); 1161await copy('source','preserved'); 1162if(!(await lstat('preserved/cycle')).isSymlink)throw Error('Preserved cycle'); 1163try{await copy('source','followed',{dereference:true});throw Error('Followed cycle forever');}catch(e){if(!(e instanceof errors.InvalidInput)||!e.message.includes('cycle'))throw e;} 1164console.log('cycle guards'); 1165"# , 1166& [], 1167); 1168assert_eq! ( output. stdout , b"cycle guards\n" ); 1169assert! ( !dir. path (). join ( "followed/cycle" ). exists ()); 1170} 1171 1172# [ cfg ( unix )] 1173# [ test ] 1174fn link_copy_checks_new_target_authority_before_removing_the_destination () { 1175let dir = tempfile:: tempdir (). unwrap (); 1176 fs:: create_dir ( dir. path (). join ( "source" )). unwrap (); 1177 fs:: create_dir_all ( dir. path (). join ( "dest/deep" )). unwrap (); 1178 fs:: write ( dir. path (). join ( "target" ), b"target" ). unwrap (); 1179 fs:: write ( dir. path (). join ( "dest/deep/link" ), b"untouched" ). unwrap (); 1180 std:: os:: unix:: fs:: symlink ( "../target" , dir. path (). join ( "source/link" )). unwrap (); 1181 fs:: write ( dir. path (). join ( "main.js" ), r#" 1182import {copy} from 'avril:fs'; 1183import {errors} from 'avril:std'; 1184try{await copy('source/link','dest/deep/link');throw Error('Planted rebased link');}catch(e){if(!(e instanceof errors.PermissionDenied)||e.message!=='symlink target for dest/deep/link')throw e;} 1185console.log('copy target checks'); 1186"# ). unwrap (); 1187let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 1188. arg ( "run" ) 1189. current_dir ( dir. path ()) 1190. args ([ "--deny-write=dest/target" , "main.js" ]) 1191. stdin ( Stdio :: null ()) 1192. stdout ( Stdio :: piped ()) 1193. stderr ( Stdio :: piped ()) 1194. spawn () 1195. unwrap (); 1196let output = common:: wait ( child); 1197assert! ( 1198 output. status . success (), 1199"{}" , 1200String :: from_utf8_lossy ( & output. stderr ) 1201); 1202assert_eq! ( output. stdout , b"copy target checks\n" ); 1203assert_eq! ( 1204 fs:: read ( dir. path (). join ( "dest/deep/link" )). unwrap (), 1205b"untouched" 1206); 1207} 1208 1209# [ cfg ( unix )] 1210# [ test ] 1211fn link_preservation_and_dereferencing_require_different_target_rights () { 1212let dir = tempfile:: tempdir (). unwrap (); 1213 fs:: create_dir ( dir. path (). join ( "links" )). unwrap (); 1214 fs:: write ( dir. path (). join ( "data" ), b"read-only" ). unwrap (); 1215 std:: os:: unix:: fs:: symlink ( "../data" , dir. path (). join ( "links/source" )). unwrap (); 1216 fs:: write ( dir. path (). join ( "main.js" ), r#" 1217import {copy,readTextFile} from 'avril:fs'; 1218import {errors,lstat} from 'avril:std'; 1219try{await copy('links/source','links/preserved');throw Error('Planted read-only target link');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;} 1220await copy('links/source','links/followed',{dereference:true}); 1221if(!(await lstat('links/followed')).isFile||await readTextFile('links/followed')!=='read-only')throw Error('Authorized read copy'); 1222console.log('follow permissions'); 1223"# ). unwrap (); 1224let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 1225. arg ( "run" ) 1226. current_dir ( dir. path ()) 1227. args ([ "--deny-write=data" , "main.js" ]) 1228. stdin ( Stdio :: null ()) 1229. stdout ( Stdio :: piped ()) 1230. stderr ( Stdio :: piped ()) 1231. spawn () 1232. unwrap (); 1233let output = common:: wait ( child); 1234assert! ( 1235 output. status . success (), 1236"{}" , 1237String :: from_utf8_lossy ( & output. stderr ) 1238); 1239assert_eq! ( output. stdout , b"follow permissions\n" ); 1240assert! ( fs:: symlink_metadata ( dir. path (). join ( "links/preserved" )). is_err ()); 1241} 1242 1243# [ cfg ( unix )] 1244# [ test ] 1245fn following_ungranted_links_does_not_disclose_canonical_targets () { 1246let dir = tempfile:: tempdir (). unwrap (); 1247 fs:: create_dir ( dir. path (). join ( "links" )). unwrap (); 1248 fs:: create_dir ( dir. path (). join ( "secret" )). unwrap (); 1249 fs:: write ( dir. path (). join ( "secret/data" ), b"private" ). unwrap (); 1250 std:: os:: unix:: fs:: symlink ( "../secret/data" , dir. path (). join ( "links/source" )). unwrap (); 1251 fs:: write ( dir. path (). join ( "main.js" ), r#" 1252import {copy} from 'avril:fs'; 1253import {errors} from 'avril:std'; 1254try{await copy('links/source','links/followed',{dereference:true});throw Error('Read secret');}catch(e){if(!(e instanceof errors.PermissionDenied)||e.message!=='read links/source')throw e;} 1255console.log('follow privacy'); 1256"# ). unwrap (); 1257let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 1258. arg ( "run" ) 1259. current_dir ( dir. path ()) 1260. args ([ 1261"-0" , 1262"--allow-read=links/" , 1263"--allow-write=links/" , 1264"main.js" , 1265]) 1266. stdin ( Stdio :: null ()) 1267. stdout ( Stdio :: piped ()) 1268. stderr ( Stdio :: piped ()) 1269. spawn () 1270. unwrap (); 1271let output = common:: wait ( child); 1272assert! ( 1273 output. status . success (), 1274"{}" , 1275String :: from_utf8_lossy ( & output. stderr ) 1276); 1277assert_eq! ( output. stdout , b"follow privacy\n" ); 1278assert! ( fs:: symlink_metadata ( dir. path (). join ( "links/followed" )). is_err ()); 1279} 1280 1281# [ cfg ( unix )] 1282# [ test ] 1283fn same_inode_link_copies_fail_without_removing_the_source_or_alias () { 1284let dir = tempfile:: tempdir (). unwrap (); 1285 fs:: write ( dir. path (). join ( "data" ), b"data" ). unwrap (); 1286 std:: os:: unix:: fs:: symlink ( "data" , dir. path (). join ( "link" )). unwrap (); 1287 fs:: hard_link ( dir. path (). join ( "link" ), dir. path (). join ( "alias" )). unwrap (); 1288 fs:: write ( dir. path (). join ( "main.js" ), r#" 1289import {copy} from 'avril:fs'; 1290import {errors,readLink} from 'avril:std'; 1291for(const name of ['link','alias']){ 1292try{await copy('link',name);throw Error('Copied same link');}catch(e){if(!(e instanceof errors.InvalidInput))throw e;} 1293if(await readLink(name)!=='data')throw Error('Removed source link'); 1294} 1295console.log('link inode identity'); 1296"# ). unwrap (); 1297let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 1298. arg ( "run" ) 1299. current_dir ( dir. path ()) 1300. arg ( "main.js" ) 1301. stdin ( Stdio :: null ()) 1302. stdout ( Stdio :: piped ()) 1303. stderr ( Stdio :: piped ()) 1304. spawn () 1305. unwrap (); 1306let output = common:: wait ( child); 1307assert! ( 1308 output. status . success (), 1309"{}" , 1310String :: from_utf8_lossy ( & output. stderr ) 1311); 1312assert_eq! ( output. stdout , b"link inode identity\n" ); 1313} 1314 1315# [ cfg ( unix )] 1316# [ test ] 1317fn dereference_options_validate_and_link_destination_replacement_never_follows_old_targets () { 1318let ( dir, output) =run ( 1319r#" 1320import {copy,readTextFile,writeTextFile} from 'avril:fs'; 1321import {symlink,readLink,mkdir,errors} from 'avril:std'; 1322await writeTextFile('oldTarget','old');await symlink('oldTarget','destination'); 1323await symlink('data','source');await copy('source','destination'); 1324if(await readLink('destination')!=='data'||await readTextFile('oldTarget')!=='old')throw Error('Followed old destination link'); 1325await mkdir('folder'); 1326try{await copy('source','folder');throw Error('Removed directory');}catch(e){if(!(e instanceof errors.IsADirectory))throw e;} 1327try{await copy('source','invalid',{dereference:'yes'});throw Error('Accepted invalid dereference');}catch(e){if(!(e instanceof TypeError))throw e;} 1328console.log('link overwrite'); 1329"# , 1330& [], 1331); 1332assert_eq! ( output. stdout , b"link overwrite\n" ); 1333assert_eq! ( fs:: read ( dir. path (). join ( "oldTarget" )). unwrap (), b"old" ); 1334} 1335 1336# [ cfg ( unix )] 1337# [ test ] 1338fn following_ungranted_dangling_links_masks_existence_for_all_filesystem_workflows () { 1339let dir = tempfile:: tempdir (). unwrap (); 1340 fs:: create_dir ( dir. path (). join ( "out" )). unwrap (); 1341 fs:: create_dir ( dir. path (). join ( "secret" )). unwrap (); 1342 fs:: write ( dir. path (). join ( "secret/exists" ), b"private" ). unwrap (); 1343 std:: os:: unix:: fs:: symlink ( "../secret/exists" , dir. path (). join ( "out/a" )). unwrap (); 1344 std:: os:: unix:: fs:: symlink ( "../secret/missing" , dir. path (). join ( "out/b" )). unwrap (); 1345 std:: os:: unix:: fs:: symlink ( "b" , dir. path (). join ( "out/chain" )). unwrap (); 1346 fs:: write ( dir. path (). join ( "main.js" ), r#" 1347import {stat,lstat,open,create,realPath,readLink,readDir,mkdir,errors} from 'avril:std'; 1348import {exists,copy} from 'avril:fs'; 1349for(const name of ['out/a','out/b','out/chain','out/a/x','out/b/x','out/chain/x']){ 1350for(const call of [()=>stat(name),()=>open(name),()=>realPath(name),()=>exists(name),()=>readDir(name).next(),()=>copy(name,'out/copied',{dereference:true})]){ 1351try{await call();throw Error('Target existence disclosed '+name+' '+call);}catch(e){if(!(e instanceof errors.PermissionDenied)||e.message!=='read '+name)throw e;} 1352} 1353try{await create(name);throw Error('Write reached ungranted target');}catch(e){if(!(e instanceof errors.PermissionDenied)||e.message!=='read '+name)throw e;} 1354try{await mkdir(name);throw Error('Created directory over link');}catch(e){ 1355if(name.endsWith('/x')){if(!(e instanceof errors.PermissionDenied)||e.message!=='write '+name)throw e;} 1356else if(!(e instanceof errors.AlreadyExists))throw e; 1357} 1358} 1359if(await readLink('out/b')!=='../secret/missing'||!(await lstat('out/b')).isSymlink)throw Error('Entry permission changed'); 1360console.log('dangling target privacy'); 1361"# ). unwrap (); 1362let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 1363. arg ( "run" ) 1364. current_dir ( dir. path ()) 1365. args ([ "-0" , "--allow-read=out/" , "--allow-write=out/" , "main.js" ]) 1366. stdin ( Stdio :: null ()) 1367. stdout ( Stdio :: piped ()) 1368. stderr ( Stdio :: piped ()) 1369. spawn () 1370. unwrap (); 1371let output = common:: wait ( child); 1372assert! ( 1373 output. status . success (), 1374"{}" , 1375String :: from_utf8_lossy ( & output. stderr ) 1376); 1377assert_eq! ( output. stdout , b"dangling target privacy\n" ); 1378assert! ( !dir. path (). join ( "secret/missing" ). exists ()); 1379assert! ( fs:: symlink_metadata ( dir. path (). join ( "out/copied" )). is_err ()); 1380} 1381 1382# [ cfg ( unix )] 1383# [ test ] 1384fn authorized_dangling_targets_are_not_found_and_can_be_created_through_links () { 1385let dir = tempfile:: tempdir (). unwrap (); 1386 fs:: create_dir ( dir. path (). join ( "out" )). unwrap (); 1387 fs:: write ( dir. path (). join ( "out/data" ), b"copied" ). unwrap (); 1388 std:: os:: unix:: fs:: symlink ( "missing" , dir. path (). join ( "out/link" )). unwrap (); 1389 std:: os:: unix:: fs:: symlink ( "link" , dir. path (). join ( "out/chain" )). unwrap (); 1390 fs:: write ( dir. path (). join ( "main.js" ), r#" 1391import {stat,lstat,create,realPath,symlink,readLink,errors} from 'avril:std'; 1392import {exists,copy,mkdirs,readTextFile} from 'avril:fs'; 1393for(const name of ['out/link','out/chain','out/link/child']){ 1394if(await exists(name))throw Error('Dangling exists'); 1395for(const call of [()=>stat(name),()=>realPath(name)]){ 1396try{await call();throw Error('Resolved missing referent');}catch(e){if(!(e instanceof errors.NotFound))throw e;} 1397} 1398} 1399await symlink('chain','out/newAlias'); 1400await symlink('future-dir','out/dir-link');await mkdirs('out/dir-link'); 1401if(!(await stat('out/dir-link')).isDirectory||!(await lstat('out/dir-link')).isSymlink)throw Error('Directory ensure did not follow authorized target'); 1402await copy('out/data','out/link'); 1403if(!(await lstat('out/link')).isSymlink||!(await stat('out/link')).isFile)throw Error('File copy did not follow authorized dangling destination'); 1404const file=await create('out/link');await file.write(new TextEncoder().encode('created'));file.close(); 1405if(await readTextFile('out/chain')!=='created'||!(await stat('out/newAlias')).isFile||!(await lstat('out/link')).isSymlink||await readLink('out/link')!=='missing')throw Error('Link creation changed entry'); 1406console.log('authorized dangling targets'); 1407"# ). unwrap (); 1408let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 1409. arg ( "run" ) 1410. current_dir ( dir. path ()) 1411. args ([ "-0" , "--allow-read=out/" , "--allow-write=out/" , "main.js" ]) 1412. stdin ( Stdio :: null ()) 1413. stdout ( Stdio :: piped ()) 1414. stderr ( Stdio :: piped ()) 1415. spawn () 1416. unwrap (); 1417let output = common:: wait ( child); 1418assert! ( 1419 output. status . success (), 1420"{}" , 1421String :: from_utf8_lossy ( & output. stderr ) 1422); 1423assert_eq! ( output. stdout , b"authorized dangling targets\n" ); 1424assert_eq! ( 1425 fs:: read ( dir. path (). join ( "out/missing" )). unwrap (), 1426b"created" 1427); 1428} 1429 1430# [ cfg ( unix )] 1431# [ test ] 1432fn dangling_relative_targets_use_physical_alias_parents_not_lexical_dotdot () { 1433let dir = tempfile:: tempdir (). unwrap (); 1434let other = tempfile:: tempdir (). unwrap (); 1435 fs:: create_dir ( dir. path (). join ( "out" )). unwrap (); 1436 fs:: create_dir ( other. path (). join ( "inside" )). unwrap (); 1437 fs:: create_dir ( other. path (). join ( "secret" )). unwrap (); 1438 std:: os:: unix:: fs:: symlink ( other. path (). join ( "inside" ), dir. path (). join ( "out/parent" )). unwrap (); 1439 std:: os:: unix:: fs:: symlink ( "../secret/missing" , other. path (). join ( "inside/link" )). unwrap (); 1440 fs:: write ( dir. path (). join ( "main.js" ), r#" 1441import {stat,create,errors} from 'avril:std'; 1442for(const [call,label] of [[()=>stat('out/parent/link'),'read out/parent/link'],[()=>create('out/parent/link'),'read out/parent/link']]){ 1443try{await call();throw Error('Lexical alias parent used');}catch(e){if(!(e instanceof errors.PermissionDenied)||e.message!==label)throw e;} 1444} 1445console.log('physical dangling parents'); 1446"# ). unwrap (); 1447let read =format! ( "--allow-read={}/" , other. path (). join ( "inside" ). display ()); 1448let write =format! ( "--allow-write={}/" , other. path (). join ( "inside" ). display ()); 1449let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 1450. arg ( "run" ) 1451. current_dir ( dir. path ()) 1452. args ([ 1453"-0" , 1454"--allow-read=out/" , 1455"--allow-write=out/" , 1456& read, 1457& write, 1458"main.js" , 1459]) 1460. stdin ( Stdio :: null ()) 1461. stdout ( Stdio :: piped ()) 1462. stderr ( Stdio :: piped ()) 1463. spawn () 1464. unwrap (); 1465let output = common:: wait ( child); 1466assert! ( 1467 output. status . success (), 1468"{}" , 1469String :: from_utf8_lossy ( & output. stderr ) 1470); 1471assert_eq! ( output. stdout , b"physical dangling parents\n" ); 1472assert! ( !other. path (). join ( "secret/missing" ). exists ()); 1473} 1474 1475# [ cfg ( unix )] 1476# [ test ] 1477fn exclusive_creation_rejects_existing_symlink_entries_even_with_missing_referents () { 1478let ( dir, output) =run ( 1479r#" 1480import {symlink,open,readLink,errors,pledge} from 'avril:std'; 1481import {writeFile,exists} from 'avril:fs'; 1482await symlink('missing','dangling');await symlink('data','existing'); 1483pledge({read:{allow:['./']},write:{allow:['./']}}); 1484for(const name of ['dangling','existing']){ 1485for(const call of [()=>open(name,{write:true,createNew:true}),()=>writeFile(name,new Uint8Array([1]),{createNew:true})]){ 1486try{await call();throw Error('Exclusive creation followed link');}catch(e){if(!(e instanceof errors.AlreadyExists))throw e;} 1487} 1488} 1489if(await exists('missing')||await readLink('dangling')!=='missing')throw Error('Exclusive creation mutated dangling target'); 1490console.log('exclusive link entries'); 1491"# , 1492& [], 1493); 1494assert_eq! ( output. stdout , b"exclusive link entries\n" ); 1495assert! ( !dir. path (). join ( "missing" ). exists ()); 1496assert_eq! ( fs:: read ( dir. path (). join ( "data" )). unwrap (), b"abcdef" ); 1497} 1498 1499# [ cfg ( unix )] 1500# [ test ] 1501fn posix_path_ops_set_modes_times_links_and_sizes () { 1502let ( dir, output) =run ( 1503r#" 1504import {chmod,chown,lchown,utime,lutime,link,truncate,stat,lstat,symlink,mkdir,errors} from 'avril:std'; 1505import {readFile,readTextFile} from 'avril:fs'; 1506await chmod('data', 0o600); 1507if (((await stat('data')).mode & 0o7777) !== 0o600) throw Error('Chmod mode'); 1508await truncate('data', 3); 1509if ((await stat('data')).size !== 3 || await readTextFile('data') !== 'abc') throw Error('Truncate shrink'); 1510await truncate('data', 6); 1511const grown = await readFile('data'); 1512if (grown.length !== 6 || grown[0] !== 97 || grown[3] !== 0) throw Error('Truncate grow'); 1513await symlink('data', 'alias'); 1514await utime('alias', new Date(1000000000000), 1000000000); 1515const followed = await stat('alias'); 1516if (followed.mtime.getTime() !== 1000000000000 || followed.atime.getTime() !== 1000000000000) throw Error('Utime follows links'); 1517await lutime('alias', 2000000000, 2000000000); 1518if ((await lstat('alias')).mtime.getTime() !== 2000000000000 || (await stat('data')).mtime.getTime() !== 1000000000000) throw Error('Lutime keeps link times'); 1519const { gid } = await stat('data'); 1520await chown('data', null, gid); 1521await chown('alias', null, gid); 1522await lchown('alias', null, gid); 1523await symlink('missing', 'dangling'); 1524await lchown('dangling', null, gid); 1525await lutime('dangling', 2000000000, 2000000000); 1526await mkdir('sub'); 1527await symlink('sub', 'dirlink'); 1528await lutime('dirlink', 2000000000, 2000000000); 1529await lutime('dirlink/', 1000000000, 1000000000); 1530if ((await lstat('dirlink')).mtime.getTime() !== 2000000000000 || (await stat('sub')).mtime.getTime() !== 1000000000000) throw Error('Lutime slash did not follow'); 1531await symlink('loop', 'loop'); 1532try { await lchown('loop/', null, gid); throw Error('Lchown ignored trailing slash'); } catch (e) { if (!(e instanceof errors.IoError)) throw e; } 1533await lchown('loop', null, gid); 1534for (const call of [()=>chown('dangling', null, gid), ()=>utime('dangling', 0, 0), ()=>chmod('dangling', 0o644)]) { 1535try { await call(); throw Error('Followed dangling link'); } catch (e) { if (!(e instanceof errors.NotFound)) throw e; } 1536} 1537await link('data', 'hard'); 1538if (await readTextFile('hard') !== await readTextFile('data')) throw Error('Hard link contents'); 1539try { await link('alias', 'linkedAlias'); throw Error('Hard-linked a symlink'); } catch (e) { if (!(e instanceof errors.InvalidInput)) throw e; } 1540for (const [call, C] of [ 1541[()=>chmod('', 0o644), errors.NotFound], 1542[()=>chmod('data', 0o10000), RangeError], 1543[()=>chmod('data', -1), RangeError], 1544[()=>chmod('data', 0o644 + 0.5), RangeError], 1545[()=>truncate('data', -1), RangeError], 1546[()=>truncate('data', 2 ** 53), RangeError], 1547[()=>truncate('data', 1.5), RangeError], 1548[()=>truncate('missing', 0), errors.NotFound], 1549[()=>link('data', 'hard'), errors.AlreadyExists], 1550[()=>link('missing', 'new'), errors.NotFound], 1551[()=>chown('data', null, null), errors.InvalidInput], 1552[()=>chown('data', 1.5, null), RangeError], 1553[()=>lchown('data', 2 ** 32, null), RangeError], 1554[()=>utime('data', NaN, 0), TypeError], 1555[()=>utime('data', 0, Infinity), TypeError], 1556[()=>lutime('data', 'now', 0), TypeError], 1557[()=>truncate(), TypeError], 1558[()=>link('data'), TypeError], 1559[()=>chown('data', null), TypeError], 1560]) { 1561try { await call(); throw Error('Accepted invalid operation'); } catch (e) { if (!(e instanceof C)) throw e; } 1562} 1563console.log('posix paths'); 1564"# , 1565& [], 1566); 1567assert_eq! ( output. stdout , b"posix paths\n" ); 1568use std:: os:: unix:: fs:: MetadataExt ; 1569assert_eq! ( 1570 fs:: metadata ( dir. path (). join ( "hard" )). unwrap (). ino (), 1571 fs:: metadata ( dir. path (). join ( "data" )). unwrap (). ino () 1572); 1573assert! ( !dir. path (). join ( "linkedAlias" ). exists ()); 1574} 1575 1576# [ cfg ( unix )] 1577# [ test ] 1578fn posix_path_ops_need_write_access_and_hide_denied_targets () { 1579let ( dir, output) =run ( 1580r#" 1581import {chmod,chown,lchown,utime,lutime,link,truncate,errors} from 'avril:std'; 1582for (const name of ['other', 'data/x']) { 1583for (const call of [()=>chmod(name, 0o644), ()=>chown(name, null, 0), ()=>lchown(name, null, 0), ()=>utime(name, 0, 0), ()=>lutime(name, 0, 0), ()=>link('data', name), ()=>truncate(name, 0)]) { 1584try { await call(); throw Error('Granted write ' + name); } catch (e) { if (!(e instanceof errors.PermissionDenied) || e.message !== 'write ' + name) throw e; } 1585} 1586try { await link(name, 'elsewhere'); throw Error('Granted read ' + name); } catch (e) { if (!(e instanceof errors.PermissionDenied) || e.message !== 'read ' + name) throw e; } 1587} 1588console.log('posix denials'); 1589"# , 1590& [ "-0" , "--allow-read=data" , "--allow-write=data" ], 1591); 1592assert_eq! ( output. stdout , b"posix denials\n" ); 1593assert_eq! ( fs:: read ( dir. path (). join ( "data" )). unwrap (), b"abcdef" ); 1594for namein [ "other" , "elsewhere" ] { 1595assert! ( !dir. path (). join ( name). exists ()); 1596} 1597let ( denied, output) =run ( 1598r#" 1599import {chmod,chown,lchown,utime,lutime,link,truncate,errors} from 'avril:std'; 1600for (const call of [()=>chmod('data', 0o644), ()=>chown('data', null, 0), ()=>lchown('data', null, 0), ()=>utime('data', 0, 0), ()=>lutime('data', 0, 0), ()=>truncate('data', 6)]) { 1601try { await call(); throw Error('Mutated without write'); } catch (e) { if (!(e instanceof errors.PermissionDenied) || e.message !== 'write data') throw e; } 1602} 1603try { await link('data', 'copy'); throw Error('Linked without write'); } catch (e) { if (!(e instanceof errors.PermissionDenied) || e.message !== 'write copy') throw e; } 1604console.log('posix write gates'); 1605"# , 1606& [ "-0" , "--allow-read=data" ], 1607); 1608assert_eq! ( output. stdout , b"posix write gates\n" ); 1609assert_eq! ( fs:: read ( denied. path (). join ( "data" )). unwrap (), b"abcdef" ); 1610assert! ( !denied. path (). join ( "copy" ). exists ()); 1611} 1612 1613# [ cfg ( unix )] 1614# [ test ] 1615fn posix_handle_ops_set_mode_owner_times_and_serialize_locks () { 1616let ( dir, output) =run ( 1617r#" 1618import {open,pledge,errors} from 'avril:std'; 1619const file = await open('data', { read: true, write: true }); 1620await file.fchmod(0o600); 1621if (((await file.stat()).mode & 0o7777) !== 0o600) throw Error('Fchmod mode'); 1622await file.futime(new Date(1500000000000), 1500000000); 1623const stamped = await file.stat(); 1624if (stamped.mtime.getTime() !== 1500000000000 || stamped.atime.getTime() !== 1500000000000) throw Error('Futime times'); 1625await file.fchown(null, stamped.gid); 1626try { await file.fchown(null, null); throw Error('Chowned nothing'); } catch (e) { if (!(e instanceof errors.InvalidInput)) throw e; } 1627await file.flock(true); 1628await file.funlock(); 1629await file.flock(false); 1630await file.funlock(); 1631const other = await open('data', { read: true, write: true }); 1632await file.flock(true); 1633let released = false; 1634const gated = other.flock(true).then(() => { released = true; }); 1635await new Promise((resolve) => setTimeout(resolve, 300)); 1636if (released) throw Error('Exclusive lock did not block'); 1637await file.funlock(); 1638let watchdog; await Promise.race([gated, new Promise((_, reject) => { watchdog = setTimeout(() => reject(Error('Unlock did not release waiter')), 2000); })]); clearTimeout(watchdog); 1639await other.funlock(); 1640const reader = await open('data'); 1641for (const call of [()=>reader.fchmod(0o644), ()=>reader.fchown(null, 0), ()=>reader.futime(0, 0), ()=>reader.flock(true)]) { 1642try { await call(); throw Error('Wrote through reader'); } catch (e) { if (!(e instanceof errors.BadResource)) throw e; } 1643} 1644await reader.flock(false); 1645await reader.funlock(); 1646reader.close(); 1647file.close(); 1648other.close(); 1649for (const call of [()=>file.fchmod(0o644), ()=>file.flock(false), ()=>file.funlock()]) { 1650try { await call(); throw Error('Used closed handle'); } catch (e) { if (!(e instanceof errors.BadResource)) throw e; } 1651} 1652const live = await open('data', { read: true, write: true }); 1653pledge({ write: false }); 1654for (const call of [()=>live.fchmod(0o600), ()=>live.fchown(null, 0), ()=>live.futime(0, 0), ()=>live.flock(true)]) { 1655try { await call(); throw Error('Retained write'); } catch (e) { if (!(e instanceof errors.PermissionDenied)) throw e; } 1656} 1657await live.funlock(); 1658live.close(); 1659console.log('posix handles'); 1660"# , 1661& [], 1662); 1663assert_eq! ( output. stdout , b"posix handles\n" ); 1664use std:: os:: unix:: fs:: PermissionsExt ; 1665assert_eq! ( 1666 fs:: metadata ( dir. path (). join ( "data" )) 1667. unwrap () 1668. permissions () 1669. mode () 1670& 0o777 , 16710o600 1672); 1673} 1674 1675# [ cfg ( unix )] 1676# [ test ] 1677fn posix_link_never_launders_symlink_sources () { 1678let ( dir, output) =run ( 1679r#" 1680import {open,symlink,link,pledge,errors} from 'avril:std'; 1681const secret = await open('secret', { write: true, create: true }); 1682await secret.write(new Uint8Array([42])); 1683secret.close(); 1684await symlink('secret', 'plug'); 1685pledge({ read: { deny: ['secret'] } }); 1686try { 1687await link('plug', 'copy'); 1688throw Error('Laundered unreadable target'); 1689} catch (e) { 1690if (!(e instanceof errors.InvalidInput) || e.message !== 'Source is a symbolic link') throw e; 1691} 1692console.log('posix link refusal'); 1693"# , 1694& [], 1695); 1696assert_eq! ( output. stdout , b"posix link refusal\n" ); 1697assert_eq! ( fs:: read ( dir. path (). join ( "secret" )). unwrap (), [ 42 ]); 1698assert! ( !dir. path (). join ( "copy" ). exists ()); 1699} 1700 1701# [ cfg ( unix )] 1702# [ test ] 1703fn posix_saturated_ids_reject_before_lookup () { 1704let ( _dir, output) =run ( 1705r#" 1706import {open,chown,lchown,errors} from 'avril:std'; 1707const file = await open('data', { read: true, write: true }); 1708for (const call of [ 1709() => chown('data', 4294967295, null), 1710() => chown('data', null, 4294967295), 1711() => chown('missing', 4294967295, 4294967295), 1712() => lchown('data', 4294967295, null), 1713() => file.fchown(4294967295, null), 1714() => file.fchown(null, 4294967295), 1715]) { 1716try { await call(); throw Error('Accepted saturated id'); } catch (e) { if (!(e instanceof RangeError)) throw e; } 1717} 1718file.close(); 1719console.log('posix id bounds'); 1720"# , 1721& [], 1722); 1723assert_eq! ( output. stdout , b"posix id bounds\n" ); 1724} 1725 1726# [ cfg ( unix )] 1727# [ test ] 1728fn posix_denied_mutations_leave_modes_and_times_untouched () { 1729let ( dir, output) =run ( 1730r#" 1731import {chmod,chown,lchown,utime,lutime,link,truncate,stat,pledge,errors} from 'avril:std'; 1732await chmod('data', 0o640); 1733await utime('data', 1000000000, 1000000000); 1734pledge({ write: false }); 1735for (const call of [() => chmod('data', 0o600), () => chown('data', null, 0), () => lchown('data', null, 0), () => utime('data', 0, 0), () => lutime('data', 0, 0), () => link('data', 'copy'), () => truncate('data', 0)]) { 1736try { await call(); throw Error('Mutated after pledge'); } catch (e) { if (!(e instanceof errors.PermissionDenied)) throw e; } 1737} 1738const after = await stat('data'); 1739if ((after.mode & 0o7777) !== 0o640 || after.mtime.getTime() !== 1000000000000 || after.atime.getTime() !== 1000000000000 || after.size !== 6) throw Error('Denied mutation leaked'); 1740console.log('posix denials keep state'); 1741"# , 1742& [], 1743); 1744assert_eq! ( output. stdout , b"posix denials keep state\n" ); 1745assert_eq! ( fs:: read ( dir. path (). join ( "data" )). unwrap (), b"abcdef" ); 1746assert! ( !dir. path (). join ( "copy" ). exists ()); 1747} 1748 1749# [ cfg ( unix )] 1750# [ test ] 1751fn posix_truncate_follows_links_rejects_dirs_and_dangling () { 1752let ( dir, output) =run ( 1753r#" 1754import {symlink,truncate,stat,mkdir,errors} from 'avril:std'; 1755await symlink('data', 'alias'); 1756await truncate('alias', 2); 1757if ((await stat('data')).size !== 2) throw Error('Truncate did not follow link'); 1758await symlink('missing', 'dangling'); 1759try { await truncate('dangling', 0); throw Error('Truncated dangling link'); } catch (e) { if (!(e instanceof errors.NotFound)) throw e; } 1760await mkdir('sub'); 1761try { await truncate('sub', 0); throw Error('Truncated directory'); } catch (e) { if (!(e instanceof errors.IsADirectory)) throw e; } 1762await symlink('loop', 'loop'); 1763try { await truncate('loop/x', 0); throw Error('Truncated looped path'); } catch (e) { 1764if (!(e instanceof errors.NotFound) || e.message !== 'Symbolic link changed or has a missing target') throw e; 1765} 1766console.log('posix truncate targets'); 1767"# , 1768& [], 1769); 1770assert_eq! ( output. stdout , b"posix truncate targets\n" ); 1771assert_eq! ( fs:: read ( dir. path (). join ( "data" )). unwrap (), b"ab" ); 1772} 1773 1774# [ cfg ( unix )] 1775# [ test ] 1776fn posix_timestamps_span_seconds_and_reject_garbage () { 1777let ( _dir, output) =run ( 1778r#" 1779import {open,utime,stat,errors} from 'avril:std'; 1780await utime('data', -0.5, 1000000000.5); 1781const split = await stat('data'); 1782if (split.atime.getTime() !== -500 || split.mtime.getTime() !== 1000000000500) throw Error('Fractional seconds'); 1783await utime('data', 8640000000000, -62135596800); 1784const bounds = await stat('data'); 1785if (bounds.atime.getTime() !== 8640000000000000 || bounds.mtime.getTime() !== -62135596800000) throw Error('Second bounds'); 1786await utime('data', new Date(1500000000123), 0); 1787if ((await stat('data')).atime.getTime() !== 1500000000123) throw Error('Date split'); 1788for (const [call, C] of [ 1789[() => utime('data', 8640000000001, 0), RangeError], 1790[() => utime('data', 8640000000000.6, 0), RangeError], 1791[() => utime('data', -8640000000001, 0), RangeError], 1792[() => utime('data', Infinity, 0), TypeError], 1793[() => utime('data', 0, new Date(NaN)), TypeError], 1794]) { 1795try { await call(); throw Error('Accepted invalid timestamp'); } catch (e) { if (!(e instanceof C)) throw e; } 1796} 1797const file = await open('data', { read: true, write: true }); 1798await file.futime(0.25, -1.75); 1799const stamped = await file.stat(); 1800if (stamped.atime.getTime() !== 250 || stamped.mtime.getTime() !== -1750) throw Error('Handle fractions'); 1801file.close(); 1802console.log('posix timestamps'); 1803"# , 1804& [], 1805); 1806assert_eq! ( output. stdout , b"posix timestamps\n" ); 1807} 1808 1809# [ cfg ( unix )] 1810# [ test ] 1811fn posix_flock_releases_waiters_and_gates_by_mode () { 1812let ( _dir, output) =run ( 1813r#" 1814import {open,errors} from 'avril:std'; 1815const file = await open('data', { read: true, write: true }); 1816const other = await open('data', { read: true, write: true }); 1817await file.flock(true); 1818let released = false; 1819const gated = other.flock(false).then(() => { released = true; }); 1820await new Promise((resolve) => setTimeout(resolve, 300)); 1821if (released) throw Error('Shared lock slipped past exclusive'); 1822await file.funlock(); 1823let watchdog; 1824await Promise.race([gated, new Promise((_, reject) => { watchdog = setTimeout(() => reject(Error('Unlock did not release waiter')), 2000); })]); 1825clearTimeout(watchdog); 1826await other.funlock(); 1827await file.flock(true); 1828const pending = other.flock(true); 1829if ((await other.stat()).size !== 6) throw Error('Queue stalled behind lock'); 1830other.close(); 1831try { await pending; throw Error('Locked past close'); } catch (e) { if (!(e instanceof errors.BadResource)) throw e; } 1832await file.funlock(); 1833file.close(); 1834const reader = await open('data'); 1835const writer = await open('data', { write: true }); 1836for (const call of [() => writer.flock(false), () => reader.flock(true)]) { 1837try { await call(); throw Error('Locked past handle mode'); } catch (e) { if (!(e instanceof errors.BadResource)) throw e; } 1838} 1839await reader.flock(false); 1840await reader.funlock(); 1841await writer.flock(true); 1842await writer.funlock(); 1843reader.close(); 1844writer.close(); 1845console.log('posix lock matrix'); 1846"# , 1847& [], 1848); 1849assert_eq! ( output. stdout , b"posix lock matrix\n" ); 1850} 1851 1852# [ cfg ( unix )] 1853# [ test ] 1854fn posix_chmod_ignores_access_bits_and_follows_links () { 1855let ( dir, output) =run ( 1856r#" 1857import {chmod,stat,symlink,mkdir,errors} from 'avril:std'; 1858await chmod('data', 0o200); 1859await chmod('data', 0o644); 1860if (((await stat('data')).mode & 0o7777) !== 0o644) throw Error('Chmod write-only file'); 1861await chmod('data', 0); 1862await chmod('data', 0o644); 1863if (((await stat('data')).mode & 0o7777) !== 0o644) throw Error('Chmod unreadable file'); 1864await mkdir('lockeddir'); 1865await chmod('lockeddir', 0); 1866await chmod('lockeddir', 0o755); 1867if (!((await stat('lockeddir')).isDirectory)) throw Error('Chmod locked directory'); 1868await symlink('data', 'alias'); 1869await chmod('alias', 0o600); 1870if (((await stat('data')).mode & 0o7777) !== 0o600) throw Error('Chmod did not follow link'); 1871await symlink('missing', 'dangling'); 1872try { await chmod('dangling', 0o644); throw Error('Chmod dangling link'); } catch (e) { 1873if (!(e instanceof errors.NotFound)) throw e; 1874} 1875await symlink('loop', 'loop'); 1876try { await chmod('loop/x', 0o644); throw Error('Chmod looped path'); } catch (e) { 1877if (!(e instanceof errors.NotFound) || e.message !== 'Symbolic link changed or has a missing target') throw e; 1878} 1879console.log('posix chmod reach'); 1880"# , 1881& [], 1882); 1883assert_eq! ( output. stdout , b"posix chmod reach\n" ); 1884use std:: os:: unix:: fs:: PermissionsExt ; 1885assert_eq! ( 1886 fs:: metadata ( dir. path (). join ( "data" )) 1887. unwrap () 1888. permissions () 1889. mode () 1890& 0o7777 , 18910o600 1892); 1893} 1894 1895# [ cfg ( unix )] 1896# [ test ] 1897fn posix_link_needs_write_access_to_its_source () { 1898let dir = tempfile:: tempdir (). unwrap (); 1899 fs:: write ( dir. path (). join ( "ro" ), b"read-only" ). unwrap (); 1900 fs:: create_dir ( dir. path (). join ( "out" )). unwrap (); 1901 fs:: write ( 1902 dir. path (). join ( "main.js" ), 1903r#" 1904import {link,errors} from 'avril:std'; 1905try { await link('ro', 'out/alias'); throw Error('Linked without source write'); } catch (e) { 1906if (!(e instanceof errors.PermissionDenied) || e.message !== 'write ro') throw e; 1907} 1908console.log('posix link source gate'); 1909"# , 1910) 1911. unwrap (); 1912let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 1913. arg ( "run" ) 1914. current_dir ( dir. path ()) 1915. args ([ "-0" , "--allow-read=." , "--allow-write=out/" , "main.js" ]) 1916. stdin ( Stdio :: null ()) 1917. stdout ( Stdio :: piped ()) 1918. stderr ( Stdio :: piped ()) 1919. spawn () 1920. unwrap (); 1921let output = common:: wait ( child); 1922assert! ( 1923 output. status . success (), 1924"{}" , 1925String :: from_utf8_lossy ( & output. stderr ) 1926); 1927assert_eq! ( output. stdout , b"posix link source gate\n" ); 1928assert_eq! ( fs:: read ( dir. path (). join ( "ro" )). unwrap (), b"read-only" ); 1929assert! ( !dir. path (). join ( "out/alias" ). exists ()); 1930} 1931 1932# [ cfg ( unix )] 1933# [ test ] 1934fn posix_denied_path_ops_disclose_neither_targets_nor_resolution_errors () { 1935let dir = tempfile:: tempdir (). unwrap (); 1936 fs:: create_dir ( dir. path (). join ( "secret" )). unwrap (); 1937 fs:: write ( dir. path (). join ( "secret/file" ), b"private" ). unwrap (); 1938 std:: os:: unix:: fs:: symlink ( "secret/file" , dir. path (). join ( "alias" )). unwrap (); 1939 std:: os:: unix:: fs:: symlink ( "loop" , dir. path (). join ( "loop" )). unwrap (); 1940 std:: os:: unix:: fs:: symlink ( "ungranted" , dir. path (). join ( "dangling" )). unwrap (); 1941 fs:: write ( 1942 dir. path (). join ( "main.js" ), 1943r#" 1944import {chmod,chown,lchown,utime,lutime,link,truncate,errors} from 'avril:std'; 1945for (const name of ['alias','secret/file/x','secret/missing','loop','missing/../secret/file','dangling']) { 1946for (const call of [()=>chmod(name, 0o644), ()=>chown(name, null, 0), ()=>lchown(name, null, 0), ()=>utime(name, 0, 0), ()=>lutime(name, 0, 0), ()=>truncate(name, 0)]) { 1947try { await call(); throw Error('Granted write ' + name); } catch (e) { if (!(e instanceof errors.PermissionDenied) || e.message !== 'write ' + name) throw e; } 1948} 1949try { await link(name, 'elsewhere'); throw Error('Granted read ' + name); } catch (e) { if (!(e instanceof errors.PermissionDenied) || e.message !== 'read ' + name) throw e; } 1950} 1951console.log('no posix oracle'); 1952"# , 1953) 1954. unwrap (); 1955let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 1956. arg ( "run" ) 1957. current_dir ( dir. path ()) 1958. args ([ "-0" , "main.js" ]) 1959. stdin ( Stdio :: null ()) 1960. stdout ( Stdio :: piped ()) 1961. stderr ( Stdio :: piped ()) 1962. spawn () 1963. unwrap (); 1964let output = common:: wait ( child); 1965assert! ( 1966 output. status . success (), 1967"{}" , 1968String :: from_utf8_lossy ( & output. stderr ) 1969); 1970assert_eq! ( output. stdout , b"no posix oracle\n" ); 1971assert_eq! ( 1972 fs:: read ( dir. path (). join ( "secret/file" )). unwrap (), 1973b"private" 1974); 1975assert! ( !dir. path (). join ( "elsewhere" ). exists ()); 1976} 1977 1978# [ cfg ( unix )] 1979# [ test ] 1980fn posix_huge_mtimes_read_as_invalid_dates_without_panicking () { 1981let dir = tempfile:: tempdir (). unwrap (); 1982 fs:: write ( dir. path (). join ( "huge" ), b"huge" ). unwrap (); 1983 fs:: File :: options () 1984. write ( true ) 1985. open ( dir. path (). join ( "huge" )) 1986. unwrap () 1987. set_modified ( std:: time:: UNIX_EPOCH + std:: time:: Duration :: new ( 20_000_000_000_000_000 , 0 )) 1988. unwrap (); 1989 fs:: write ( 1990 dir. path (). join ( "main.js" ), 1991r#" 1992import {stat,open} from 'avril:std'; 1993const stamped = await stat('huge'); 1994if (stamped.mtime.getTime() === stamped.mtime.getTime()) throw Error('Huge mtime looked valid'); 1995const file = await open('huge'); 1996await file.stat(); 1997file.close(); 1998console.log('huge mtime safe'); 1999"# , 2000) 2001. unwrap (); 2002let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 2003. arg ( "run" ) 2004. current_dir ( dir. path ()) 2005. arg ( "main.js" ) 2006. stdin ( Stdio :: null ()) 2007. stdout ( Stdio :: piped ()) 2008. stderr ( Stdio :: piped ()) 2009. spawn () 2010. unwrap (); 2011let output = common:: wait ( child); 2012assert! ( 2013 output. status . success (), 2014"{}" , 2015String :: from_utf8_lossy ( & output. stderr ) 2016); 2017assert_eq! ( output. stdout , b"huge mtime safe\n" ); 2018} 2019 2020# [ cfg ( unix )] 2021# [ test ] 2022fn posix_granted_dir_probes_disclose_no_escapes () { 2023let dir = tempfile:: tempdir (). unwrap (); 2024 fs:: write ( dir. path (). join ( "secret" ), b"private" ). unwrap (); 2025 fs:: create_dir ( dir. path (). join ( "out" )). unwrap (); 2026 std:: os:: unix:: fs:: symlink ( "../secret" , dir. path (). join ( "out/a" )). unwrap (); 2027let before = fs:: metadata ( dir. path (). join ( "secret" )). unwrap (); 2028 fs:: write ( 2029 dir. path (). join ( "main.js" ), 2030r#" 2031import {chmod,chown,lchown,utime,lutime,link,truncate,stat,errors} from 'avril:std'; 2032for (const name of ['out/a', 'out/../secret', 'out/a/x']) { 2033for (const call of [()=>chmod(name, 0o644), ()=>chown(name, null, 0), ()=>utime(name, 0, 0), ()=>truncate(name, 0)]) { 2034try { await call(); throw Error('Granted escape ' + name); } catch (e) { if (!(e instanceof errors.PermissionDenied) || e.message !== 'write ' + name) throw e; } 2035} 2036} 2037// Link-acting ops stay on the granted link itself. 2038const { gid } = await stat('out'); 2039await lchown('out/a', null, gid); 2040await lutime('out/a', 0, 0); 2041try { await link('out/../secret', 'elsewhere'); throw Error('Granted read out/../secret'); } catch (e) { if (!(e instanceof errors.PermissionDenied) || e.message !== 'read out/../secret') throw e; } 2042try { await link('out/a', 'elsewhere'); throw Error('Granted write elsewhere'); } catch (e) { if (!(e instanceof errors.PermissionDenied) || e.message !== 'write elsewhere') throw e; } 2043console.log('no granted-dir oracle'); 2044"# , 2045) 2046. unwrap (); 2047let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 2048. arg ( "run" ) 2049. current_dir ( dir. path ()) 2050. args ([ "-0" , "--allow-read=out" , "--allow-write=out" , "main.js" ]) 2051. stdin ( Stdio :: null ()) 2052. stdout ( Stdio :: piped ()) 2053. stderr ( Stdio :: piped ()) 2054. spawn () 2055. unwrap (); 2056let output = common:: wait ( child); 2057assert! ( 2058 output. status . success (), 2059"{}" , 2060String :: from_utf8_lossy ( & output. stderr ) 2061); 2062assert_eq! ( output. stdout , b"no granted-dir oracle\n" ); 2063assert_eq! ( fs:: read ( dir. path (). join ( "secret" )). unwrap (), b"private" ); 2064let after = fs:: metadata ( dir. path (). join ( "secret" )). unwrap (); 2065assert_eq! ( after. modified (). unwrap (), before. modified (). unwrap ()); 2066assert! ( !dir. path (). join ( "elsewhere" ). exists ()); 2067} 2068 2069# [ test ] 2070fn direct_writev_writes_ordered_vectors_and_skips_empty_views () { 2071let ( dir, output) =run ( 2072r#" 2073import {create,open,SeekMode,errors} from 'avril:std'; 2074const file=await create('out'); 2075const full=new Uint8Array([0,1,2,3,4,5,6,7,8,9]); 2076if(await file.writev([full.subarray(0,3),new Uint8Array(0),full.subarray(3,7),full.subarray(7)])!==10)throw Error('Count'); 2077if(await file.seek(0,SeekMode.Start)!==0)throw Error('Seek'); 2078if(await file.write(new Uint8Array([9]))!==1)throw Error('Write'); 2079if(await file.writev([new Uint8Array([8]),new Uint8Array([7,6])])!==3)throw Error('Vector'); 2080if(await file.writev([])!==0||await file.writev([new Uint8Array(0),new Uint8Array(0)])!==0)throw Error('Empty'); 2081if(await file.seek(0,SeekMode.Current)!==4)throw Error('Cursor'); 2082file.close(); 2083const shared=new Uint8Array(new SharedArrayBuffer(2));shared.set([11,12]); 2084const resizable=new ArrayBuffer(4,{maxByteLength:8}),tracked=new Uint8Array(resizable,1,2);tracked.set([13,14]); 2085const extra=await create('extra'); 2086if(await extra.writev([shared,tracked])!==4)throw Error('Shared'); 2087extra.close(); 2088const check=await open('out'),back=new Uint8Array(10); 2089let offset=0,n; 2090while(offset<10&&(n=await check.read(back.subarray(offset)))!==null)offset+=n; 2091if(offset!==10||back.join()!=='9,8,7,6,4,5,6,7,8,9')throw Error('Order '+back.join()); 2092check.close(); 2093const reread=await open('extra'),again=new Uint8Array(4); 2094offset=0; 2095while(offset<4&&(n=await reread.read(again.subarray(offset)))!==null)offset+=n; 2096if(offset!==4||again.join()!=='11,12,13,14')throw Error('Buffers '+again.join()); 2097reread.close(); 2098try{await file.writev([new Uint8Array([1])]);throw Error('Closed');}catch(e){if(!(e instanceof errors.BadResource))throw e;} 2099console.log('vectors'); 2100"# , 2101& [], 2102); 2103assert_eq! ( output. stdout , b"vectors\n" ); 2104assert_eq! ( 2105 fs:: read ( dir. path (). join ( "out" )). unwrap (), 2106[ 9 , 8 , 7 , 6 , 4 , 5 , 6 , 7 , 8 , 9 ] 2107); 2108assert_eq! ( 2109 fs:: read ( dir. path (). join ( "extra" )). unwrap (), 2110[ 11 , 12 , 13 , 14 ] 2111); 2112} 2113 2114# [ test ] 2115fn direct_writev_completes_large_vectors_and_snapshots_queued_bytes () { 2116let ( dir, output) =run ( 2117r#" 2118import {create,open} from 'avril:std'; 2119const big=await create('big'); 2120const segs=[new Uint8Array(0)]; 2121for(let i=0;i<20;i++){const view=new Uint8Array(5000);view.fill(i+1);segs.push(view,new Uint8Array(0));} 2122const writing=big.writev(segs); 2123for(const view of segs)view.fill(99); 2124segs[segs.length-2]=new Uint8Array([0]); 2125if(await writing!==100000)throw Error('Total'); 2126big.close(); 2127const check=await open('big'),back=new Uint8Array(100000); 2128let offset=0,n; 2129while(offset<100000&&(n=await check.read(back.subarray(offset)))!==null)offset+=n; 2130if(offset!==100000)throw Error('Length'); 2131for(let i=0;i<20;i++)for(let j=0;j<5000;j++)if(back[i*5000+j]!==i+1)throw Error('Order '+(i*5000+j)); 2132check.close(); 2133const many=await create('many'),bufs=[]; 2134for(let i=0;i<3000;i++)bufs.push(new Uint8Array([i&255])); 2135if(await many.writev(bufs)!==3000)throw Error('Small total'); 2136many.close(); 2137const reread=await open('many'),again=new Uint8Array(3000); 2138offset=0; 2139while(offset<3000&&(n=await reread.read(again.subarray(offset)))!==null)offset+=n; 2140if(offset!==3000)throw Error('Small length'); 2141for(let i=0;i<3000;i++)if(again[i]!==(i&255))throw Error('Small order '+i); 2142reread.close(); 2143const snap=await create('snap'); 2144const first=new Uint8Array(100000);first.fill(7); 2145const one=snap.write(first),parts=[new Uint8Array([1,2]),new Uint8Array([3])]; 2146const two=snap.writev(parts); 2147parts[0].fill(9);parts[1]=new Uint8Array([4]);parts.push(new Uint8Array([0])); 2148const n1=await one; 2149if(!(n1>0&&n1<=65536))throw Error('Queued count'); 2150if(await two!==3)throw Error('Vector total'); 2151snap.close(); 2152const tail=await open('snap'),end=new Uint8Array(n1+3); 2153offset=0; 2154while(offset<n1+3&&(n=await tail.read(end.subarray(offset)))!==null)offset+=n; 2155if(offset!==n1+3)throw Error('Queued length'); 2156if(end[n1]!==1||end[n1+1]!==2||end[n1+2]!==3)throw Error('Snapshot'); 2157tail.close(); 2158console.log('prefixes'); 2159"# , 2160& [], 2161); 2162assert_eq! ( output. stdout , b"prefixes\n" ); 2163assert_eq! ( fs:: read ( dir. path (). join ( "many" )). unwrap (). len (), 3000 ); 2164} 2165 2166# [ test ] 2167fn direct_writev_validates_vectors_before_writing_and_honors_close () { 2168let ( dir, output) =run ( 2169r#" 2170import {create,open,SeekMode,errors} from 'avril:std'; 2171const file=await create('valid'); 2172async function rejects(vector){ 2173try{await file.writev(vector);} 2174catch(e){if(e instanceof TypeError)return;throw e;} 2175throw Error('Accepted vector'); 2176} 2177for(const bad of ['bytes',42,null,undefined,{},new Uint8Array([1]),new Set([new Uint8Array([1])]),(function*(){yield new Uint8Array([1]);})()])await rejects(bad); 2178for(const bad of [[new Uint8Array([1]),'x'],[[1,2]],[new DataView(new ArrayBuffer(1))],[new Uint16Array([1])],[new Uint8Array([1]),undefined],[null]])await rejects(bad); 2179const detached=new Uint8Array([1,2]); 2180structuredClone(detached.buffer,{transfer:[detached.buffer]}); 2181await rejects([detached]); 2182const resizable=new ArrayBuffer(4,{maxByteLength:8}),outOfBounds=new Uint8Array(resizable,1,2); 2183resizable.resize(0); 2184await rejects([outOfBounds]); 2185await rejects([new Uint8Array([5,5]),'x']); 2186const wide=[];for(let i=0;i<20;i++)wide.push(new Uint8Array(5000));wide.push('x'); 2187await rejects(wide); 2188for(const size of [4294967296,-1,NaN,Infinity,1.5,'1']){ 2189try{await file.writev(new Proxy([],{get:(t,p)=>p==='length'?size:new Uint8Array(0)}));throw Error('Length');} 2190catch(e){if(!(e instanceof RangeError))throw e;} 2191} 2192const readOnly=await open('valid'); 2193try{await readOnly.writev([]);throw Error('Read-only');} 2194catch(e){if(!(e instanceof errors.BadResource))throw e;} 2195readOnly.close(); 2196if(await file.seek(0,SeekMode.Current)!==0)throw Error('Side effect'); 2197const check=await open('valid'); 2198if(await check.read(new Uint8Array(1))!==null)throw Error('Partial write'); 2199check.close(); 2200const ok=new Uint8Array([1,2]),closedVector=[new Uint8Array([1])]; 2201globalThis.Uint8Array=()=>{throw Error('Mutable intrinsic');}; 2202Array.prototype.push=()=>{throw Error('Mutable intrinsic');}; 2203if(await file.writev([ok])!==2)throw Error('Captured'); 2204file.close(); 2205for(const call of [()=>file.writev(closedVector),()=>file.writevSync(closedVector)]){ 2206try{await call();throw Error('Closed');}catch(e){if(!(e instanceof errors.BadResource))throw e;} 2207} 2208console.log('validated'); 2209"# , 2210& [], 2211); 2212assert_eq! ( output. stdout , b"validated\n" ); 2213assert_eq! ( fs:: read ( dir. path (). join ( "valid" )). unwrap (), [ 1 , 2 ]); 2214} 2215 2216# [ test ] 2217fn direct_writev_holds_one_queue_position_against_sync_and_async () { 2218let ( dir, output) =run ( 2219r#" 2220import {create,open,SeekMode} from 'avril:std'; 2221const file=await create('ord'); 2222const big=[];for(let i=0;i<20;i++){const view=new Uint8Array(5000);view.fill(i+1);big.push(view);} 2223const pending=file.writev(big); 2224if(file.seekSync(0,SeekMode.Start)!==0)throw Error('SeekSync waited'); 2225if(file.writevSync([new Uint8Array([201,202])])!==2)throw Error('Sync vector'); 2226if(await pending!==100000)throw Error('Total'); 2227file.close(); 2228const check=await open('ord'),back=new Uint8Array(100000); 2229let offset=0,n; 2230while(offset<100000&&(n=await check.read(back.subarray(offset)))!==null)offset+=n; 2231if(offset!==100000)throw Error('Length'); 2232if(back[0]!==201||back[1]!==202)throw Error('Sync order'); 2233for(let i=0;i<20;i++)for(let j=0;j<5000;j++){const k=i*5000+j;if(k>1&&back[k]!==i+1)throw Error('Bytes '+k);} 2234check.close(); 2235const second=await create('ord2'); 2236const first=second.writev(big); 2237if(await second.write(new Uint8Array([7,7,7]))!==3)throw Error('Write'); 2238if(await first!==100000)throw Error('First'); 2239second.close(); 2240const tail=await open('ord2'),end=new Uint8Array(100003); 2241offset=0; 2242while(offset<100003&&(n=await tail.read(end.subarray(offset)))!==null)offset+=n; 2243if(offset!==100003)throw Error('Second length'); 2244if(end[100000]!==7||end[100001]!==7||end[100002]!==7)throw Error('Async order'); 2245tail.close(); 2246console.log('ordered'); 2247"# , 2248& [], 2249); 2250assert_eq! ( output. stdout , b"ordered\n" ); 2251assert_eq! ( fs:: read ( dir. path (). join ( "ord2" )). unwrap (). len (), 100003 ); 2252}