char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
51266f9
main
1use std:: collections::{ BTreeMap , BTreeSet , HashSet }; 2use std:: io::{ Read , Write }; 3use std:: path::{ Path , PathBuf }; 4use std:: sync::{ Arc , Mutex }; 5use std:: time:: Duration ; 6 7use bytes:: Bytes ; 8use cap_fs_ext:: OpenOptionsFollowExt ; 9use futures::{ 10FutureExt , 11 channel:: oneshot, 12 future::{ BoxFuture , Shared }, 13}; 14 15type Acquisition =Shared < BoxFuture < ' static , Result < Arc < Vec < u8 >>, RegistryError >>>; 16type RemoteAcquisition =Shared < BoxFuture < ' static , Result < Arc < RemoteArtifact >, RegistryError >>>; 17pub ( super ) type NpmAcquisition = 18Shared < BoxFuture < ' static , Result < Arc < npm:: NpmArtifact >, npm:: NpmError >>>; 19 20use http_body_util::{ BodyExt , Empty }; 21use hyper:: Request ; 22use hyper_rustls::{ HttpsConnector , HttpsConnectorBuilder }; 23use hyper_util::{ 24 client:: legacy::{ Client , connect:: HttpConnector }, 25 rt:: TokioExecutor , 26}; 27use serde::{ Deserialize , Serialize }; 28use sha2::{ Digest , Sha256 }; 29use url:: Url ; 30 31mod error; 32mod http; 33mod jsr; 34pub mod npm; 35 36pub use error:: RegistryError ; 37use http::{ acquire_remote, http_url, remote_blob, remote_vendor_path, validate_remote_entry}; 38pub use jsr::{ JsrRequest , parse_jsr_request}; 39use jsr::{ PackageIndex , VersionMetadata , manifest_path, vendor_path}; 40 41pub ( crate ) use jsr:: validate_name; 42 43pub const MAX_ARTIFACT_SIZE : usize =32 * 1024 * 1024 ; 44 45/// Progress during package acquisition. Callbacks must not block or call 46/// back into the store. Most callbacks run on worker threads; npm solver 47/// callbacks run on the caller thread. No callback runs while in-memory 48/// store or resolver state locks are held. 49# [ derive ( Clone , Debug , PartialEq , Eq )] 50pub enum ProgressEvent { 51NpmMetadata { name : String }, 52NpmArchive { name : String , version : String }, 53NpmMaterialized { name : String , version : String }, 54SourceDownload { url : String }, 55NpmMaterializing { name : String , version : String }, 56NpmResolving , 57} 58 59pub type ProgressSink =Arc < dyn Fn ( & ProgressEvent ) +Send +Sync >; 60 61struct AcquisitionContext { 62client : Client < HttpsConnector < HttpConnector >, Empty < Bytes >>, 63options : Options , 64authority : Option < crate :: authority:: SourceAuthority >, 65progress : Option < ProgressSink >, 66} 67 68# [ derive ( Clone , Debug )] 69pub struct Options { 70pub jsr_registry : Url , 71pub npm_registry : Url , 72pub cache : PathBuf , 73pub lock : PathBuf , 74pub project_root : PathBuf , 75pub offline : bool , 76pub frozen : bool , 77pub update : bool , 78/// Read and resolve against the lock in memory, but never write it back. 79pub ephemeral_lock : bool , 80pub vendor : Option < PathBuf >, 81} 82 83# [ derive ( Clone , Debug , PartialEq , Eq , Serialize , Deserialize )] 84# [ serde ( deny_unknown_fields , rename_all = "camelCase" )] 85pub struct Lockfile { 86pub version : u32 , 87pub jsr_registry : String , 88# [ serde ( default , skip_serializing_if = "Option::is_none" )] 89pub npm_registry : Option < String >, 90pub specifiers : BTreeMap < String , String >, 91pub packages : BTreeMap < String , String >, 92# [ serde ( default , skip_serializing_if = "BTreeMap::is_empty" )] 93pub remote : BTreeMap < String , RemoteEntry >, 94# [ serde ( default , skip_serializing_if = "NpmLock::is_empty" )] 95pub npm : NpmLock , 96} 97 98# [ derive ( Clone , Debug , Default , PartialEq , Eq , Serialize , Deserialize )] 99# [ serde ( deny_unknown_fields )] 100pub struct NpmLock { 101# [ serde ( default )] 102pub specifiers : BTreeMap < String , String >, 103# [ serde ( default )] 104pub packages : BTreeMap < String , NpmPin >, 105# [ serde ( default )] 106pub roots : BTreeMap < String , String >, 107# [ serde ( default )] 108pub instances : BTreeMap < String , NpmInstancePin >, 109# [ serde ( default , skip_serializing_if = "BTreeMap::is_empty" )] 110pub types : BTreeMap < String , Option < String >>, 111} 112 113impl NpmLock { 114fn is_empty ( & self ) ->bool { 115self . specifiers . is_empty () 116 &&self . packages . is_empty () 117 &&self . roots . is_empty () 118 &&self . instances . is_empty () 119 &&self . types . is_empty () 120} 121} 122 123# [ derive ( Clone , Debug , PartialEq , Eq , Serialize , Deserialize )] 124# [ serde ( deny_unknown_fields )] 125pub struct NpmInstancePin { 126pub package : String , 127# [ serde ( default )] 128pub deps : BTreeMap < String , String >, 129# [ serde ( default )] 130pub peers : BTreeMap < String , String >, 131} 132 133// Keep unvisited legacy file maps inline until their sidecars are persisted. 134# [ derive ( Clone , Debug , PartialEq , Eq , Serialize , Deserialize )] 135# [ serde ( deny_unknown_fields )] 136pub struct NpmPin { 137pub tarball : String , 138pub integrity : String , 139# [ serde ( default , skip_serializing_if = "Option::is_none" )] 140pub manifest : Option < String >, 141# [ serde ( default , skip_serializing_if = "Option::is_none" )] 142pub files : Option < BTreeMap < String , String >>, 143} 144 145impl NpmPin { 146fn is_legacy ( & self ) ->bool { 147self . files . is_some () 148} 149 150// Conflict checks accept migration; structural equality still detects lock edits. 151fn same_content ( & self , other : & Self ) ->bool { 152self . tarball == other. tarball 153 &&self . integrity == other. integrity 154 &&matches! (( self . digest (), other. digest ()), ( Some ( a), Some ( b)) if a == b) 155} 156 157fn digest ( & self ) ->Option < String > { 158if let Some ( manifest) =& self . manifest { 159return Some ( manifest. clone ()); 160} 161self . files 162. as_ref () 163. and_then ( |files| npm:: storage:: manifest_digest ( files). ok ()) 164} 165} 166 167# [ derive ( Clone , Debug , PartialEq , Eq , Serialize , Deserialize )] 168# [ serde ( deny_unknown_fields )] 169pub struct RemoteEntry { 170pub checksum : String , 171pub url : String , 172} 173 174struct RemoteArtifact { 175bytes : Vec < u8 >, 176entry : RemoteEntry , 177} 178 179# [ derive ( Default , Clone )] 180struct Used { 181specifiers : BTreeSet < String >, 182packages : BTreeSet < String >, 183sources : BTreeSet < String >, 184pending_specifiers : BTreeSet < String >, 185pending_packages : BTreeSet < String >, 186remote : BTreeSet < String >, 187pending_remote : BTreeSet < String >, 188configs : BTreeSet < String >, 189npm_specifiers : BTreeSet < String >, 190npm_packages : BTreeSet < String >, 191pending_npm_specifiers : BTreeSet < String >, 192pending_npm_packages : BTreeSet < String >, 193npm_roots : BTreeSet < String >, 194npm_instances : BTreeSet < String >, 195pending_npm_roots : BTreeSet < String >, 196pending_npm_instances : BTreeSet < String >, 197npm_types : BTreeSet < String >, 198pending_npm_types : BTreeSet < String >, 199npm_types_discovered : BTreeSet < String >, 200} 201 202/// Acquires packages on a caller-owned Tokio runtime, which must stay alive and driven. 203/// Synchronous consumers need a runtime whose workers run independently. 204pub struct PackageStore { 205options : Options , 206authority : Option < crate :: authority:: SourceAuthority >, 207progress : Option < ProgressSink >, 208npm_packuments : Arc < Mutex < BTreeMap < String , npm:: SharedPackument >>>, 209npm_downloads : Arc < tokio:: sync:: Semaphore >, 210npm_types_gate : tokio:: sync:: Mutex <()>, 211lock : Mutex < Lockfile >, 212used : Mutex < Used >, 213metadata : Mutex < BTreeMap < String , Arc < VersionMetadata >>>, 214indexes : Mutex < BTreeMap < String , Arc < PackageIndex >>>, 215inflight : Mutex < BTreeMap < String , Acquisition >>, 216remote_acquisitions : Mutex < BTreeMap < String , RemoteAcquisition >>, 217remote_bases : Mutex < BTreeMap < String , String >>, 218npm_inflight : Mutex < BTreeMap < String , NpmAcquisition >>, 219runtime : tokio:: runtime:: Handle , 220client : Client < HttpsConnector < HttpConnector >, Empty < Bytes >>, 221} 222 223impl PackageStore { 224pub fn new ( runtime : tokio:: runtime:: Handle , options : Options ) ->Result < Self , RegistryError > { 225Self :: construct ( runtime, options, None ) 226} 227 228/// Establish source authority before any lockfile or source IO, so a 229/// Read graph never parses an unreadable lock. Execute skips the gate 230/// to preserve existing host-side lock handling. Use one authority for 231/// the graph, packages, and store. 232pub fn new_with_authority ( 233runtime : tokio:: runtime:: Handle , 234options : Options , 235authority : crate :: authority:: SourceAuthority , 236) ->Result < Self , RegistryError > { 237Self :: construct ( runtime, options, Some ( authority)) 238} 239 240fn construct ( 241runtime : tokio:: runtime:: Handle , 242options : Options , 243authority : Option < crate :: authority:: SourceAuthority >, 244) ->Result < Self , RegistryError > { 245if options. update && options. frozen { 246return Err ( RegistryError :: UpdateAndFrozen ); 247} 248validate_registry ( & options. jsr_registry ) ?; 249validate_registry ( & options. npm_registry ) ?; 250if !options. project_root . is_absolute () { 251return Err ( RegistryError :: NpmProjectRootNotAbsolute ); 252} 253let mut lock =match read_lock ( & options. lock , authority. as_ref ()) ?{ 254Some ( lock) => lock, 255None if options. frozen =>{ 256return Err ( RegistryError :: FrozenLockfileDoesNotExist { 257path : options. lock . clone (), 258}); 259} 260None =>Lockfile { 261version : 1 , 262jsr_registry : options. jsr_registry . to_string (), 263npm_registry : None , 264specifiers : BTreeMap :: new (), 265packages : BTreeMap :: new (), 266remote : BTreeMap :: new (), 267npm : NpmLock :: default (), 268}, 269}; 270validate_lock ( & lock, & options. jsr_registry , & options. npm_registry ) ?; 271if options. update { 272 lock. specifiers . clear (); 273 lock. npm . specifiers . clear (); 274 lock. npm . roots . clear (); 275 lock. npm . instances . clear (); 276} 277let connector =HttpsConnectorBuilder :: new () 278. with_webpki_roots () 279. https_or_http () 280. enable_http1 () 281. enable_http2 () 282. build (); 283let client =Client :: builder ( TokioExecutor :: new ()). build ( connector); 284Ok ( Self { 285 options, 286 authority, 287progress : None , 288npm_packuments : Arc :: new ( Mutex :: new ( BTreeMap :: new ())), 289npm_downloads : Arc :: new ( tokio:: sync:: Semaphore :: new ( 290 npm:: MAX_NPM_CONCURRENT_DOWNLOADS , 291)), 292npm_types_gate : tokio:: sync:: Mutex :: new (()), 293lock : Mutex :: new ( lock), 294used : Mutex :: default (), 295metadata : Mutex :: default (), 296indexes : Mutex :: default (), 297inflight : Mutex :: default (), 298remote_acquisitions : Mutex :: default (), 299remote_bases : Mutex :: default (), 300npm_inflight : Mutex :: default (), 301 runtime, 302 client, 303}) 304} 305 306pub fn with_capabilities ( mut self , caps : Arc < crate :: caps:: Capabilities >) ->Self { 307self . authority =Some ( crate :: authority:: SourceAuthority :: execute ( caps)); 308self 309} 310 311pub fn with_progress ( mut self , sink : ProgressSink ) ->Self { 312self . progress =Some ( sink); 313self 314} 315 316pub fn options ( & self ) ->& Options { 317& self . options 318} 319 320pub async fn remote_source ( & self , name : & str ) ->Result < String , RegistryError > { 321if name. starts_with ( self . options . jsr_registry . as_str ()) { 322return Ok ( self . source ( name). await ?); 323} 324self . remote_artifact ( name, |_|Ok (())). await 325} 326 327pub async fn remote_config ( & self , name : & str ) ->Result < String , RegistryError > { 328if name. starts_with ( self . options . jsr_registry . as_str ()) { 329return Err ( RegistryError :: HttpConfigUsesJsrRegistry { name : name. to_owned () }); 330} 331let source =self 332. remote_artifact ( name, |entry|{ 333if entry. url . starts_with ( self . options . jsr_registry . as_str ()) { 334Err ( RegistryError :: HttpConfigRedirectsIntoJsr { url : entry. url . clone () }) 335} else { 336Ok (()) 337} 338}) 339. await ?; 340self . used . lock (). unwrap (). configs . insert ( name. to_owned ()); 341Ok ( source) 342} 343 344async fn remote_artifact ( 345& self , 346name : & str , 347validate : impl FnOnce ( & RemoteEntry ) ->Result <(), RegistryError >, 348) ->Result < String , RegistryError > { 349let url =http_url ( name) ?; 350let access =Access :: Module ( url. clone ()); 351 access. check ( self . authority . as_ref ()) ?; 352let expected =self . lock . lock (). unwrap (). remote . get ( name). cloned (); 353if expected. is_none () &&self . options . frozen { 354return Err ( RegistryError :: FrozenLockfileMissingRemote { name : name. to_owned () }); 355} 356let future ={ 357let mut acquisitions =self . remote_acquisitions . lock (). unwrap (); 358 acquisitions 359. entry ( name. to_owned ()) 360. or_insert_with ( ||{ 361let context =AcquisitionContext { 362options : self . options . clone (), 363client : self . client . clone (), 364authority : self . authority . clone (), 365progress : self . progress . clone (), 366}; 367let access = access. clone (); 368let name = name. to_owned (); 369let ( send, receive) = oneshot:: channel (); 370self . runtime . spawn ( async move { 371let result =acquire_remote ( context, name, url, expected, access) 372. await 373. map ( Arc :: new); 374let _ = send. send ( result); 375}); 376async move { 377 receive 378. await 379. map_err ( |_|RegistryError :: HttpAcquisitionWorkerStopped ) ? 380. map_err ( Into :: into) 381} 382. boxed () 383. shared () 384}) 385. clone () 386}; 387let result = future. clone (). await ; 388if result. is_err () { 389let mut acquisitions =self . remote_acquisitions . lock (). unwrap (); 390if acquisitions 391. get ( name) 392. is_some_and ( |current| current. ptr_eq ( & future)) 393{ 394 acquisitions. remove ( name); 395} 396} 397 access. check ( self . authority . as_ref ()) ?; 398let artifact = result?; 399validate ( & artifact. entry ) ?; 400if let Some ( authority) =self . authority . as_ref () { 401 authority. check_remote_final ( & artifact. entry . url ) ?; 402} 403let source =String :: from_utf8 ( artifact. bytes . clone ()) 404. map_err ( |_|RegistryError :: ModuleNotUtf8 { name : name. to_owned () }) ?; 405let mut lock =self . lock . lock (). unwrap (); 406if !self . options . update 407 &&let Some ( existing) = lock. remote . get ( name) 408 && existing !=& artifact. entry 409{ 410return Err ( RegistryError :: RemoteIntegrityMismatch { name : name. to_owned () }); 411} 412let mut used =self . used . lock (). unwrap (); 413 used. remote . insert ( name. to_owned ()); 414if lock. remote . get ( name) !=Some ( & artifact. entry ) ||self . options . update { 415 used. pending_remote . insert ( name. to_owned ()); 416} 417 lock. remote . insert ( name. to_owned (), artifact. entry . clone ()); 418self . remote_bases 419. lock () 420. unwrap () 421. insert ( name. to_owned (), artifact. entry . url . clone ()); 422Ok ( source) 423} 424 425pub fn remote_base ( & self , name : & str ) ->Result < String , RegistryError > { 426http_url ( name) ?; 427if name. starts_with ( self . options . jsr_registry . as_str ()) { 428return Ok ( name. to_owned ()); 429} 430self . remote_bases 431. lock () 432. unwrap () 433. get ( name) 434. cloned () 435. ok_or_else ( ||RegistryError :: HttpModuleNotAcquired { name : name. to_owned () }) 436} 437 438/// Reauthorize remote access by acquired identity under the caller's 439/// authority: the requested URL always, plus the recorded final URL 440/// under Read. Execute keeps requested-only redirect semantics. 441pub fn check_acquired_remote ( 442& self , 443name : & str , 444authority : & crate :: authority:: SourceAuthority , 445) ->Result <(), RegistryError > { 446let url =http_url ( name) ?; 447Access :: Module ( url). check ( Some ( authority)) ?; 448if let Some ( final_url) =self . remote_bases . lock (). unwrap (). get ( name). cloned () { 449 authority. check_remote_final ( & final_url) ?; 450} 451Ok (()) 452} 453 454async fn bytes ( 455& self , 456url : Url , 457expected : Option < String >, 458refresh : bool , 459access : Access , 460) ->Result < Vec < u8 >, RegistryError > { 461 access. check ( self . authority . as_ref ()) ?; 462let key = url. to_string (); 463let future ={ 464let mut inflight =self . inflight . lock (). unwrap (); 465 inflight 466. entry ( key. clone ()) 467. or_insert_with ( ||{ 468let context =AcquisitionContext { 469options : self . options . clone (), 470client : self . client . clone (), 471authority : self . authority . clone (), 472progress : self . progress . clone (), 473}; 474let access = access. clone (); 475let expected = expected. clone (); 476let ( send, receive) = oneshot:: channel (); 477self . runtime . spawn ( async move { 478let result =acquire ( context, url, expected, refresh, access) 479. await 480. map ( Arc :: new); 481let _ = send. send ( result); 482}); 483async move { 484 receive 485. await 486. map_err ( |_|RegistryError :: PackageAcquisitionWorkerStopped ) ? 487} 488. boxed () 489. shared () 490}) 491. clone () 492}; 493let result = future. clone (). await ; 494{ 495let mut inflight =self . inflight . lock (). unwrap (); 496if inflight 497. get ( & key) 498. is_some_and ( |current| current. ptr_eq ( & future)) 499{ 500 inflight. remove ( & key); 501} 502} 503let bytes = result?; 504 access. check ( self . authority . as_ref ()) ?; 505if let Some ( expected) = expected{ 506verify ( & bytes, & expected) ?; 507} 508Ok (( * bytes). clone ()) 509} 510 511pub fn save ( & self ) ->Result <(), RegistryError > { 512let ( lock, mut used) ={ 513let lock =self . lock . lock (). unwrap (); 514( lock. clone (), self . used . lock (). unwrap (). clone ()) 515}; 516let pending_types = std:: mem:: take ( & mut used. pending_npm_types ); 517 used. npm_types . retain ( |key| !pending_types. contains ( key)); 518self . save_used ( lock, used, false ) 519} 520 521pub fn save_graph ( 522& self , 523specifiers : & BTreeSet < String >, 524modules : & HashSet < String >, 525) ->Result <(), RegistryError > { 526let packages: BTreeSet < _ > = modules 527. iter () 528. filter ( |name| name. starts_with ( self . options . jsr_registry . as_str ())) 529. map ( |name|{ 530self . module_base ( name) 531. map ( |base|format! ( "{}_meta.json" , base. as_str (). trim_end_matches ( '/' ))) 532}) 533. collect ::< Result < _ , _ >>() ?; 534let ( lock, mut used) ={ 535let lock =self . lock . lock (). unwrap (); 536( lock. clone (), self . used . lock (). unwrap (). clone ()) 537}; 538 used. specifiers . retain ( |key| specifiers. contains ( key)); 539 used. packages . retain ( |url| packages. contains ( url)); 540 used. pending_specifiers 541. retain ( |key| specifiers. contains ( key)); 542 used. pending_packages . retain ( |url| packages. contains ( url)); 543 used. remote 544. retain ( |url| modules. contains ( url) || used. configs . contains ( url)); 545 used. pending_remote 546. retain ( |url| modules. contains ( url) || used. configs . contains ( url)); 547// npm entries persist only for this successful walk: acquisitions 548// record usage while preparing, but a resolver success the module 549// graph never completes (bad syntax, builtins, CJS) must not leak 550// into the lock. Exact tracked roots stay, plus declared roots 551// sharing a successful batch context so unused dependencies replay 552// under frozen; unrelated or failed contexts are dropped, as are 553// the instances, pins, and selections only those reference. 554let mut kept_roots: BTreeSet < String > = specifiers 555. iter () 556. filter ( |key| used. npm_roots . contains ( * key)) 557. cloned () 558. collect (); 559let suffixes: BTreeSet < String > = kept_roots 560. iter () 561. filter_map ( |key| key. split_once ( '\0' ). map ( |( _, suffix) | suffix. to_owned ())) 562. collect (); 563for keyin & used. npm_roots { 564if let Some (( _, suffix)) = key. split_once ( '\0' ) 565 && suffixes. contains ( suffix) 566{ 567 kept_roots. insert ( key. clone ()); 568} 569} 570let mut kept_instances: BTreeSet < String > = kept_roots 571. iter () 572. filter_map ( |key| lock. npm . roots . get ( key)) 573. cloned () 574. collect (); 575let mut frontier: Vec < String > = kept_instances. iter (). cloned (). collect (); 576while let Some ( id) = frontier. pop () { 577if let Some ( pin) = lock. npm . instances . get ( & id) { 578for nextin pin. deps . values (). chain ( pin. peers . values ()) { 579if kept_instances. insert ( next. clone ()) { 580 frontier. push ( next. clone ()); 581} 582} 583} 584} 585let kept_packages: BTreeSet < String > = kept_instances 586. iter () 587. filter_map ( |id| lock. npm . instances . get ( id)) 588. map ( |pin| pin. package . clone ()) 589. collect (); 590 used. npm_roots . retain ( |key| kept_roots. contains ( key)); 591 used. pending_npm_roots 592. retain ( |key| kept_roots. contains ( key)); 593 used. npm_instances 594. retain ( |key| kept_instances. contains ( key)); 595 used. pending_npm_instances 596. retain ( |key| kept_instances. contains ( key)); 597 used. npm_packages . retain ( |key| kept_packages. contains ( key)); 598 used. pending_npm_packages 599. retain ( |key| kept_packages. contains ( key)); 600 used. npm_specifiers . retain ( |key|{ 601 npm:: parse_npm_request ( key). is_ok_and ( |request|{ 602 lock. npm . specifiers . get ( key). is_some_and ( |version|{ 603 kept_packages. iter (). any ( |package|{ 604 package. as_str () 605 ==format! ( 606"{}|{}|{version}" , 607self . options . npm_registry . as_str (), 608 request. name 609) 610}) 611}) 612}) 613}); 614 used. pending_npm_specifiers 615. retain ( |key| used. npm_specifiers . contains ( key)); 616 used. npm_types . retain ( |key|{ 617 kept_packages. contains ( key) 618 && lock. npm . types . get ( key). is_some_and ( |request|{ 619 request. as_ref (). is_none_or ( |request|{ 620 kept_roots. iter (). any ( |root|{ 621 root == request || root. starts_with ( & format! ( "{request}\0ctx:" )) 622}) 623}) 624}) 625}); 626 used. pending_npm_types 627. retain ( |key| used. npm_types . contains ( key)); 628self . save_used ( lock, used, false ) 629} 630 631pub fn prune ( & self ) ->Result <(), RegistryError > { 632if self . options . frozen { 633return Err ( RegistryError :: PruneFrozen ); 634} 635let ( lock, mut used) ={ 636let lock =self . lock . lock (). unwrap (); 637( lock. clone (), self . used . lock (). unwrap (). clone ()) 638}; 639// Prune the npm graph to its full closure: saved roots, every 640// instance reachable from them, and the specifiers and pins those 641// reference. Roots only a failed walk prepared are pending without 642// a file entry, so they (and their pending writes) drop out here; 643// every CLI prune follows the walk save that files successes. 644// Computed includes from earlier walks stay when reached. 645let disk =read_lock ( & self . options . lock , self . authority . as_ref ()) ?; 646let disk_roots: BTreeSet < String > = disk 647. iter () 648. flat_map ( |disk| disk. npm . roots . keys (). cloned ()) 649. collect (); 650let disk_packages: BTreeSet < String > = disk 651. iter () 652. flat_map ( |disk| disk. npm . packages . keys (). cloned ()) 653. collect (); 654let mut reachable =BTreeSet :: new (); 655let mut frontier: Vec < String > = used 656. npm_roots 657. iter () 658. filter ( |key| disk_roots. contains ( key. as_str ())) 659. filter_map ( |key| lock. npm . roots . get ( key). cloned ()) 660. collect (); 661while let Some ( instance) = frontier. pop () { 662if !reachable. insert ( instance. clone ()) { 663continue ; 664} 665if let Some ( pin) = lock. npm . instances . get ( & instance) { 666 frontier. extend ( pin. deps . values (). cloned ()); 667 frontier. extend ( pin. peers . values (). cloned ()); 668} 669} 670 used. npm_roots . retain ( |key|{ 671 lock. npm 672. roots 673. get ( key) 674. is_some_and ( |instance| reachable. contains ( instance)) 675}); 676 used. pending_npm_roots 677. retain ( |key| used. npm_roots . contains ( key)); 678 used. npm_instances . retain ( |key| reachable. contains ( key)); 679 used. pending_npm_instances 680. retain ( |key| reachable. contains ( key)); 681// Direct acquisitions record packages without graph roots; ones an 682// earlier save persisted stay, while pins no successful walk kept 683// never land. Acquisitions alone cannot tell those apart, but the 684// lockfile can: only file-backed pins join the closure here. 685let mut packages: BTreeSet < String > = used 686. npm_packages 687. iter () 688. filter ( |key| disk_packages. contains ( key. as_str ())) 689. cloned () 690. collect (); 691 packages. extend ( 692 reachable 693. iter () 694. filter_map ( |instance| lock. npm . instances . get ( instance)) 695. map ( |pin| pin. package . clone ()), 696); 697 used. npm_packages . retain ( |key| packages. contains ( key)); 698 used. pending_npm_packages 699. retain ( |key| packages. contains ( key)); 700 used. npm_specifiers . retain ( |key|{ 701 npm:: parse_npm_request ( key). is_ok_and ( |request|{ 702 lock. npm . specifiers . get ( key). is_some_and ( |version|{ 703 packages. iter (). any ( |package|{ 704 package. as_str () 705 ==format! ( 706"{}|{}|{version}" , 707self . options . npm_registry . as_str (), 708 request. name 709) 710}) 711}) 712}) 713}); 714 used. pending_npm_specifiers 715. retain ( |key| used. npm_specifiers . contains ( key)); 716 used. npm_types . retain ( |key|{ 717 !used. pending_npm_types . contains ( key) 718 && packages. contains ( key) 719 && lock. npm . types . get ( key). is_some_and ( |request|{ 720 request. as_ref (). is_none_or ( |request|{ 721let Ok (( registry, _, _)) = npm:: parse_npm_key ( key) else { 722return false ; 723}; 724let Ok ( request) = npm:: parse_npm_request ( request) else { 725return false ; 726}; 727 lock. npm 728. specifiers 729. get ( & request. request_key ()) 730. is_some_and ( |version|{ 731 packages 732. contains ( & format! ( "{}|{}|{version}" , registry, request. name )) 733}) 734}) 735}) 736}); 737 used. pending_npm_types 738. retain ( |key| used. npm_types . contains ( key)); 739self . save_used ( lock, used, true ) 740} 741 742fn save_used ( & self , lock : Lockfile , used : Used , prune : bool ) ->Result <(), RegistryError > { 743if self . options . frozen 744 ||self . options . ephemeral_lock 745 ||( !prune 746 && used. packages . is_empty () 747 && used. specifiers . is_empty () 748 && used. remote . is_empty () 749 && used. npm_specifiers . is_empty () 750 && used. npm_packages . is_empty () 751 && used. npm_roots . is_empty () 752 && used. npm_instances . is_empty () 753 && used. npm_types . is_empty ()) 754{ 755return Ok (()); 756} 757if !prune 758 && used. pending_packages . is_empty () 759 && used. pending_specifiers . is_empty () 760 && used. pending_remote . is_empty () 761 && used. pending_npm_specifiers . is_empty () 762 && used. pending_npm_packages . is_empty () 763 && used. pending_npm_roots . is_empty () 764 && used. pending_npm_instances . is_empty () 765 && used. pending_npm_types . is_empty () 766{ 767if let Some ( disk) =read_lock ( & self . options . lock , self . authority . as_ref ()) ?{ 768self . merge ( & lock, & used, disk) ?; 769} 770return Ok (()); 771} 772let path =& self . options . lock ; 773let parent = path. parent (). ok_or ( RegistryError :: LockfileNeedsParent ) ?; 774 std:: fs:: create_dir_all ( parent) ?; 775update_file ( 776 path, 777 ||read_lock_bytes ( path, self . authority . as_ref ()), 778 |disk|{ 779let mut merged =match disk{ 780Some ( bytes) =>parse_lock ( path, bytes) ?, 781None =>Lockfile { 782version : 1 , 783jsr_registry : lock. jsr_registry . clone (), 784npm_registry : None , 785specifiers : BTreeMap :: new (), 786packages : BTreeMap :: new (), 787remote : BTreeMap :: new (), 788npm : NpmLock :: default (), 789}, 790}; 791let previous = merged. clone (); 792 merged =self . merge ( & lock, & used, merged) ?; 793if prune{ 794 merged 795. specifiers 796. retain ( |key, _| used. specifiers . contains ( key)); 797 merged. packages . retain ( |url, _| used. packages . contains ( url)); 798 merged. remote . retain ( |url, _| used. remote . contains ( url)); 799 merged 800. npm 801. specifiers 802. retain ( |key, _| used. npm_specifiers . contains ( key)); 803 merged 804. npm 805. packages 806. retain ( |key, _| used. npm_packages . contains ( key)); 807 merged 808. npm 809. roots 810. retain ( |key, _| used. npm_roots . contains ( key)); 811 merged 812. npm 813. instances 814. retain ( |key, _| used. npm_instances . contains ( key)); 815 merged 816. npm 817. types 818. retain ( |key, _| used. npm_types . contains ( key)); 819} 820if merged == previous{ 821return Ok ( None ); 822} 823let value = serde_json:: to_value ( & merged) ?; 824let mut bytes = serde_fjson:: stringify ( & value) ?. into_bytes (); 825 bytes. push ( b'\n' ); 826Ok ( Some ( bytes)) 827}, 828) ?; 829let mut current =self . used . lock (). unwrap (); 830for keyin used. pending_specifiers { 831 current. pending_specifiers . remove ( & key); 832} 833for urlin used. pending_packages { 834 current. pending_packages . remove ( & url); 835} 836for urlin used. pending_remote { 837 current. pending_remote . remove ( & url); 838} 839for keyin used. pending_npm_specifiers { 840 current. pending_npm_specifiers . remove ( & key); 841} 842for keyin used. pending_npm_packages { 843 current. pending_npm_packages . remove ( & key); 844} 845for keyin used. pending_npm_roots { 846 current. pending_npm_roots . remove ( & key); 847} 848for keyin used. pending_npm_instances { 849 current. pending_npm_instances . remove ( & key); 850} 851for keyin used. pending_npm_types { 852 current. pending_npm_types . remove ( & key); 853} 854Ok (()) 855} 856 857fn merge ( 858& self , 859lock : & Lockfile , 860used : & Used , 861mut merged : Lockfile , 862) ->Result < Lockfile , RegistryError > { 863validate_lock ( 864& merged, 865& self . options . jsr_registry , 866& self . options . npm_registry , 867) ?; 868for keyin & used. specifiers { 869if !self . options . update 870 && merged 871. specifiers 872. get ( key) 873. is_some_and ( |value| value !=& lock. specifiers [ key]) 874{ 875return Err ( RegistryError :: LockfileChanged { key : key. to_owned () }); 876} 877} 878for urlin & used. packages { 879if merged 880. packages 881. get ( url) 882. is_some_and ( |value| value !=& lock. packages [ url]) 883{ 884return Err ( RegistryError :: LockedMetadataChanged { url : url. to_owned () }); 885} 886} 887for urlin & used. remote { 888if !self . options . update 889 && merged 890. remote 891. get ( url) 892. is_some_and ( |value| value !=& lock. remote [ url]) 893{ 894return Err ( RegistryError :: LockfileChanged { key : url. to_owned () }); 895} 896} 897for urlin & used. pending_remote { 898 merged. remote . insert ( url. clone (), lock. remote [ url]. clone ()); 899} 900for keyin & used. pending_specifiers { 901let version =& lock. specifiers [ key]; 902 merged. specifiers . insert ( key. clone (), version. clone ()); 903} 904for urlin & used. pending_packages { 905let checksum =& lock. packages [ url]; 906 merged. packages . insert ( url. clone (), checksum. clone ()); 907} 908for keyin & used. npm_specifiers { 909if !self . options . update 910 && merged 911. npm 912. specifiers 913. get ( key) 914. is_some_and ( |value| value !=& lock. npm . specifiers [ key]) 915{ 916return Err ( RegistryError :: LockfileChanged { key : key. to_owned () }); 917} 918} 919for keyin & used. npm_packages { 920if merged 921. npm 922. packages 923. get ( key) 924. is_some_and ( |value| !value. same_content ( & lock. npm . packages [ key])) 925{ 926return Err ( RegistryError :: LockedNpmPackageChanged { key : key. to_owned () }); 927} 928} 929for keyin & used. pending_npm_specifiers { 930let version =& lock. npm . specifiers [ key]; 931 merged. npm . specifiers . insert ( key. clone (), version. clone ()); 932} 933for keyin & used. pending_npm_packages { 934let pin =& lock. npm . packages [ key]; 935 merged. npm . packages . insert ( key. clone (), pin. clone ()); 936} 937for keyin & used. npm_roots { 938if !self . options . update 939 && merged 940. npm 941. roots 942. get ( key) 943. is_some_and ( |value| value !=& lock. npm . roots [ key]) 944{ 945return Err ( RegistryError :: LockfileChanged { key : key. to_owned () }); 946} 947} 948for keyin & used. npm_instances { 949if merged 950. npm 951. instances 952. get ( key) 953. is_some_and ( |value| value !=& lock. npm . instances [ key]) 954{ 955return Err ( RegistryError :: LockedNpmInstanceChanged { id : key. to_owned () }); 956} 957} 958for keyin & used. pending_npm_roots { 959let instance =& lock. npm . roots [ key]; 960 merged. npm . roots . insert ( key. clone (), instance. clone ()); 961} 962for keyin & used. pending_npm_instances { 963let pin =& lock. npm . instances [ key]; 964 merged. npm . instances . insert ( key. clone (), pin. clone ()); 965} 966for keyin & used. npm_types { 967if !self . options . update 968 && !used. pending_npm_types . contains ( key) 969 && merged. npm . types . get ( key) != lock. npm . types . get ( key) 970{ 971return Err ( RegistryError :: LockfileChanged { key : key. clone () }); 972} 973} 974for keyin & used. pending_npm_types { 975let association = lock. npm . types [ key]. clone (); 976if !self . options . update 977 && merged 978. npm 979. types 980. get ( key) 981. is_some_and ( |existing| existing !=& association) 982{ 983return Err ( RegistryError :: LockfileChanged { key : key. clone () }); 984} 985 merged. npm . types . insert ( key. clone (), association); 986} 987if merged. npm . is_empty () { 988 merged. version =1 ; 989 merged. npm_registry =None ; 990} else { 991 merged. version =2 ; 992 merged. npm_registry =Some ( self . options . npm_registry . to_string ()); 993} 994Ok ( merged) 995} 996 997pub fn vendor_relative ( & self , name : & str ) ->Result < PathBuf , RegistryError > { 998if !name. starts_with ( self . options . jsr_registry . as_str ()) { 999return Ok ( remote_vendor_path ( name, & self . remote_base ( name) ?) ?); 1000} 1001let url =Url :: parse ( name) ?; 1002Ok ( vendor_path ( & self . options . jsr_registry , & url) ?) 1003} 1004 1005pub fn vendor ( & self , destination : & Path ) ->Result <(), RegistryError > { 1006let ( lock, used) ={ 1007let lock =self . lock . lock (). unwrap (); 1008( lock. clone (), self . used . lock (). unwrap (). clone ()) 1009}; 1010for namein & used. remote { 1011let entry =& lock. remote [ name]; 1012let relative =self . vendor_relative ( name) ?; 1013let bytes =match read_artifact ( & remote_blob ( & self . options . cache , & entry. checksum )) ?{ 1014Some ( bytes) =>Some ( bytes), 1015None =>match & self . options . vendor { 1016Some ( vendor) =>read_artifact ( & vendor. join ( & relative)) ?, 1017None =>None , 1018}, 1019} 1020. ok_or_else ( ||RegistryError :: MissingVendoredArtifact { name : name. to_owned () }) ?; 1021verify ( & bytes, & entry. checksum ) ?; 1022let target = destination. join ( relative); 1023 std:: fs:: create_dir_all ( target. parent (). unwrap ()) ?; 1024atomic_write ( & target, & bytes) ?; 1025} 1026for namein used. packages . iter (). chain ( & used. sources ) { 1027let url =Url :: parse ( name) ?; 1028let relative =vendor_path ( & self . options . jsr_registry , & url) ?; 1029let bytes =match & self . options . vendor { 1030Some ( vendor) =>match read_artifact ( & vendor. join ( & relative)) ?{ 1031Some ( bytes) =>Some ( bytes), 1032None =>{ 1033read_artifact ( & self . options . cache . join ( "files" ). join ( artifact_key ( name))) ? 1034} 1035}, 1036None =>read_artifact ( & self . options . cache . join ( "files" ). join ( artifact_key ( name))) ?, 1037} 1038. ok_or_else ( ||RegistryError :: MissingVendoredArtifact { name : name. to_owned () }) ?; 1039let expected =if let Some ( hash) = lock. packages . get ( name) { 1040 hash. clone () 1041} else { 1042let base =self . module_base ( name) ?; 1043let metadata_url =format! ( "{}_meta.json" , base. as_str (). trim_end_matches ( '/' )); 1044let metadata =self . metadata . lock (). unwrap (); 1045 metadata[ & metadata_url]. manifest [ & manifest_path ( & base, & url) ?] 1046. checksum 1047. clone () 1048}; 1049verify ( & bytes, & expected) ?; 1050let target = destination. join ( relative); 1051 std:: fs:: create_dir_all ( target. parent (). unwrap ()) ?; 1052atomic_write ( & target, & bytes) ?; 1053} 1054let targets: BTreeSet < String > = used 1055. npm_instances 1056. iter () 1057. filter_map ( |id| lock. npm . instances . get ( id)) 1058. map ( |pin| pin. package . clone ()) 1059. collect (); 1060for packagein & targets{ 1061let Some ( pin_entry) = lock. npm . packages . get ( package) else { 1062return Err ( RegistryError :: MissingVendoredArtifact { name : package. to_owned () }); 1063}; 1064let ( registry, name, version) = npm:: parse_npm_key ( package) ?; 1065if registry !=self . options . npm_registry { 1066return Err ( RegistryError :: NpmInstanceRegistryMismatch { 1067package : package. to_owned (), 1068}); 1069} 1070let source = npm:: storage:: npm_package_dir ( 1071& self . options , 1072 npm:: NPM_DEFAULT_INSTANCE , 1073& name, 1074& version, 1075); 1076let relative = npm:: storage:: npm_vendor_relative ( 1077& self . options , 1078 npm:: NPM_DEFAULT_INSTANCE , 1079& self . options . npm_registry , 1080& name, 1081& version, 1082) ?; 1083// The file map comes from the digest-authenticated sidecar, 1084// never from an unverified tree listing. 1085let files = npm:: storage:: load_vendor_source_files ( & self . options , package, pin_entry) ?; 1086for ( rel, expected) in & files{ 1087let bytes = npm:: storage:: read_npm_project_file ( & self . options , & source, rel) ?; 1088if checksum ( & bytes) !=* expected{ 1089return Err ( RegistryError :: NpmFileIntegrityMismatch { rel : rel. to_owned () }); 1090} 1091 npm:: storage:: write_vendor_file ( destination, & relative. join ( rel), & bytes) ?; 1092} 1093 npm:: storage:: write_vendor_manifest ( destination, & files) ?; 1094} 1095Ok (()) 1096} 1097} 1098 1099async fn acquire ( 1100context : AcquisitionContext , 1101url : Url , 1102expected : Option < String >, 1103refresh : bool , 1104access : Access , 1105) ->Result < Vec < u8 >, RegistryError > { 1106let AcquisitionContext { 1107 client, 1108 options, 1109 authority, 1110 progress, 1111} = context; 1112 access. check ( authority. as_ref ()) ?; 1113let key =artifact_key ( url. as_str ()); 1114let file = options. cache . join ( "files" ). join ( & key); 1115let cached = || ->Result < Option < Vec < u8 >>, RegistryError > { 1116if refresh && !options. offline { 1117return Ok ( None ); 1118} 1119let bytes =match & options. vendor { 1120Some ( vendor) =>{ 1121match read_artifact ( & vendor. join ( vendor_path ( & options. jsr_registry , & url) ?)) ?{ 1122Some ( bytes) =>Some ( bytes), 1123None =>read_artifact ( & file) ?, 1124} 1125} 1126None =>read_artifact ( & file) ?, 1127}; 1128if let ( Some ( bytes), Some ( expected)) =( & bytes, & expected) { 1129verify ( bytes, expected) 1130. map_err ( |_|RegistryError :: VerifyMismatch { name : url. to_string () }) ?; 1131} 1132Ok ( bytes) 1133}; 1134if let Some ( bytes) =cached () ?{ 1135return Ok ( bytes); 1136} 1137let lock_path = options. cache . join ( "locks" ). join ( & key); 1138 std:: fs:: create_dir_all ( lock_path. parent (). unwrap ()) ?; 1139let _guard = tokio:: task:: spawn_blocking ( move ||{ 1140let mut guard = fslock:: LockFile :: open ( & lock_path) ?; 1141 guard. lock () ?; 1142Ok ::< _ , RegistryError >( guard) 1143}) 1144. await 1145. map_err ( |detail|RegistryError :: WorkerJoin { detail : detail. to_string () }) ??; 1146// Recheck after locking because another process may have filled the cache. 1147if let Some ( bytes) =cached () ?{ 1148return Ok ( bytes); 1149} 1150 access. check ( authority. as_ref ()) ?; 1151if options. offline { 1152return Err ( RegistryError :: OfflineCacheMiss { url : url. to_string () }); 1153} 1154if let Some ( sink) =& progress{ 1155sink ( & ProgressEvent :: SourceDownload { url : url. to_string () }); 1156 access. check ( authority. as_ref ()) ?; 1157} 1158let download =async { 1159let request =Request :: get ( url. as_str ()) 1160. header ( "Accept" , "application/json, application/typescript, */*" ) 1161. header ( "User-Agent" , concat! ( "avril/" , env! ( "CARGO_PKG_VERSION" ))) 1162. body ( Empty :: new ()) 1163. map_err ( |detail|RegistryError :: HttpRequest { detail : detail. to_string () }) ?; 1164let response = client 1165. request ( request) 1166. await 1167. map_err ( |detail|RegistryError :: HttpRequest { detail : detail. to_string () }) ?; 1168if !response. status (). is_success () { 1169return Err ( RegistryError :: RegistryStatus { 1170status : response. status (). to_string (), 1171url : url. to_string (), 1172}); 1173} 1174if response 1175. headers () 1176. get ( "content-type" ) 1177. and_then ( |value| value. to_str (). ok ()) 1178. is_some_and ( |value| value. starts_with ( "text/html" )) 1179{ 1180return Err ( RegistryError :: RegistryHtmlArtifact { url : url. to_string () }); 1181} 1182let mut body = response. into_body (); 1183let mut bytes =Vec :: new (); 1184while let Some ( frame) = body. frame (). await { 1185if let Ok ( data) = frame 1186. map_err ( |error|RegistryError :: Hyper ( Arc :: new ( error))) ? 1187. into_data () 1188{ 1189if bytes. len () + data. len () >MAX_ARTIFACT_SIZE { 1190return Err ( RegistryError :: RegistryArtifactTooLarge { url : url. to_string () }); 1191} 1192 bytes. extend_from_slice ( & data); 1193} 1194} 1195Ok ::< _ , RegistryError >( bytes) 1196}; 1197let bytes = tokio:: time:: timeout ( Duration :: from_secs ( 30 ), download) 1198. await 1199. map_err ( |_|RegistryError :: RegistryTimeout { url : url. to_string () }) ??; 1200if let Some ( expected) =& expected{ 1201verify ( & bytes, expected) 1202. map_err ( |_|RegistryError :: VerifyMismatch { name : url. to_string () }) ?; 1203} 1204 std:: fs:: create_dir_all ( file. parent (). unwrap ()) ?; 1205 access. check ( authority. as_ref ()) ?; 1206atomic_write ( & file, & bytes) ?; 1207Ok ( bytes) 1208} 1209 1210# [ derive ( Clone )] 1211enum Access { 1212Package ( Url ), 1213Module ( Url ), 1214} 1215 1216impl Access { 1217fn check ( 1218& self , 1219authority : Option < & crate :: authority:: SourceAuthority >, 1220) ->Result <(), crate :: caps:: CapabilitiesError > { 1221let Some ( authority) = authorityelse { 1222return Ok (()); 1223}; 1224match self { 1225Self :: Package ( base) => authority. check_remote_package ( base), 1226Self :: Module ( url) => authority. check_remote_module ( url), 1227} 1228} 1229} 1230 1231pub fn checksum ( bytes : & [ u8 ]) ->String { 1232format! ( "sha256-{:x}" , Sha256 :: digest ( bytes)) 1233} 1234 1235pub fn artifact_key ( url : & str ) ->String { 1236format! ( "{:x}" , Sha256 :: digest ( url. as_bytes ())) 1237} 1238 1239fn valid_checksum ( checksum : & str ) ->bool { 1240 checksum 1241. strip_prefix ( "sha256-" ) 1242. is_some_and ( |hex| hex. len () ==64 && hex. bytes (). all ( |ch| ch. is_ascii_hexdigit ())) 1243} 1244 1245fn verify ( bytes : & [ u8 ], expected : & str ) ->Result <(), RegistryError > { 1246if checksum ( bytes) != expected{ 1247return Err ( RegistryError :: IntegrityMismatch ); 1248} 1249Ok (()) 1250} 1251 1252fn read_artifact ( path : & Path ) ->Result < Option < Vec < u8 >>, RegistryError > { 1253let mut options = std:: fs:: OpenOptions :: new (); 1254 options. read ( true ); 1255# [ cfg ( unix )] 1256{ 1257use std:: os:: unix:: fs:: OpenOptionsExt ; 1258 options. custom_flags ( libc:: O_NONBLOCK | libc:: O_NOFOLLOW ); 1259} 1260let file =match options. open ( path) { 1261Ok ( file) => file, 1262Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>return Ok ( None ), 1263Err ( error) =>return Err ( error. into ()), 1264}; 1265let metadata = file. metadata () ?; 1266if !metadata. is_file () { 1267return Err ( RegistryError :: ArtifactNotRegularFile ); 1268} 1269let capacity = usize:: try_from ( metadata. len (). min ( MAX_ARTIFACT_SIZE as u64 )). unwrap_or ( 0 ); 1270let mut bytes =Vec :: with_capacity ( capacity); 1271 file. take ( MAX_ARTIFACT_SIZE as u64 +1 ) 1272. read_to_end ( & mut bytes) ?; 1273if bytes. len () >MAX_ARTIFACT_SIZE { 1274return Err ( RegistryError :: CachedArtifactTooLarge ); 1275} 1276Ok ( Some ( bytes)) 1277} 1278 1279fn read_lock ( 1280path : & Path , 1281authority : Option < & crate :: authority:: SourceAuthority >, 1282) ->Result < Option < Lockfile >, RegistryError > { 1283read_lock_bytes ( path, authority) ? 1284. map ( |bytes|parse_lock ( path, & bytes)) 1285. transpose () 1286} 1287 1288fn read_lock_bytes ( 1289path : & Path , 1290authority : Option < & crate :: authority:: SourceAuthority >, 1291) ->Result < Option < Vec < u8 >>, RegistryError > { 1292let Some ( authority) = authority. filter ( |authority| authority. is_read ()) else { 1293return read_artifact ( path); 1294}; 1295let target =crate :: caps:: PathTarget :: for_entry ( path) ?; 1296 authority. check_local ( & target) ?; 1297let bytes =read_lock_nofollow ( & target) ?; 1298 authority. check_local ( & target) ?; 1299Ok ( bytes) 1300} 1301 1302fn read_lock_nofollow ( target : & crate :: caps:: PathTarget ) ->Result < Option < Vec < u8 >>, RegistryError > { 1303let ( directory, name) =crate :: caps:: fs:: parent ( target. as_path ()) ?; 1304let mut options = cap_std:: fs:: OpenOptions :: new (); 1305 options. read ( true ). follow ( cap_fs_ext:: FollowSymlinks :: No ); 1306# [ cfg ( unix )] 1307{ 1308use cap_fs_ext:: OpenOptionsExt ; 1309 options. custom_flags ( libc:: O_NONBLOCK ); 1310} 1311let file =match directory. open_with ( Path :: new ( & name), & options) { 1312Ok ( file) => file, 1313Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>return Ok ( None ), 1314Err ( error) =>return Err ( error. into ()), 1315}; 1316let metadata = file. metadata () ?; 1317if !metadata. is_file () { 1318return Err ( RegistryError :: LockfileNotRegularFile ); 1319} 1320let capacity = usize:: try_from ( metadata. len (). min ( MAX_ARTIFACT_SIZE as u64 )). unwrap_or ( 0 ); 1321let mut bytes =Vec :: with_capacity ( capacity); 1322 file. take ( MAX_ARTIFACT_SIZE as u64 +1 ) 1323. read_to_end ( & mut bytes) ?; 1324if bytes. len () >MAX_ARTIFACT_SIZE { 1325return Err ( RegistryError :: LockfileTooLarge ); 1326} 1327Ok ( Some ( bytes)) 1328} 1329 1330fn parse_lock ( path : & Path , bytes : & [ u8 ]) ->Result < Lockfile , RegistryError > { 1331let name = path. to_string_lossy (); 1332let source = std:: str:: from_utf8 ( bytes) 1333. map_err ( |_|RegistryError :: LockfileNotUtf8 { name : name. to_string () }) ?; 1334let value = serde_fjson:: parse ( source). map_err ( |error|RegistryError :: LockfileParse { 1335detail : error. render ( & name, source), 1336}) ?; 1337 serde_json:: from_value ( value). map_err ( |source|RegistryError :: LockfileJson { 1338name : name. to_string (), 1339source : Arc :: new ( source), 1340}) 1341} 1342 1343fn validate_registry ( registry : & Url ) ->Result <(), RegistryError > { 1344if !registry. as_str (). ends_with ( '/' ) 1345 || registry. query (). is_some () 1346 || registry. fragment (). is_some () 1347 || !registry. username (). is_empty () 1348 || registry. password (). is_some () 1349{ 1350return Err ( RegistryError :: InvalidRegistryBaseUrl ); 1351} 1352let loopback =matches! ( 1353 registry. host_str (), 1354Some ( "localhost" |"127.0.0.1" |"[::1]" ) 1355); 1356if registry. scheme () !="https" && !( registry. scheme () =="http" && loopback) { 1357return Err ( RegistryError :: RegistryRequiresHttps ); 1358} 1359Ok (()) 1360} 1361 1362fn validate_lock ( 1363lock : & Lockfile , 1364jsr_registry : & Url , 1365npm_registry : & Url , 1366) ->Result <(), RegistryError > { 1367if lock. version !=1 && lock. version !=2 { 1368return Err ( RegistryError :: UnsupportedLockfileVersion { version : lock. version }); 1369} 1370if lock. jsr_registry != jsr_registry. as_str () { 1371return Err ( RegistryError :: LockfileJsrRegistryMismatch ); 1372} 1373if lock. version ==1 { 1374if lock. npm_registry . is_some () || !lock. npm . is_empty () { 1375return Err ( RegistryError :: LockfileV1NpmPins ); 1376} 1377} else if let Some ( npm_registry_name) =& lock. npm_registry { 1378if npm_registry_name != npm_registry. as_str () { 1379return Err ( RegistryError :: LockfileNpmRegistryMismatch ); 1380} 1381} else if !lock. npm . is_empty () { 1382return Err ( RegistryError :: LockfileNpmPackagesWithoutRegistry ); 1383} 1384for ( name, entry) in & lock. remote { 1385http_url ( name) ?; 1386validate_remote_entry ( entry) ?; 1387if name. starts_with ( jsr_registry. as_str ()) { 1388return Err ( RegistryError :: RegistryModuleRemoteLockEntry { name : name. to_owned () }); 1389} 1390} 1391 npm:: validate_lock_npm ( & lock. npm , npm_registry) ?; 1392Ok (()) 1393} 1394 1395pub fn atomic_write ( path : & Path , bytes : & [ u8 ]) ->Result <(), RegistryError > { 1396stage ( path, bytes) ? 1397. persist ( path) 1398. map_err ( |detail|RegistryError :: Persist { detail : detail. to_string () }) ?; 1399Ok (()) 1400} 1401 1402/// Atomically rewrite a file that other processes may update concurrently: `edit` derives the 1403/// new contents (or `None` to leave it alone) from what `read` returns, and is run again 1404/// whenever the file has changed by the time the rewrite is ready. A writer that lands between 1405/// that final check and the rename can still be overwritten. 1406pub fn update_file < T : PartialEq , E >( 1407path : & Path , 1408read : impl Fn () ->Result < T , E >, 1409mut edit : impl FnMut ( & T ) ->Result < Option < Vec < u8 >>, E >, 1410) ->Result <(), E > 1411where 1412E : From < RegistryError >, 1413{ 1414for _in 0 ..16 { 1415let current =read () ?; 1416let Some ( bytes) =edit ( & current) ?else { 1417return Ok (()); 1418}; 1419let staged =stage ( path, & bytes) ?; 1420if read () ? == current{ 1421 staged 1422. persist ( path) 1423. map_err ( |error|RegistryError :: Persist { detail : error. to_string () }) ?; 1424return Ok (()); 1425} 1426} 1427Err ( RegistryError :: FileKeptChanging ( path. display (). to_string ()). into ()) 1428} 1429 1430fn stage ( path : & Path , bytes : & [ u8 ]) ->Result < tempfile:: NamedTempFile , RegistryError > { 1431let parent = path. parent (). ok_or ( RegistryError :: OutputNeedsParent ) ?; 1432let mut builder = tempfile:: Builder :: new (); 1433# [ cfg ( unix )] 1434{ 1435use std:: os:: unix:: fs:: PermissionsExt ; 1436 builder. permissions ( std:: fs:: Permissions :: from_mode ( 0o644 )); 1437} 1438let mut file = builder. tempfile_in ( parent) ?; 1439match std:: fs:: symlink_metadata ( path) { 1440Ok ( metadata) =>{ 1441if !metadata. is_file () || metadata. is_symlink () { 1442return Err ( RegistryError :: OutputNotRegularFile ); 1443} 1444 file. as_file (). set_permissions ( metadata. permissions ()) ?; 1445} 1446Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>{} 1447Err ( error) =>return Err ( error. into ()), 1448} 1449 file. write_all ( bytes) ?; 1450 file. as_file (). sync_all () ?; 1451Ok ( file) 1452} 1453 1454# [ cfg ( test )] 1455mod tests{ 1456use super ::{ MAX_ARTIFACT_SIZE , RegistryError , read_artifact, read_lock_nofollow, update_file}; 1457 1458# [ test ] 1459fn bounded_readers_preserve_bytes_through_the_size_limit () { 1460let directory = tempfile:: tempdir (). unwrap (); 1461for sizein [ 0 , 1 , MAX_ARTIFACT_SIZE -1 , MAX_ARTIFACT_SIZE ] { 1462let bytes: Vec < u8 > =( 0 ..size). map ( |index|( index %251 ) as u8 ). collect (); 1463let path = directory. path (). join ( "artifact" ); 1464 std:: fs:: write ( & path, & bytes). unwrap (); 1465assert_eq! ( read_artifact ( & path). unwrap (), Some ( bytes. clone ())); 1466 1467let target =crate :: caps:: PathTarget :: for_entry ( & path). unwrap (); 1468assert_eq! ( read_lock_nofollow ( & target). unwrap (), Some ( bytes)); 1469} 1470} 1471 1472# [ test ] 1473fn bounded_readers_reject_oversized_sparse_files () { 1474let directory = tempfile:: tempdir (). unwrap (); 1475let path = directory. path (). join ( "oversized" ); 1476let file = std:: fs:: File :: create ( & path). unwrap (); 1477 file. set_len ( MAX_ARTIFACT_SIZE as u64 +1 ). unwrap (); 1478assert! ( matches! ( 1479read_artifact ( & path), 1480Err ( RegistryError :: CachedArtifactTooLarge ) 1481)); 1482 1483let target =crate :: caps:: PathTarget :: for_entry ( & path). unwrap (); 1484assert! ( matches! ( 1485read_lock_nofollow ( & target), 1486Err ( RegistryError :: LockfileTooLarge ) 1487)); 1488} 1489 1490# [ test ] 1491fn bounded_readers_reject_nonregular_files () { 1492let directory = tempfile:: tempdir (). unwrap (); 1493let path = directory. path (). join ( "directory" ); 1494 std:: fs:: create_dir ( & path). unwrap (); 1495// Windows can reject directories at open, before the regular-file check. 1496let error =read_artifact ( & path). unwrap_err (); 1497assert! ( 1498matches! ( error, RegistryError :: ArtifactNotRegularFile ) 1499 ||cfg! ( windows) 1500 &&matches! ( & error, RegistryError :: Io ( error) 1501if error. kind () == std:: io:: ErrorKind :: PermissionDenied ), 1502"{error}" 1503); 1504 1505let target =crate :: caps:: PathTarget :: for_entry ( & path). unwrap (); 1506let error =read_lock_nofollow ( & target). unwrap_err (); 1507assert! ( 1508matches! ( error, RegistryError :: LockfileNotRegularFile ) 1509 ||cfg! ( windows) 1510 &&matches! ( & error, RegistryError :: Io ( error) 1511if error. kind () == std:: io:: ErrorKind :: PermissionDenied ), 1512"{error}" 1513); 1514} 1515 1516# [ test ] 1517fn updates_are_recomputed_when_another_writer_lands_first () { 1518let directory = tempfile:: tempdir (). unwrap (); 1519let path = directory. path (). join ( "entries" ); 1520let mut raced =false ; 1521update_file ( 1522& path, 1523 ||read_artifact ( & path), 1524 |current|{ 1525if !raced{ 1526 raced =true ; 1527 std:: fs:: write ( & path, "theirs\n" ). unwrap (); 1528} 1529let mut bytes = current. clone (). unwrap_or_default (); 1530 bytes. extend_from_slice ( b"ours\n" ); 1531Ok ( Some ( bytes)) 1532}, 1533) 1534. unwrap (); 1535assert_eq! ( std:: fs:: read ( & path). unwrap (), b"theirs\nours\n" ); 1536} 1537}