char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
7325e8c
main
1#! [ allow ( dead_code )] 2// Shared across test targets; each uses only part of this module. 3use std:: collections::{ BTreeMap , BTreeSet }; 4use std:: io::{ Read , Write }; 5use std:: net:: TcpListener ; 6use std:: path:: Path ; 7use std:: process::{ Command , Output }; 8use std:: sync::{ 9Arc , Mutex , 10 atomic::{ AtomicBool , AtomicUsize , Ordering }, 11}; 12use std:: thread:: JoinHandle ; 13use std:: time:: Duration ; 14 15use avril_runtime:: registry::{ Options , PackageStore , checksum, npm:: resolution:: NpmResolver }; 16use serde_json:: json; 17use url:: Url ; 18 19/// The directory's `avril.lock` as data: locks are fJSON, so plain JSON parsing misses 20/// implicit root braces and other fJSON spellings. 21pub fn read_lock ( directory : & std:: path:: Path ) -> serde_json:: Value { 22let path = directory. join ( "avril.lock" ); 23let source = std:: fs:: read_to_string ( & path). unwrap (); 24 serde_fjson:: parse ( & source). unwrap () 25} 26 27// Downloads must remain driven while tests wait outside Tokio. 28pub fn runtime_handle () -> tokio:: runtime:: Handle { 29static RUNTIME : std:: sync:: OnceLock < tokio:: runtime:: Runtime > = std:: sync:: OnceLock :: new (); 30RUNTIME 31. get_or_init ( ||{ 32 tokio:: runtime:: Builder :: new_multi_thread () 33. worker_threads ( 2 ) 34. enable_all () 35. build () 36. unwrap () 37}) 38. handle () 39. clone () 40} 41 42pub fn manifest ( name : & str , version : & str , extra : serde_json:: Value ) ->String { 43let mut package =json! ({ "name" : name, "version" : version, "type" : "module" }); 44 package 45. as_object_mut () 46. unwrap () 47. extend ( extra. as_object (). unwrap (). clone ()); 48 package. to_string () 49} 50 51pub fn resolver ( options : Options ) ->NpmResolver { 52NpmResolver :: new ( Arc :: new ( 53PackageStore :: new ( runtime_handle (), options). unwrap (), 54)) 55} 56 57pub fn files ( dir : & Path , entries : & [( & str , & str )]) { 58for ( name, source) in entries{ 59write ( dir, name, source); 60} 61} 62 63pub fn write ( dir : & Path , name : & str , source : & str ) { 64let path = dir. join ( name); 65 std:: fs:: create_dir_all ( path. parent (). unwrap ()). unwrap (); 66 std:: fs:: write ( path, source). unwrap (); 67} 68 69pub fn success ( output : & Output ) ->String { 70assert! ( 71 output. status . success (), 72"{}" , 73String :: from_utf8_lossy ( & output. stderr ) 74); 75String :: from_utf8 ( output. stdout . clone ()). unwrap () 76} 77 78pub fn failure ( output : & Output ) ->String { 79assert! ( 80 !output. status . success (), 81"{}" , 82String :: from_utf8_lossy ( & output. stdout ) 83); 84String :: from_utf8_lossy ( & output. stderr ). into_owned () 85} 86 87struct RegistryState { 88files : Arc < Mutex < BTreeMap < String , Vec < u8 >>>>, 89abbreviated : Arc < Mutex < BTreeMap < String , Vec < u8 >>>>, 90requests : Arc < Mutex < Vec < String >>>, 91responses : Arc < Mutex < Vec < String >>>, 92stop : Arc < AtomicBool >, 93hold_tarballs : Arc < AtomicBool >, 94hold_paths : Arc < Mutex < BTreeSet < String >>>, 95active : Arc < AtomicUsize >, 96max : Arc < AtomicUsize >, 97} 98 99struct ActiveGuard { 100state : Arc < RegistryState >, 101} 102 103impl Drop for ActiveGuard { 104fn drop ( & mut self ) { 105self . state . active . fetch_sub ( 1 , Ordering :: SeqCst ); 106} 107} 108 109pub struct Registry { 110pub url : Url , 111pub files : Arc < Mutex < BTreeMap < String , Vec < u8 >>>>, 112pub requests : Arc < Mutex < Vec < String >>>, 113/// When set, tarball responses stall until cleared, so guest code can 114/// revoke authority mid-acquisition deterministically. 115pub hold_tarballs : Arc < AtomicBool >, 116/// Opt-in concurrent serving for overlap/bound tests; default off keeps 117/// the original serial accept loop. 118pub concurrent : Arc < AtomicBool >, 119/// Tarball paths to stall while present; stall loops also watch stop. 120pub hold_paths : Arc < Mutex < BTreeSet < String >>>, 121pub active_tarballs : Arc < AtomicUsize >, 122pub max_tarballs : Arc < AtomicUsize >, 123pub responses : Arc < Mutex < Vec < String >>>, 124abbreviated : Arc < Mutex < BTreeMap < String , Vec < u8 >>>>, 125stop : Arc < AtomicBool >, 126worker : Option < JoinHandle <()>>, 127handlers : Arc < Mutex < Vec < JoinHandle <()>>>>, 128} 129 130impl Registry { 131pub fn new () ->Self { 132let listener =TcpListener :: bind ( "127.0.0.1:0" ). unwrap (); 133let url =Url :: parse ( & format! ( "http://{}/" , listener. local_addr (). unwrap ())). unwrap (); 134 listener. set_nonblocking ( true ). unwrap (); 135let files =Arc :: new ( Mutex :: new ( BTreeMap ::< String , Vec < u8 >>:: new ())); 136let requests =Arc :: new ( Mutex :: new ( Vec :: new ())); 137let stop =Arc :: new ( AtomicBool :: new ( false )); 138let hold_tarballs =Arc :: new ( AtomicBool :: new ( false )); 139let concurrent =Arc :: new ( AtomicBool :: new ( false )); 140let hold_paths =Arc :: new ( Mutex :: new ( BTreeSet ::< String >:: new ())); 141let active =Arc :: new ( AtomicUsize :: new ( 0 )); 142let max =Arc :: new ( AtomicUsize :: new ( 0 )); 143let responses =Arc :: new ( Mutex :: new ( Vec ::< String >:: new ())); 144let abbreviated =Arc :: new ( Mutex :: new ( BTreeMap ::< String , Vec < u8 >>:: new ())); 145let handlers =Arc :: new ( Mutex :: new ( Vec ::< JoinHandle <()>>:: new ())); 146let state =Arc :: new ( RegistryState { 147files : files. clone (), 148abbreviated : abbreviated. clone (), 149requests : requests. clone (), 150responses : responses. clone (), 151stop : stop. clone (), 152hold_tarballs : hold_tarballs. clone (), 153hold_paths : hold_paths. clone (), 154active : active. clone (), 155max : max. clone (), 156}); 157let accept_concurrent = concurrent. clone (); 158let accept_handlers = handlers. clone (); 159let worker = std:: thread:: spawn ( move ||{ 160while !state. stop . load ( Ordering :: Relaxed ) { 161let stream =match listener. accept () { 162Ok (( stream, _)) => stream, 163Err ( error) if error. kind () == std:: io:: ErrorKind :: WouldBlock =>{ 164 std:: thread:: sleep ( Duration :: from_millis ( 1 )); 165continue ; 166} 167Err ( error) =>panic! ( "{error}" ), 168}; 169if accept_concurrent. load ( Ordering :: Relaxed ) { 170let state = state. clone (); 171 accept_handlers 172. lock () 173. unwrap () 174. push ( std:: thread:: spawn ( move ||{ 175serve_stream ( state, stream); 176})); 177continue ; 178} 179serve_stream ( state. clone (), stream); 180} 181}); 182Self { 183 url, 184 files, 185 abbreviated, 186 requests, 187 hold_tarballs, 188 concurrent, 189 hold_paths, 190active_tarballs : active, 191max_tarballs : max, 192 responses, 193 stop, 194worker : Some ( worker), 195 handlers, 196} 197} 198 199pub fn package ( 200& self , 201name : & str , 202version : & str , 203files : & [( & str , & str )], 204exports : & [( & str , & str )], 205) { 206let mut artifacts =self . files . lock (). unwrap (); 207let index_path =format! ( "/{name}/meta.json" ); 208let mut index = artifacts 209. get ( & index_path) 210. map ( |bytes| serde_json:: from_slice ::< serde_json:: Value >( bytes). unwrap ()) 211. unwrap_or_else ( ||json! ({ "versions" : {}})); 212 index[ "versions" ][ version] =json! ({}); 213 artifacts. insert ( index_path, serde_json:: to_vec ( & index). unwrap ()); 214let manifest: BTreeMap < _ , _ > = files 215. iter () 216. map ( |( path, source) |{ 217 artifacts. insert ( 218format! ( "/{name}/{version}/{path}" ), 219 source. as_bytes (). to_vec (), 220); 221( 222format! ( "/{path}" ), 223json! ({ "size" : source. len (), "checksum" : checksum ( source. as_bytes ())}), 224) 225}) 226. collect (); 227let exports: BTreeMap < _ , _ > = exports. iter (). copied (). collect (); 228 artifacts. insert ( 229format! ( "/{name}/{version}_meta.json" ), 230 serde_json:: to_vec ( 231& json! ({ "manifest" : manifest, "exports" : exports, "moduleGraph2" : {}}), 232) 233. unwrap (), 234); 235} 236 237pub fn tarball_bytes ( files : & [( & str , & str )]) ->Vec < u8 > { 238use flate2::{ Compression , write:: GzEncoder }; 239let mut tar_data =Vec :: new (); 240{ 241let mut tar = tar:: Builder :: new ( & mut tar_data); 242for ( path, source) in files{ 243let full =format! ( "package/{path}" ); 244let mut header = tar:: Header :: new_gnu (); 245 header. set_mode ( 0o644 ); 246 header. set_size ( source. len () as u64 ); 247 header. set_cksum (); 248 tar. append_data ( & mut header, & full, source. as_bytes ()) 249. unwrap (); 250} 251 tar. finish (). unwrap (); 252} 253let mut encoder =GzEncoder :: new ( Vec :: new (), Compression :: default ()); 254use std:: io:: Write ; 255 encoder. write_all ( & tar_data). unwrap (); 256 encoder. finish (). unwrap () 257} 258 259pub fn tarball_integrity ( bytes : & [ u8 ]) ->String { 260use base64:: Engine ; 261use sha2:: Digest ; 262let digest = sha2:: Sha512 :: digest ( bytes); 263format! ( 264"sha512-{}" , 265 base64:: engine:: general_purpose:: STANDARD . encode ( digest) 266) 267} 268 269fn tarball_path ( name : & str , version : & str ) ->String { 270let leaf = name. rsplit ( '/' ). next (). unwrap (); 271format! ( "/{name}/-/{leaf}-{version}.tgz" ) 272} 273 274pub fn publish_package ( 275& self , 276name : & str , 277version : & str , 278extra : serde_json:: Value , 279sources : & [( & str , & str )], 280) { 281let package =manifest ( name, version, extra); 282let mut files =vec! [( "package.json" , package. as_str ())]; 283if !sources. iter (). any ( |( path, _) | path. starts_with ( "index." )) { 284 files. push (( "index.js" , "export const value = 1;\nexport default 1;" )); 285} 286 files. extend_from_slice ( sources); 287self . publish_npm ( name, version, & files); 288} 289 290pub fn resolver ( & self , dir : & Path ) ->NpmResolver { 291resolver ( self . options ( dir)) 292} 293 294pub fn frozen_resolver ( & self , dir : & Path ) ->NpmResolver { 295let mut options =self . options ( dir); 296 options. offline =true ; 297 options. frozen =true ; 298resolver ( options) 299} 300 301pub fn command ( & self , dir : & Path , subcommand : & str ) ->Command { 302// This fixture also builds in CLI unit tests, which never launch commands. 303let Some ( binary) =option_env! ( "CARGO_BIN_EXE_avril" ) else { 304panic! ( "CLI commands require an integration-test target" ); 305}; 306let mut command =Command :: new ( binary); 307 command 308. current_dir ( dir) 309. arg ( subcommand) 310. arg ( format! ( "--jsr-registry={}" , self . url )) 311. arg ( format! ( "--npm-registry={}" , self . url )) 312. arg ( format! ( "--cache-dir={}" , dir. join ( "cache" ). display ())); 313 command 314} 315 316pub fn run_npm ( & self , dir : & Path , args : & [ & str ], entry : & str ) ->Output { 317let mut command =self . command ( dir, args[ 0 ]); 318 command. args ( & args[ 1 ..]); 319// `add` and friends take no entry module. 320if !entry. is_empty () { 321 command. arg ( entry); 322} 323 command. output (). unwrap () 324} 325 326pub fn publish_npm ( & self , name : & str , version : & str , files : & [( & str , & str )]) { 327let tarball =Self :: tarball_bytes ( files); 328let integrity =Self :: tarball_integrity ( & tarball); 329self . publish_npm_dist ( name, version, tarball, & integrity, None , true ); 330} 331 332pub fn publish_npm_dist ( 333& self , 334name : & str , 335version : & str , 336tarball : Vec < u8 >, 337integrity : & str , 338shasum : Option < & str >, 339latest : bool , 340) { 341let mut artifacts =self . files . lock (). unwrap (); 342let tarball_path =Self :: tarball_path ( name, version); 343let tarball_url =format! ( "{}{tarball_path}" , self . url . as_str (). trim_end_matches ( '/' )); 344 artifacts. insert ( tarball_path, tarball); 345let packument_path =format! ( "/{name}" ); 346let mut packument = artifacts 347. get ( & packument_path) 348. map ( |bytes| serde_json:: from_slice ::< serde_json:: Value >( bytes). unwrap ()) 349. unwrap_or_else ( ||json! ({ "name" : name, "dist-tags" : {}, "versions" : {}})); 350 packument[ "versions" ][ version] =json! ({ 351"name" : name, 352"version" : version, 353"dist" : { 354"tarball" : tarball_url, 355"integrity" : integrity, 356}, 357}); 358if let Some ( shasum) = shasum{ 359 packument[ "versions" ][ version][ "dist" ][ "shasum" ] =json! ( shasum); 360} 361if latest{ 362 packument[ "dist-tags" ][ "latest" ] =json! ( version); 363} 364 artifacts. insert ( packument_path, serde_json:: to_vec ( & packument). unwrap ()); 365} 366 367/// Abbreviated metadata for Accept-negotiating clients; others get the ordinary response. 368pub fn publish_npm_abbreviated ( & self , name : & str , body : Vec < u8 >) { 369self . abbreviated 370. lock () 371. unwrap () 372. insert ( format! ( "/{name}" ), body); 373} 374 375pub fn options ( & self , directory : & std:: path:: Path ) ->Options { 376Options { 377jsr_registry : self . url . clone (), 378npm_registry : self . url . clone (), 379cache : directory. join ( "cache" ), 380lock : directory. join ( "avril.lock" ), 381project_root : directory. to_owned (), 382offline : false , 383frozen : false , 384update : false , 385ephemeral_lock : false , 386vendor : None , 387} 388} 389} 390 391fn serve_stream ( state : Arc < RegistryState >, mut stream : std:: net:: TcpStream ) { 392 stream 393. set_read_timeout ( Some ( Duration :: from_secs ( 3 ))) 394. unwrap (); 395let mut request =Vec :: new (); 396let mut buf =[ 0 ; 1024 ]; 397while !request. windows ( 4 ). any ( |bytes| bytes ==b"\r\n\r\n" ) { 398let length =match stream. read ( & mut buf) { 399Ok ( 0 ) |Err ( _) =>break , 400Ok ( length) => length, 401}; 402 request. extend_from_slice ( & buf[ ..length]); 403} 404let request =String :: from_utf8_lossy ( & request); 405let Some ( path) = request 406. lines () 407. next () 408. and_then ( |line| line. split_whitespace (). nth ( 1 )) 409else { 410return ; 411}; 412assert! ( !request. to_ascii_lowercase (). contains ( "accept: text/html" )); 413let abbreviated = request. lines (). any ( |line|{ 414 line. to_ascii_lowercase (). starts_with ( "accept:" ) 415 && line. contains ( "application/vnd.npm.install-v1+json" ) 416}); 417let _guard = path. ends_with ( ".tgz" ). then ( ||{ 418let active = state. active . fetch_add ( 1 , Ordering :: SeqCst ) +1 ; 419 state. max . fetch_max ( active, Ordering :: SeqCst ); 420ActiveGuard { state : state. clone () } 421}); 422 state. requests . lock (). unwrap (). push ( path. to_owned ()); 423while !state. stop . load ( Ordering :: Relaxed ) 424 &&(( path. ends_with ( ".tgz" ) && state. hold_tarballs . load ( Ordering :: Relaxed )) 425 || state. hold_paths . lock (). unwrap (). contains ( path)) 426{ 427 std:: thread:: sleep ( Duration :: from_millis ( 10 )); 428} 429if state. stop . load ( Ordering :: Relaxed ) { 430return ; 431} 432let mut bytes =None ; 433if abbreviated{ 434 bytes = state. abbreviated . lock (). unwrap (). get ( path). cloned (); 435} 436let bytes = bytes. or_else ( || state. files . lock (). unwrap (). get ( path). cloned ()); 437let ( status, bytes) =match bytes{ 438Some ( bytes) =>( "200 OK" , bytes), 439None =>( "404 Not Found" , b"not found" . to_vec ()), 440}; 441let headers =format! ( 442"HTTP/1.1 {status}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n" , 443 bytes. len () 444); 445 stream 446. set_write_timeout ( Some ( Duration :: from_secs ( 3 ))) 447. unwrap (); 448let _ = stream 449. write_all ( headers. as_bytes ()) 450. and_then ( |_| stream. write_all ( & bytes)); 451 state. responses . lock (). unwrap (). push ( path. to_owned ()); 452} 453 454impl Drop for Registry { 455fn drop ( & mut self ) { 456self . stop . store ( true , Ordering :: Relaxed ); 457self . worker . take (). unwrap (). join (). unwrap (); 458for handlerin self . handlers . lock (). unwrap (). drain ( ..) { 459if std:: thread:: panicking () { 460let _ = handler. join (); 461} else { 462 handler. join (). unwrap (); 463} 464} 465} 466}