char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
214dd78
main
1//! Project-tree storage for npm packages. 2//! 3//! The project root is opened once as a pinned handle; everything below 4//! descends without following symlinks (see [`super::fs`]). Content is 5//! verified after publication, so a concurrent verified winner is accepted. 6 7use std:: collections:: BTreeMap ; 8use std:: path::{ Component , Path , PathBuf }; 9use std:: sync:: Arc ; 10 11use cap_fs_ext:: DirExt ; 12use cap_std:: fs:: Dir ; 13use url:: Url ; 14 15use super :: fs; 16use super ::{ MAX_NPM_FILE_BYTES , MAX_NPM_TARBALL_COMPRESSED , NpmArtifact , NpmIdentity }; 17use crate :: registry::{ NpmPin , Options }; 18 19/// Failures reading and publishing verified npm package trees. 20/// Acquire-time validation lives in the resolver; only verified bytes land here. 21# [ derive ( Clone , Debug , thiserror :: Error )] 22pub enum StorageError { 23# [ error ( "npm vendor registry mismatch" )] 24NpmVendorRegistryMismatch , 25# [ error ( "npm path escapes its project" )] 26NpmPathEscapesProject , 27# [ error ( "npm package tree is corrupted: {}" , dir . display ())] 28NpmPackageTreeCorrupted { dir : std:: path:: PathBuf }, 29# [ error ( "npm package tree is corrupted: {} ({detail})" , dir . display ())] 30NpmPackageTreeCorruptedDetail { 31dir : std:: path:: PathBuf , 32detail : String , 33}, 34# [ error ( "npm cache entry is not usable" )] 35NpmCacheEntryUnusable , 36# [ error ( "npm manifest is not usable" )] 37NpmManifestUnusable , 38# [ error ( "npm vendor manifest is not usable" )] 39NpmVendorManifestUnusable , 40# [ error ( "npm file is missing: {rel}" )] 41NpmFileMissing { rel : String }, 42# [ error ( "npm project root is not usable: {source}" )] 43NpmProjectRootUnusable { source : Arc < std:: io:: Error > }, 44# [ error ( "Invalid npm package.json: {source}" )] 45InvalidNpmPackageJson { source : Arc < serde_json:: Error > }, 46# [ error ( "npm manifest is too large" )] 47NpmManifestTooLarge , 48# [ error ( "npm manifest digest mismatch" )] 49NpmManifestDigestMismatch , 50# [ error ( "npm manifest changed unexpectedly" )] 51NpmManifestChanged , 52# [ error ( "npm manifest does not match its locked files" )] 53NpmManifestFilesMismatch , 54# [ error ( "npm archive changed" )] 55NpmArchiveChanged , 56# [ error ( "npm path is not usable" )] 57NpmPathUnusable , 58# [ error ( "{detail}" )] 59NpmStaging { detail : String }, 60# [ error ( "npm staging collided repeatedly" )] 61NpmStagingCollided , 62# [ error ( "npm staged file changed: {rel}" )] 63NpmStagedFileChanged { rel : String }, 64 65# [ error ( "npm staged file set changed" )] 66NpmStagedFileSetChanged , 67# [ error ( "npm lock path is not usable" )] 68NpmLockPathUnusable , 69# [ error ( "npm vendor tree is corrupted: {rel}" )] 70NpmVendorTreeCorrupted { rel : String }, 71# [ error ( "npm vendor tree is not usable: {source}" )] 72NpmVendorTreeUnusable { source : Arc < std:: io:: Error > }, 73# [ error ( "npm vendor tree is corrupted: {detail}" )] 74NpmVendorTreeCorruptedDetail { detail : String }, 75# [ error ( "npm tarball is too large: {name}@{version}" )] 76NpmTarballTooLarge { name : String , version : String }, 77# [ error ( "npm archive changed for {name}@{version}" )] 78NpmArchiveChangedFor { name : String , version : String }, 79# [ error ( "npm pin is missing its manifest" )] 80NpmPinMissingManifest , 81# [ error ( "npm backing file changed: {rel}" )] 82NpmBackingFileChanged { rel : String }, 83 84# [ error ( "npm tarball has a duplicate file: {path}" )] 85NpmTarballDuplicate { path : String }, 86 87# [ error ( "Invalid npm manifest" )] 88InvalidNpmManifest , 89 90# [ error ( "npm file integrity mismatch: {rel}" )] 91NpmFileIntegrityMismatch { rel : String }, 92 93# [ error ( "npm manifest is missing" )] 94NpmManifestMissing , 95 96# [ error ( "npm pin is missing package.json" )] 97NpmPinMissingPackageJson , 98 99# [ error ( transparent )] 100Npm ( Box < super :: NpmError >), 101# [ error ( transparent )] 102Io ( std:: sync:: Arc < std:: io:: Error >), 103# [ error ( transparent )] 104Json ( std:: sync:: Arc < serde_json:: Error >), 105} 106 107impl From < std:: io:: Error > for StorageError { 108fn from ( error : std:: io:: Error ) ->Self { 109Self :: Io ( std:: sync:: Arc :: new ( error)) 110} 111} 112 113impl From < serde_json:: Error > for StorageError { 114fn from ( error : serde_json:: Error ) ->Self { 115Self :: Json ( std:: sync:: Arc :: new ( error)) 116} 117} 118 119impl From < super :: NpmError > for StorageError { 120fn from ( error : super :: NpmError ) ->Self { 121Self :: Npm ( Box :: new ( error)) 122} 123} 124 125fn npm_root ( options : & Options ) ->PathBuf { 126 options. project_root . join ( ".avril" ). join ( "npm" ) 127} 128 129fn registry_tag ( registry : & Url ) ->String { 130crate :: registry:: artifact_key ( registry. as_str ()) 131} 132 133pub ( crate ) fn npm_package_dir ( 134options : & Options , 135instance : & str , 136name : & str , 137version : & str , 138) ->PathBuf { 139npm_root ( options) 140. join ( instance) 141. join ( registry_tag ( & options. npm_registry )) 142. join ( name) 143. join ( version) 144} 145 146/// Dependency-free instances share the acquisition tree; other instances 147/// retain their own paths, with hard-linked content where supported. 148pub ( crate ) fn npm_disk_instance < ' a >( id : &' a str , package : & str ) ->&' a str { 149if id == package{ 150super :: NPM_DEFAULT_INSTANCE 151} else { 152 id 153} 154} 155 156fn packument_cache_path ( options : & Options , name : & str ) ->PathBuf { 157let safe = name. replace ( '/' , "%2f" ); 158npm_root ( options) 159. join ( ".metadata" ) 160. join ( "packuments" ) 161. join ( registry_tag ( & options. npm_registry )) 162. join ( format! ( "{safe}.json" )) 163} 164 165fn tarball_cache_path ( options : & Options , tarball : & str ) ->PathBuf { 166npm_root ( options) 167. join ( ".artifacts" ) 168. join ( "tarballs" ) 169. join ( crate :: registry:: artifact_key ( tarball)) 170} 171 172pub ( crate ) fn npm_vendor_relative ( 173options : & Options , 174instance : & str , 175registry : & Url , 176name : & str , 177version : & str , 178) ->Result < PathBuf , StorageError > { 179super :: validate_npm_instance_id ( instance) ?; 180super :: validate_npm_name ( name) ?; 181super :: validate_npm_version ( version) ?; 182if !super :: same_origin ( registry, & options. npm_registry ) { 183return Err ( StorageError :: NpmVendorRegistryMismatch ); 184} 185Ok ( PathBuf :: from ( "npm" ) 186. join ( instance) 187. join ( registry_tag ( registry)) 188. join ( name) 189. join ( version)) 190} 191 192fn relative_path ( anchor : & Path , path : & Path ) ->Result < PathBuf , StorageError > { 193let rel = path 194. strip_prefix ( anchor) 195. map_err ( |_|StorageError :: NpmPathEscapesProject ) ?; 196let mut out =PathBuf :: new (); 197for partin rel. components () { 198let Component :: Normal ( name) = partelse { 199return Err ( StorageError :: NpmPathEscapesProject ); 200}; 201 out. push ( name); 202} 203Ok ( out) 204} 205 206fn open_project ( options : & Options ) ->Result < Dir , StorageError > { 207 fs:: open_root ( & options. project_root ) 208. map_err ( |source|StorageError :: NpmProjectRootUnusable { source : Arc :: new ( source) }) 209} 210 211// No-follow open, fstat-confirmed regular file, bounded read. 212fn read_file_in_dir ( 213directory : & Dir , 214name : & str , 215rel : & str , 216max_bytes : usize , 217) ->Result < Vec < u8 >, StorageError > { 218let file = fs:: open_file ( directory, name) 219. map_err ( |_|StorageError :: NpmFileMissing { rel : rel. to_owned () }) ?; 220Ok ( fs:: read_bounded ( & file, max_bytesas u64 , rel) ?) 221} 222 223pub ( crate ) fn read_project_file ( 224project_root : & Path , 225dir : & Path , 226rel : & str , 227max_bytes : usize , 228) ->Result < Vec < u8 >, StorageError > { 229let root = fs:: open_root ( project_root) ?; 230super :: validate_npm_relpath ( rel) ?; 231let dir_rel =relative_path ( project_root, dir) ?; 232let package = fs:: walk ( & root, & dir_rel) ?; 233let ( parent, name) = rel. rsplit_once ( '/' ). unwrap_or (( "" , rel)); 234let directory = fs:: walk ( & package, Path :: new ( parent)) 235. map_err ( |_|StorageError :: NpmFileMissing { rel : rel. to_owned () }) ?; 236read_file_in_dir ( & directory, name, rel, max_bytes) 237} 238 239// Verifies every pinned file with one bounded read each and returns the 240// package document parsed from those same bytes. 241fn verify_and_load ( 242package : & Dir , 243files : & BTreeMap < String , String >, 244) ->Result < serde_json:: Value , StorageError > { 245let mut package_bytes: Option < Vec < u8 >> =None ; 246let mut parent_cache: Option <( & str , Dir )> =None ; 247for ( rel, expected) in files{ 248super :: validate_npm_relpath ( rel) ?; 249let ( parent, name) = rel. rsplit_once ( '/' ). unwrap_or (( "" , rel)); 250let parent_path =Path :: new ( parent); 251if parent_cache 252. as_ref () 253. is_none_or ( |( cached, _) |Path :: new ( cached) != parent_path) 254{ 255let directory = fs:: walk ( package, parent_path) 256. map_err ( |_|StorageError :: NpmFileMissing { rel : rel. to_owned () }) ?; 257 parent_cache =Some (( parent, directory)); 258} 259let directory =& parent_cache. as_ref (). unwrap (). 1 ; 260let bytes =read_file_in_dir ( directory, name, rel, MAX_NPM_FILE_BYTES ) ?; 261if crate :: registry:: checksum ( & bytes) !=* expected{ 262return Err ( StorageError :: NpmFileIntegrityMismatch { rel : rel. to_owned () }); 263} 264if * rel =="package.json" { 265 package_bytes =Some ( bytes); 266} 267} 268let package_bytes = package_bytes. ok_or ( StorageError :: NpmPinMissingPackageJson ) ?; 269 serde_json:: from_slice ( & package_bytes) 270. map_err ( |source|StorageError :: InvalidNpmPackageJson { source : Arc :: new ( source) }) 271} 272 273pub ( crate ) fn verify_and_load_npm_dir ( 274project_root : & Path , 275dir : & Path , 276files : & BTreeMap < String , String >, 277) ->Result < serde_json:: Value , StorageError > { 278let root = fs:: open_root ( project_root) ?; 279let dir_rel =relative_path ( project_root, dir) ?; 280let package = fs:: walk ( & root, & dir_rel) ?; 281verify_and_load ( & package, files) 282} 283 284# [ derive ( Clone , PartialEq , Eq )] 285pub ( crate ) enum NpmDirState { 286Absent , 287Verified ( serde_json:: Value ), 288} 289 290pub ( crate ) fn verify_npm_metadata ( 291project_root : & Path , 292dir : & Path , 293files : & BTreeMap < String , String >, 294) ->Result < Option < serde_json:: Value >, StorageError > { 295let root = fs:: open_root ( project_root) ?; 296let dir_rel =relative_path ( project_root, dir) ?; 297let package =match fs:: walk ( & root, & dir_rel) { 298Ok ( package) => package, 299Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>return Ok ( None ), 300Err ( _) =>return Err ( StorageError :: NpmPackageTreeCorrupted { dir : dir. to_owned () }), 301}; 302let expected = files 303. get ( "package.json" ) 304. ok_or ( StorageError :: NpmPinMissingPackageJson ) ?; 305let result =( ||{ 306let bytes =read_file_in_dir ( & package, "package.json" , "package.json" , MAX_NPM_FILE_BYTES ) ?; 307if crate :: registry:: checksum ( & bytes) !=* expected{ 308return Err ( StorageError :: NpmFileIntegrityMismatch { rel : "package.json" . into () }); 309} 310 serde_json:: from_slice ( & bytes) 311. map_err ( |source|StorageError :: InvalidNpmPackageJson { source : Arc :: new ( source) }) 312})(); 313 result 314. map ( Some ) 315. map_err ( |error|StorageError :: NpmPackageTreeCorruptedDetail { 316dir : dir. to_owned (), 317detail : error. to_string (), 318}) 319} 320 321pub ( crate ) fn npm_dir_state ( 322project_root : & Path , 323dir : & Path , 324files : & BTreeMap < String , String >, 325) ->Result < NpmDirState , StorageError > { 326let root = fs:: open_root ( project_root) ?; 327let dir_rel =relative_path ( project_root, dir) ?; 328let package =match fs:: walk ( & root, & dir_rel) { 329Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>{ 330return Ok ( NpmDirState :: Absent ); 331} 332Err ( _) =>{ 333return Err ( StorageError :: NpmPackageTreeCorrupted { dir : dir. to_owned () }); 334} 335Ok ( package) => package, 336}; 337match verify_and_load ( & package, files) { 338Ok ( package) =>Ok ( NpmDirState :: Verified ( package)), 339Err ( error) =>Err ( StorageError :: NpmPackageTreeCorruptedDetail { 340dir : dir. to_owned (), 341detail : error. to_string (), 342}), 343} 344} 345 346/// Readiness is not authentication: contents are verified when consumed. Missing 347/// storage is restored during async preparation, never by synchronous require. 348pub ( crate ) fn npm_instance_ready ( 349options : & Options , 350dir : & Path , 351backing : & Path , 352pin : & NpmPin , 353) ->Result < bool , StorageError > { 354if pin. is_legacy () && !options. frozen { 355return Ok ( false ); 356} 357let root =open_project ( options) ?; 358if pin. files . is_none () { 359let digest = pin 360. manifest 361. as_ref () 362. ok_or ( StorageError :: NpmPinMissingManifest ) ?; 363let rel =manifest_rel ( digest) ?; 364let parent =match fs:: walk ( & root, rel. parent (). unwrap ()) { 365Ok ( parent) => parent, 366Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>return Ok ( false ), 367Err ( _) =>return Err ( StorageError :: NpmManifestUnusable ), 368}; 369match fs:: open_file ( & parent, rel. file_name (). unwrap (). to_str (). unwrap ()) { 370Ok ( file) if file. metadata () ?. is_file () =>{} 371Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>return Ok ( false ), 372 _ =>return Err ( StorageError :: NpmManifestUnusable ), 373} 374} 375for pathin std:: iter:: once ( dir). chain (( dir != backing). then_some ( backing)) { 376match fs:: walk ( & root, & relative_path ( & options. project_root , path) ?) { 377Ok ( _) =>{} 378Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>return Ok ( false ), 379Err ( _) =>return Err ( StorageError :: NpmPackageTreeCorrupted { dir : path. to_owned () }), 380} 381} 382Ok ( true ) 383} 384 385pub ( crate ) fn ensure_npm_instance ( 386options : & Options , 387backing : & NpmArtifact , 388instance : & str , 389) ->Result < PathBuf , StorageError > { 390super :: validate_npm_instance_id ( instance) ?; 391let identity =& backing. identity ; 392let files =& backing. files ; 393let dir =npm_package_dir ( options, instance, & identity. name , & identity. version ); 394// Acquisition already authenticated the metadata at this exact path. 395if dir == backing. dir { 396return Ok ( dir); 397} 398if verify_npm_metadata ( & options. project_root , & dir, files) ?. is_some () { 399return Ok ( dir); 400} 401let _guard =lock_package ( options, & identity. key ()) ?; 402if verify_npm_metadata ( & options. project_root , & dir, files) ?. is_some () { 403return Ok ( dir); 404} 405let root =open_project ( options) ?; 406let source_rel =relative_path ( & options. project_root , & backing. dir ) ?; 407let destination_rel =relative_path ( & options. project_root , & dir) ?; 408let source = fs:: walk ( & root, & source_rel) ?; 409publish_dir ( & root, & destination_rel, |staging|{ 410for ( rel, expected) in files{ 411link_or_copy_file ( & source, staging, rel, expected) ?; 412} 413Ok (()) 414}) ?; 415verify_and_load_npm_dir ( & options. project_root , & dir, files) ?; 416Ok ( dir) 417} 418 419fn link_or_copy_file ( 420source : & Dir , 421staging : & Dir , 422rel : & str , 423expected : & str , 424) ->Result <(), StorageError > { 425super :: validate_npm_relpath ( rel) ?; 426let ( parent, name) = rel. rsplit_once ( '/' ). unwrap_or (( "" , rel)); 427let source_parent = fs:: walk ( source, Path :: new ( parent)) ?; 428let bytes =read_file_in_dir ( & source_parent, name, rel, MAX_NPM_FILE_BYTES ) ?; 429if crate :: registry:: checksum ( & bytes) != expected{ 430return Err ( StorageError :: NpmFileIntegrityMismatch { rel : rel. to_owned () }); 431} 432let mut destination_parent = staging. try_clone () ?; 433for partin parent. split ( '/' ). filter ( |part| !part. is_empty ()) { 434 destination_parent =match destination_parent. open_dir_nofollow ( part) { 435Ok ( child) => child, 436Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>{ 437 destination_parent. create_dir ( part) ?; 438 destination_parent. open_dir_nofollow ( part) ? 439} 440Err ( error) =>return Err ( StorageError :: NpmStaging { detail : error. to_string () }), 441}; 442} 443match source_parent. hard_link ( name, & destination_parent, name) { 444Ok (()) =>Ok (()), 445Err ( _) =>stage_file ( staging, rel, & bytes, expected), 446} 447} 448 449// Missing entries read as absent; anything else unusable fails. 450fn read_cache ( root : & Dir , rel : & Path , max_bytes : usize ) ->Result < Option < Vec < u8 >>, StorageError > { 451let ( parent_rel, name) =match rel. parent (). zip ( rel. file_name ()) { 452Some (( parent, name)) =>( parent. to_owned (), name. to_owned ()), 453None =>return Err ( StorageError :: NpmCacheEntryUnusable ), 454}; 455let name = name. to_str (). ok_or ( StorageError :: NpmCacheEntryUnusable ) ?; 456let parent =match fs:: walk ( root, & parent_rel) { 457Ok ( parent) => parent, 458Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>return Ok ( None ), 459Err ( _) =>return Err ( StorageError :: NpmCacheEntryUnusable ), 460}; 461let file =match fs:: open_file ( & parent, name) { 462Ok ( file) => file, 463Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>return Ok ( None ), 464Err ( _) =>return Err ( StorageError :: NpmCacheEntryUnusable ), 465}; 466match fs:: read_bounded ( & file, max_bytesas u64 , name) { 467Ok ( bytes) =>Ok ( Some ( bytes)), 468Err ( _) =>Err ( StorageError :: NpmCacheEntryUnusable ), 469} 470} 471 472pub ( crate ) fn read_packument_cache ( 473options : & Options , 474name : & str , 475max_bytes : usize , 476) ->Result < Option < Vec < u8 >>, StorageError > { 477let root =open_project ( options) ?; 478let rel =relative_path ( & options. project_root , & packument_cache_path ( options, name)) ?; 479read_cache ( & root, & rel, max_bytes) 480} 481 482pub ( crate ) fn read_tarball_cache ( 483options : & Options , 484tarball : & str , 485max_bytes : usize , 486) ->Result < Option < Vec < u8 >>, StorageError > { 487let root =open_project ( options) ?; 488let rel =relative_path ( & options. project_root , & tarball_cache_path ( options, tarball)) ?; 489read_cache ( & root, & rel, max_bytes) 490} 491 492fn write_cache ( root : & Dir , rel : & Path , bytes : & [ u8 ]) ->Result <(), StorageError > { 493let ( parent_rel, name) =match rel. parent (). zip ( rel. file_name ()) { 494Some (( parent, name)) =>( parent. to_owned (), name. to_owned ()), 495None =>return Err ( StorageError :: NpmCacheEntryUnusable ), 496}; 497let name = name 498. to_str () 499. ok_or ( StorageError :: NpmCacheEntryUnusable ) ? 500. to_owned (); 501let parent = fs:: mkdirs ( root, & parent_rel) ?; 502Ok ( fs:: write_atomic ( & parent, & name, bytes) ?) 503} 504 505pub ( crate ) fn write_vendor_file ( 506destination : & Path , 507relative : & Path , 508bytes : & [ u8 ], 509) ->Result <(), StorageError > { 510 std:: fs:: create_dir_all ( destination) ?; 511let root = fs:: open_root ( destination) ?; 512write_cache ( & root, relative, bytes) 513} 514 515pub ( crate ) fn write_packument_cache ( 516options : & Options , 517name : & str , 518bytes : & [ u8 ], 519) ->Result <(), StorageError > { 520let root =open_project ( options) ?; 521let rel =relative_path ( & options. project_root , & packument_cache_path ( options, name)) ?; 522write_cache ( & root, & rel, bytes) 523} 524 525pub ( crate ) fn write_tarball_cache ( 526options : & Options , 527tarball : & str , 528bytes : & [ u8 ], 529) ->Result <(), StorageError > { 530let root =open_project ( options) ?; 531let rel =relative_path ( & options. project_root , & tarball_cache_path ( options, tarball)) ?; 532write_cache ( & root, & rel, bytes) 533} 534 535const MAX_NPM_MANIFEST_BYTES : usize =8 * 1024 * 1024 ; 536 537// Hash exactly the canonical bytes we store; peer instances share this manifest. 538pub ( crate ) fn manifest_digest ( files : & BTreeMap < String , String >) ->Result < String , StorageError > { 539Ok ( crate :: registry:: checksum ( & manifest_bytes ( files) ?)) 540} 541 542fn manifest_bytes ( files : & BTreeMap < String , String >) ->Result < Vec < u8 >, StorageError > { 543let bytes = serde_json:: to_vec ( files) ?; 544if bytes. len () >MAX_NPM_MANIFEST_BYTES { 545return Err ( StorageError :: NpmManifestTooLarge ); 546} 547Ok ( bytes) 548} 549 550fn manifest_rel ( digest : & str ) ->Result < PathBuf , StorageError > { 551Ok ( PathBuf :: from ( ".avril" ) 552. join ( "npm" ) 553. join ( ".manifests" ) 554. join ( super :: manifest_file_name ( digest) ?)) 555} 556 557fn vendor_manifest_rel ( digest : & str ) ->Result < PathBuf , StorageError > { 558Ok ( PathBuf :: from ( "npm" ) 559. join ( ".manifests" ) 560. join ( super :: manifest_file_name ( digest) ?)) 561} 562 563fn parse_manifest_map ( 564bytes : & [ u8 ], 565digest : & str , 566) ->Result < BTreeMap < String , String >, StorageError > { 567if crate :: registry:: checksum ( bytes) != digest{ 568return Err ( StorageError :: NpmManifestDigestMismatch ); 569} 570let map: BTreeMap < String , String > = 571 serde_json:: from_slice ( bytes). map_err ( |_|StorageError :: InvalidNpmManifest ) ?; 572super :: validate_files_map ( & map) ?; 573Ok ( map) 574} 575 576/// Load and authenticate the project sidecar. Missing reads as absent; 577/// anything else unusable (symlink, FIFO, corrupt bytes, digest mismatch) 578/// fails closed with no repair. 579pub ( crate ) fn load_manifest_map ( 580options : & Options , 581digest : & str , 582) ->Result < Option < BTreeMap < String , String >>, StorageError > { 583let root =open_project ( options) ?; 584let rel =manifest_rel ( digest) ?; 585match read_cache ( & root, & rel, MAX_NPM_MANIFEST_BYTES ) { 586Ok ( None ) =>Ok ( None ), 587Ok ( Some ( bytes)) =>parse_manifest_map ( & bytes, digest). map ( Some ), 588Err ( StorageError :: NpmCacheEntryUnusable ) =>Err ( StorageError :: NpmManifestUnusable ), 589Err ( error) =>Err ( error), 590} 591} 592 593/// Load and authenticate the vendor mirror sidecar. Unconfigured vendor 594/// roots and absent sidecars read as missing; anything else fails. 595pub ( crate ) fn load_vendor_map ( 596options : & Options , 597digest : & str , 598) ->Result < Option < BTreeMap < String , String >>, StorageError > { 599let Some ( vendor) =open_vendor ( options) ?else { 600return Ok ( None ); 601}; 602let rel =vendor_manifest_rel ( digest) ?; 603match read_cache ( & vendor, & rel, MAX_NPM_MANIFEST_BYTES ) { 604Ok ( None ) =>Ok ( None ), 605Ok ( Some ( bytes)) =>parse_manifest_map ( & bytes, digest). map ( Some ), 606Err ( StorageError :: NpmCacheEntryUnusable ) =>Err ( StorageError :: NpmVendorManifestUnusable ), 607Err ( error) =>Err ( error), 608} 609} 610 611/// Publish the sidecar for an authenticated map, creating it only when 612/// absent; existing divergent bytes fail instead of being repaired. 613pub ( crate ) fn ensure_manifest_sidecar ( 614options : & Options , 615files : & BTreeMap < String , String >, 616) ->Result < String , StorageError > { 617super :: validate_files_map ( files) ?; 618let bytes =manifest_bytes ( files) ?; 619let digest =crate :: registry:: checksum ( & bytes); 620let root =open_project ( options) ?; 621let rel =manifest_rel ( & digest) ?; 622match read_cache ( & root, & rel, MAX_NPM_MANIFEST_BYTES ) { 623Ok ( None ) =>{ 624write_cache ( & root, & rel, & bytes) ?; 625Ok ( digest) 626} 627Ok ( Some ( existing)) =>{ 628if crate :: registry:: checksum ( & existing) != digest{ 629return Err ( StorageError :: NpmManifestChanged ); 630} 631Ok ( digest) 632} 633Err ( StorageError :: NpmCacheEntryUnusable ) =>Err ( StorageError :: NpmManifestUnusable ), 634Err ( error) =>Err ( error), 635} 636} 637 638/// Authenticated file map for vendoring: legacy inline maps directly, 639/// compact pins through the project sidecar, with a tarball-cache 640/// recovery that re-checks the digest. Never lists the tree. 641pub ( crate ) fn load_vendor_source_files ( 642options : & Options , 643identity_key : & str , 644pin : & NpmPin , 645) ->Result < BTreeMap < String , String >, StorageError > { 646if let Some ( files) =& pin. files { 647if let Some ( manifest) =& pin. manifest 648 &&manifest_digest ( files) ? !=* manifest 649{ 650return Err ( StorageError :: NpmManifestFilesMismatch ); 651} 652super :: validate_files_map ( files) ?; 653return Ok ( files. clone ()); 654} 655let manifest = pin 656. manifest 657. as_ref () 658. ok_or ( StorageError :: NpmPinMissingManifest ) ?; 659if let Some ( map) =load_manifest_map ( options, manifest) ?{ 660return Ok ( map); 661} 662let bytes =read_tarball_cache ( options, & pin. tarball , MAX_NPM_TARBALL_COMPRESSED ) ? 663. ok_or ( StorageError :: NpmManifestMissing ) ?; 664super :: verify_sri ( & bytes, & pin. integrity ) ?; 665let ( _, name, version) =super :: parse_npm_key ( identity_key) ?; 666let data =super :: collect_npm_files ( & bytes) ?; 667let ( derived, _) =super :: bind_npm_files ( & data, & name, & version) ?; 668if manifest_digest ( & derived) ? !=* manifest{ 669return Err ( StorageError :: NpmArchiveChanged ); 670} 671ensure_manifest_sidecar ( options, & derived) ?; 672Ok ( derived) 673} 674 675/// Canonical sidecar bytes for the vendor mirror. 676pub ( crate ) fn write_vendor_manifest ( 677destination : & Path , 678files : & BTreeMap < String , String >, 679) ->Result <(), StorageError > { 680let bytes =manifest_bytes ( files) ?; 681let digest =crate :: registry:: checksum ( & bytes); 682write_vendor_file ( destination, & vendor_manifest_rel ( & digest) ?, & bytes) 683} 684 685// Stage under a unique name in the central staging area, then rename over 686// the destination. The rename is the only destination mutation, so a 687// concurrent verified winner stays complete. A swapped symlink at the 688// destination fails verification instead of being trusted. 689fn publish_dir ( 690root : & Dir , 691dest_rel : & Path , 692fill : impl FnOnce ( & Dir ) ->Result <(), StorageError >, 693) ->Result <(), StorageError > { 694let ( parent_rel, name) =match dest_rel. parent (). zip ( dest_rel. file_name ()) { 695Some (( parent, name)) =>( parent. to_owned (), name. to_owned ()), 696None =>return Err ( StorageError :: NpmPathUnusable ), 697}; 698let name = name 699. to_str () 700. ok_or ( StorageError :: NpmPathUnusable ) ? 701. to_owned (); 702let parent = fs:: mkdirs ( root, & parent_rel) ?; 703let staging_root = fs:: mkdirs ( root, Path :: new ( ".avril/npm/.tmp" )) ?; 704let staging =( || ->Result < String , StorageError > { 705for _in 0 ..8 { 706let candidate = fs:: unique_tmp (); 707match staging_root. create_dir ( & candidate) { 708Ok (()) =>return Ok ( candidate), 709Err ( error) if error. kind () == std:: io:: ErrorKind :: AlreadyExists =>continue , 710Err ( error) =>return Err ( StorageError :: NpmStaging { detail : error. to_string () }), 711} 712} 713Err ( StorageError :: NpmStagingCollided ) 714})() ?; 715let staging_dir = staging_root. open_dir_nofollow ( & staging) ?; 716if let Err ( error) =fill ( & staging_dir) { 717 fs:: remove_all ( & staging_root, & staging); 718return Err ( error); 719} 720match staging_root. rename ( & staging, & parent, & name) { 721Ok (()) =>Ok (()), 722Err ( error) 723if error. kind () == std:: io:: ErrorKind :: AlreadyExists 724 || error. kind () == std:: io:: ErrorKind :: DirectoryNotEmpty => 725{ 726 fs:: remove_all ( & staging_root, & staging); 727Ok (()) 728} 729Err ( error) =>{ 730 fs:: remove_all ( & staging_root, & staging); 731Err ( error. into ()) 732} 733} 734} 735 736fn stage_file ( staging : & Dir , rel : & str , bytes : & [ u8 ], expected : & str ) ->Result <(), StorageError > { 737super :: validate_npm_relpath ( rel) ?; 738if crate :: registry:: checksum ( bytes) != expected{ 739return Err ( StorageError :: NpmStagedFileChanged { rel : rel. to_owned () }); 740} 741let mut dir = staging. try_clone () ?; 742let mut parts: Vec < & str > = rel. split ( '/' ). collect (); 743let name = parts. pop (). unwrap_or_default (); 744for partin parts{ 745 dir =match dir. open_dir_nofollow ( part) { 746Ok ( child) => child, 747Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>{ 748 dir. create_dir ( part) ?; 749 dir. open_dir_nofollow ( part) ? 750} 751Err ( error) =>return Err ( StorageError :: NpmStaging { detail : error. to_string () }), 752}; 753} 754if fs:: open_file ( & dir, name). is_ok () { 755return Err ( StorageError :: NpmTarballDuplicate { path : rel. to_owned () }); 756} 757let file = fs:: write_new ( & dir, name, bytes) ?; 758 fs:: set_readable ( & file) ?; 759 file. sync_all () ?; 760Ok (()) 761} 762 763fn stage_data_files ( 764staging : & Dir , 765data : & BTreeMap < String , Vec < u8 >>, 766files : & BTreeMap < String , String >, 767) ->Result <(), StorageError > { 768if data. len () != files. len () { 769return Err ( StorageError :: NpmStagedFileSetChanged ); 770} 771for ( rel, bytes) in data{ 772let expected = files 773. get ( rel. as_str ()) 774. ok_or ( StorageError :: NpmStagedFileChanged { rel : rel. to_owned () }) ?; 775stage_file ( staging, rel, bytes, expected) ?; 776} 777Ok (()) 778} 779 780// Per-package guard file locked on its opened inode. Verified hits return 781// before this runs, so reads take no lock. 782fn lock_package ( options : & Options , identity_key : & str ) ->Result < fs:: Guard , StorageError > { 783let root =open_project ( options) ?; 784let guard =npm_root ( options). join ( ".locks" ). join ( format! ( 785"npm-{}" , 786crate :: registry:: artifact_key ( identity_key) 787)); 788let rel =relative_path ( & options. project_root , & guard) ?; 789let ( parent_rel, name) =match rel. parent (). zip ( rel. file_name ()) { 790Some (( parent, name)) =>( parent. to_owned (), name. to_owned ()), 791None =>return Err ( StorageError :: NpmLockPathUnusable ), 792}; 793let name = name. to_str (). ok_or ( StorageError :: NpmLockPathUnusable ) ?; 794let parent = fs:: mkdirs ( & root, & parent_rel) ?; 795Ok ( fs:: lock_file ( & parent, name) ?) 796} 797 798/// Create `.avril/.gitignore` containing `*` when absent, leaving any 799/// existing file exactly alone. 800pub ( crate ) fn ensure_npm_gitignore ( project_root : & Path ) ->Result <(), StorageError > { 801let root = fs:: open_root ( project_root) ?; 802let avril = fs:: mkdirs ( & root, Path :: new ( ".avril" )) ?; 803match fs:: open_file ( & avril, ".gitignore" ) { 804Ok ( _) =>Ok (()), 805Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>{ 806match fs:: write_new ( & avril, ".gitignore" , b"*\n" ) { 807Ok ( file) =>Ok ( file. sync_all () ?), 808Err ( error) if error. kind () == std:: io:: ErrorKind :: AlreadyExists =>Ok (()), 809Err ( error) =>Err ( error. into ()), 810} 811} 812Err ( error) =>Err ( error. into ()), 813} 814} 815 816pub ( crate ) fn read_npm_project_file ( 817options : & Options , 818dir : & Path , 819rel : & str , 820) ->Result < Vec < u8 >, StorageError > { 821read_project_file ( & options. project_root , dir, rel, MAX_NPM_FILE_BYTES ) 822} 823 824/// Registry coordinates of one published archive, as recorded by a lockfile. 825pub ( crate ) struct NpmArchive < ' a > { 826pub registry : &' a Url , 827pub name : &' a str , 828pub version : &' a str , 829pub tarball : &' a str , 830pub integrity : &' a str , 831} 832 833fn build_artifact ( 834archive : NpmArchive < ' _ >, 835files : BTreeMap < String , String >, 836dir : PathBuf , 837package : serde_json:: Value , 838) ->NpmArtifact { 839NpmArtifact { 840identity : NpmIdentity { 841registry : archive. registry . clone (), 842name : archive. name . to_owned (), 843version : archive. version . to_owned (), 844}, 845tarball : archive. tarball . to_owned (), 846integrity : archive. integrity . to_owned (), 847 dir, 848 files, 849 package, 850} 851} 852 853fn read_vendor_file ( 854vendor : & Dir , 855vendor_rel : & Path , 856rel : & str , 857expected : & str , 858) ->Result < Vec < u8 >, StorageError > { 859super :: validate_npm_relpath ( rel) ?; 860let mut dir_rel = vendor_rel. to_owned (); 861let mut parts: Vec < & str > = rel. split ( '/' ). collect (); 862let name = parts. pop (). unwrap_or_default (); 863for partin parts{ 864 dir_rel. push ( part); 865} 866let parent = fs:: walk ( vendor, & dir_rel) 867. map_err ( |_|StorageError :: NpmVendorTreeCorrupted { rel : rel. to_owned () }) ?; 868let file = fs:: open_file ( & parent, name) 869. map_err ( |_|StorageError :: NpmVendorTreeCorrupted { rel : rel. to_owned () }) ?; 870let bytes = fs:: read_bounded ( & file, MAX_NPM_FILE_BYTES as u64 , rel) 871. map_err ( |_|StorageError :: NpmVendorTreeCorrupted { rel : rel. to_owned () }) ?; 872if crate :: registry:: checksum ( & bytes) != expected{ 873return Err ( StorageError :: NpmVendorTreeCorrupted { rel : rel. to_owned () }); 874} 875Ok ( bytes) 876} 877 878fn verify_vendor_dir ( 879vendor : & Dir , 880vendor_rel : & Path , 881files : & BTreeMap < String , String >, 882) ->Result < serde_json:: Value , StorageError > { 883let mut package_bytes: Option < Vec < u8 >> =None ; 884for ( rel, expected) in files{ 885let bytes =read_vendor_file ( vendor, vendor_rel, rel, expected) ?; 886if * rel =="package.json" { 887 package_bytes =Some ( bytes); 888} 889} 890let package_bytes = package_bytes. ok_or ( StorageError :: NpmPinMissingPackageJson ) ?; 891 serde_json:: from_slice ( & package_bytes) 892. map_err ( |source|StorageError :: InvalidNpmPackageJson { source : Arc :: new ( source) }) 893} 894 895// Absent or unconfigured vendor roots read as missing so acquisition falls 896// through; any other failure (symlinked parent, unreadable tree) fails. 897fn open_vendor ( options : & Options ) ->Result < Option < Dir >, StorageError > { 898let Some ( vendor) = options. vendor . as_ref () else { 899return Ok ( None ); 900}; 901match fs:: open_root ( vendor) { 902Ok ( root) =>Ok ( Some ( root)), 903Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>Ok ( None ), 904Err ( error) =>Err ( StorageError :: NpmVendorTreeUnusable { source : Arc :: new ( error) }), 905} 906} 907 908pub ( crate ) fn seed_npm_from_vendor ( 909options : & Options , 910npm_registry : & Url , 911instance : & str , 912name : & str , 913version : & str , 914pin : & NpmPin , 915files : & BTreeMap < String , String >, 916) ->Result < Option < NpmArtifact >, StorageError > { 917let Some ( vendor) =open_vendor ( options) ?else { 918return Ok ( None ); 919}; 920let vendor_rel: PathBuf =[ 921"npm" , 922 instance, 923& registry_tag ( & options. npm_registry ), 924 name, 925 version, 926] 927. iter () 928. collect (); 929// A missing vendor subtree falls through to verified sources; anything 930// present but unreadable fails below. 931if let Err ( error) = fs:: walk ( & vendor, & vendor_rel) { 932if error. kind () == std:: io:: ErrorKind :: NotFound { 933return Ok ( None ); 934} 935return Err ( StorageError :: NpmVendorTreeCorruptedDetail { detail : error. to_string () }); 936} 937let package =verify_vendor_dir ( & vendor, & vendor_rel, files) ?; 938let root =open_project ( options) ?; 939let dir =npm_package_dir ( options, instance, name, version); 940let dir_rel =relative_path ( & options. project_root , & dir) ?; 941publish_dir ( & root, & dir_rel, |staging|{ 942for ( rel, expected_hash) in files{ 943let bytes =read_vendor_file ( & vendor, & vendor_rel, rel, expected_hash) ?; 944stage_file ( staging, rel, & bytes, expected_hash) ?; 945} 946Ok (()) 947}) ?; 948let verified =verify_and_load_npm_dir ( & options. project_root , & dir, files) ?; 949debug_assert_eq! ( verified, package); 950ensure_manifest_sidecar ( options, files) ?; 951ensure_npm_gitignore ( & options. project_root ) ?; 952Ok ( Some ( build_artifact ( 953NpmArchive { 954registry : npm_registry, 955 name, 956 version, 957tarball : & pin. tarball , 958integrity : & pin. integrity , 959}, 960 files. clone (), 961 dir, 962 verified, 963))) 964} 965 966// Materialize one verified tarball under a per-package lock. Read-only 967// hits return before any locking or writing. 968pub ( crate ) fn materialize_npm ( 969options : & Options , 970authority : & Option < crate :: authority:: SourceAuthority >, 971archive : NpmArchive < ' _ >, 972instance : & str , 973bytes : Vec < u8 >, 974expected : Option < & NpmPin >, 975) ->Result < NpmArtifact , StorageError > { 976let NpmArchive { 977registry : npm_registry, 978 name, 979 version, 980 tarball, 981 integrity, 982} = archive; 983if bytes. len () >MAX_NPM_TARBALL_COMPRESSED { 984return Err ( StorageError :: NpmTarballTooLarge { 985name : name. to_owned (), 986version : version. to_owned (), 987}); 988} 989super :: verify_sri ( & bytes, integrity) ?; 990let data =super :: collect_npm_files ( & bytes) ?; 991let ( files, _) =super :: bind_npm_files ( & data, name, version) ?; 992if let Some ( expected) = expected{ 993if expected. tarball != tarball || expected. integrity != integrity{ 994return Err ( StorageError :: NpmArchiveChangedFor { 995name : name. to_owned (), 996version : version. to_owned (), 997}); 998} 999match ( & expected. manifest , & expected. files ) { 1000( Some ( manifest), _) =>{ 1001if manifest_digest ( & files) ? !=* manifest{ 1002return Err ( StorageError :: NpmArchiveChangedFor { 1003name : name. to_owned (), 1004version : version. to_owned (), 1005}); 1006} 1007} 1008( None , Some ( locked)) =>{ 1009if * locked != files{ 1010return Err ( StorageError :: NpmArchiveChangedFor { 1011name : name. to_owned (), 1012version : version. to_owned (), 1013}); 1014} 1015} 1016( None , None ) =>{ 1017return Err ( StorageError :: NpmPinMissingManifest ); 1018} 1019} 1020} 1021super :: check_npm_exec ( authority. as_ref (), npm_registry, name, version, & files) ?; 1022let dir =npm_package_dir ( options, instance, name, version); 1023if npm_dir_state ( & options. project_root , & dir, & files) ? ==NpmDirState :: Absent { 1024let _guard =lock_package ( 1025 options, 1026& format! ( "{}|{name}|{version}" , npm_registry. as_str ()), 1027) ?; 1028if npm_dir_state ( & options. project_root , & dir, & files) ? ==NpmDirState :: Absent { 1029let root =open_project ( options) ?; 1030let dir_rel =relative_path ( & options. project_root , & dir) ?; 1031publish_dir ( & root, & dir_rel, |staging|{ 1032stage_data_files ( staging, & data, & files) 1033}) ?; 1034ensure_npm_gitignore ( & options. project_root ) ?; 1035} 1036} 1037let package =verify_and_load_npm_dir ( & options. project_root , & dir, & files) ?; 1038ensure_manifest_sidecar ( options, & files) ?; 1039Ok ( NpmArtifact { 1040identity : NpmIdentity { 1041registry : npm_registry. clone (), 1042name : name. to_owned (), 1043version : version. to_owned (), 1044}, 1045tarball : tarball. to_owned (), 1046integrity : integrity. to_owned (), 1047 dir, 1048 files, 1049 package, 1050}) 1051}