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# [ path = "common/registry.rs" ] 2mod fixture; 3 4use avril_runtime:: registry::{ Options , PackageStore , npm:: parse_npm_specifier}; 5use fixture::{ Registry , success}; 6use futures:: executor:: block_on; 7use std:: sync:: Arc ; 8 9fn offline_frozen_options ( server : & Registry , directory : & std:: path:: Path ) ->Options { 10let mut options = server. options ( directory); 11 options. offline =true ; 12 options. frozen =true ; 13 options 14} 15 16fn parity_files ( version : & str ) ->Vec <( &' static str , String )> { 17vec! [ 18( 19"package.json" , 20format! ( 21"{{\"name\": \"parity\", \"version\": \"{version}\", \"type\": \"module\", \"exports\": {{\".\": \"./index.js\", \"./sub\": \"./sub.js\"}}}}" 22), 23), 24( "index.js" , format! ( "export default 'v{version}';" )), 25( "sub.js" , format! ( "export default 'sub-{version}';" )), 26] 27} 28 29fn info_cli ( server : & Registry , directory : & std:: path:: Path ) -> serde_json:: Value { 30let output = server 31. command ( directory, "info" ) 32. arg ( "main.js" ) 33. output () 34. unwrap (); 35 serde_json:: from_str ( & success ( & output)). unwrap () 36} 37 38/// Project-independent identity of the resolved npm modules: the local 39/// materialization paths differ per project by construction. 40fn parity_packages ( info : & serde_json:: Value ) ->Vec < serde_json:: Value > { 41let mut packages: Vec < _ > = info[ "modules" ] 42. as_object () 43. unwrap () 44. values () 45. filter_map ( |module|{ 46let package =& module[ "package" ]; 47if package. get ( "name" ). and_then ( |name| name. as_str ()) !=Some ( "parity" ) { 48return None ; 49} 50let mut package = package. clone (); 51 package. as_object_mut (). unwrap (). remove ( "file" ); 52Some ( package) 53}) 54. collect (); 55 packages. sort_by ( |a, b| a[ "instance" ]. as_str (). cmp ( & b[ "instance" ]. as_str ())); 56 packages 57} 58 59fn leftpad_files ( version : & str ) ->Vec <( &' static str , String )> { 60vec! [ 61( 62"package.json" , 63format! ( 64"{{\"name\": \"leftpad\", \"version\": \"{version}\", \"type\": \"module\", \"exports\": {{\".\": \"./index.js\"}}}}" 65), 66), 67( "index.js" , format! ( "export default {version};" )), 68] 69} 70 71fn files_ref < ' a >( files : &' a [( &' a str , String )]) ->Vec <( &' a str , &' a str )> { 72 files 73. iter () 74. map ( |( path, source) |( * path, source. as_str ())) 75. collect () 76} 77 78# [ test ] 79fn selects_ranges_tags_and_latest_and_pins_verified_files () { 80let server =Registry :: new (); 81 server. publish_npm ( "leftpad" , "1.0.0" , & files_ref ( & leftpad_files ( "1.0.0" ))); 82 server. publish_npm ( "leftpad" , "1.1.0" , & files_ref ( & leftpad_files ( "1.1.0" ))); 83let dir = tempfile:: tempdir (). unwrap (); 84let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 85assert! ( parse_npm_specifier ( "npm:leftpad" ). is_ok ()); 86assert! ( parse_npm_specifier ( "npm:leftpad/sub" ). is_err ()); 87assert! ( parse_npm_specifier ( "npm:../evil" ). is_err ()); 88let latest =block_on ( store. npm_acquire ( "npm:leftpad" )). unwrap (); 89assert_eq! ( latest. identity . version , "1.1.0" ); 90assert_eq! ( latest. identity . name , "leftpad" ); 91assert! ( 92 latest 93. dir 94. starts_with ( dir. path (). join ( ".avril" ). join ( "npm" )) 95); 96assert! ( !latest. dir . to_string_lossy (). contains ( "node_modules" )); 97assert_eq! ( 98 std:: fs:: read_to_string ( latest. dir . join ( "index.js" )). unwrap (), 99"export default 1.1.0;" 100); 101assert_eq! ( 102 latest. package . get ( "name" ). and_then ( |v| v. as_str ()), 103Some ( "leftpad" ) 104); 105let ranged =block_on ( store. npm_acquire ( "npm:leftpad@^1.0.0" )). unwrap (); 106assert_eq! ( ranged. identity . version , "1.1.0" ); 107assert_eq! ( ranged. dir , latest. dir ); 108let exact =block_on ( store. npm_acquire ( "npm:leftpad@1.0.0" )). unwrap (); 109assert_eq! ( exact. identity . version , "1.0.0" ); 110assert_ne! ( exact. dir , latest. dir ); 111 store. save (). unwrap (); 112let lock_text = std:: fs:: read_to_string ( dir. path (). join ( "avril.lock" )). unwrap (); 113assert! ( !lock_text. contains ( dir. path (). to_string_lossy (). as_ref ())); 114assert! ( !lock_text. contains ( "node_modules" )); 115let lock = fixture:: read_lock ( dir. path ()); 116assert_eq! ( lock[ "version" ], 2 ); 117assert_eq! ( lock[ "npm" ][ "specifiers" ][ "npm:leftpad" ], "1.1.0" ); 118assert! ( 119 lock[ "npm" ][ "packages" ] 120. as_object () 121. unwrap () 122. values () 123. all ( |pin| pin[ "tarball" ] 124. as_str () 125. unwrap () 126. starts_with ( server. url . as_str ()) 127 && pin[ "integrity" ]. as_str (). unwrap (). starts_with ( "sha512-" ) 128 && pin[ "manifest" ] 129. as_str () 130. is_some_and ( |digest| digest. starts_with ( "sha256-" )) 131 && pin. get ( "files" ). is_none ()), 132"compact pins carry one manifest digest, never the file map" 133); 134assert_eq! ( 135 std:: fs:: read_to_string ( dir. path (). join ( ".avril" ). join ( ".gitignore" )). unwrap (), 136"*\n" 137); 138} 139 140# [ test ] 141fn updates_reselect_ranges_but_frozen_requires_the_exact_request () { 142let server =Registry :: new (); 143 server. publish_npm ( "leftpad" , "1.0.0" , & files_ref ( & leftpad_files ( "1.0.0" ))); 144let dir = tempfile:: tempdir (). unwrap (); 145let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 146block_on ( store. npm_acquire ( "npm:leftpad@^1" )). unwrap (); 147 store. save (). unwrap (); 148 server. publish_npm ( "leftpad" , "1.1.0" , & files_ref ( & leftpad_files ( "1.1.0" ))); 149let mut options = server. options ( dir. path ()); 150 options. update =true ; 151let store =PackageStore :: new ( fixture:: runtime_handle (), options). unwrap (); 152assert_eq! ( 153block_on ( store. npm_acquire ( "npm:leftpad@^1" )) 154. unwrap () 155. identity 156. version , 157"1.1.0" 158); 159 store. save (). unwrap (); 160let mut options = server. options ( dir. path ()); 161 options. frozen =true ; 162let store =PackageStore :: new ( fixture:: runtime_handle (), options). unwrap (); 163let error =block_on ( store. npm_acquire ( "npm:leftpad@~1.1.0" )) 164. unwrap_err () 165. to_string (); 166assert! ( error. contains ( "Frozen lockfile is missing" ), "{error}" ); 167} 168 169# [ test ] 170fn provenance_urls_encode_literal_file_names () { 171use avril_runtime:: registry:: npm:: npm_virtual_module; 172let registry = url:: Url :: parse ( "https://registry.npmjs.org/" ). unwrap (); 173let url =npm_virtual_module ( & registry, "example" , "1.0.0" , "%2e%2e/asset#name?.js" ). unwrap (); 174assert_eq! ( 175 url. as_str (), 176"https://registry.npmjs.org/example/1.0.0/%252e%252e/asset%23name%3F.js" 177); 178assert! ( url. query (). is_none ()); 179assert! ( url. fragment (). is_none ()); 180} 181 182# [ test ] 183fn warmed_tree_runs_offline_frozen_without_network_or_global_cache () { 184let server =Registry :: new (); 185 server. publish_npm ( 186"tiny" , 187"2.0.0" , 188& [ 189( 190"package.json" , 191"{\"name\": \"tiny\", \"version\": \"2.0.0\"}" , 192), 193( "index.js" , "export default 1;" ), 194], 195); 196let dir = tempfile:: tempdir (). unwrap (); 197let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 198let before_requests = server. requests . lock (). unwrap (). len (); 199block_on ( store. npm_acquire ( "npm:tiny" )). unwrap (); 200 store. save (). unwrap (); 201let warmed_requests = server. requests . lock (). unwrap (). len (); 202assert! ( warmed_requests > before_requests); 203let lock_before = std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (); 204let mut offline =offline_frozen_options ( & server, dir. path ()); 205 offline. cache = dir. path (). join ( "fresh-cache-that-must-stay-empty" ); 206let store =PackageStore :: new ( fixture:: runtime_handle (), offline). unwrap (); 207let artifact =block_on ( store. npm_acquire ( "npm:tiny" )). unwrap (); 208assert_eq! ( 209 std:: fs:: read_to_string ( artifact. dir . join ( "index.js" )). unwrap (), 210"export default 1;" 211); 212assert_eq! ( 213 server. requests . lock (). unwrap (). len (), 214 warmed_requests, 215"frozen offline hits must not touch the network" 216); 217assert! ( 218 !dir. path (). join ( "fresh-cache-that-must-stay-empty" ). exists (), 219"npm must not depend on the global cache" 220); 221 store. save (). unwrap (); 222assert_eq! ( 223 std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (), 224 lock_before 225); 226} 227 228# [ test ] 229fn corrupted_trees_fail_closed_while_absent_trees_materialize () { 230let server =Registry :: new (); 231 server. publish_npm ( 232"mut" , 233"1.0.0" , 234& [ 235( 236"package.json" , 237"{\"name\": \"mut\", \"version\": \"1.0.0\"}" , 238), 239( "index.js" , "export default 1;" ), 240], 241); 242let dir = tempfile:: tempdir (). unwrap (); 243let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 244let artifact =block_on ( store. npm_acquire ( "npm:mut" )). unwrap (); 245 store. save (). unwrap (); 246 std:: fs:: write ( artifact. dir . join ( "index.js" ), "tampered" ). unwrap (); 247// A corrupted existing tree fails closed: it is never deleted or 248// repaired in place, so publication stays atomic and immutable. 249let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 250assert! ( 251block_on ( store. npm_acquire ( "npm:mut" )) 252. unwrap_err () 253. to_string () 254. contains ( "corrupted" ) 255); 256assert_eq! ( 257 std:: fs:: read_to_string ( artifact. dir . join ( "index.js" )). unwrap (), 258"tampered" 259); 260// An absent tree materializes from the verified cache instead. 261 std:: fs:: remove_dir_all ( & artifact. dir ). unwrap (); 262let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 263let restored =block_on ( store. npm_acquire ( "npm:mut" )). unwrap (); 264assert_eq! ( 265 std:: fs:: read_to_string ( restored. dir . join ( "index.js" )). unwrap (), 266"export default 1;" 267); 268 store. save (). unwrap (); 269// A tampered legacy pin must fail closed instead of repairing: rewrite 270// the entry in legacy wire form with one file hash pointed at other 271// bytes, so the tree no longer matches the authenticated map. 272let mut lock_value = fixture:: read_lock ( dir. path ()); 273let pin = lock_value[ "npm" ][ "packages" ] 274. as_object_mut () 275. unwrap () 276. values_mut () 277. next () 278. unwrap (); 279let sidecar = dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".manifests" ); 280let sidecar = std:: fs:: read_dir ( & sidecar) 281. unwrap () 282. next () 283. unwrap () 284. unwrap () 285. path (); 286let mut files: serde_json:: Value = 287 serde_json:: from_slice ( & std:: fs:: read ( & sidecar). unwrap ()). unwrap (); 288 files[ "index.js" ] = serde_json:: Value :: String ( avril_runtime:: registry:: checksum ( b"tampered" )); 289* pin = serde_json:: json!({ 290"tarball" : pin[ "tarball" ]. clone (), 291"integrity" : pin[ "integrity" ]. clone (), 292"files" : files, 293}); 294 std:: fs:: write ( dir. path (). join ( "avril.lock" ), format! ( "{lock_value}\n" )). unwrap (); 295let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 296assert! ( 297block_on ( store. npm_acquire ( "npm:mut" )) 298. unwrap_err () 299. to_string () 300. contains ( "corrupted" ) 301); 302} 303 304# [ test ] 305fn malicious_archives_are_rejected () { 306let server =Registry :: new (); 307// tar::Builder refuses `..` itself, so the traversal entry is crafted as 308// raw ustar blocks: the extractor must still reject it. 309let traversal ={ 310use flate2::{ Compression , write:: GzEncoder }; 311use std:: io:: Write ; 312fn ustar_bytes ( name : & str , data : & [ u8 ]) ->Vec < u8 > { 313let mut header =[ 0u8 ; 512 ]; 314 header[ ..name. len ()]. copy_from_slice ( name. as_bytes ()); 315 header[ 100 ..108 ]. copy_from_slice ( format! ( "{:07o}\0" , 0o644 ). as_bytes ()); 316 header[ 124 ..136 ]. copy_from_slice ( format! ( "{:011o}\0" , data. len ()). as_bytes ()); 317 header[ 136 ..148 ]. copy_from_slice ( format! ( "{:011o}\0" , 0 ). as_bytes ()); 318 header[ 156 ] =b'0' ; 319 header[ 257 ..262 ]. copy_from_slice ( b"ustar" ); 320 header[ 263 ..265 ]. copy_from_slice ( b"00" ); 321let sum: u32 = header. iter (). map ( |byte|* byteas u32 ). sum ::< u32 >() +8 * 32 ; 322 header[ 148 ..156 ]. copy_from_slice ( format! ( "{sum:06o}\0 " ). as_bytes ()); 323let mut out = header. to_vec (); 324 out. extend_from_slice ( data); 325while out. len () %512 !=0 { 326 out. push ( 0 ); 327} 328 out 329} 330let package ="{\"name\": \"evil\", \"version\": \"1.0.0\"}" ; 331let mut tar_data =ustar_bytes ( "package/package.json" , package. as_bytes ()); 332 tar_data. extend_from_slice ( & ustar_bytes ( "package/../evil.js" , b"evil" )); 333 tar_data. extend_from_slice ( & [ 0u8 ; 1024 ]); 334let mut encoder =GzEncoder :: new ( Vec :: new (), Compression :: default ()); 335 encoder. write_all ( & tar_data). unwrap (); 336 encoder. finish (). unwrap () 337}; 338let integrity =Registry :: tarball_integrity ( & traversal); 339 server. publish_npm_dist ( "evil" , "1.0.0" , traversal, & integrity, None , true ); 340let dir = tempfile:: tempdir (). unwrap (); 341let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 342assert! ( 343block_on ( store. npm_acquire ( "npm:evil" )) 344. unwrap_err () 345. to_string () 346. contains ( "Invalid npm file path" ) 347); 348 349let symlink ={ 350use flate2::{ Compression , write:: GzEncoder }; 351use std:: io:: Write ; 352let mut tar_data =Vec :: new (); 353{ 354let mut tar = tar:: Builder :: new ( & mut tar_data); 355let package ="{\"name\": \"link\", \"version\": \"1.0.0\"}" ; 356let mut header = tar:: Header :: new_gnu (); 357 header. set_mode ( 0o644 ); 358 header. set_size ( package. len () as u64 ); 359 header. set_cksum (); 360 tar. append_data ( & mut header, "package/package.json" , package. as_bytes ()) 361. unwrap (); 362let mut header = tar:: Header :: new_gnu (); 363 header. set_entry_type ( tar:: EntryType :: Symlink ); 364 header. set_size ( 0 ); 365 header. set_cksum (); 366 tar. append_link ( & mut header, "package/link.js" , "/etc/passwd" ) 367. unwrap (); 368 tar. finish (). unwrap (); 369} 370let mut encoder =GzEncoder :: new ( Vec :: new (), Compression :: default ()); 371 encoder. write_all ( & tar_data). unwrap (); 372 encoder. finish (). unwrap () 373}; 374let integrity =Registry :: tarball_integrity ( & symlink); 375 server. publish_npm_dist ( "link" , "1.0.0" , symlink, & integrity, None , true ); 376let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 377assert! ( 378block_on ( store. npm_acquire ( "npm:link" )) 379. unwrap_err () 380. to_string () 381. contains ( "only regular files and directories" ) 382); 383} 384 385# [ test ] 386fn concurrent_acquisitions_share_a_single_download () { 387let server =Registry :: new (); 388 server. publish_npm ( 389"shared" , 390"3.0.0" , 391& [ 392( 393"package.json" , 394"{\"name\": \"shared\", \"version\": \"3.0.0\"}" , 395), 396( "index.js" , "export default 1;" ), 397], 398); 399let dir = tempfile:: tempdir (). unwrap (); 400let store = 401Arc :: new ( PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap ()); 402// Same-specifier concurrency shares one download via the inflight map; 403// distinct specifiers resolving to one version may still fetch twice and 404// converge on the lock through the per-package file lock. 405let ( a, b) =block_on ( futures:: future:: join ( 406 store. npm_acquire ( "npm:shared" ), 407 store. npm_acquire ( "npm:shared" ), 408)); 409assert! ( a. is_ok () && b. is_ok ()); 410let tarballs = server 411. requests 412. lock () 413. unwrap () 414. iter () 415. filter ( |path| path. ends_with ( ".tgz" )) 416. count (); 417assert_eq! ( tarballs, 1 ); 418 store. save (). unwrap (); 419} 420 421# [ test ] 422fn shasum_fallback_works_and_external_tarballs_are_rejected () { 423let server =Registry :: new (); 424let tarball =Registry :: tarball_bytes ( & [ 425( 426"package.json" , 427"{\"name\": \"legacy\", \"version\": \"1.0.0\"}" , 428), 429( "index.js" , "export default 1;" ), 430]); 431let shasum ={ 432use sha1:: Digest ; 433let digest = sha1:: Sha1 :: digest ( & tarball); 434 digest 435. iter () 436. map ( |byte|format! ( "{byte:02x}" )) 437. collect ::< String >() 438}; 439 server. publish_npm_dist ( 440"legacy" , 441"1.0.0" , 442 tarball, 443"sha512-invalid-will-be-ignored" , 444Some ( & shasum), 445true , 446); 447// Repair the fixture entry to carry only the shasum fallback. 448{ 449let mut files = server. files . lock (). unwrap (); 450let mut packument: serde_json:: Value = serde_json:: from_slice ( & files[ "/legacy" ]). unwrap (); 451 packument[ "versions" ][ "1.0.0" ][ "dist" ] 452. as_object_mut () 453. unwrap () 454. remove ( "integrity" ); 455 files. insert ( "/legacy" . into (), serde_json:: to_vec ( & packument). unwrap ()); 456} 457let dir = tempfile:: tempdir (). unwrap (); 458let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 459let artifact =block_on ( store. npm_acquire ( "npm:legacy" )). unwrap (); 460assert! ( artifact. integrity . starts_with ( "sha1-" )); 461 462 server. publish_npm ( 463"ext" , 464"1.0.0" , 465& [( 466"package.json" , 467"{\"name\": \"ext\", \"version\": \"1.0.0\"}" , 468)], 469); 470{ 471let mut files = server. files . lock (). unwrap (); 472let mut packument: serde_json:: Value = serde_json:: from_slice ( & files[ "/ext" ]). unwrap (); 473 packument[ "versions" ][ "1.0.0" ][ "dist" ][ "tarball" ] = 474 serde_json:: Value :: String ( "https://example.com/ext-1.0.0.tgz" . into ()); 475 files. insert ( "/ext" . into (), serde_json:: to_vec ( & packument). unwrap ()); 476} 477let tgz_before = server 478. requests 479. lock () 480. unwrap () 481. iter () 482. filter ( |path| path. ends_with ( ".tgz" )) 483. count (); 484let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 485assert! ( 486block_on ( store. npm_acquire ( "npm:ext" )) 487. unwrap_err () 488. to_string () 489. contains ( "escapes the registry" ) 490); 491// Learning the external tarball URL costs one packument fetch; the 492// tarball itself must never be fetched. 493assert_eq! ( 494 server 495. requests 496. lock () 497. unwrap () 498. iter () 499. filter ( |path| path. ends_with ( ".tgz" )) 500. count (), 501 tgz_before 502); 503} 504 505# [ test ] 506fn denied_packages_are_not_fetched () { 507use avril_runtime:: caps::{ Capabilities , Category , Initial }; 508let server =Registry :: new (); 509 server. publish_npm ( 510"secret" , 511"1.0.0" , 512& [( 513"package.json" , 514"{\"name\": \"secret\", \"version\": \"1.0.0\"}" , 515)], 516); 517let dir = tempfile:: tempdir (). unwrap (); 518let mut initial =Initial :: new ( true ); 519 initial 520. grant ( Category :: JsExec , true , "https://jsr.io/" , dir. path ()) 521. unwrap (); 522let caps =Capabilities :: new ( initial, dir. path (). to_owned ()); 523let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())) 524. unwrap () 525. with_capabilities ( caps); 526assert! ( 527block_on ( store. npm_acquire ( "npm:secret" )) 528. unwrap_err () 529. to_string () 530. contains ( "PermissionDenied" ) 531); 532assert! ( server. requests . lock (). unwrap (). is_empty ()); 533} 534 535# [ test ] 536fn locks_merge_and_prune_npm_pins () { 537let server =Registry :: new (); 538 server. publish_npm ( 539"alpha" , 540"1.0.0" , 541& [( 542"package.json" , 543"{\"name\": \"alpha\", \"version\": \"1.0.0\"}" , 544)], 545); 546 server. publish_npm ( 547"beta" , 548"1.0.0" , 549& [( 550"package.json" , 551"{\"name\": \"beta\", \"version\": \"1.0.0\"}" , 552)], 553); 554let dir = tempfile:: tempdir (). unwrap (); 555let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 556block_on ( store. npm_acquire ( "npm:alpha" )). unwrap (); 557block_on ( store. npm_acquire ( "npm:beta" )). unwrap (); 558 store. save (). unwrap (); 559assert_eq! ( 560 fixture:: read_lock ( dir. path ())[ "npm" ][ "specifiers" ] 561. as_object () 562. unwrap () 563. len (), 5642 565); 566let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 567block_on ( store. npm_acquire ( "npm:alpha" )). unwrap (); 568 store. prune (). unwrap (); 569let lock = fixture:: read_lock ( dir. path ()); 570assert! ( lock[ "npm" ][ "specifiers" ]. get ( "npm:alpha" ). is_some ()); 571assert! ( lock[ "npm" ][ "specifiers" ]. get ( "npm:beta" ). is_none ()); 572} 573 574# [ test ] 575fn cold_frozen_pins_download_without_metadata_queries () { 576let server =Registry :: new (); 577 server. publish_npm ( 578"cold" , 579"1.2.3" , 580& [ 581( 582"package.json" , 583"{\"name\": \"cold\", \"version\": \"1.2.3\"}" , 584), 585( "index.js" , "export default 1;" ), 586], 587); 588let dir = tempfile:: tempdir (). unwrap (); 589let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 590let artifact =block_on ( store. npm_acquire ( "npm:cold@^1" )). unwrap (); 591 store. save (). unwrap (); 592let warmed = server. requests . lock (). unwrap (). len (); 593// Cold: remove the tree and the tarball cache but keep the lock. 594 std:: fs:: remove_dir_all ( & artifact. dir ). unwrap (); 595 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".artifacts" )). unwrap (); 596let mut frozen = server. options ( dir. path ()); 597 frozen. frozen =true ; 598let store =PackageStore :: new ( fixture:: runtime_handle (), frozen). unwrap (); 599let restored =block_on ( store. npm_acquire ( "npm:cold@^1" )). unwrap (); 600assert_eq! ( restored. identity . version , "1.2.3" ); 601let paths = server. requests . lock (). unwrap (). clone (); 602assert! ( 603 paths[ warmed..]. iter (). all ( |path| path. ends_with ( ".tgz" )), 604"frozen pins must not query mutable packument metadata: {paths:?}" 605); 606assert! ( 607 paths[ warmed..]. iter (). any ( |path| path. ends_with ( ".tgz" )), 608"frozen pins still download their artifacts: {paths:?}" 609); 610} 611 612# [ test ] 613fn offline_update_selects_from_cached_metadata () { 614let server =Registry :: new (); 615 server. publish_npm ( 616"cached" , 617"1.0.0" , 618& [( 619"package.json" , 620"{\"name\": \"cached\", \"version\": \"1.0.0\"}" , 621)], 622); 623let dir = tempfile:: tempdir (). unwrap (); 624let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 625block_on ( store. npm_acquire ( "npm:cached@1.0.0" )). unwrap (); 626 store. save (). unwrap (); 627let warmed = server. requests . lock (). unwrap (). len (); 628// update clears specifiers; offline forces selection from cached metadata. 629let mut offline = server. options ( dir. path ()); 630 offline. update =true ; 631 offline. offline =true ; 632let store =PackageStore :: new ( fixture:: runtime_handle (), offline). unwrap (); 633let artifact =block_on ( store. npm_acquire ( "npm:cached@*" )). unwrap (); 634assert_eq! ( artifact. identity . version , "1.0.0" ); 635assert_eq! ( server. requests . lock (). unwrap (). len (), warmed); 636} 637 638# [ test ] 639fn new_ranges_reuse_pinned_artifacts_without_metadata () { 640let server =Registry :: new (); 641 server. publish_npm ( 642"reuse" , 643"2.1.0" , 644& [ 645( 646"package.json" , 647"{\"name\": \"reuse\", \"version\": \"2.1.0\"}" , 648), 649( "index.js" , "export default 1;" ), 650], 651); 652let dir = tempfile:: tempdir (). unwrap (); 653let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 654let first =block_on ( store. npm_acquire ( "npm:reuse@^2.0.0" )). unwrap (); 655 store. save (). unwrap (); 656let warmed = server. requests . lock (). unwrap (). len (); 657let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 658let second =block_on ( store. npm_acquire ( "npm:reuse@~2.1.0" )). unwrap (); 659assert_eq! ( second. dir , first. dir ); 660assert_eq! ( 661 server. requests . lock (). unwrap (). len (), 662 warmed, 663"an already-pinned version needs no metadata round trip" 664); 665 store. save (). unwrap (); 666let lock = fixture:: read_lock ( dir. path ()); 667assert_eq! ( lock[ "npm" ][ "specifiers" ][ "npm:reuse@~2.1.0" ], "2.1.0" ); 668} 669 670# [ test ] 671fn archive_directories_materialize_nested_files () { 672let server =Registry :: new (); 673let tarball ={ 674use flate2::{ Compression , write:: GzEncoder }; 675use std:: io:: Write ; 676let mut tar_data =Vec :: new (); 677{ 678let mut tar = tar:: Builder :: new ( & mut tar_data); 679 tar. append_dir ( "package/" , "." ). unwrap (); 680 tar. append_dir ( "package/lib/" , "." ). unwrap (); 681for ( path, data) in [ 682( 683"package/package.json" , 684"{\"name\": \"nested\", \"version\": \"1.0.0\"}" , 685), 686( "package/index.js" , "export * from './lib/util.js';" ), 687( "package/lib/util.js" , "export default 1;" ), 688] { 689let mut header = tar:: Header :: new_gnu (); 690 header. set_mode ( 0o644 ); 691 header. set_size ( data. len () as u64 ); 692 header. set_cksum (); 693 tar. append_data ( & mut header, path, data. as_bytes ()). unwrap (); 694} 695 tar. finish (). unwrap (); 696} 697let mut encoder =GzEncoder :: new ( Vec :: new (), Compression :: default ()); 698 encoder. write_all ( & tar_data). unwrap (); 699 encoder. finish (). unwrap () 700}; 701let integrity =Registry :: tarball_integrity ( & tarball); 702 server. publish_npm_dist ( "nested" , "1.0.0" , tarball, & integrity, None , true ); 703let dir = tempfile:: tempdir (). unwrap (); 704let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 705let artifact =block_on ( store. npm_acquire ( "npm:nested" )). unwrap (); 706assert_eq! ( 707 std:: fs:: read_to_string ( artifact. dir . join ( "lib" ). join ( "util.js" )). unwrap (), 708"export default 1;" 709); 710assert_eq! ( 711 artifact. read_file ( dir. path (), "lib/util.js" ). unwrap (), 712b"export default 1;" 713); 714assert! ( !artifact. files . contains_key ( "lib" )); 715} 716 717# [ test ] 718fn warmed_scan_checks_nested_siblings_and_unused_files () { 719let server =Registry :: new (); 720 server. publish_npm ( 721"nested-files" , 722"1.0.0" , 723& [ 724( 725"package.json" , 726r#"{"name":"nested-files","version":"1.0.0"}"# , 727), 728( "lib/entry.js" , "export const value = 1;" ), 729( "lib/sibling.js" , "export const sibling = 2;" ), 730( "lib/README.txt" , "not executable code" ), 731], 732); 733let dir = tempfile:: tempdir (). unwrap (); 734let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 735let artifact =block_on ( store. npm_acquire ( "npm:nested-files" )). unwrap (); 736 store. save (). unwrap (); 737 std:: fs:: write ( artifact. dir . join ( "lib/README.txt" ), "tampered" ). unwrap (); 738 739let offline =offline_frozen_options ( & server, dir. path ()); 740let store =PackageStore :: new ( fixture:: runtime_handle (), offline). unwrap (); 741assert! ( 742block_on ( store. npm_acquire ( "npm:nested-files" )) 743. unwrap_err () 744. to_string () 745. contains ( "corrupted" ) 746); 747} 748 749# [ cfg ( unix )] 750# [ test ] 751fn warmed_scan_rejects_nested_directory_symlinks () { 752use std:: os:: unix:: fs:: symlink; 753 754let server =Registry :: new (); 755 server. publish_npm ( 756"nested-link" , 757"1.0.0" , 758& [ 759( 760"package.json" , 761r#"{"name":"nested-link","version":"1.0.0"}"# , 762), 763( "lib/entry.js" , "export const value = 1;" ), 764], 765); 766let dir = tempfile:: tempdir (). unwrap (); 767let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 768let artifact =block_on ( store. npm_acquire ( "npm:nested-link" )). unwrap (); 769 store. save (). unwrap (); 770let outside = dir. path (). join ( "outside" ); 771 std:: fs:: create_dir ( & outside). unwrap (); 772 std:: fs:: write ( outside. join ( "entry.js" ), "export const value = 1;" ). unwrap (); 773 std:: fs:: remove_dir_all ( artifact. dir . join ( "lib" )). unwrap (); 774symlink ( outside, artifact. dir . join ( "lib" )). unwrap (); 775 776let offline =offline_frozen_options ( & server, dir. path ()); 777let store =PackageStore :: new ( fixture:: runtime_handle (), offline). unwrap (); 778assert! ( 779block_on ( store. npm_acquire ( "npm:nested-link" )) 780. unwrap_err () 781. to_string () 782. contains ( "corrupted" ) 783); 784} 785 786# [ test ] 787fn acquisition_rechecks_tree_after_materialization_callback () { 788use avril_runtime:: registry:: ProgressEvent ; 789use std:: sync:: atomic::{ AtomicBool , Ordering }; 790 791let server =Registry :: new (); 792 server. publish_npm ( 793"finish-check" , 794"1.0.0" , 795& [ 796( 797"package.json" , 798r#"{"name":"finish-check","version":"1.0.0"}"# , 799), 800( "index.js" , "export const value = 1;" ), 801], 802); 803let dir = tempfile:: tempdir (). unwrap (); 804let root = dir. path (). to_owned (); 805let tampered =Arc :: new ( AtomicBool :: new ( false )); 806let callback_tampered = tampered. clone (); 807let sink: Arc < dyn Fn ( & ProgressEvent ) +Send +Sync > =Arc :: new ( move |event|{ 808if matches! ( event, ProgressEvent :: NpmMaterialized { name, ..} if name =="finish-check" ) { 809let package_root = root. join ( ".avril/npm/default" ); 810let registry_dir = std:: fs:: read_dir ( package_root) 811. unwrap () 812. next () 813. unwrap () 814. unwrap () 815. path (); 816 std:: fs:: write ( registry_dir. join ( "finish-check/1.0.0/index.js" ), "tampered" ). unwrap (); 817 callback_tampered. store ( true , Ordering :: SeqCst ); 818} 819}); 820let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())) 821. unwrap () 822. with_progress ( sink); 823let error =block_on ( store. npm_acquire ( "npm:finish-check" )) 824. expect_err ( "acquisition must reject callback tampering" ); 825assert! ( 826 tampered. load ( Ordering :: SeqCst ), 827"callback did not tamper with the file" 828); 829assert! ( 830 error. to_string (). contains ( "integrity mismatch: index.js" ), 831"unexpected acquisition error: {error}" 832); 833} 834 835# [ test ] 836fn acquired_artifact_rechecks_bytes_when_read () { 837let server =Registry :: new (); 838 server. publish_npm ( 839"read-check" , 840"1.0.0" , 841& [ 842( "package.json" , r#"{"name":"read-check","version":"1.0.0"}"# ), 843( "lib/entry.js" , "export const value = 1;" ), 844], 845); 846let dir = tempfile:: tempdir (). unwrap (); 847let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 848let artifact =block_on ( store. npm_acquire ( "npm:read-check" )). unwrap (); 849assert_eq! ( 850 artifact. read_file ( dir. path (), "lib/entry.js" ). unwrap (), 851b"export const value = 1;" 852); 853 std:: fs:: write ( artifact. dir . join ( "lib/entry.js" ), "tampered" ). unwrap (); 854let error = artifact. read_file ( dir. path (), "lib/entry.js" ). unwrap_err (); 855assert! ( 856 error 857. to_string () 858. contains ( "integrity mismatch: lib/entry.js" ), 859"unexpected read error: {error}" 860); 861} 862 863# [ test ] 864fn malicious_tags_and_versions_never_reach_the_filesystem () { 865let server =Registry :: new (); 866 server. publish_npm ( 867"tagged" , 868"1.0.0" , 869& [( 870"package.json" , 871"{\"name\": \"tagged\", \"version\": \"1.0.0\"}" , 872)], 873); 874{ 875let mut files = server. files . lock (). unwrap (); 876let mut packument: serde_json:: Value = serde_json:: from_slice ( & files[ "/tagged" ]). unwrap (); 877 packument[ "dist-tags" ][ "evil" ] = serde_json:: Value :: String ( "../../evil" . into ()); 878 packument[ "versions" ][ "../../evil" ] = serde_json:: json!({ 879"name" : "tagged" , 880"version" : "../../evil" , 881"dist" : { "tarball" : "http://127.0.0.1:9/x.tgz" , "shasum" : "da39a3ee5e6b4b0d3255bfef95601890afd80709" }, 882}); 883 files. insert ( "/tagged" . into (), serde_json:: to_vec ( & packument). unwrap ()); 884} 885let dir = tempfile:: tempdir (). unwrap (); 886let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 887let before = server. requests . lock (). unwrap (). len (); 888assert! ( 889block_on ( store. npm_acquire ( "npm:tagged@evil" )) 890. unwrap_err () 891. to_string () 892. contains ( "invalid version" ) 893); 894assert! ( 895 server. requests . lock (). unwrap ()[ before..] 896. iter () 897. all ( |path| !path. ends_with ( ".tgz" )), 898"invalid selected versions must never be fetched" 899); 900assert! ( 901 !dir. path () 902. join ( ".avril" ) 903. join ( "npm" ) 904. join ( "default" ) 905. exists () 906); 907} 908 909# [ test ] 910# [ cfg ( unix )] 911fn warmed_readonly_trees_need_no_writes () { 912use std:: os:: unix:: fs:: PermissionsExt ; 913let server =Registry :: new (); 914 server. publish_npm ( 915"ro" , 916"1.0.0" , 917& [ 918( "package.json" , "{\"name\": \"ro\", \"version\": \"1.0.0\"}" ), 919( "index.js" , "export default 1;" ), 920], 921); 922let dir = tempfile:: tempdir (). unwrap (); 923let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 924let artifact =block_on ( store. npm_acquire ( "npm:ro" )). unwrap (); 925 store. save (). unwrap (); 926let lock_before = std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (); 927fn readonly ( path : & std:: path:: Path ) { 928for entryin walkdir ( path) { 929 std:: fs:: set_permissions ( & entry, std:: fs:: Permissions :: from_mode ( 0o555 )). unwrap (); 930} 931 std:: fs:: set_permissions ( path, std:: fs:: Permissions :: from_mode ( 0o555 )). unwrap (); 932} 933fn walkdir ( path : & std:: path:: Path ) ->Vec < std:: path:: PathBuf > { 934let mut out =Vec :: new (); 935for entryin std:: fs:: read_dir ( path). unwrap () { 936let entry = entry. unwrap (). path (); 937if entry. is_dir () { 938 out. extend ( walkdir ( & entry)); 939} 940 out. push ( entry); 941} 942 out 943} 944let npm_tree = dir. path (). join ( ".avril" ). join ( "npm" ); 945readonly ( & npm_tree); 946let offline =offline_frozen_options ( & server, dir. path ()); 947let store =PackageStore :: new ( fixture:: runtime_handle (), offline). unwrap (); 948let hit =block_on ( store. npm_acquire ( "npm:ro" )). unwrap (); 949assert_eq! ( hit. dir , artifact. dir ); 950assert_eq! ( 951 std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (), 952 lock_before 953); 954for entryin walkdir ( & npm_tree) { 955let _ = std:: fs:: set_permissions ( & entry, std:: fs:: Permissions :: from_mode ( 0o755 )); 956} 957let _ = std:: fs:: set_permissions ( & npm_tree, std:: fs:: Permissions :: from_mode ( 0o755 )); 958} 959 960# [ test ] 961fn registries_are_isolated_on_disk_and_in_locks () { 962let first =Registry :: new (); 963 first. publish_npm ( 964"iso" , 965"1.0.0" , 966& [( 967"package.json" , 968"{\"name\": \"iso\", \"version\": \"1.0.0\"}" , 969)], 970); 971let second =Registry :: new (); 972 second. publish_npm ( 973"iso" , 974"9.9.9" , 975& [( 976"package.json" , 977"{\"name\": \"iso\", \"version\": \"9.9.9\"}" , 978)], 979); 980let dir = tempfile:: tempdir (). unwrap (); 981let mut a = first. options ( dir. path ()); 982 a. lock = dir. path (). join ( "a.lock" ); 983let store =PackageStore :: new ( fixture:: runtime_handle (), a). unwrap (); 984let artifact_a =block_on ( store. npm_acquire ( "npm:iso" )). unwrap (); 985 store. save (). unwrap (); 986let mut b = second. options ( dir. path ()); 987 b. lock = dir. path (). join ( "b.lock" ); 988let store =PackageStore :: new ( fixture:: runtime_handle (), b). unwrap (); 989let artifact_b =block_on ( store. npm_acquire ( "npm:iso" )). unwrap (); 990assert_eq! ( artifact_b. identity . version , "9.9.9" ); 991assert_ne! ( artifact_a. dir , artifact_b. dir ); 992// A lock pinned to one registry is rejected under another. 993let mut mismatched = first. options ( dir. path ()); 994 mismatched. lock = dir. path (). join ( "a.lock" ); 995 mismatched. npm_registry = second. url . clone (); 996assert! ( 997PackageStore :: new ( fixture:: runtime_handle (), mismatched) 998. map ( |_|()) 999. unwrap_err () 1000. to_string () 1001. contains ( "npm registry" ) 1002); 1003} 1004 1005# [ test ] 1006fn corrupted_caches_fail_closed () { 1007let server =Registry :: new (); 1008 server. publish_npm ( 1009"cache" , 1010"1.0.0" , 1011& [ 1012( 1013"package.json" , 1014"{\"name\": \"cache\", \"version\": \"1.0.0\"}" , 1015), 1016( "index.js" , "export default 1;" ), 1017], 1018); 1019let dir = tempfile:: tempdir (). unwrap (); 1020let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1021block_on ( store. npm_acquire ( "npm:cache" )). unwrap (); 1022 store. save (). unwrap (); 1023let mut blob =None ; 1024let artifacts = dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".artifacts" ); 1025for entryin std:: fs:: read_dir ( artifacts. join ( "tarballs" )). unwrap () { 1026let path = entry. unwrap (). path (); 1027 blob =Some ( path); 1028} 1029let blob = blob. unwrap (); 1030let mut bytes = std:: fs:: read ( & blob). unwrap (); 1031let middle = bytes. len () /2 ; 1032 bytes[ middle] ^=0xff ; 1033 std:: fs:: write ( & blob, bytes). unwrap (); 1034// Evict the tree so the corrupt cache entry is actually read. 1035for entryin std:: fs:: read_dir ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( "default" )). unwrap () { 1036 std:: fs:: remove_dir_all ( entry. unwrap (). path ()). unwrap (); 1037} 1038let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1039assert! ( 1040block_on ( store. npm_acquire ( "npm:cache" )) 1041. unwrap_err () 1042. to_string () 1043. contains ( "integrity mismatch" ) 1044); 1045} 1046 1047# [ test ] 1048fn version_and_file_grants_authorize_without_tarball_urls () { 1049use avril_runtime:: caps::{ Capabilities , Category , Initial }; 1050let server =Registry :: new (); 1051 server. publish_npm ( 1052"scoped" , 1053"4.0.0" , 1054& [ 1055( 1056"package.json" , 1057"{\"name\": \"scoped\", \"version\": \"4.0.0\"}" , 1058), 1059( "index.js" , "export default 1;" ), 1060( "secret.js" , "export default 2;" ), 1061], 1062); 1063let dir = tempfile:: tempdir (). unwrap (); 1064let version_url =format! ( "{}scoped/4.0.0/" , server. url ); 1065let mut initial =Initial :: new ( true ); 1066 initial 1067. grant ( Category :: JsExec , false , & version_url, dir. path ()) 1068. unwrap (); 1069let caps =Capabilities :: new ( initial, dir. path (). to_owned ()); 1070let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())) 1071. unwrap () 1072. with_capabilities ( caps); 1073let artifact =block_on ( store. npm_acquire ( "npm:scoped" )). unwrap (); 1074assert_eq! ( artifact. identity . version , "4.0.0" ); 1075 1076// A grant on one file alone still authorizes the package fetch. 1077let dir = tempfile:: tempdir (). unwrap (); 1078let file_url =format! ( "{}scoped/4.0.0/index.js" , server. url ); 1079let mut initial =Initial :: new ( true ); 1080 initial 1081. grant ( Category :: JsExec , false , & file_url, dir. path ()) 1082. unwrap (); 1083let caps =Capabilities :: new ( initial, dir. path (). to_owned ()); 1084// Seed the second project from the first server without extra grants. 1085let mut options = server. options ( dir. path ()); 1086 options. cache = dir. path (). join ( "cache" ); 1087let store =PackageStore :: new ( fixture:: runtime_handle (), options) 1088. unwrap () 1089. with_capabilities ( caps); 1090let artifact =block_on ( store. npm_acquire ( "npm:scoped" )). unwrap (); 1091assert_eq! ( 1092 artifact. read_file ( dir. path (), "index.js" ). unwrap (), 1093b"export default 1;" 1094); 1095 1096// A denied sibling does not block the package, and revoking the version 1097// base blocks later acquisitions without further fetches. 1098let dir = tempfile:: tempdir (). unwrap (); 1099let mut initial =Initial :: new ( true ); 1100 initial 1101. grant ( Category :: JsExec , false , & version_url, dir. path ()) 1102. unwrap (); 1103let caps =Capabilities :: new ( initial, dir. path (). to_owned ()); 1104let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())) 1105. unwrap () 1106. with_capabilities ( caps. clone ()); 1107block_on ( store. npm_acquire ( "npm:scoped" )). unwrap (); 1108let fetched = server. requests . lock (). unwrap (). len (); 1109 caps. pledge ( vec! [( Category :: JsExec , None , vec! [ version_url. clone ()])]) 1110. unwrap (); 1111assert! ( 1112block_on ( store. npm_acquire ( "npm:scoped" )) 1113. unwrap_err () 1114. to_string () 1115. contains ( "PermissionDenied" ) 1116); 1117assert_eq! ( server. requests . lock (). unwrap (). len (), fetched); 1118} 1119 1120# [ test ] 1121fn failed_prepares_do_not_leak_acquisitions_into_save_graph () { 1122use avril_runtime:: registry:: npm:: resolution::{ NpmResolver , NpmRootRequest }; 1123use std:: collections::{ BTreeMap , BTreeSet , HashSet }; 1124let server =Registry :: new (); 1125 server. publish_npm ( 1126"plug" , 1127"1.0.0" , 1128& [ 1129( "package.json" , "{\"name\": \"plug\", \"version\": \"1.0.0\", \"type\": \"module\", \"peerDependencies\": {\"host\": \"^1.0.0\"}}" ), 1130( "index.js" , "export {version} from 'host';" ), 1131], 1132); 1133for versionin [ "1.0.0" , "2.0.0" ] { 1134 server. publish_npm ( 1135"host" , 1136 version, 1137& [ 1138( 1139"package.json" , 1140& format! ( 1141"{{\"name\": \"host\", \"version\": \"{version}\", \"type\": \"module\"}}" 1142), 1143), 1144( "index.js" , "export const version = 'h';" ), 1145], 1146); 1147} 1148 server. publish_npm ( 1149"good" , 1150"1.0.0" , 1151& [( 1152"package.json" , 1153"{\"name\": \"good\", \"version\": \"1.0.0\", \"type\": \"module\"}" , 1154)], 1155); 1156let dir = tempfile:: tempdir (). unwrap (); 1157let store = 1158Arc :: new ( PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap ()); 1159let resolver =NpmResolver :: new ( store. clone ()); 1160let host2 =format! ( "{}|host|2.0.0" , server. url . as_str ()); 1161// The conflicting peer fails the graph after its acquisitions recorded 1162// usage; the concurrent success must not save any of that. 1163let ( failed, succeeded) =block_on ( futures:: future:: join ( 1164 resolver. prepare ( & [ NpmRootRequest { 1165request : "npm:plug@1" . into (), 1166context : BTreeMap :: from ([( "host" . to_owned (), host2)]), 1167}]), 1168 resolver. prepare ( & [ NpmRootRequest { 1169request : "npm:good@1" . into (), 1170context : BTreeMap :: new (), 1171}]), 1172)); 1173 failed. unwrap_err (); 1174let handle = succeeded. unwrap (); 1175let tracked: BTreeSet < String > = handle. roots . keys (). cloned (). collect (); 1176 store. save_graph ( & tracked, & HashSet :: new ()). unwrap (); 1177let lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 1178let text = serde_json:: to_string ( & lock). unwrap (); 1179assert! ( text. contains ( "npm:good@1" ), "{text}" ); 1180assert! ( !text. contains ( "plug" ), "{text}" ); 1181// Pruning afterwards cannot resurrect the failed acquisitions either. 1182 store. prune (). unwrap (); 1183let lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 1184let text = serde_json:: to_string ( & lock). unwrap (); 1185assert! ( text. contains ( "npm:good@1" ), "{text}" ); 1186assert! ( !text. contains ( "plug" ), "{text}" ); 1187} 1188 1189# [ test ] 1190fn save_graph_and_prune_keep_only_successful_contexts () { 1191use avril_runtime:: registry:: npm:: resolution::{ NpmResolver , NpmRootRequest }; 1192use std:: collections::{ BTreeMap , BTreeSet , HashSet }; 1193let server =Registry :: new (); 1194 server. publish_npm ( 1195"plug" , 1196"1.0.0" , 1197& [ 1198( "package.json" , "{\"name\": \"plug\", \"version\": \"1.0.0\", \"type\": \"module\", \"peerDependencies\": {\"host\": \"*\"}}" ), 1199( "index.js" , "export {version} from 'host';" ), 1200], 1201); 1202for versionin [ "1.0.0" , "2.0.0" ] { 1203 server. publish_npm ( 1204"host" , 1205 version, 1206& [ 1207( 1208"package.json" , 1209& format! ( 1210"{{\"name\": \"host\", \"version\": \"{version}\", \"type\": \"module\"}}" 1211), 1212), 1213( "index.js" , "export const version = 'h';" ), 1214], 1215); 1216} 1217let dir = tempfile:: tempdir (). unwrap (); 1218let store = 1219Arc :: new ( PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap ()); 1220let resolver =NpmResolver :: new ( store. clone ()); 1221let context = |version : & str |{ 1222BTreeMap :: from ([( 1223"host" . to_owned (), 1224format! ( "{}|host|{version}" , server. url . as_str ()), 1225)]) 1226}; 1227let _first =block_on ( resolver. prepare ( & [ NpmRootRequest { 1228request : "npm:plug@1" . into (), 1229context : context ( "1.0.0" ), 1230}])) 1231. unwrap (); 1232let second =block_on ( resolver. prepare ( & [ NpmRootRequest { 1233request : "npm:plug@1" . into (), 1234context : context ( "2.0.0" ), 1235}])) 1236. unwrap (); 1237// Same request, two peer contexts: saving only the second keeps 1238// exactly its roots and instances, never base-wide leftovers. 1239let tracked: BTreeSet < String > = second. roots . keys (). cloned (). collect (); 1240assert! ( !tracked. is_empty ()); 1241 store. save_graph ( & tracked, & HashSet :: new ()). unwrap (); 1242let lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 1243let roots = lock[ "npm" ][ "roots" ]. as_object (). unwrap (); 1244assert_eq! ( roots. len (), 1 , "{roots:?}" ); 1245assert! ( 1246 roots. keys (). next (). unwrap (). contains ( "|host|2.0.0" ), 1247"{roots:?}" 1248); 1249let instances = lock[ "npm" ][ "instances" ]. as_object (). unwrap (); 1250assert! ( 1251 instances. keys (). all ( |id|{ 1252let pin =& instances[ id]; 1253 !serde_json:: to_string ( pin). unwrap (). contains ( "|host|1.0.0" ) 1254}), 1255"{instances:?}" 1256); 1257 store. prune (). unwrap (); 1258let lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 1259assert_eq! ( lock[ "npm" ][ "roots" ]. as_object (). unwrap (). len (), 1 ); 1260} 1261 1262# [ test ] 1263# [ cfg ( unix )] 1264fn symlinked_npm_parent_fails_closed () { 1265use std:: os:: unix:: fs:: symlink; 1266let server =Registry :: new (); 1267 server. publish_npm ( 1268"linkparent" , 1269"1.0.0" , 1270& [( 1271"package.json" , 1272"{\"name\": \"linkparent\", \"version\": \"1.0.0\"}" , 1273)], 1274); 1275let dir = tempfile:: tempdir (). unwrap (); 1276let evil = tempfile:: tempdir (). unwrap (); 1277symlink ( evil. path (), dir. path (). join ( ".avril" )). unwrap (); 1278let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1279let error =block_on ( store. npm_acquire ( "npm:linkparent" )) 1280. unwrap_err () 1281. to_string (); 1282// No-follow descents reject the swapped parent instead of writing 1283// through it; the exact OS error text is platform dependent. 1284assert! ( 1285 error. contains ( "usable" ) 1286 || error. contains ( "corrupt" ) 1287 || error. contains ( "project" ) 1288 || error. contains ( "directory" ) 1289 || error. contains ( "Directory" ), 1290"{error}" 1291); 1292// Nothing was written through the link. 1293assert! ( 1294 std:: fs:: read_dir ( evil. path ()). unwrap (). next (). is_none (), 1295"symlinked parent must not be written through" 1296); 1297} 1298 1299# [ test ] 1300# [ cfg ( unix )] 1301fn cache_file_symlink_fails_closed () { 1302use std:: os:: unix:: fs:: symlink; 1303let server =Registry :: new (); 1304 server. publish_npm ( 1305"cachelink" , 1306"1.0.0" , 1307& [ 1308( 1309"package.json" , 1310"{\"name\": \"cachelink\", \"version\": \"1.0.0\"}" , 1311), 1312( "index.js" , "export default 1;" ), 1313], 1314); 1315let dir = tempfile:: tempdir (). unwrap (); 1316let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1317let artifact =block_on ( store. npm_acquire ( "npm:cachelink" )). unwrap (); 1318 store. save (). unwrap (); 1319// Replace the tarball cache blob with a symlink, even one pointing at 1320// valid bytes, and evict the tree so the cache is actually read. 1321let mut blob =None ; 1322for entryin std:: fs:: read_dir ( 1323 dir. path () 1324. join ( ".avril" ) 1325. join ( "npm" ) 1326. join ( ".artifacts" ) 1327. join ( "tarballs" ), 1328) 1329. unwrap () 1330{ 1331 blob =Some ( entry. unwrap (). path ()); 1332} 1333let blob = blob. unwrap (); 1334let bytes = std:: fs:: read ( & blob). unwrap (); 1335 std:: fs:: remove_file ( & blob). unwrap (); 1336let elsewhere = dir. path (). join ( "elsewhere.tgz" ); 1337 std:: fs:: write ( & elsewhere, & bytes). unwrap (); 1338symlink ( & elsewhere, & blob). unwrap (); 1339 std:: fs:: remove_dir_all ( & artifact. dir ). unwrap (); 1340let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1341let error =block_on ( store. npm_acquire ( "npm:cachelink" )) 1342. unwrap_err () 1343. to_string (); 1344assert! ( 1345 error. contains ( "usable" ) || error. contains ( "integrity" ) || error. contains ( "corrupt" ), 1346"{error}" 1347); 1348} 1349 1350# [ test ] 1351# [ cfg ( unix )] 1352fn lock_path_symlink_and_fifo_fail_without_hanging () { 1353use std:: os:: unix:: fs:: symlink; 1354let server =Registry :: new (); 1355 server. publish_npm ( 1356"locklink" , 1357"1.0.0" , 1358& [ 1359( 1360"package.json" , 1361"{\"name\": \"locklink\", \"version\": \"1.0.0\"}" , 1362), 1363( "index.js" , "export default 1;" ), 1364], 1365); 1366let dir = tempfile:: tempdir (). unwrap (); 1367let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1368let artifact =block_on ( store. npm_acquire ( "npm:locklink" )). unwrap (); 1369 store. save (). unwrap (); 1370// Verified hits take no lock: removing the lock tree entirely still hits. 1371 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".locks" )). unwrap (); 1372let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1373assert! ( block_on ( store. npm_acquire ( "npm:locklink" )). is_ok ()); 1374// A symlinked lock directory fails a miss instead of locking elsewhere. 1375 std:: fs:: remove_dir_all ( & artifact. dir ). unwrap (); 1376let evil = tempfile:: tempdir (). unwrap (); 1377symlink ( 1378 evil. path (), 1379 dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".locks" ), 1380) 1381. unwrap (); 1382let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1383let error =block_on ( store. npm_acquire ( "npm:locklink" )) 1384. unwrap_err () 1385. to_string (); 1386assert! ( 1387 error. contains ( "usable" ) 1388 || error. contains ( "regular" ) 1389 || error. contains ( "corrupt" ) 1390 || error. contains ( "lock" ) 1391 || error. contains ( "directory" ) 1392 || error. contains ( "Directory" ), 1393"{error}" 1394); 1395assert! ( std:: fs:: read_dir ( evil. path ()). unwrap (). next (). is_none ()); 1396// A FIFO in place of the lock directory fails promptly instead of hanging. 1397 std:: fs:: remove_file ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".locks" )). unwrap (); 1398nix_fifo ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".locks" )); 1399let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1400let error =block_on ( store. npm_acquire ( "npm:locklink" )) 1401. unwrap_err () 1402. to_string (); 1403assert! ( 1404 error. contains ( "usable" ) 1405 || error. contains ( "directory" ) 1406 || error. contains ( "corrupt" ) 1407 || error. contains ( "lock" ), 1408"{error}" 1409); 1410} 1411 1412# [ test ] 1413# [ cfg ( unix )] 1414fn materialized_fifo_sources_fail_promptly () { 1415let server =Registry :: new (); 1416 server. publish_npm ( 1417"fifosrc" , 1418"1.0.0" , 1419& [ 1420( 1421"package.json" , 1422"{\"name\": \"fifosrc\", \"version\": \"1.0.0\"}" , 1423), 1424( "index.js" , "export default 1;" ), 1425], 1426); 1427let dir = tempfile:: tempdir (). unwrap (); 1428let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1429let artifact =block_on ( store. npm_acquire ( "npm:fifosrc" )). unwrap (); 1430 store. save (). unwrap (); 1431// Swap a materialized source for a FIFO: the bounded regular-file read 1432// must fail promptly (non-blocking open + fstat) rather than hang. 1433 std:: fs:: remove_file ( artifact. dir . join ( "index.js" )). unwrap (); 1434nix_fifo ( artifact. dir . join ( "index.js" )); 1435let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1436let ( send, receive) = std:: sync:: mpsc:: channel (); 1437 std:: thread:: spawn ( move ||{ 1438 send. send ( block_on ( store. npm_acquire ( "npm:fifosrc" ))) 1439. unwrap (); 1440}); 1441let result = receive 1442. recv_timeout ( std:: time:: Duration :: from_secs ( 15 )) 1443. expect ( "materialized FIFO must fail promptly, not hang" ); 1444let error = result. unwrap_err (). to_string (); 1445assert! ( 1446 error. contains ( "regular" ) || error. contains ( "corrupt" ) || error. contains ( "missing" ), 1447"{error}" 1448); 1449// The loader-facing primitive fails the same way. 1450let error = artifact 1451. read_file ( dir. path (), "index.js" ) 1452. unwrap_err () 1453. to_string (); 1454assert! ( 1455 error. contains ( "regular" ) || error. contains ( "missing" ) || error. contains ( "corrupt" ), 1456"{error}" 1457); 1458} 1459 1460# [ test ] 1461# [ cfg ( unix )] 1462fn symlinked_vendor_parent_fails_closed () { 1463use std:: collections::{ BTreeMap , HashSet }; 1464use std:: os:: unix:: fs:: symlink; 1465let server =Registry :: new (); 1466 server. publish_npm ( 1467"vendlink" , 1468"1.0.0" , 1469& [ 1470( 1471"package.json" , 1472"{\"name\": \"vendlink\", \"version\": \"1.0.0\"}" , 1473), 1474( "index.js" , "export default 1;" ), 1475], 1476); 1477let dir = tempfile:: tempdir (). unwrap (); 1478let store = std:: sync:: Arc :: new ( 1479PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (), 1480); 1481let resolver = avril_runtime:: registry:: npm:: resolution:: NpmResolver :: new ( store. clone ()); 1482let handle = 1483block_on ( 1484 resolver. prepare ( & [ avril_runtime:: registry:: npm:: resolution:: NpmRootRequest { 1485request : "npm:vendlink" . into (), 1486context : BTreeMap :: new (), 1487}]), 1488) 1489. unwrap (); 1490let tracked: std:: collections:: BTreeSet < String > = handle. roots . keys (). cloned (). collect (); 1491 store. save_graph ( & tracked, & HashSet :: new ()). unwrap (); 1492let vendor = dir. path (). join ( "vendor-real" ); 1493 store. vendor ( & vendor). unwrap (); 1494assert! ( vendor. join ( "npm" ). exists ()); 1495let redirected = tempfile:: tempdir (). unwrap (); 1496 std:: fs:: rename ( vendor. join ( "npm" ), vendor. join ( "npm-real" )). unwrap (); 1497symlink ( redirected. path (), vendor. join ( "npm" )). unwrap (); 1498assert! ( store. vendor ( & vendor). is_err ()); 1499assert! ( 1500 std:: fs:: read_dir ( redirected. path ()) 1501. unwrap () 1502. next () 1503. is_none () 1504); 1505 std:: fs:: remove_file ( vendor. join ( "npm" )). unwrap (); 1506 std:: fs:: rename ( vendor. join ( "npm-real" ), vendor. join ( "npm" )). unwrap (); 1507 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" )). unwrap (); 1508// A symlinked directory inside the vendor mirror must fail instead of 1509// being followed or silently ignored. 1510let evil = tempfile:: tempdir (). unwrap (); 1511 std:: fs:: rename ( vendor. join ( "npm" ), vendor. join ( "npm-real" )). unwrap (); 1512symlink ( evil. path (), vendor. join ( "npm" )). unwrap (); 1513let mut options =offline_frozen_options ( & server, dir. path ()); 1514 options. vendor =Some ( vendor. clone ()); 1515let store =PackageStore :: new ( fixture:: runtime_handle (), options). unwrap (); 1516let error =block_on ( store. npm_acquire ( "npm:vendlink" )) 1517. unwrap_err () 1518. to_string (); 1519assert! ( 1520 error. contains ( "vendor" ) 1521 || error. contains ( "usable" ) 1522 || error. contains ( "corrupt" ) 1523 || error. contains ( "Offline" ), 1524"{error}" 1525); 1526assert! ( std:: fs:: read_dir ( evil. path ()). unwrap (). next (). is_none ()); 1527// Sanity: the intact mirror reconstructs the same tree offline. 1528 std:: fs:: remove_file ( vendor. join ( "npm" )). unwrap (); 1529 std:: fs:: rename ( vendor. join ( "npm-real" ), vendor. join ( "npm" )). unwrap (); 1530let mut options =offline_frozen_options ( & server, dir. path ()); 1531 options. vendor =Some ( vendor); 1532let store =PackageStore :: new ( fixture:: runtime_handle (), options). unwrap (); 1533let restored =block_on ( store. npm_acquire ( "npm:vendlink" )). unwrap (); 1534assert_eq! ( 1535 std:: fs:: read_to_string ( restored. dir . join ( "index.js" )). unwrap (), 1536"export default 1;" 1537); 1538} 1539 1540# [ cfg ( unix )] 1541fn nix_fifo ( path : std:: path:: PathBuf ) { 1542let status = std:: process:: Command :: new ( "mkfifo" ) 1543. arg ( & path) 1544. status () 1545. expect ( "mkfifo is required for FIFO tests" ); 1546assert! ( status. success ()); 1547} 1548 1549/// Rewrite every compact pin on disk into legacy wire form using the 1550/// published sidecars, then drop the sidecars: the tree stays warm but only 1551/// legacy maps authenticate it. 1552fn downconvert_lock_to_legacy ( dir : & std:: path:: Path ) { 1553let manifests = dir. join ( ".avril" ). join ( "npm" ). join ( ".manifests" ); 1554let mut lock = fixture:: read_lock ( dir); 1555for pinin lock[ "npm" ][ "packages" ] 1556. as_object_mut () 1557. unwrap () 1558. values_mut () 1559{ 1560let manifest = pin[ "manifest" ]. as_str (). unwrap (). to_owned (); 1561let hex = manifest. strip_prefix ( "sha256-" ). unwrap (); 1562let bytes = std:: fs:: read ( manifests. join ( format! ( "{hex}.json" ))). unwrap (); 1563let files: serde_json:: Value = serde_json:: from_slice ( & bytes). unwrap (); 1564* pin = serde_json:: json!({ 1565"tarball" : pin[ "tarball" ]. clone (), 1566"integrity" : pin[ "integrity" ]. clone (), 1567"files" : files, 1568}); 1569} 1570 std:: fs:: write ( dir. join ( "avril.lock" ), format! ( "{lock}\n" )). unwrap (); 1571 std:: fs:: remove_dir_all ( manifests). unwrap (); 1572} 1573 1574# [ test ] 1575fn compact_lock_carries_no_file_maps () { 1576let server =Registry :: new (); 1577let mut many =vec! [( 1578"package.json" , 1579"{\"name\": \"many\", \"version\": \"1.0.0\"}" . to_string (), 1580)]; 1581for indexin 0 ..40 { 1582 many. push (( 1583Box :: leak ( format! ( "data/{index}.js" ). into_boxed_str ()) as &' static str , 1584format! ( "export default {index};" ), 1585)); 1586} 1587let refs: Vec <( & str , & str )> = many 1588. iter () 1589. map ( |( path, source) |( * path, source. as_str ())) 1590. collect (); 1591 server. publish_npm ( "many" , "1.0.0" , & refs); 1592let dir = tempfile:: tempdir (). unwrap (); 1593let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1594let artifact =block_on ( store. npm_acquire ( "npm:many" )). unwrap (); 1595assert_eq! ( artifact. files . len (), 41 ); 1596 store. save (). unwrap (); 1597let lock = fixture:: read_lock ( dir. path ()); 1598let pins = lock[ "npm" ][ "packages" ]. as_object (). unwrap (); 1599assert_eq! ( pins. len (), 1 ); 1600let pin = pins. values (). next (). unwrap (); 1601assert! ( 1602 pin[ "manifest" ] 1603. as_str () 1604. is_some_and ( |digest| digest. starts_with ( "sha256-" )), 1605"{pin}" 1606); 1607assert! ( pin. get ( "files" ). is_none (), "compact pins omit the file map" ); 1608// One short digest regardless of file count: the whole entry stays small. 1609assert! ( serde_json:: to_string ( pin). unwrap (). len () <1024 , "{pin}" ); 1610// The sidecar holds the full authenticated map instead. 1611let manifests = std:: fs:: read_dir ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".manifests" )) 1612. unwrap () 1613. count (); 1614assert_eq! ( manifests, 1 ); 1615} 1616 1617# [ test ] 1618fn warm_offline_frozen_runs_without_archives_or_metadata () { 1619let server =Registry :: new (); 1620 server. publish_npm ( 1621"slim" , 1622"1.0.0" , 1623& [ 1624( 1625"package.json" , 1626"{\"name\": \"slim\", \"version\": \"1.0.0\"}" , 1627), 1628( "index.js" , "export default 1;" ), 1629], 1630); 1631let dir = tempfile:: tempdir (). unwrap (); 1632let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1633block_on ( store. npm_acquire ( "npm:slim" )). unwrap (); 1634 store. save (). unwrap (); 1635// No retained tarball is required: drop both caches, keep the sidecar. 1636 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".artifacts" )). unwrap (); 1637 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".metadata" )). unwrap (); 1638assert! ( 1639 dir. path () 1640. join ( ".avril" ) 1641. join ( "npm" ) 1642. join ( ".manifests" ) 1643. is_dir (), 1644"sidecars live outside the deletable caches" 1645); 1646let warmed = server. requests . lock (). unwrap (). len (); 1647let offline =offline_frozen_options ( & server, dir. path ()); 1648let store =PackageStore :: new ( fixture:: runtime_handle (), offline). unwrap (); 1649let hit =block_on ( store. npm_acquire ( "npm:slim" )). unwrap (); 1650assert_eq! ( 1651 std:: fs:: read_to_string ( hit. dir . join ( "index.js" )). unwrap (), 1652"export default 1;" 1653); 1654assert_eq! ( server. requests . lock (). unwrap (). len (), warmed); 1655// Relocated read-only copies keep working: sidecars travel with the tree. 1656let moved = tempfile:: tempdir (). unwrap (); 1657copy_npm_tree ( 1658& dir. path (). join ( ".avril" ). join ( "npm" ), 1659& moved. path (). join ( ".avril" ). join ( "npm" ), 1660); 1661 std:: fs:: copy ( 1662 dir. path (). join ( "avril.lock" ), 1663 moved. path (). join ( "avril.lock" ), 1664) 1665. unwrap (); 1666# [ cfg ( unix )] 1667chmod_readonly ( & moved. path (). join ( ".avril" )); 1668let mut relocated = server. options ( moved. path ()); 1669 relocated. offline =true ; 1670 relocated. frozen =true ; 1671// A relocated lock stays valid only with matching registry options. 1672 relocated. jsr_registry = server. url . clone (); 1673 relocated. npm_registry = server. url . clone (); 1674let store =PackageStore :: new ( fixture:: runtime_handle (), relocated). unwrap (); 1675assert! ( block_on ( store. npm_acquire ( "npm:slim" )). is_ok ()); 1676assert_eq! ( server. requests . lock (). unwrap (). len (), warmed); 1677# [ cfg ( unix )] 1678chmod_writable ( & moved. path (). join ( ".avril" )); 1679} 1680 1681# [ cfg ( unix )] 1682fn chmod_readonly ( path : & std:: path:: Path ) { 1683use std:: os:: unix:: fs:: PermissionsExt ; 1684let mut stack =vec! [ path. to_owned ()]; 1685while let Some ( next) = stack. pop () { 1686for entryin std:: fs:: read_dir ( & next). unwrap () { 1687let entry = entry. unwrap (). path (); 1688// Directories keep their search bit so the walk below them 1689// still works; only the final mode on each level is locked down. 1690if entry. is_dir () { 1691 stack. push ( entry. clone ()); 1692 std:: fs:: set_permissions ( & entry, std:: fs:: Permissions :: from_mode ( 0o555 )). unwrap (); 1693} else { 1694 std:: fs:: set_permissions ( & entry, std:: fs:: Permissions :: from_mode ( 0o444 )). unwrap (); 1695} 1696} 1697 std:: fs:: set_permissions ( & next, std:: fs:: Permissions :: from_mode ( 0o555 )). unwrap (); 1698} 1699} 1700 1701# [ cfg ( unix )] 1702fn chmod_writable ( path : & std:: path:: Path ) { 1703use std:: os:: unix:: fs:: PermissionsExt ; 1704let mut stack =vec! [ path. to_owned ()]; 1705while let Some ( next) = stack. pop () { 1706for entryin std:: fs:: read_dir ( & next). unwrap () { 1707let entry = entry. unwrap (). path (); 1708if entry. is_dir () { 1709 stack. push ( entry. clone ()); 1710} 1711let _ = std:: fs:: set_permissions ( & entry, std:: fs:: Permissions :: from_mode ( 0o755 )); 1712} 1713let _ = std:: fs:: set_permissions ( next, std:: fs:: Permissions :: from_mode ( 0o755 )); 1714} 1715} 1716 1717fn copy_npm_tree ( from : & std:: path:: Path , to : & std:: path:: Path ) { 1718 std:: fs:: create_dir_all ( to). unwrap (); 1719for entryin std:: fs:: read_dir ( from). unwrap () { 1720let entry = entry. unwrap (); 1721let target = to. join ( entry. file_name ()); 1722if entry. file_type (). unwrap (). is_dir () { 1723copy_npm_tree ( & entry. path (), & target); 1724} else { 1725 std:: fs:: copy ( entry. path (), target). unwrap (); 1726} 1727} 1728} 1729 1730# [ test ] 1731fn tampered_sidecar_and_tree_fail_without_tarball () { 1732let server =Registry :: new (); 1733 server. publish_npm ( 1734"sealed" , 1735"1.0.0" , 1736& [ 1737( 1738"package.json" , 1739"{\"name\": \"sealed\", \"version\": \"1.0.0\"}" , 1740), 1741( "index.js" , "export default 1;" ), 1742], 1743); 1744let dir = tempfile:: tempdir (). unwrap (); 1745let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1746let artifact =block_on ( store. npm_acquire ( "npm:sealed" )). unwrap (); 1747 store. save (). unwrap (); 1748// No tarball anywhere: authentication must stand on the sidecar alone. 1749 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".artifacts" )). unwrap (); 1750let sidecar = std:: fs:: read_dir ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".manifests" )) 1751. unwrap () 1752. next () 1753. unwrap () 1754. unwrap () 1755. path (); 1756// A forged map with a valid shape but a changed file hash fails the 1757// digest even with matching tampered tree bytes and no tarball present. 1758let map_path = sidecar. clone (); 1759let mut map: serde_json:: Value = 1760 serde_json:: from_slice ( & std:: fs:: read ( & map_path). unwrap ()). unwrap (); 1761 map[ "index.js" ] = serde_json:: Value :: String ( avril_runtime:: registry:: checksum ( b"tampered" )); 1762 std:: fs:: write ( & sidecar, serde_json:: to_vec ( & map). unwrap ()). unwrap (); 1763 std:: fs:: write ( artifact. dir . join ( "index.js" ), "tampered" ). unwrap (); 1764let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1765assert! ( 1766block_on ( store. npm_acquire ( "npm:sealed" )) 1767. unwrap_err () 1768. to_string () 1769. contains ( "mismatch" ) 1770); 1771// A deleted sidecar plus a tampered tree fails offline with no archive. 1772 std:: fs:: remove_file ( & sidecar). unwrap (); 1773let mut offline = server. options ( dir. path ()); 1774 offline. offline =true ; 1775let store =PackageStore :: new ( fixture:: runtime_handle (), offline). unwrap (); 1776assert! ( block_on ( store. npm_acquire ( "npm:sealed" )). is_err ()); 1777// Healing the tree rebuilds from the network and republishes the sidecar. 1778 std:: fs:: remove_dir_all ( & artifact. dir ). unwrap (); 1779let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1780let healed =block_on ( store. npm_acquire ( "npm:sealed" )). unwrap (); 1781assert_eq! ( 1782 std:: fs:: read_to_string ( healed. dir . join ( "index.js" )). unwrap (), 1783"export default 1;" 1784); 1785assert_eq! ( 1786 std:: fs:: read_dir ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".manifests" )) 1787. unwrap () 1788. count (), 17891 1790); 1791} 1792 1793# [ test ] 1794fn missing_sidecar_recovers_from_archive_and_cold_fetch () { 1795let server =Registry :: new (); 1796 server. publish_npm ( 1797"rebuild" , 1798"1.0.0" , 1799& [ 1800( 1801"package.json" , 1802"{\"name\": \"rebuild\", \"version\": \"1.0.0\"}" , 1803), 1804( "index.js" , "export default 1;" ), 1805], 1806); 1807let dir = tempfile:: tempdir (). unwrap (); 1808let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1809let artifact =block_on ( store. npm_acquire ( "npm:rebuild@^1" )). unwrap (); 1810 store. save (). unwrap (); 1811let warmed = server. requests . lock (). unwrap (). len (); 1812// Tree and sidecar gone, tarball cache kept: offline archive rebuild. 1813 std:: fs:: remove_dir_all ( & artifact. dir ). unwrap (); 1814 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".manifests" )). unwrap (); 1815let mut offline = server. options ( dir. path ()); 1816 offline. offline =true ; 1817let store =PackageStore :: new ( fixture:: runtime_handle (), offline). unwrap (); 1818let rebuilt =block_on ( store. npm_acquire ( "npm:rebuild" )). unwrap (); 1819assert_eq! ( 1820 std:: fs:: read_to_string ( rebuilt. dir . join ( "index.js" )). unwrap (), 1821"export default 1;" 1822); 1823assert_eq! ( server. requests . lock (). unwrap (). len (), warmed); 1824assert_eq! ( 1825 std:: fs:: read_dir ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".manifests" )) 1826. unwrap () 1827. count (), 18281 , 1829"archive recovery republishes the sidecar" 1830); 1831// Nothing local at all: a cold pinned fetch still works, and frozen pins 1832// download artifacts without metadata queries. 1833 std:: fs:: remove_dir_all ( & artifact. dir ). unwrap (); 1834 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".manifests" )). unwrap (); 1835 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".artifacts" )). unwrap (); 1836let mut frozen = server. options ( dir. path ()); 1837 frozen. frozen =true ; 1838let store =PackageStore :: new ( fixture:: runtime_handle (), frozen). unwrap (); 1839let cold =block_on ( store. npm_acquire ( "npm:rebuild@^1" )). unwrap (); 1840assert_eq! ( cold. identity . version , "1.0.0" ); 1841let paths = server. requests . lock (). unwrap (). clone (); 1842assert! ( 1843 paths[ warmed..]. iter (). all ( |path| path. ends_with ( ".tgz" )), 1844"frozen pins must not query mutable packument metadata: {paths:?}" 1845); 1846} 1847 1848# [ test ] 1849# [ cfg ( unix )] 1850fn corrupt_symlink_and_fifo_manifests_fail_promptly () { 1851let server =Registry :: new (); 1852 server. publish_npm ( 1853"manifested" , 1854"1.0.0" , 1855& [ 1856( 1857"package.json" , 1858"{\"name\": \"manifested\", \"version\": \"1.0.0\"}" , 1859), 1860( "index.js" , "export default 1;" ), 1861], 1862); 1863let dir = tempfile:: tempdir (). unwrap (); 1864let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1865block_on ( store. npm_acquire ( "npm:manifested" )). unwrap (); 1866 store. save (). unwrap (); 1867let sidecar = std:: fs:: read_dir ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".manifests" )) 1868. unwrap () 1869. next () 1870. unwrap () 1871. unwrap () 1872. path (); 1873// Garbage bytes fail on the digest before any parse. 1874 std:: fs:: write ( & sidecar, b"not json at all" ). unwrap (); 1875let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1876assert! ( 1877block_on ( store. npm_acquire ( "npm:manifested" )) 1878. unwrap_err () 1879. to_string () 1880. contains ( "mismatch" ) 1881); 1882// A final-component symlink is never followed. 1883 std:: fs:: remove_file ( & sidecar). unwrap (); 1884 std:: os:: unix:: fs:: symlink ( dir. path (). join ( "avril.lock" ), & sidecar). unwrap (); 1885let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1886assert! ( 1887block_on ( store. npm_acquire ( "npm:manifested" )) 1888. unwrap_err () 1889. to_string () 1890. contains ( "manifest" ) 1891); 1892// A FIFO fails promptly instead of hanging the caller. 1893 std:: fs:: remove_file ( & sidecar). unwrap (); 1894nix_fifo ( sidecar. clone ()); 1895let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 1896let ( send, receive) = std:: sync:: mpsc:: channel (); 1897 std:: thread:: spawn ( move ||{ 1898 send. send ( block_on ( store. npm_acquire ( "npm:manifested" ))) 1899. unwrap (); 1900}); 1901let error = receive 1902. recv_timeout ( std:: time:: Duration :: from_secs ( 15 )) 1903. expect ( "manifest FIFO must fail promptly, not hang" ) 1904. unwrap_err () 1905. to_string (); 1906assert! ( error. contains ( "manifest" ), "{error}" ); 1907} 1908 1909# [ test ] 1910fn vendor_mirror_restores_offline_frozen_with_peers () { 1911use avril_runtime:: registry:: npm:: resolution::{ NpmResolver , NpmRootRequest }; 1912use std:: collections::{ BTreeMap , BTreeSet , HashSet }; 1913let server =Registry :: new (); 1914 server. publish_npm ( 1915"vutil" , 1916"1.0.0" , 1917& [( 1918"package.json" , 1919"{\"name\": \"vutil\", \"version\": \"1.0.0\", \"type\": \"module\"}" , 1920)], 1921); 1922 server. publish_npm ( 1923"vlib" , 1924"1.0.0" , 1925& [( 1926"package.json" , 1927"{\"name\": \"vlib\", \"version\": \"1.0.0\", \"type\": \"module\", \"peerDependencies\": {\"vutil\": \"*\"}}" , 1928)], 1929); 1930let dir = tempfile:: tempdir (). unwrap (); 1931let store = 1932Arc :: new ( PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap ()); 1933let resolver =NpmResolver :: new ( store. clone ()); 1934let handle =block_on ( resolver. prepare ( & [ 1935NpmRootRequest { 1936request : "npm:vlib@1" . into (), 1937context : BTreeMap :: new (), 1938}, 1939NpmRootRequest { 1940request : "npm:vutil@1" . into (), 1941context : BTreeMap :: new (), 1942}, 1943])) 1944. unwrap (); 1945let tracked: BTreeSet < String > = handle. roots . keys (). cloned (). collect (); 1946 store. save_graph ( & tracked, & HashSet :: new ()). unwrap (); 1947let vendor = dir. path (). join ( "vendor" ); 1948 store. vendor ( & vendor). unwrap (); 1949assert! ( 1950 vendor 1951. join ( "npm" ) 1952. join ( ".manifests" ) 1953. read_dir () 1954. unwrap () 1955. count () 1956 >=2 , 1957"the mirror carries one sidecar per artifact content" 1958); 1959let before: BTreeMap < String , String > = handle 1960. roots 1961. iter () 1962. map ( |( k, v) |( k. clone (), v. clone ())) 1963. collect (); 1964// Pure mirror: wipe all local state except the lock, then restore 1965// offline and frozen, including the peer-scoped instance. 1966 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" )). unwrap (); 1967let mut options =offline_frozen_options ( & server, dir. path ()); 1968 options. vendor =Some ( vendor); 1969let store =Arc :: new ( PackageStore :: new ( fixture:: runtime_handle (), options). unwrap ()); 1970let resolver =NpmResolver :: new ( store. clone ()); 1971let restored =block_on ( resolver. prepare ( & [ 1972NpmRootRequest { 1973request : "npm:vlib@1" . into (), 1974context : BTreeMap :: new (), 1975}, 1976NpmRootRequest { 1977request : "npm:vutil@1" . into (), 1978context : BTreeMap :: new (), 1979}, 1980])) 1981. unwrap (); 1982assert_eq! ( restored. roots . len (), before. len ()); 1983for ( key, instance) in & before{ 1984assert_eq! ( restored. roots . get ( key), Some ( instance), "{key}" ); 1985} 1986} 1987 1988# [ test ] 1989fn legacy_lock_migrates_used_and_preserves_unvisited () { 1990let server =Registry :: new (); 1991for namein [ "legacya" , "legacyb" ] { 1992 server. publish_npm ( 1993 name, 1994"1.0.0" , 1995& [( 1996"package.json" , 1997& format! ( "{{\"name\": \"{name}\", \"version\": \"1.0.0\"}}" ), 1998)], 1999); 2000} 2001let dir = tempfile:: tempdir (). unwrap (); 2002let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 2003block_on ( store. npm_acquire ( "npm:legacya" )). unwrap (); 2004block_on ( store. npm_acquire ( "npm:legacyb" )). unwrap (); 2005 store. save (). unwrap (); 2006downconvert_lock_to_legacy ( dir. path ()); 2007// Only legacya is visited: it migrates, legacyb stays byte-identical. 2008let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 2009block_on ( store. npm_acquire ( "npm:legacya" )). unwrap (); 2010 store. save (). unwrap (); 2011let lock = fixture:: read_lock ( dir. path ()); 2012let packages = lock[ "npm" ][ "packages" ]. as_object (). unwrap (); 2013let key_a = packages 2014. keys () 2015. find ( |key| key. contains ( "legacya" )) 2016. unwrap () 2017. clone (); 2018let key_b = packages 2019. keys () 2020. find ( |key| key. contains ( "legacyb" )) 2021. unwrap () 2022. clone (); 2023assert! ( packages[ & key_a]. get ( "files" ). is_none (), "used pins migrate" ); 2024assert! ( 2025 packages[ & key_a][ "manifest" ] 2026. as_str () 2027. is_some_and ( |digest| digest. starts_with ( "sha256-" )) 2028); 2029assert! ( 2030 packages[ & key_b][ "files" ]. as_object (). is_some (), 2031"unvisited legacy pins keep their inline map" 2032); 2033assert! ( packages[ & key_b]. get ( "manifest" ). is_none ()); 2034assert_eq! ( 2035 std:: fs:: read_dir ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".manifests" )) 2036. unwrap () 2037. count (), 20381 , 2039"sidecars publish only for visited pins" 2040); 2041// The unvisited legacy entry still acquires frozen, offline, and 2042// read-only with no tarballs: its warm tree authenticates inline. 2043 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".artifacts" )). unwrap (); 2044let lock_before = std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (); 2045# [ cfg ( unix )] 2046{ 2047use std:: os:: unix:: fs:: PermissionsExt ; 2048chmod_readonly ( & dir. path (). join ( ".avril" )); 2049 std:: fs:: set_permissions ( 2050 dir. path (). join ( "avril.lock" ), 2051 std:: fs:: Permissions :: from_mode ( 0o444 ), 2052) 2053. unwrap (); 2054} 2055let frozen =offline_frozen_options ( & server, dir. path ()); 2056let store =PackageStore :: new ( fixture:: runtime_handle (), frozen). unwrap (); 2057assert! ( block_on ( store. npm_acquire ( "npm:legacya" )). is_ok ()); 2058assert! ( block_on ( store. npm_acquire ( "npm:legacyb" )). is_ok ()); 2059 store. save (). unwrap (); 2060assert_eq! ( 2061 std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (), 2062 lock_before 2063); 2064# [ cfg ( unix )] 2065{ 2066use std:: os:: unix:: fs:: PermissionsExt ; 2067chmod_writable ( & dir. path (). join ( ".avril" )); 2068let _ = std:: fs:: set_permissions ( 2069 dir. path (). join ( "avril.lock" ), 2070 std:: fs:: Permissions :: from_mode ( 0o644 ), 2071); 2072} 2073} 2074 2075# [ test ] 2076# [ cfg ( unix )] 2077fn frozen_legacy_lock_needs_no_writes_and_stays_identical () { 2078use std:: os:: unix:: fs:: PermissionsExt ; 2079let server =Registry :: new (); 2080 server. publish_npm ( 2081"frozenlegacy" , 2082"1.0.0" , 2083& [( 2084"package.json" , 2085"{\"name\": \"frozenlegacy\", \"version\": \"1.0.0\"}" , 2086)], 2087); 2088let dir = tempfile:: tempdir (). unwrap (); 2089let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 2090block_on ( store. npm_acquire ( "npm:frozenlegacy" )). unwrap (); 2091 store. save (). unwrap (); 2092downconvert_lock_to_legacy ( dir. path ()); 2093// No caches at all: the warm tree plus the inline map must suffice. 2094 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".artifacts" )). unwrap (); 2095 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".metadata" )). unwrap (); 2096let lock_before = std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (); 2097chmod_readonly ( & dir. path (). join ( ".avril" )); 2098 std:: fs:: set_permissions ( 2099 dir. path (). join ( "avril.lock" ), 2100 std:: fs:: Permissions :: from_mode ( 0o444 ), 2101) 2102. unwrap (); 2103let frozen =offline_frozen_options ( & server, dir. path ()); 2104let store =PackageStore :: new ( fixture:: runtime_handle (), frozen). unwrap (); 2105assert! ( block_on ( store. npm_acquire ( "npm:frozenlegacy" )). is_ok ()); 2106 store. save (). unwrap (); 2107assert_eq! ( 2108 std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (), 2109 lock_before, 2110"frozen legacy locks stay byte-identical" 2111); 2112assert! ( 2113 !dir. path () 2114. join ( ".avril" ) 2115. join ( "npm" ) 2116. join ( ".manifests" ) 2117. exists (), 2118"frozen warm hits publish no sidecars" 2119); 2120chmod_writable ( & dir. path (). join ( ".avril" )); 2121let _ = std:: fs:: set_permissions ( 2122 dir. path (). join ( "avril.lock" ), 2123 std:: fs:: Permissions :: from_mode ( 0o644 ), 2124); 2125} 2126 2127# [ test ] 2128fn malformed_pins_are_rejected_at_open () { 2129let server =Registry :: new (); 2130let dir = tempfile:: tempdir (). unwrap (); 2131let key =format! ( "{}|pkg|1.0.0" , server. url . as_str ()); 2132let tarball =format! ( 2133"{}/pkg/-/pkg-1.0.0.tgz" , 2134 server. url . as_str (). trim_end_matches ( '/' ) 2135); 2136let integrity =Registry :: tarball_integrity ( & []); 2137let good_files = serde_json:: json!({ 2138"package.json" : avril_runtime:: registry:: checksum ( b"{}" ), 2139"index.js" : avril_runtime:: registry:: checksum ( b"1" ), 2140}); 2141let good_map: std:: collections:: BTreeMap < String , String > = good_files 2142. as_object () 2143. unwrap () 2144. iter () 2145. map ( |( k, v) |( k. clone (), v. as_str (). unwrap (). to_owned ())) 2146. collect (); 2147let manifest ={ 2148use sha2:: Digest ; 2149let digest = sha2:: Sha256 :: digest ( serde_json:: to_vec ( & good_map). unwrap ()); 2150format! ( 2151"sha256-{}" , 2152 digest 2153. iter () 2154. map ( |byte|format! ( "{byte:02x}" )) 2155. collect ::< String >() 2156) 2157}; 2158let valid = serde_json:: json!({ 2159"tarball" : tarball, 2160"integrity" : integrity, 2161"manifest" : manifest, 2162}); 2163let cases: Vec <( & str , serde_json:: Value )> =vec! [ 2164( 2165"neither" , 2166 serde_json:: json!({ "tarball" : tarball, "integrity" : integrity}), 2167), 2168( 2169"mismatch" , 2170 serde_json:: json!({ "tarball" : tarball, "integrity" : integrity, "manifest" : manifest, "files" : good_files}), 2171), 2172( 2173"bad-digest" , 2174 serde_json:: json!({ "tarball" : tarball, "integrity" : integrity, "manifest" : "sha256-zzz" }), 2175), 2176( 2177"empty-files" , 2178 serde_json:: json!({ "tarball" : tarball, "integrity" : integrity, "files" : {}}), 2179), 2180( 2181"bad-path" , 2182 serde_json:: json!({ "tarball" : tarball, "integrity" : integrity, "files" : { "../evil" : good_files[ "index.js" ], "package.json" : good_files[ "package.json" ]}}), 2183), 2184( 2185"no-package-json" , 2186 serde_json:: json!({ "tarball" : tarball, "integrity" : integrity, "files" : { "index . js" : good_files [ "index.js" ]}}), 2187), 2188( 2189"unknown-field" , 2190 serde_json:: json!({ "tarball" : tarball, "integrity" : integrity, "manifest" : manifest, "future" : 1 }), 2191), 2192]; 2193for ( label, mut pin) in cases{ 2194if label =="mismatch" { 2195let mut tampered = good_files. clone (); 2196 tampered[ "index.js" ] = 2197 serde_json:: Value :: String ( avril_runtime:: registry:: checksum ( b"other" )); 2198 pin[ "files" ] = tampered; 2199} 2200let mut lock = serde_json:: json!({ 2201"version" : 2 , 2202"jsrRegistry" : server. url . as_str (), 2203"npmRegistry" : server. url . as_str (), 2204"specifiers" : {}, 2205"packages" : {}, 2206"npm" : { " specifiers " : {}, "packages" : {}, "roots" : {}, "instances" : {}}, 2207}); 2208 lock[ "npm" ][ "packages" ] 2209. as_object_mut () 2210. unwrap () 2211. insert ( key. clone (), pin); 2212 std:: fs:: write ( dir. path (). join ( "avril.lock" ), format! ( "{lock}\n" )). unwrap (); 2213let error =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())) 2214. err () 2215. expect ( "malformed pins must be rejected" ) 2216. to_string (); 2217assert! ( !error. is_empty (), "{label} must be rejected" ); 2218} 2219// The valid compact pin opens cleanly. 2220let mut lock = serde_json:: json!({ 2221"version" : 2 , 2222"jsrRegistry" : server. url . as_str (), 2223"npmRegistry" : server. url . as_str (), 2224"specifiers" : {}, 2225"packages" : {}, 2226"npm" : { " specifiers " : {}, "packages" : {}, "roots" : {}, "instances" : {}}, 2227}); 2228 lock[ "npm" ][ "packages" ] 2229. as_object_mut () 2230. unwrap () 2231. insert ( key. clone (), valid); 2232 std:: fs:: write ( dir. path (). join ( "avril.lock" ), format! ( "{lock}\n" )). unwrap (); 2233assert! ( PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). is_ok ()); 2234} 2235 2236# [ test ] 2237fn cold_frozen_publishes_sidecars_for_readonly_replay () { 2238let server =Registry :: new (); 2239 server. publish_npm ( 2240"coldfrozen" , 2241"1.0.0" , 2242& [ 2243( 2244"package.json" , 2245"{\"name\": \"coldfrozen\", \"version\": \"1.0.0\"}" , 2246), 2247( "index.js" , "export default 1;" ), 2248], 2249); 2250let dir = tempfile:: tempdir (). unwrap (); 2251let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 2252block_on ( store. npm_acquire ( "npm:coldfrozen@^1" )). unwrap (); 2253 store. save (). unwrap (); 2254// Cold frozen fetch over the network still publishes the sidecar. 2255 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( "default" )). unwrap (); 2256 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".manifests" )). unwrap (); 2257let mut frozen = server. options ( dir. path ()); 2258 frozen. frozen =true ; 2259let store =PackageStore :: new ( fixture:: runtime_handle (), frozen). unwrap (); 2260let cold =block_on ( store. npm_acquire ( "npm:coldfrozen@^1" )). unwrap (); 2261assert_eq! ( cold. identity . version , "1.0.0" ); 2262assert_eq! ( 2263 std:: fs:: read_dir ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".manifests" )) 2264. unwrap () 2265. count (), 22661 , 2267"frozen acquisition leaves a complete tree" 2268); 2269// Strip every cache, go read-only, and replay offline and frozen. 2270 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".artifacts" )). unwrap (); 2271 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".metadata" )). unwrap (); 2272# [ cfg ( unix )] 2273{ 2274use std:: os:: unix:: fs:: PermissionsExt ; 2275chmod_readonly ( & dir. path (). join ( ".avril" )); 2276 std:: fs:: set_permissions ( 2277 dir. path (). join ( "avril.lock" ), 2278 std:: fs:: Permissions :: from_mode ( 0o444 ), 2279) 2280. unwrap (); 2281} 2282let warmed = server. requests . lock (). unwrap (). len (); 2283let mut replay = server. options ( dir. path ()); 2284 replay. offline =true ; 2285 replay. frozen =true ; 2286let store =PackageStore :: new ( fixture:: runtime_handle (), replay). unwrap (); 2287let hit =block_on ( store. npm_acquire ( "npm:coldfrozen@^1" )). unwrap (); 2288assert_eq! ( 2289 std:: fs:: read_to_string ( hit. dir . join ( "index.js" )). unwrap (), 2290"export default 1;" 2291); 2292assert_eq! ( server. requests . lock (). unwrap (). len (), warmed); 2293# [ cfg ( unix )] 2294{ 2295use std:: os:: unix:: fs:: PermissionsExt ; 2296chmod_writable ( & dir. path (). join ( ".avril" )); 2297let _ = std:: fs:: set_permissions ( 2298 dir. path (). join ( "avril.lock" ), 2299 std:: fs:: Permissions :: from_mode ( 0o644 ), 2300); 2301} 2302} 2303 2304fn tar_entry ( name : & str , entry_type : tar:: EntryType , data : & [ u8 ]) ->Vec < u8 > { 2305let mut header = tar:: Header :: new_gnu (); 2306 header. set_mode ( if entry_type. is_dir () { 0o755 } else { 0o644 }); 2307 header. set_size ( if entry_type. is_dir () { 23080 2309} else { 2310 data. len () as u64 2311}); 2312 header. set_entry_type ( entry_type); 2313 header. as_mut_bytes ()[ ..name. len ()]. copy_from_slice ( name. as_bytes ()); 2314 header. set_cksum (); 2315let mut out = header. as_bytes (). to_vec (); 2316if !entry_type. is_dir () { 2317 out. extend_from_slice ( data); 2318while !out. len (). is_multiple_of ( 512 ) { 2319 out. push ( 0 ); 2320} 2321} 2322 out 2323} 2324 2325fn gzip_tarball ( blocks : & [ Vec < u8 >]) ->Vec < u8 > { 2326use flate2::{ Compression , write:: GzEncoder }; 2327use std:: io:: Write ; 2328let mut tar_data =Vec :: new (); 2329for blockin blocks{ 2330 tar_data. extend_from_slice ( block); 2331} 2332 tar_data. extend_from_slice ( & [ 0u8 ; 1024 ]); 2333let mut encoder =GzEncoder :: new ( Vec :: new (), Compression :: default ()); 2334 encoder. write_all ( & tar_data). unwrap (); 2335 encoder. finish (). unwrap () 2336} 2337 2338# [ test ] 2339fn dot_segments_normalize_to_canonical_paths () { 2340let package_json ="{\"name\": \"dotted\", \"version\": \"1.0.0\"}" ; 2341let index_js ="export default 1;" ; 2342let util_js ="export const util = 2;" ; 2343use tar:: EntryType ::{ Directory , Regular }; 2344let blocks =vec! [ 2345tar_entry ( "package/./" , Directory , b"" ), 2346tar_entry ( "package/./dist/" , Directory , b"" ), 2347tar_entry ( "package/./package.json" , Regular , package_json. as_bytes ()), 2348tar_entry ( "package/./dist/index.js" , Regular , index_js. as_bytes ()), 2349tar_entry ( "package/dist/./util.js" , Regular , util_js. as_bytes ()), 2350]; 2351let tarball =gzip_tarball ( & blocks); 2352let server =Registry :: new (); 2353let integrity =Registry :: tarball_integrity ( & tarball); 2354 server. publish_npm_dist ( "dotted" , "1.0.0" , tarball, & integrity, None , true ); 2355let dir = tempfile:: tempdir (). unwrap (); 2356let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 2357let artifact =block_on ( store. npm_acquire ( "npm:dotted" )). unwrap (); 2358assert_eq! ( artifact. identity . version , "1.0.0" ); 2359let keys: Vec < & str > = artifact. files . keys (). map ( String :: as_str). collect (); 2360assert_eq! ( keys, vec! [ "dist/index.js" , "dist/util.js" , "package.json" ]); 2361assert_eq! ( 2362 artifact. read_file ( dir. path (), "dist/index.js" ). unwrap (), 2363 index_js. as_bytes () 2364); 2365assert_eq! ( 2366 artifact. read_file ( dir. path (), "dist/util.js" ). unwrap (), 2367 util_js. as_bytes () 2368); 2369assert_eq! ( 2370 std:: fs:: read_to_string ( artifact. dir . join ( "dist" ). join ( "index.js" )). unwrap (), 2371 index_js 2372); 2373assert_eq! ( 2374 std:: fs:: read_to_string ( artifact. dir . join ( "dist" ). join ( "util.js" )). unwrap (), 2375 util_js 2376); 2377 store. save (). unwrap (); 2378let warmed = server. requests . lock (). unwrap (). len (); 2379let lock_before = std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (); 2380let mut offline =offline_frozen_options ( & server, dir. path ()); 2381 offline. cache = dir. path (). join ( "fresh-cache-that-must-stay-empty" ); 2382let store =PackageStore :: new ( fixture:: runtime_handle (), offline). unwrap (); 2383let hit =block_on ( store. npm_acquire ( "npm:dotted" )). unwrap (); 2384assert_eq! ( hit. files , artifact. files ); 2385assert_eq! ( 2386 std:: fs:: read_to_string ( hit. dir . join ( "dist" ). join ( "index.js" )). unwrap (), 2387 index_js 2388); 2389assert_eq! ( server. requests . lock (). unwrap (). len (), warmed); 2390assert! ( 2391 !dir. path (). join ( "fresh-cache-that-must-stay-empty" ). exists (), 2392"npm must not depend on the global cache" 2393); 2394 store. save (). unwrap (); 2395assert_eq! ( 2396 std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (), 2397 lock_before 2398); 2399} 2400 2401# [ test ] 2402fn duplicate_canonical_dot_paths_are_rejected () { 2403use tar:: EntryType :: Regular ; 2404let package_json ="{\"name\": \"dupdot\", \"version\": \"1.0.0\"}" ; 2405let blocks =vec! [ 2406tar_entry ( "package/package.json" , Regular , package_json. as_bytes ()), 2407tar_entry ( "package/index.js" , Regular , b"export default 1;" ), 2408tar_entry ( "package/./index.js" , Regular , b"export default 2;" ), 2409]; 2410let tarball =gzip_tarball ( & blocks); 2411let server =Registry :: new (); 2412let integrity =Registry :: tarball_integrity ( & tarball); 2413 server. publish_npm_dist ( "dupdot" , "1.0.0" , tarball, & integrity, None , true ); 2414let dir = tempfile:: tempdir (). unwrap (); 2415let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 2416assert! ( 2417block_on ( store. npm_acquire ( "npm:dupdot" )) 2418. unwrap_err () 2419. to_string () 2420. contains ( "duplicate" ) 2421); 2422} 2423 2424# [ test ] 2425fn identical_dot_alias_duplicates_coalesce_in_either_order () { 2426use tar:: EntryType :: Regular ; 2427for ( name, dotted_first) in [( "dupok" , false ), ( "dupok_rev" , true )] { 2428let package_json =format! ( "{{\"name\": \"{name}\", \"version\": \"1.0.0\"}}" ); 2429let index_js ="export default 1;" ; 2430let plain =tar_entry ( "package/dist/index.js" , Regular , index_js. as_bytes ()); 2431let dotted =tar_entry ( "package/./dist/index.js" , Regular , index_js. as_bytes ()); 2432let mut blocks =vec! [ tar_entry ( 2433"package/package.json" , 2434Regular , 2435 package_json. as_bytes (), 2436)]; 2437if dotted_first{ 2438 blocks. push ( dotted); 2439 blocks. push ( plain); 2440} else { 2441 blocks. push ( plain); 2442 blocks. push ( dotted); 2443} 2444let tarball =gzip_tarball ( & blocks); 2445let server =Registry :: new (); 2446let integrity =Registry :: tarball_integrity ( & tarball); 2447 server. publish_npm_dist ( name, "1.0.0" , tarball, & integrity, None , true ); 2448let dir = tempfile:: tempdir (). unwrap (); 2449let store = 2450PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 2451let artifact =block_on ( store. npm_acquire ( & format! ( "npm:{name}" ))). unwrap (); 2452let keys: Vec < & str > = artifact. files . keys (). map ( String :: as_str). collect (); 2453assert_eq! ( keys, vec! [ "dist/index.js" , "package.json" ], "{name}" ); 2454assert_eq! ( 2455 artifact. read_file ( dir. path (), "dist/index.js" ). unwrap (), 2456 index_js. as_bytes () 2457); 2458assert_eq! ( 2459 std:: fs:: read_to_string ( artifact. dir . join ( "dist" ). join ( "index.js" )). unwrap (), 2460 index_js 2461); 2462 store. save (). unwrap (); 2463let warmed = server. requests . lock (). unwrap (). len (); 2464let lock_before = std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (); 2465let mut offline =offline_frozen_options ( & server, dir. path ()); 2466 offline. cache = dir. path (). join ( "fresh-cache-that-must-stay-empty" ); 2467let store =PackageStore :: new ( fixture:: runtime_handle (), offline). unwrap (); 2468let hit =block_on ( store. npm_acquire ( & format! ( "npm:{name}" ))). unwrap (); 2469assert_eq! ( hit. files , artifact. files ); 2470assert_eq! ( server. requests . lock (). unwrap (). len (), warmed); 2471assert! ( !dir. path (). join ( "fresh-cache-that-must-stay-empty" ). exists ()); 2472 store. save (). unwrap (); 2473assert_eq! ( 2474 std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (), 2475 lock_before 2476); 2477} 2478} 2479 2480# [ test ] 2481fn identical_exact_name_duplicates_coalesce () { 2482use tar:: EntryType :: Regular ; 2483let package_json ="{\"name\": \"dupx\", \"version\": \"1.0.0\"}" ; 2484let index_js ="export default 1;" ; 2485let blocks =vec! [ 2486tar_entry ( "package/package.json" , Regular , package_json. as_bytes ()), 2487tar_entry ( "package/index.js" , Regular , index_js. as_bytes ()), 2488tar_entry ( "package/index.js" , Regular , index_js. as_bytes ()), 2489]; 2490let tarball =gzip_tarball ( & blocks); 2491let server =Registry :: new (); 2492let integrity =Registry :: tarball_integrity ( & tarball); 2493 server. publish_npm_dist ( "dupx" , "1.0.0" , tarball, & integrity, None , true ); 2494let dir = tempfile:: tempdir (). unwrap (); 2495let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 2496let artifact =block_on ( store. npm_acquire ( "npm:dupx" )). unwrap (); 2497let keys: Vec < & str > = artifact. files . keys (). map ( String :: as_str). collect (); 2498assert_eq! ( keys, vec! [ "index.js" , "package.json" ]); 2499} 2500 2501# [ test ] 2502fn duplicate_entries_count_toward_tarball_limits () { 2503use tar:: EntryType :: Regular ; 2504let package_json ="{\"name\": \"many\", \"version\": \"1.0.0\"}" ; 2505let mut blocks =vec! [ 2506tar_entry ( "package/package.json" , Regular , package_json. as_bytes ()), 2507tar_entry ( "package/index.js" , Regular , b"export default 1;" ), 2508]; 2509for _in 0 ..65_535 { 2510 blocks. push ( tar_entry ( "package/index.js" , Regular , b"export default 1;" )); 2511} 2512let tarball =gzip_tarball ( & blocks); 2513let server =Registry :: new (); 2514let integrity =Registry :: tarball_integrity ( & tarball); 2515 server. publish_npm_dist ( "many" , "1.0.0" , tarball, & integrity, None , true ); 2516let dir = tempfile:: tempdir (). unwrap (); 2517let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 2518assert! ( 2519block_on ( store. npm_acquire ( "npm:many" )) 2520. unwrap_err () 2521. to_string () 2522. contains ( "too many entries" ) 2523); 2524} 2525 2526# [ test ] 2527fn legacy_single_directory_roots_are_accepted () { 2528use tar:: EntryType ::{ Directory , Regular }; 2529let package_json ="{\"name\": \"@types/retry\", \"version\": \"0.12.0\"}" ; 2530let index ="export declare const retry: number;" ; 2531let file_blocks = ||{ 2532vec! [ 2533tar_entry ( "retry/package.json" , Regular , package_json. as_bytes ()), 2534tar_entry ( "retry/index.d.ts" , Regular , index. as_bytes ()), 2535tar_entry ( "retry/README.md" , Regular , b"# retry\n" ), 2536tar_entry ( "retry/LICENSE" , Regular , b"MIT\n" ), 2537] 2538}; 2539let variants: & [( & str , & [ & str ], & [ & str ])] =& [ 2540( "no header" , & [], & []), 2541( "root header first" , & [ "retry/" ], & []), 2542( "root header last" , & [], & [ "retry/" ]), 2543( "bare root header" , & [ "retry" ], & []), 2544]; 2545for ( label, first, last) in variants{ 2546let mut blocks =vec! []; 2547for namein * first{ 2548 blocks. push ( tar_entry ( name, Directory , b"" )); 2549} 2550 blocks. extend ( file_blocks ()); 2551for namein * last{ 2552 blocks. push ( tar_entry ( name, Directory , b"" )); 2553} 2554let tarball =gzip_tarball ( & blocks); 2555let server =Registry :: new (); 2556let integrity =Registry :: tarball_integrity ( & tarball); 2557 server. publish_npm_dist ( "@types/retry" , "0.12.0" , tarball, & integrity, None , true ); 2558let dir = tempfile:: tempdir (). unwrap (); 2559let store = 2560PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 2561let artifact =block_on ( store. npm_acquire ( "npm:@types/retry" )). unwrap (); 2562let keys: Vec < & str > = artifact. files . keys (). map ( String :: as_str). collect (); 2563assert_eq! ( 2564 keys, 2565vec! [ "LICENSE" , "README.md" , "index.d.ts" , "package.json" ], 2566"{label}" 2567); 2568assert_eq! ( 2569 artifact. read_file ( dir. path (), "index.d.ts" ). unwrap (), 2570 index. as_bytes () 2571); 2572assert_eq! ( 2573 std:: fs:: read_to_string ( artifact. dir . join ( "index.d.ts" )). unwrap (), 2574 index 2575); 2576 store. save (). unwrap (); 2577let warmed = server. requests . lock (). unwrap (). len (); 2578let lock_before = std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (); 2579let mut offline =offline_frozen_options ( & server, dir. path ()); 2580 offline. cache = dir. path (). join ( "fresh-cache-that-must-stay-empty" ); 2581let store =PackageStore :: new ( fixture:: runtime_handle (), offline). unwrap (); 2582let hit =block_on ( store. npm_acquire ( "npm:@types/retry" )). unwrap (); 2583assert_eq! ( hit. files , artifact. files ); 2584assert_eq! ( server. requests . lock (). unwrap (). len (), warmed); 2585assert! ( !dir. path (). join ( "fresh-cache-that-must-stay-empty" ). exists ()); 2586 store. save (). unwrap (); 2587assert_eq! ( 2588 std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (), 2589 lock_before 2590); 2591} 2592} 2593 2594# [ test ] 2595fn mixed_and_unsafe_archive_roots_are_rejected () { 2596use tar:: EntryType ::{ Directory , Regular }; 2597let package_json ="{\"name\": \"t\", \"version\": \"1.0.0\"}" ; 2598let file = |name : & str |tar_entry ( name, Regular , b"x" ); 2599let dir = |name : & str |tar_entry ( name, Directory , b"" ); 2600let pkg = ||tar_entry ( "retry/package.json" , Regular , package_json. as_bytes ()); 2601let cases: Vec <( & str , Vec < Vec < u8 >>, & str )> =vec! [ 2602( 2603"mixed file roots" , 2604vec! [ pkg (), file ( "package/index.js" )], 2605"mixes archive roots" , 2606), 2607( 2608"stray directory root" , 2609vec! [ pkg (), dir ( "other/" )], 2610"mixes archive roots" , 2611), 2612( 2613"root-only file" , 2614vec! [ tar_entry ( "retry" , Regular , b"x" )], 2615"Invalid npm file path" , 2616), 2617( 2618"dot-dot root" , 2619vec! [ file ( "../evil.js" )], 2620"Invalid npm file path" , 2621), 2622( "dot root" , vec! [ file ( "./evil.js" )], "Invalid npm file path" ), 2623( 2624"absolute root" , 2625vec! [ file ( "/evil.js" )], 2626"Invalid npm file path" , 2627), 2628( 2629"backslash root" , 2630vec! [ file ( "a\\b/evil.js" )], 2631"Invalid npm file path" , 2632), 2633( 2634"colon root" , 2635vec! [ file ( "a:b/evil.js" )], 2636"Invalid npm file path" , 2637), 2638( 2639"control root" , 2640vec! [ file ( "a\u{1}b/evil.js" )], 2641"Invalid npm file path" , 2642), 2643( 2644"encoded separator root" , 2645vec! [ file ( "a%2fb/evil.js" )], 2646"Invalid npm file path" , 2647), 2648( 2649"traversal below root" , 2650vec! [ pkg (), file ( "retry/../evil.js" )], 2651"Invalid npm file path" , 2652), 2653( 2654"missing package.json" , 2655vec! [ file ( "retry/index.js" )], 2656"missing package.json" , 2657), 2658( 2659"mismatched identity" , 2660vec! [ tar_entry ( 2661"retry/package.json" , 2662Regular , 2663b"{\"name\": \"other\", \"version\": \"1.0.0\"}" , 2664)], 2665"does not match" , 2666), 2667]; 2668for ( label, blocks, expected) in & cases{ 2669let tarball =gzip_tarball ( blocks); 2670let server =Registry :: new (); 2671let integrity =Registry :: tarball_integrity ( & tarball); 2672 server. publish_npm_dist ( "t" , "1.0.0" , tarball, & integrity, None , true ); 2673let dir = tempfile:: tempdir (). unwrap (); 2674let store = 2675PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 2676assert! ( 2677block_on ( store. npm_acquire ( "npm:t" )) 2678. unwrap_err () 2679. to_string () 2680. contains ( expected), 2681"{label}" 2682); 2683assert! ( !dir. path (). join ( "evil.js" ). exists (), "{label}" ); 2684assert! ( 2685 !dir. path () 2686. join ( ".avril" ) 2687. join ( "npm" ) 2688. join ( "default" ) 2689. exists (), 2690"{label}" 2691); 2692} 2693} 2694 2695# [ test ] 2696fn dot_prefixed_unsafe_paths_are_still_rejected () { 2697use tar:: EntryType ::{ Directory , Regular }; 2698let server =Registry :: new (); 2699let cases =[ 2700( "dotdot-file" , "package/./../evil.js" , false ), 2701( "backslash-file" , "package/./foo\\bar.js" , false ), 2702( "empty-file" , "package//index.js" , false ), 2703( "internal-empty-file" , "package/dist//index.js" , false ), 2704( "traversal-file" , "package/dist/../evil.js" , false ), 2705( "trailing-slash-file" , "package/./dist/index.js/" , false ), 2706( "root-file" , "package/./" , false ), 2707( "empty-dir" , "package//" , true ), 2708( "dot-double-slash-dir" , "package/.//" , true ), 2709( "internal-empty-dir" , "package/a//b/" , true ), 2710( "dotdot-dir" , "package/./../evil/" , true ), 2711( "traversal-dir" , "package/dist/../evil/" , true ), 2712]; 2713for ( suffix, entry, is_dir) in cases{ 2714let name =format! ( "evil-{suffix}" ); 2715let package_json =format! ( "{{\"name\": \"{name}\", \"version\": \"1.0.0\"}}" ); 2716let blocks =vec! [ 2717tar_entry ( "package/package.json" , Regular , package_json. as_bytes ()), 2718tar_entry ( 2719 entry, 2720if is_dir{ Directory } else { Regular }, 2721if is_dir{ 2722b"" . as_slice () 2723} else { 2724b"evil" . as_slice () 2725}, 2726), 2727]; 2728let tarball =gzip_tarball ( & blocks); 2729let integrity =Registry :: tarball_integrity ( & tarball); 2730 server. publish_npm_dist ( & name, "1.0.0" , tarball, & integrity, None , true ); 2731} 2732let dir = tempfile:: tempdir (). unwrap (); 2733for ( suffix, _, _) in cases{ 2734let name =format! ( "evil-{suffix}" ); 2735let store = 2736PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 2737assert! ( 2738block_on ( store. npm_acquire ( & format! ( "npm:{name}" ))) 2739. unwrap_err () 2740. to_string () 2741. contains ( "Invalid npm file path" ), 2742"{suffix} must be rejected" 2743); 2744} 2745} 2746 2747# [ test ] 2748fn npm_progress_reports_network_fetches_but_not_warm_hits () { 2749use avril_runtime:: registry:: ProgressEvent ; 2750use std:: sync:: Mutex ; 2751let server =Registry :: new (); 2752 server. publish_npm ( 2753"tool" , 2754"1.0.0" , 2755& [ 2756( 2757"package.json" , 2758"{\"name\": \"tool\", \"version\": \"1.0.0\"}" , 2759), 2760( "index.js" , "export default 1;" ), 2761], 2762); 2763let dir = tempfile:: tempdir (). unwrap (); 2764let events =Arc :: new ( Mutex :: new ( Vec :: new ())); 2765let sink_events = events. clone (); 2766let sink: Arc < dyn Fn ( & ProgressEvent ) +Send +Sync > = 2767Arc :: new ( move |event| sink_events. lock (). unwrap (). push ( event. clone ())); 2768let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())) 2769. unwrap () 2770. with_progress ( sink); 2771let artifact =block_on ( store. npm_acquire ( "npm:tool" )). unwrap (); 2772assert_eq! ( artifact. identity . version , "1.0.0" ); 2773let seen = events. lock (). unwrap (). clone (); 2774assert! ( 2775 seen. iter () 2776. any ( |event|* event ==ProgressEvent :: NpmMetadata { name : "tool" . into () }), 2777"metadata fetch must be reported: {seen:?}" 2778); 2779assert! ( 2780 seen. iter (). any ( |event|* event 2781 ==ProgressEvent :: NpmArchive { 2782name : "tool" . into (), 2783version : "1.0.0" . into (), 2784}), 2785"archive download must be reported: {seen:?}" 2786); 2787 store. save (). unwrap (); 2788let warm_events =Arc :: new ( Mutex :: new ( Vec :: new ())); 2789let warm_sink_events = warm_events. clone (); 2790let warm_sink: Arc < dyn Fn ( & ProgressEvent ) +Send +Sync > = 2791Arc :: new ( move |event| warm_sink_events. lock (). unwrap (). push ( event. clone ())); 2792let mut offline =offline_frozen_options ( & server, dir. path ()); 2793 offline. cache = dir. path (). join ( "fresh-cache-that-must-stay-empty" ); 2794let store =PackageStore :: new ( fixture:: runtime_handle (), offline) 2795. unwrap () 2796. with_progress ( warm_sink); 2797let hit =block_on ( store. npm_acquire ( "npm:tool" )). unwrap (); 2798assert_eq! ( hit. identity . version , "1.0.0" ); 2799let warm = warm_events. lock (). unwrap (). clone (); 2800assert! ( 2801 !warm. iter (). any ( |event|matches! ( 2802 event, 2803ProgressEvent :: NpmMetadata { ..} |ProgressEvent :: NpmArchive { ..} 2804)), 2805"warm offline hits must not claim network work: {warm:?}" 2806); 2807} 2808 2809# [ test ] 2810fn npm_progress_arrives_before_download_completes () { 2811use avril_runtime:: registry:: ProgressEvent ; 2812use std:: sync:: Mutex ; 2813use std:: sync:: atomic:: Ordering ; 2814use std:: time::{ Duration , Instant }; 2815let server =Registry :: new (); 2816 server. publish_npm ( 2817"tool" , 2818"1.0.0" , 2819& [ 2820( 2821"package.json" , 2822"{\"name\": \"tool\", \"version\": \"1.0.0\"}" , 2823), 2824( "index.js" , "export default 1;" ), 2825], 2826); 2827let dir = tempfile:: tempdir (). unwrap (); 2828let events =Arc :: new ( Mutex :: new ( Vec :: new ())); 2829let sink_events = events. clone (); 2830let sink: Arc < dyn Fn ( & ProgressEvent ) +Send +Sync > = 2831Arc :: new ( move |event| sink_events. lock (). unwrap (). push ( event. clone ())); 2832let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())) 2833. unwrap () 2834. with_progress ( sink); 2835 server. hold_tarballs . store ( true , Ordering :: Relaxed ); 2836let handle = std:: thread:: spawn ( move ||block_on ( store. npm_acquire ( "npm:tool" ))); 2837let deadline =Instant :: now () +Duration :: from_secs ( 10 ); 2838loop { 2839let has_archive = events. lock (). unwrap (). iter (). any ( |event|{ 2840* event 2841 ==ProgressEvent :: NpmArchive { 2842name : "tool" . into (), 2843version : "1.0.0" . into (), 2844} 2845}); 2846if has_archive{ 2847break ; 2848} 2849if Instant :: now () > deadline{ 2850 server. hold_tarballs . store ( false , Ordering :: Relaxed ); 2851panic! ( "archive progress must arrive while the download is stalled" ); 2852} 2853 std:: thread:: sleep ( Duration :: from_millis ( 10 )); 2854} 2855assert! ( 2856 !handle. is_finished (), 2857"archive progress must precede download completion" 2858); 2859 server. hold_tarballs . store ( false , Ordering :: Relaxed ); 2860let artifact = handle 2861. join () 2862. expect ( "acquisition thread must finish" ) 2863. expect ( "stalled acquisition must succeed once released" ); 2864assert_eq! ( artifact. identity . version , "1.0.0" ); 2865} 2866 2867# [ test ] 2868fn npm_select_version_uses_tags_ranges_scopes_and_subpaths () { 2869let server =Registry :: new (); 2870for versionin [ "1.0.0" , "1.1.0" , "2.0.0" ] { 2871 server. publish_npm ( 2872"tool" , 2873 version, 2874& [ 2875( 2876"package.json" , 2877& format! ( "{{\"name\": \"tool\", \"version\": \"{version}\"}}" ), 2878), 2879( "index.js" , "export default 1;" ), 2880], 2881); 2882} 2883{ 2884let mut files = server. files . lock (). unwrap (); 2885let mut packument: serde_json:: Value = serde_json:: from_slice ( & files[ "/tool" ]). unwrap (); 2886 packument[ "dist-tags" ][ "latest" ] = serde_json:: Value :: String ( "1.1.0" . into ()); 2887 files. insert ( "/tool" . into (), serde_json:: to_vec ( & packument). unwrap ()); 2888} 2889 server. publish_npm ( 2890"@scope/pkg" , 2891"1.0.0" , 2892& [( 2893"package.json" , 2894"{\"name\": \"@scope/pkg\", \"version\": \"1.0.0\"}" , 2895)], 2896); 2897let dir = tempfile:: tempdir (). unwrap (); 2898let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 2899for ( specifier, version) in [ 2900( "npm:tool" , "1.1.0" ), 2901( "npm:tool@latest" , "1.1.0" ), 2902( "npm:tool@^1" , "1.1.0" ), 2903( "npm:tool@2.0.0" , "2.0.0" ), 2904( "npm:tool/sub" , "1.1.0" ), 2905( "npm:@scope/pkg" , "1.0.0" ), 2906] { 2907assert_eq! ( 2908block_on ( store. npm_select_version ( specifier)). unwrap (), 2909 version, 2910"{specifier}" 2911); 2912} 2913assert! ( 2914 server 2915. requests 2916. lock () 2917. unwrap () 2918. iter () 2919. all ( |path| !path. ends_with ( ".tgz" )), 2920"selection must not fetch archives" 2921); 2922 store. save (). unwrap (); 2923if dir. path (). join ( "avril.lock" ). exists () { 2924let lock = fixture:: read_lock ( dir. path ()); 2925assert! ( lock[ "npm" ][ "packages" ]. as_object (). unwrap (). is_empty ()); 2926assert! ( lock[ "npm" ][ "specifiers" ]. as_object (). unwrap (). is_empty ()); 2927} 2928} 2929 2930# [ test ] 2931fn npm_select_version_succeeds_when_archives_or_deps_are_broken () { 2932let server =Registry :: new (); 2933 server. publish_npm ( 2934"broken" , 2935"1.0.0" , 2936& [( 2937"package.json" , 2938"{\"name\": \"broken\", \"version\": \"1.0.0\"}" , 2939)], 2940); 2941{ 2942let mut files = server. files . lock (). unwrap (); 2943let tarball = files 2944. keys () 2945. find ( |path| path. ends_with ( ".tgz" )) 2946. unwrap () 2947. clone (); 2948 files. insert ( tarball, b"not gzip" . to_vec ()); 2949} 2950 server. publish_npm ( 2951"hasdep" , 2952"1.0.0" , 2953& [( 2954"package.json" , 2955"{\"name\": \"hasdep\", \"version\": \"1.0.0\", \"dependencies\": {\"nodep\": \"^1.0.0\"}}" , 2956)], 2957); 2958let dir = tempfile:: tempdir (). unwrap (); 2959let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 2960assert_eq! ( 2961block_on ( store. npm_select_version ( "npm:broken" )). unwrap (), 2962"1.0.0" 2963); 2964assert_eq! ( 2965block_on ( store. npm_select_version ( "npm:hasdep" )). unwrap (), 2966"1.0.0" 2967); 2968assert! ( 2969 server 2970. requests 2971. lock () 2972. unwrap () 2973. iter () 2974. all ( |path| !path. ends_with ( ".tgz" )), 2975"selection must not fetch archives" 2976); 2977assert! ( block_on ( store. npm_acquire ( "npm:broken" )). is_err ()); 2978{ 2979use avril_runtime:: registry:: npm:: resolution::{ NpmResolver , NpmRootRequest }; 2980use std:: collections:: BTreeMap ; 2981use std:: sync:: Arc ; 2982let store =Arc :: new ( store); 2983let resolver =NpmResolver :: new ( store); 2984assert! ( 2985block_on ( resolver. prepare ( & [ NpmRootRequest { 2986request : "npm:hasdep" . into (), 2987context : BTreeMap :: new (), 2988}])) 2989. is_err () 2990); 2991} 2992} 2993 2994# [ test ] 2995fn npm_select_version_reuses_locks_and_caches () { 2996use avril_runtime:: registry:: ProgressEvent ; 2997use std:: sync:: Mutex ; 2998let server =Registry :: new (); 2999 server. publish_npm ( 3000"tool" , 3001"1.0.0" , 3002& [( 3003"package.json" , 3004"{\"name\": \"tool\", \"version\": \"1.0.0\"}" , 3005)], 3006); 3007 server. publish_npm ( 3008"tool" , 3009"1.1.0" , 3010& [( 3011"package.json" , 3012"{\"name\": \"tool\", \"version\": \"1.1.0\"}" , 3013)], 3014); 3015let dir = tempfile:: tempdir (). unwrap (); 3016let events =Arc :: new ( Mutex :: new ( Vec :: new ())); 3017let sink_events = events. clone (); 3018let sink: Arc < dyn Fn ( & ProgressEvent ) +Send +Sync > = 3019Arc :: new ( move |event| sink_events. lock (). unwrap (). push ( event. clone ())); 3020let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())) 3021. unwrap () 3022. with_progress ( sink); 3023assert_eq! ( 3024block_on ( store. npm_select_version ( "npm:tool@^1" )). unwrap (), 3025"1.1.0" 3026); 3027let packuments = server 3028. requests 3029. lock () 3030. unwrap () 3031. iter () 3032. filter ( |path| path. as_str () =="/tool" ) 3033. count (); 3034assert_eq! ( packuments, 1 ); 3035assert! ( 3036 events 3037. lock () 3038. unwrap () 3039. iter () 3040. any ( |event|* event ==ProgressEvent :: NpmMetadata { name : "tool" . into () }) 3041); 3042assert! ( !events. lock (). unwrap (). iter (). any ( |event|matches! ( 3043 event, 3044ProgressEvent :: NpmArchive { ..} |ProgressEvent :: NpmMaterialized { ..} 3045))); 3046let artifact =block_on ( store. npm_acquire ( "npm:tool@^1" )). unwrap (); 3047assert_eq! ( artifact. identity . version , "1.1.0" ); 3048let packuments = server 3049. requests 3050. lock () 3051. unwrap () 3052. iter () 3053. filter ( |path| path. as_str () =="/tool" ) 3054. count (); 3055assert_eq! ( 3056 packuments, 1 , 3057"acquisition must reuse the selected packument" 3058); 3059 store. save (). unwrap (); 3060 std:: fs:: remove_dir_all ( & artifact. dir ). unwrap (); 3061assert_eq! ( 3062block_on ( store. npm_select_version ( "npm:tool@^1" )). unwrap (), 3063"1.1.0" 3064); 3065let offline =offline_frozen_options ( & server, dir. path ()); 3066let store =PackageStore :: new ( fixture:: runtime_handle (), offline). unwrap (); 3067assert_eq! ( 3068block_on ( store. npm_select_version ( "npm:tool@^1" )). unwrap (), 3069"1.1.0" 3070); 3071let warmed = server. requests . lock (). unwrap (). len (); 3072assert! ( 3073block_on ( store. npm_select_version ( "npm:other" )) 3074. unwrap_err () 3075. to_string () 3076. contains ( "Frozen" ) 3077); 3078assert_eq! ( server. requests . lock (). unwrap (). len (), warmed); 3079for specifierin [ "npm:../evil" , "npm:tool@" , "npm:@bad" , "npm:tool@nope" ] { 3080assert! ( 3081block_on ( store. npm_select_version ( specifier)). is_err (), 3082"{specifier}" 3083); 3084} 3085assert_eq! ( server. requests . lock (). unwrap (). len (), warmed); 3086} 3087 3088# [ test ] 3089fn npm_select_version_denies_without_network () { 3090use avril_runtime:: caps::{ Capabilities , Category , Initial }; 3091let server =Registry :: new (); 3092 server. publish_npm ( 3093"secret" , 3094"1.0.0" , 3095& [( 3096"package.json" , 3097"{\"name\": \"secret\", \"version\": \"1.0.0\"}" , 3098)], 3099); 3100let dir = tempfile:: tempdir (). unwrap (); 3101let mut initial =Initial :: new ( true ); 3102 initial 3103. grant ( Category :: JsExec , true , "https://jsr.io/" , dir. path ()) 3104. unwrap (); 3105let caps =Capabilities :: new ( initial, dir. path (). to_owned ()); 3106let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())) 3107. unwrap () 3108. with_capabilities ( caps); 3109assert! ( 3110block_on ( store. npm_select_version ( "npm:secret" )) 3111. unwrap_err () 3112. to_string () 3113. contains ( "PermissionDenied" ) 3114); 3115assert! ( server. requests . lock (). unwrap (). is_empty ()); 3116} 3117 3118# [ test ] 3119fn npm_packument_shared_across_concurrent_requests () { 3120let server =Registry :: new (); 3121 server. publish_npm ( 3122"tool" , 3123"1.0.0" , 3124& [( 3125"package.json" , 3126"{\"name\": \"tool\", \"version\": \"1.0.0\"}" , 3127)], 3128); 3129let dir = tempfile:: tempdir (). unwrap (); 3130let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 3131let ( first, second, third) =block_on ( async { 3132 futures:: join!( 3133 store. npm_select_version ( "npm:tool" ), 3134 store. npm_select_version ( "npm:tool@^1" ), 3135 store. npm_acquire ( "npm:tool@^1" ), 3136) 3137}); 3138assert_eq! ( first. unwrap (), "1.0.0" ); 3139assert_eq! ( second. unwrap (), "1.0.0" ); 3140assert_eq! ( third. unwrap (). identity . version , "1.0.0" ); 3141assert_eq! ( 3142 server 3143. requests 3144. lock () 3145. unwrap () 3146. iter () 3147. filter ( |path| path. as_str () =="/tool" ) 3148. count (), 31491 , 3150"concurrent requests must share one metadata fetch" 3151); 3152} 3153 3154# [ test ] 3155fn npm_packument_failures_evict_for_retry_on_same_store () { 3156let server =Registry :: new (); 3157let dir = tempfile:: tempdir (). unwrap (); 3158let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 3159assert! ( block_on ( store. npm_select_version ( "npm:tool" )). is_err ()); 3160assert_eq! ( server. requests . lock (). unwrap (). len (), 1 ); 3161 server. publish_npm ( 3162"tool" , 3163"1.0.0" , 3164& [( 3165"package.json" , 3166"{\"name\": \"tool\", \"version\": \"1.0.0\"}" , 3167)], 3168); 3169assert_eq! ( 3170block_on ( store. npm_select_version ( "npm:tool" )). unwrap (), 3171"1.0.0" 3172); 3173assert_eq! ( server. requests . lock (). unwrap (). len (), 2 ); 3174{ 3175let mut files = server. files . lock (). unwrap (); 3176let mut packument: serde_json:: Value = serde_json:: from_slice ( & files[ "/tool" ]). unwrap (); 3177 packument[ "name" ] = serde_json:: Value :: String ( "evil" . into ()); 3178 files. insert ( "/tool" . into (), serde_json:: to_vec ( & packument). unwrap ()); 3179} 3180let dir = tempfile:: tempdir (). unwrap (); 3181let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 3182assert! ( 3183block_on ( store. npm_select_version ( "npm:tool" )) 3184. unwrap_err () 3185. to_string () 3186. contains ( "mismatch" ) 3187); 3188{ 3189let mut files = server. files . lock (). unwrap (); 3190let mut packument: serde_json:: Value = serde_json:: from_slice ( & files[ "/tool" ]). unwrap (); 3191 packument[ "name" ] = serde_json:: Value :: String ( "tool" . into ()); 3192 files. insert ( "/tool" . into (), serde_json:: to_vec ( & packument). unwrap ()); 3193} 3194assert_eq! ( 3195block_on ( store. npm_select_version ( "npm:tool" )). unwrap (), 3196"1.0.0" 3197); 3198} 3199 3200# [ test ] 3201fn npm_select_version_uses_offline_disk_cache_without_pins () { 3202let server =Registry :: new (); 3203 server. publish_npm ( 3204"tool" , 3205"1.0.0" , 3206& [( 3207"package.json" , 3208"{\"name\": \"tool\", \"version\": \"1.0.0\"}" , 3209)], 3210); 3211let dir = tempfile:: tempdir (). unwrap (); 3212let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 3213assert_eq! ( 3214block_on ( store. npm_select_version ( "npm:tool" )). unwrap (), 3215"1.0.0" 3216); 3217let warmed = server. requests . lock (). unwrap (). len (); 3218assert! ( warmed >0 ); 3219assert! ( 3220 server 3221. requests 3222. lock () 3223. unwrap () 3224. iter () 3225. all ( |path| !path. ends_with ( ".tgz" )) 3226); 3227let mut offline = server. options ( dir. path ()); 3228 offline. offline =true ; 3229let store =PackageStore :: new ( fixture:: runtime_handle (), offline). unwrap (); 3230assert_eq! ( 3231block_on ( store. npm_select_version ( "npm:tool" )). unwrap (), 3232"1.0.0" 3233); 3234assert_eq! ( server. requests . lock (). unwrap (). len (), warmed); 3235assert! ( 3236 server 3237. requests 3238. lock () 3239. unwrap () 3240. iter () 3241. all ( |path| !path. ends_with ( ".tgz" )) 3242); 3243} 3244 3245# [ test ] 3246fn npm_select_version_rejects_bad_metadata_without_evicting_valid_snapshot () { 3247let server =Registry :: new (); 3248 server. publish_npm ( 3249"tool" , 3250"1.0.0" , 3251& [( 3252"package.json" , 3253"{\"name\": \"tool\", \"version\": \"1.0.0\"}" , 3254)], 3255); 3256{ 3257let mut files = server. files . lock (). unwrap (); 3258let mut packument: serde_json:: Value = serde_json:: from_slice ( & files[ "/tool" ]). unwrap (); 3259 packument[ "dist-tags" ][ "latest" ] = serde_json:: Value :: String ( "../../evil" . into ()); 3260 files. insert ( "/tool" . into (), serde_json:: to_vec ( & packument). unwrap ()); 3261} 3262let dir = tempfile:: tempdir (). unwrap (); 3263let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 3264assert! ( block_on ( store. npm_select_version ( "npm:tool" )). is_err ()); 3265assert_eq! ( 3266block_on ( store. npm_select_version ( "npm:tool@1.0.0" )). unwrap (), 3267"1.0.0" 3268); 3269assert_eq! ( 3270 server 3271. requests 3272. lock () 3273. unwrap () 3274. iter () 3275. filter ( |path| path. as_str () =="/tool" ) 3276. count (), 32771 , 3278"selection errors reuse the valid snapshot" 3279); 3280assert! ( block_on ( store. npm_select_version ( "npm:tool@nope" )). is_err ()); 3281} 3282 3283fn abbreviated_doc ( full : & [ u8 ]) ->Vec < u8 > { 3284let full: serde_json:: Value = serde_json:: from_slice ( full). unwrap (); 3285let mut versions = serde_json:: Map :: new (); 3286for ( version, entry) in full[ "versions" ]. as_object (). unwrap () { 3287 versions. insert ( version. clone (), serde_json:: json!({ "dist" : entry[ "dist" ]})); 3288} 3289 serde_json:: to_vec ( & serde_json:: json!({ 3290"name" : full[ "name" ], 3291"dist-tags" : full[ "dist-tags" ], 3292"versions" : versions, 3293})) 3294. unwrap () 3295} 3296 3297# [ test ] 3298fn abbreviated_install_metadata_selects_and_acquires () { 3299let server =Registry :: new (); 3300for versionin [ "1.0.0" , "1.1.0" , "2.0.0" ] { 3301 server. publish_npm ( 3302"tool" , 3303 version, 3304& [ 3305( 3306"package.json" , 3307& format! ( "{{\"name\": \"tool\", \"version\": \"{version}\"}}" ), 3308), 3309( "index.js" , "export default 1;" ), 3310], 3311); 3312} 3313 server. publish_npm ( 3314"@scope/pkg" , 3315"1.0.0" , 3316& [( 3317"package.json" , 3318"{\"name\": \"@scope/pkg\", \"version\": \"1.0.0\"}" , 3319)], 3320); 3321{ 3322let mut files = server. files . lock (). unwrap (); 3323let mut packument: serde_json:: Value = serde_json:: from_slice ( & files[ "/tool" ]). unwrap (); 3324 packument[ "dist-tags" ][ "latest" ] = serde_json:: Value :: String ( "1.1.0" . into ()); 3325 files. insert ( "/tool" . into (), serde_json:: to_vec ( & packument). unwrap ()); 3326 server. publish_npm_abbreviated ( "tool" , abbreviated_doc ( & files[ "/tool" ])); 3327 server. publish_npm_abbreviated ( "@scope/pkg" , abbreviated_doc ( & files[ "/@scope/pkg" ])); 3328 files. insert ( "/tool" . into (), b"{\"name\": \"tool\"}" . to_vec ()); 3329 files. insert ( "/@scope/pkg" . into (), b"{\"name\": \"@scope/pkg\"}" . to_vec ()); 3330} 3331let dir = tempfile:: tempdir (). unwrap (); 3332let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 3333assert_eq! ( 3334block_on ( store. npm_select_version ( "npm:tool" )). unwrap (), 3335"1.1.0" 3336); 3337assert_eq! ( 3338block_on ( store. npm_select_version ( "npm:@scope/pkg" )). unwrap (), 3339"1.0.0" 3340); 3341 store. save (). unwrap (); 3342assert! ( 3343 !dir. path (). join ( "avril.lock" ). exists (), 3344"select-only leaves no pins" 3345); 3346{ 3347let before = server. requests . lock (). unwrap (). len (); 3348let mut offline = server. options ( dir. path ()); 3349 offline. offline =true ; 3350let store =PackageStore :: new ( fixture:: runtime_handle (), offline). unwrap (); 3351assert_eq! ( 3352block_on ( store. npm_select_version ( "npm:tool@^1" )). unwrap (), 3353"1.1.0" 3354); 3355assert_eq! ( 3356 server. requests . lock (). unwrap (). len (), 3357 before, 3358"offline reuse makes no network requests" 3359); 3360} 3361let fetched = server. requests . lock (). unwrap (). len (); 3362assert_eq! ( 3363 server 3364. requests 3365. lock () 3366. unwrap () 3367. iter () 3368. filter ( |path| path. as_str () =="/tool" ) 3369. count (), 33701 , 3371"metadata fetched once" 3372); 3373let artifact =block_on ( store. npm_acquire ( "npm:tool" )). unwrap (); 3374assert_eq! ( artifact. identity . version , "1.1.0" ); 3375assert_eq! ( 3376 std:: fs:: read_to_string ( artifact. dir . join ( "index.js" )). unwrap (), 3377"export default 1;" 3378); 3379let scoped =block_on ( store. npm_acquire ( "npm:@scope/pkg" )). unwrap (); 3380assert_eq! ( scoped. identity . version , "1.0.0" ); 3381assert_eq! ( 3382 server 3383. requests 3384. lock () 3385. unwrap () 3386. iter () 3387. filter ( |path| path. as_str () =="/tool" ) 3388. count (), 33891 , 3390"acquisition reuses selected metadata" 3391); 3392assert_eq! ( server. requests . lock (). unwrap (). len (), fetched +2 ); 3393} 3394 3395# [ test ] 3396fn oversized_abbreviated_metadata_is_rejected_within_cap () { 3397let server =Registry :: new (); 3398 server. publish_npm ( 3399"big" , 3400"1.0.0" , 3401& [( 3402"package.json" , 3403"{\"name\": \"big\", \"version\": \"1.0.0\"}" , 3404)], 3405); 3406 server. publish_npm_abbreviated ( "big" , vec! [ b'z' ; 8 * 1024 * 1024 +1 ]); 3407let dir = tempfile:: tempdir (). unwrap (); 3408let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (); 3409assert! ( 3410block_on ( store. npm_select_version ( "npm:big" )) 3411. unwrap_err () 3412. to_string () 3413. contains ( "too large" ), 3414"cap still applies to abbreviated responses" 3415); 3416} 3417 3418# [ test ] 3419fn direct_and_binding_imports_agree_on_selection_locking_and_modules () { 3420let server =Registry :: new (); 3421for versionin [ "1.0.0" , "1.1.0" ] { 3422let files =parity_files ( version); 3423 server. publish_npm ( 3424"parity" , 3425 version, 3426& files 3427. iter () 3428. map ( |( path, source) |( * path, source. as_str ())) 3429. collect ::< Vec < _ >>(), 3430); 3431} 3432let direct = tempfile:: tempdir (). unwrap (); 3433 std:: fs:: write ( 3434 direct. path (). join ( "main.js" ), 3435"import value from 'npm:parity@^1';\nimport sub from 'npm:parity@^1/sub';\nconsole.log(value, sub);" , 3436) 3437. unwrap (); 3438let bound = tempfile:: tempdir (). unwrap (); 3439 std:: fs:: write ( bound. path (). join ( "avril.conf" ), "deps: ['npm:parity@^1']" ). unwrap (); 3440 std:: fs:: write ( 3441 bound. path (). join ( "main.js" ), 3442"import value from 'parity';\nimport sub from 'parity/sub';\nconsole.log(value, sub);" , 3443) 3444. unwrap (); 3445let direct_stdout =success ( & server. run_npm ( direct. path (), & [ "run" ], "main.js" )); 3446let bound_stdout =success ( & server. run_npm ( bound. path (), & [ "run" ], "main.js" )); 3447// The caret range resolves identically through both routes. 3448assert_eq! ( direct_stdout, bound_stdout); 3449assert_eq! ( direct_stdout, "v1.1.0 sub-1.1.0\n" ); 3450let direct_lock = fixture:: read_lock ( direct. path ()); 3451let bound_lock = fixture:: read_lock ( bound. path ()); 3452assert_eq! ( 3453 direct_lock[ "npm" ][ "specifiers" ], 3454 bound_lock[ "npm" ][ "specifiers" ] 3455); 3456assert_eq! ( direct_lock[ "npm" ][ "specifiers" ][ "npm:parity@^1" ], "1.1.0" ); 3457assert_eq! ( 3458 direct_lock[ "npm" ][ "packages" ], 3459 bound_lock[ "npm" ][ "packages" ] 3460); 3461assert_eq! ( direct_lock, bound_lock); 3462// The subpath resolves to the same package instance through both routes. 3463// (Module file paths are per-project materializations, so identity is 3464// compared via the registry/name/version/instance record instead.) 3465assert_eq! ( 3466parity_packages ( & info_cli ( & server, direct. path ())), 3467parity_packages ( & info_cli ( & server, bound. path ())) 3468); 3469}