char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
98acb46
main
1use std:: collections:: BTreeMap ; 2use std:: io:: Read ; 3use std:: path::{ Path , PathBuf }; 4use std:: sync::{ Arc , Mutex }; 5use std:: time:: Duration ; 6 7use bytes:: Bytes ; 8use deno_semver:: npm:: NpmPackageReqReference ; 9use http_body_util::{ BodyExt , Empty }; 10use hyper:: Request ; 11use hyper_rustls:: HttpsConnector ; 12use hyper_util:: client:: legacy::{ Client , connect:: HttpConnector }; 13use serde:: Deserialize ; 14use url:: Url ; 15 16use super ::{ 17Access , NpmAcquisition , NpmLock , NpmPin , Options , PackageStore , artifact_key, checksum, 18}; 19use storage:: NpmArchive ; 20/// Failures resolving, downloading, and verifying npm packages. 21# [ derive ( Clone , Debug , thiserror :: Error )] 22pub enum NpmError { 23# [ error ( "Locked npm package changed for {key}" )] 24LockedNpmPackageChanged { key : String }, 25# [ error ( "npm file integrity mismatch: {rel}" )] 26NpmFileIntegrityMismatch { rel : String }, 27# [ error ( "Frozen lockfile is missing {name}" )] 28FrozenLockfileMissing { name : String }, 29# [ error ( "Invalid npm package.json: {source}" )] 30InvalidNpmPackageJson { source : Arc < serde_json:: Error > }, 31# [ error ( "npm manifest does not match its locked files" )] 32NpmManifestFilesMismatch , 33# [ error ( "npm archive changed for {name}@{version}" )] 34NpmArchiveChangedFor { name : String , version : String }, 35# [ error ( "npm pin is missing its manifest" )] 36NpmPinMissingManifest , 37# [ error ( "npm package.json is missing its name" )] 38NpmPackageJsonMissingName , 39# [ error ( "npm package.json is missing its version" )] 40NpmPackageJsonMissingVersion , 41# [ error ( "npm package.json does not match its archive identity" )] 42NpmPackageJsonIdentityMismatch , 43# [ error ( "npm file is not in the package manifest: {path}" )] 44NpmFileNotInManifest { path : String }, 45# [ error ( "Expected an npm: request, not {specifier:?}" )] 46ExpectedNpmRequest { specifier : String }, 47# [ error ( "Invalid npm request: {specifier:?}" )] 48InvalidNpmRequest { specifier : String }, 49# [ error ( "{detail}" )] 50NpmVersionReq { detail : String }, 51# [ error ( "{detail}" )] 52NpmPackageReqReference { detail : String }, 53# [ error ( "npm subpaths resolve later; acquire the package first" )] 54NpmSubpathNeedsAcquire , 55# [ error ( "Invalid npm package name: {name}" )] 56InvalidNpmPackageName { name : String }, 57# [ error ( "Invalid npm version: {version}" )] 58InvalidNpmVersion { version : String }, 59# [ error ( "Non-canonical npm version: {version}" )] 60NonCanonicalNpmVersion { version : String }, 61# [ error ( "Invalid npm registry URL" )] 62InvalidNpmRegistryUrl , 63# [ error ( "Invalid locked npm request: {specifier}" )] 64InvalidLockedNpmRequest { specifier : String }, 65# [ error ( "Invalid locked npm root: {key}" )] 66InvalidLockedNpmRoot { key : String }, 67# [ error ( "Lockfile is missing npm instance {instance}" )] 68LockfileMissingNpmInstance { instance : String }, 69# [ error ( "Lockfile is missing npm package {}" , package )] 70LockfileMissingNpmPackage { package : String }, 71# [ error ( "Lockfile is missing npm instance {edge}" )] 72LockfileMissingNpmEdge { edge : String }, 73# [ error ( "Invalid npm instance: {instance}" )] 74InvalidNpmInstance { instance : String }, 75# [ error ( "Invalid npm lock identity" )] 76InvalidNpmLockIdentity , 77# [ error ( "Invalid automatic type association for {identity}" )] 78InvalidNpmTypesAssociation { identity : String }, 79# [ error ( "Invalid npm manifest digest" )] 80InvalidNpmManifestDigest , 81# [ error ( "Invalid npm manifest" )] 82InvalidNpmManifest , 83# [ error ( "Invalid npm file checksum: {path}" )] 84InvalidNpmFileChecksum { path : String }, 85# [ error ( "npm pin is missing package.json" )] 86NpmPinMissingPackageJson , 87# [ error ( "Invalid npm file path: {path}" )] 88InvalidNpmFilePath { path : String }, 89# [ error ( "npm tarball path must be UTF-8" )] 90NpmTarballPathNotUtf8 , 91# [ error ( "npm tarball mixes archive roots: {raw}" )] 92NpmTarballMixedRoots { raw : String }, 93# [ error ( "Invalid npm tarball URL: {raw}" )] 94InvalidNpmTarballUrl { raw : String }, 95# [ error ( "npm tarball credentials are not supported: {raw}" )] 96NpmTarballCredentials { raw : String }, 97# [ error ( "npm URLs require HTTPS: {url}" )] 98NpmUrlRequiresHttps { url : String }, 99# [ error ( "Unsupported npm integrity algorithm: {algorithm}" )] 100UnsupportedNpmIntegrityAlgorithm { algorithm : String }, 101# [ error ( "Invalid npm integrity encoding" )] 102InvalidNpmIntegrityEncoding , 103# [ error ( "Invalid npm integrity length" )] 104InvalidNpmIntegrityLength , 105# [ error ( "Invalid npm integrity" )] 106InvalidNpmIntegrity , 107# [ error ( "Invalid npm shasum" )] 108InvalidNpmShasum , 109# [ error ( "npm tarball integrity mismatch" )] 110NpmTarballIntegrityMismatch , 111# [ error ( "npm tag {tag:?} does not exist for {}" , name )] 112NpmTagMissing { tag : String , name : String }, 113# [ error ( "npm tag {tag:?} points at invalid version {version}" )] 114NpmTagInvalidVersion { tag : String , version : String }, 115# [ error ( "npm tag {tag:?} points at missing version {version}" )] 116NpmTagMissingVersion { tag : String , version : String }, 117# [ error ( "npm package {name} has no latest tag" )] 118NpmPackageNoLatestTag { name : String }, 119# [ error ( "npm latest tag points at invalid version {version}" )] 120NpmLatestTagInvalidVersion { version : String }, 121# [ error ( "npm latest tag points at missing version {version}" )] 122NpmLatestTagMissingVersion { version : String }, 123# [ error ( "No npm version satisfies {}" , version_text )] 124NoNpmVersionSatisfies { version_text : String }, 125# [ error ( "npm origin is outside the registry: {url}" )] 126NpmOriginOutsideRegistry { url : String }, 127# [ error ( "{detail}" )] 128NpmHttp { detail : String }, 129# [ error ( "npm redirect has no valid Location: {url}" )] 130NpmRedirectNoLocation { url : String }, 131# [ error ( "npm redirect escapes the registry: {next}" )] 132NpmRedirectEscapesRegistry { next : String }, 133# [ error ( "npm server returned HTML instead of {what}: {url}" )] 134NpmServerHtml { what : String , url : String }, 135# [ error ( "npm {what} is too large: {url}" )] 136NpmResponseTooLarge { what : String , url : String }, 137# [ error ( "Invalid npm tarball" )] 138InvalidNpmTarball , 139# [ error ( "Invalid npm tarball path" )] 140InvalidNpmTarballPath , 141# [ error ( "Invalid npm tarball entry" )] 142InvalidNpmTarballEntry , 143# [ error ( "npm tarball has too many entries" )] 144NpmTarballTooManyEntries , 145# [ error ( "npm file is too large: {path}" )] 146NpmFileTooLarge { path : String }, 147# [ error ( "npm tarball expands to too many bytes" )] 148NpmTarballExpandsTooMuch , 149# [ error ( "npm tarball has a duplicate file: {path}" )] 150NpmTarballDuplicate { path : String }, 151# [ error ( "npm tarball has too many files" )] 152NpmTarballTooManyFiles , 153# [ error ( "npm tarball is empty" )] 154NpmTarballEmpty , 155# [ error ( "npm download worker stopped" )] 156NpmDownloadWorkerStopped , 157# [ error ( "npm metadata worker stopped" )] 158NpmMetadataWorkerStopped , 159# [ error ( "npm acquisition worker stopped" )] 160NpmAcquisitionWorkerStopped , 161# [ error ( "Locked version {version} does not satisfy {specifier}" )] 162NpmLockedVersionUnsatisfied { version : String , specifier : String }, 163# [ error ( "Too many npm redirects: {url}" )] 164NpmTooManyRedirects { url : String }, 165# [ error ( "npm redirect credentials are not supported: {next}" )] 166NpmRedirectCredentials { next : String }, 167# [ error ( "npm server returned {status} for {url}" )] 168NpmServerStatus { status : u16 , url : String }, 169# [ error ( 170"Type packages are not prepared for {identity}; run `avril cache` or configure `types.imports`" 171)] 172NpmTypesNotPrepared { identity : String }, 173# [ error ( "npm request timed out: {url}" )] 174NpmRequestTimeout { url : String }, 175# [ error ( "npm tarball is missing package.json" )] 176NpmTarballMissingPackageJson , 177# [ error ( "npm tarball is too large" )] 178NpmTarballTooLargeBare , 179# [ error ( "npm tarball escapes the registry: {tarball}" )] 180NpmTarballEscapesRegistry { tarball : String }, 181# [ error ( "Offline npm cache miss: {name}" )] 182NpmOfflineCacheMiss { name : String }, 183# [ error ( "Offline npm metadata miss: {name}" )] 184NpmOfflineMetadataMiss { name : String }, 185# [ error ( "Invalid npm packument for {name}: {source}" )] 186InvalidNpmPackument { 187name : String , 188source : Arc < serde_json:: Error >, 189}, 190# [ error ( "npm packument name mismatch for {name}" )] 191NpmPackumentNameMismatch { name : String }, 192# [ error ( "Conflicting npm version selections for {request_key}" )] 193NpmConflictingVersionSelections { request_key : String }, 194# [ error ( "npm version is missing tarball integrity" )] 195NpmVersionMissingIntegrity , 196# [ error ( "npm tarball must contain only regular files and directories" )] 197NpmTarballMustContainRegularFiles , 198# [ error ( transparent )] 199Storage ( # [ from ] storage:: StorageError ), 200# [ error ( transparent )] 201Package ( # [ from ] package:: PackageError ), 202# [ error ( transparent )] 203Url ( # [ from ] url:: ParseError ), 204# [ error ( transparent )] 205Caps ( # [ from ] crate :: caps:: CapabilitiesError ), 206} 207 208mod fs; 209pub mod package; 210pub mod resolution; 211pub ( crate ) mod storage; 212 213pub ( super ) type SharedPackument = futures:: future:: Shared < 214 futures:: future:: BoxFuture < ' static , Result < Arc < NpmPackument >, NpmError >>, 215>; 216 217pub const NPM_DEFAULT_INSTANCE : & str ="default" ; 218 219pub ( super ) const MAX_NPM_CONCURRENT_DOWNLOADS : usize =8 ; 220 221const MAX_NPM_PACKUMENT_BYTES : usize =8 * 1024 * 1024 ; 222const MAX_NPM_TARBALL_COMPRESSED : usize =32 * 1024 * 1024 ; 223const MAX_NPM_TARBALL_EXPANDED : usize =128 * 1024 * 1024 ; 224const MAX_NPM_FILES : usize =65_536 ; 225const MAX_NPM_FILE_BYTES : usize =32 * 1024 * 1024 ; 226const MAX_NPM_TAR_STREAM : usize =MAX_NPM_TARBALL_EXPANDED +MAX_NPM_FILES * 512 +8192 ; 227const NPM_FETCH_TIMEOUT : Duration =Duration :: from_secs ( 30 ); 228 229# [ derive ( Clone , Copy , Debug , PartialEq , Eq )] 230enum NpmVerification { 231Full , 232OnUse , 233} 234 235# [ derive ( Clone , Debug , PartialEq , Eq )] 236pub struct NpmIdentity { 237pub registry : Url , 238pub name : String , 239pub version : String , 240} 241 242impl NpmIdentity { 243pub fn key ( & self ) ->String { 244format! ( "{}|{}|{}" , self . registry . as_str (), self . name , self . version ) 245} 246} 247 248# [ derive ( Clone , Debug )] 249pub struct NpmArtifact { 250pub identity : NpmIdentity , 251pub tarball : String , 252pub integrity : String , 253pub dir : PathBuf , 254pub files : BTreeMap < String , String >, 255pub package : serde_json:: Value , 256} 257 258impl NpmArtifact { 259/// Read one verified package file. This is the loader-facing primitive: 260/// callers must use these bytes rather than reading the tree directly, so 261/// verification cannot be bypassed between check and use. 262pub fn read_file ( & self , project_root : & Path , path : & str ) ->Result < Vec < u8 >, NpmError > { 263let expected =self 264. files 265. get ( path) 266. ok_or_else ( ||NpmError :: NpmFileNotInManifest { path : path. to_owned () }) ?; 267let bytes = storage:: read_project_file ( project_root, & self . dir , path, MAX_NPM_FILE_BYTES ) ?; 268if checksum ( & bytes) !=* expected{ 269return Err ( NpmError :: NpmFileIntegrityMismatch { rel : path. to_owned () }); 270} 271Ok ( bytes) 272} 273} 274 275# [ derive ( Deserialize , Clone )] 276pub ( super ) struct NpmPackument { 277name : String , 278# [ serde ( default , rename = "dist-tags" )] 279dist_tags : BTreeMap < String , String >, 280versions : BTreeMap < String , NpmVersionEntry >, 281} 282 283# [ derive ( Deserialize , Default , Clone )] 284struct NpmVersionEntry { 285# [ serde ( default )] 286dist : NpmDist , 287} 288 289# [ derive ( Deserialize , Default , Clone )] 290struct NpmDist { 291# [ serde ( default )] 292tarball : String , 293# [ serde ( default )] 294integrity : Option < String >, 295# [ serde ( default )] 296shasum : Option < String >, 297} 298 299/// A parsed `npm:` request using manifest range grammar (unions, spaces, 300/// comparators), which URL-grammar parsers reject. `request_key` preserves 301/// the declared range text so lock entries stay stable. 302# [ derive ( Clone , Debug , PartialEq , Eq )] 303pub struct NpmRequest { 304pub name : String , 305pub range : Option < String >, 306pub version_req : deno_semver:: VersionReq , 307pub subpath : Option < String >, 308} 309 310impl NpmRequest { 311pub fn request_key ( & self ) ->String { 312match & self . range { 313None =>format! ( "npm:{}" , self . name ), 314Some ( range) =>format! ( "npm:{}@{range}" , self . name ), 315} 316} 317} 318 319pub fn parse_npm_request ( specifier : & str ) ->Result < NpmRequest , NpmError > { 320let text = specifier 321. strip_prefix ( "npm:" ) 322. ok_or_else ( ||NpmError :: ExpectedNpmRequest { specifier : specifier. to_owned () }) ?; 323if text. is_empty () { 324return Err ( NpmError :: ExpectedNpmRequest { specifier : specifier. to_owned () }); 325} 326let ( head, subpath): ( String , Option < String >) =if let Some ( rest) = text. strip_prefix ( '@' ) { 327match rest. split_once ( '/' ) { 328None =>( text. to_owned (), None ), 329Some (( _, tail)) =>match tail. split_once ( '/' ) { 330None =>( text. to_owned (), None ), 331Some (( _, sub)) =>( 332 text[ ..text. len () - sub. len () -1 ]. to_owned (), 333Some ( sub. to_owned ()), 334), 335}, 336} 337} else { 338match text. split_once ( '/' ) { 339None =>( text. to_owned (), None ), 340Some (( name, sub)) =>( name. to_owned (), Some ( sub. to_owned ())), 341} 342}; 343let ( name, range): ( String , Option < String >) =if let Some ( rest) = head. strip_prefix ( '@' ) { 344match rest. rsplit_once ( '@' ) { 345None =>( head. clone (), None ), 346Some (( scope_name, range)) =>{ 347if range. is_empty () { 348return Err ( NpmError :: InvalidNpmRequest { specifier : specifier. to_owned () }); 349} 350( format! ( "@{scope_name}" ), Some ( range. to_owned ())) 351} 352} 353} else { 354match head. rsplit_once ( '@' ) { 355None =>( head. clone (), None ), 356Some (( name, range)) =>{ 357if range. is_empty () { 358return Err ( NpmError :: InvalidNpmRequest { specifier : specifier. to_owned () }); 359} 360( name. to_owned (), Some ( range. to_owned ())) 361} 362} 363}; 364validate_npm_name ( & name) ?; 365let version_req =match & range{ 366None => deno_semver:: VersionReq :: parse_from_npm ( "*" ) 367. map_err ( |detail|NpmError :: NpmVersionReq { detail : detail. to_string () }) ?, 368Some ( range) => deno_semver:: VersionReq :: parse_from_npm ( range) 369. map_err ( |detail|NpmError :: NpmVersionReq { detail : detail. to_string () }) ?, 370}; 371Ok ( NpmRequest { 372 name, 373 range, 374 version_req, 375 subpath, 376}) 377} 378 379pub fn parse_npm_specifier ( specifier : & str ) ->Result < NpmPackageReqReference , NpmError > { 380let request =parse_npm_request ( specifier) ?; 381if request. subpath . is_some () { 382return Err ( NpmError :: NpmSubpathNeedsAcquire ); 383} 384NpmPackageReqReference :: from_str ( & request. request_key ()) 385. map_err ( |detail|NpmError :: NpmPackageReqReference { detail : detail. to_string () }) 386} 387 388pub fn validate_npm_name ( name : & str ) ->Result <(), NpmError > { 389if name. is_empty () || name. len () >214 { 390return Err ( NpmError :: InvalidNpmPackageName { name : name. to_owned () }); 391} 392if let Some ( rest) = name. strip_prefix ( '@' ) { 393let Some (( scope, leaf)) = rest. split_once ( '/' ) else { 394return Err ( NpmError :: InvalidNpmPackageName { name : name. to_owned () }); 395}; 396validate_npm_part ( scope) 397. map_err ( |_|NpmError :: InvalidNpmPackageName { name : name. to_owned () }) ?; 398validate_npm_part ( leaf) 399. map_err ( |_|NpmError :: InvalidNpmPackageName { name : name. to_owned () }) ?; 400if name. split ( '/' ). count () !=2 { 401return Err ( NpmError :: InvalidNpmPackageName { name : name. to_owned () }); 402} 403} else { 404if name. contains ( '/' ) { 405return Err ( NpmError :: InvalidNpmPackageName { name : name. to_owned () }); 406} 407validate_npm_part ( name) 408. map_err ( |_|NpmError :: InvalidNpmPackageName { name : name. to_owned () }) ?; 409} 410Ok (()) 411} 412 413fn validate_npm_part ( part : & str ) ->Result <(), ()> { 414if part. is_empty () 415 || part. len () >214 416 || part. starts_with ( '.' ) 417 || part. starts_with ( '_' ) 418 || part 419. bytes () 420. any ( |ch| !( ch. is_ascii_alphanumeric () ||matches! ( ch, b'-' |b'_' |b'.' |b'~' ))) 421{ 422return Err (()); 423} 424Ok (()) 425} 426 427fn validate_npm_version ( version : & str ) ->Result < deno_semver:: Version , NpmError > { 428let parsed = deno_semver:: Version :: parse_from_npm ( version) 429. map_err ( |_|NpmError :: InvalidNpmVersion { version : version. to_owned () }) ?; 430if parsed. to_string () != version{ 431return Err ( NpmError :: NonCanonicalNpmVersion { version : version. to_owned () }); 432} 433Ok ( parsed) 434} 435 436/// Provenance base for acquisition checks: always slash-terminated so sibling 437/// names cannot collide through string-prefix grants. 438pub fn npm_package_base ( registry : & Url , name : & str ) ->Result < Url , NpmError > { 439validate_npm_name ( name) ?; 440let mut base = registry. join ( & format! ( "{name}/" )) ?; 441 base. set_query ( None ); 442 base. set_fragment ( None ); 443Ok ( base) 444} 445 446/// Version base for execution checks and virtual file URLs. 447pub fn npm_version_base ( registry : & Url , name : & str , version : & str ) ->Result < Url , NpmError > { 448validate_npm_version ( version) ?; 449let package =npm_package_base ( registry, name) ?; 450Ok ( package. join ( & format! ( "{version}/" )) ?) 451} 452 453/// Canonical virtual URL of one package file, for capability grants and the 454/// next loader chunk. Transport never fetches these; they name authority. 455pub fn npm_virtual_module ( 456registry : & Url , 457name : & str , 458version : & str , 459path : & str , 460) ->Result < Url , NpmError > { 461validate_npm_relpath ( path) ?; 462let mut url =npm_version_base ( registry, name, version) ?; 463 url. path_segments_mut () 464. map_err ( |_|NpmError :: InvalidNpmRegistryUrl ) ? 465. pop_if_empty () 466. extend ( path. split ( '/' )); 467Ok ( url) 468} 469 470pub ( super ) fn validate_lock_npm ( npm : & NpmLock , expected_registry : & Url ) ->Result <(), NpmError > { 471for ( identity, request) in & npm. types { 472let ( registry, name, version) =parse_npm_key ( identity) 473. map_err ( |_|NpmError :: InvalidNpmTypesAssociation { identity : identity. clone () }) ?; 474let _ =npm_package_base ( & registry, & name) ?; 475if & registry != expected_registry || !npm. packages . contains_key ( identity) { 476return Err ( NpmError :: InvalidNpmTypesAssociation { identity : identity. clone () }); 477} 478let source_version =validate_npm_version ( & version) ?; 479if let Some ( request) = request{ 480let parsed =parse_npm_request ( request) 481. map_err ( |_|NpmError :: InvalidNpmTypesAssociation { identity : identity. clone () }) ?; 482let expected = package:: types_package_name ( & name); 483let next_major = source_version. major . checked_add ( 1 ). ok_or_else ( ||{ 484NpmError :: InvalidNpmTypesAssociation { identity : identity. clone () } 485}) ?; 486let expected_range =format! ( ">={}.0.0 <{next_major}.0.0" , source_version. major ); 487let type_version = npm. specifiers . get ( request). ok_or_else ( ||{ 488NpmError :: InvalidNpmTypesAssociation { identity : identity. clone () } 489}) ?; 490let type_key =format! ( "{}|{}|{type_version}" , expected_registry, parsed. name ); 491if name. starts_with ( "@types/" ) 492 || parsed. name != expected 493 || parsed. subpath . is_some () 494 || parsed. range . as_deref () !=Some ( expected_range. as_str ()) 495 || parsed. request_key () !=* request 496 || !npm. packages . contains_key ( & type_key) 497{ 498return Err ( NpmError :: InvalidNpmTypesAssociation { identity : identity. clone () }); 499} 500} 501} 502for ( key, pin) in & npm. packages { 503let ( registry, name, version) =parse_npm_key ( key) ?; 504let _ =npm_package_base ( & registry, & name) ?; 505validate_npm_version ( & version) ?; 506validate_npm_pin ( pin) ?; 507let tarball =npm_tarball_url ( & pin. tarball ) ?; 508if !same_origin ( & registry, & tarball) { 509return Err ( NpmError :: NpmTarballEscapesRegistry { tarball : pin. tarball . clone () }); 510} 511check_https ( & tarball) ?; 512} 513for ( specifier, version) in & npm. specifiers { 514let request =parse_npm_request ( specifier) ?; 515if request. subpath . is_some () { 516return Err ( NpmError :: InvalidLockedNpmRequest { specifier : specifier. to_owned () }); 517} 518let locked =validate_npm_version ( version) ?; 519if !locked_satisfies ( request. version_req . clone (), & locked) { 520return Err ( NpmError :: NpmLockedVersionUnsatisfied { 521version : version. clone (), 522specifier : specifier. to_owned (), 523}); 524} 525} 526for ( key, instance) in & npm. roots { 527let ( request, context) =match key. split_once ( '\0' ) { 528None =>( key. as_str (), None ), 529Some (( request, suffix)) =>{ 530let context = suffix 531. strip_prefix ( "ctx:" ) 532. ok_or_else ( ||NpmError :: InvalidLockedNpmRoot { key : key. to_owned () }) ?; 533( request, Some ( context)) 534} 535}; 536let parsed =parse_npm_request ( request) ?; 537if parsed. subpath . is_some () { 538return Err ( NpmError :: InvalidLockedNpmRoot { key : key. to_owned () }); 539} 540if let Some ( context) = context{ 541if context. is_empty () { 542return Err ( NpmError :: InvalidLockedNpmRoot { key : key. to_owned () }); 543} 544for entryin context. split ( ',' ) { 545let ( peer, provider) = entry 546. split_once ( '=' ) 547. ok_or_else ( ||NpmError :: InvalidLockedNpmRoot { key : key. to_owned () }) ?; 548validate_npm_name ( peer) ?; 549parse_npm_key ( provider) ?; 550} 551} 552validate_npm_instance_id ( instance) ?; 553if !npm. instances . contains_key ( instance) { 554return Err ( NpmError :: LockfileMissingNpmInstance { instance : instance. to_owned () }); 555} 556} 557for ( instance, pin) in & npm. instances { 558validate_npm_instance_id ( instance) ?; 559let ( registry, name, version) =parse_npm_key ( & pin. package ) ?; 560let _ =npm_package_base ( & registry, & name) ?; 561validate_npm_version ( & version) ?; 562if !npm. packages . contains_key ( & pin. package ) { 563return Err ( NpmError :: LockfileMissingNpmPackage { package : pin. package . clone () }); 564} 565if instance !=& pin. package && !instance. starts_with ( "p-" ) { 566return Err ( NpmError :: InvalidNpmInstance { instance : instance. to_owned () }); 567} 568for ( alias, edge) in pin. deps . iter (). chain ( pin. peers . iter ()) { 569validate_npm_name ( alias) ?; 570validate_npm_instance_id ( edge) ?; 571if !npm. instances . contains_key ( edge) { 572return Err ( NpmError :: LockfileMissingNpmEdge { edge : edge. to_owned () }); 573} 574} 575} 576Ok (()) 577} 578 579fn validate_npm_instance_id ( instance : & str ) ->Result <(), NpmError > { 580if instance ==NPM_DEFAULT_INSTANCE { 581return Ok (()); 582} 583if let Some ( rest) = instance. strip_prefix ( "p-" ) { 584if ( 16 ..=64 ). contains ( & rest. len ()) && rest. bytes (). all ( |ch| ch. is_ascii_hexdigit ()) { 585return Ok (()); 586} 587return Err ( NpmError :: InvalidNpmInstance { instance : instance. to_owned () }); 588} 589parse_npm_key ( instance) ?; 590Ok (()) 591} 592 593pub ( super ) fn parse_npm_key ( key : & str ) ->Result <( Url , String , String ), NpmError > { 594let mut parts = key. splitn ( 3 , '|' ); 595let ( Some ( registry), Some ( name), Some ( version)) =( parts. next (), parts. next (), parts. next ()) 596else { 597return Err ( NpmError :: InvalidNpmLockIdentity ); 598}; 599if parts. next (). is_some () { 600return Err ( NpmError :: InvalidNpmLockIdentity ); 601} 602let url =Url :: parse ( registry). map_err ( |_|NpmError :: InvalidNpmLockIdentity ) ?; 603if url. as_str () != registry{ 604return Err ( NpmError :: InvalidNpmLockIdentity ); 605} 606validate_npm_name ( name) ?; 607validate_npm_version ( version) ?; 608Ok (( url, name. to_owned (), version. to_owned ())) 609} 610 611# [ cfg ( test )] 612mod types_lock_tests{ 613use super ::{ NpmLock , NpmPin , parse_npm_request, validate_lock_npm}; 614use std:: collections:: BTreeMap ; 615 616# [ test ] 617fn validates_automatic_type_associations () { 618let source ="https://registry.npmjs.org/|@scope/library|0.7.4" ; 619let request ="npm:@types/scope__library@>=0.0.0 <1.0.0" ; 620let lock =NpmLock { 621specifiers : BTreeMap :: from ([( request. to_owned (), "0.9.0" . to_owned ())]), 622packages : BTreeMap :: from ([ 623( source. to_owned (), test_pin ()), 624( 625"https://registry.npmjs.org/|@types/scope__library|0.9.0" . to_owned (), 626test_pin (), 627), 628]), 629types : BTreeMap :: from ([( source. to_owned (), Some ( request. to_owned ()))]), 630 ..NpmLock :: default () 631}; 632validate_lock_npm ( 633& lock, 634& url:: Url :: parse ( "https://registry.npmjs.org/" ). unwrap (), 635) 636. unwrap (); 637assert_eq! ( 638parse_npm_request ( request). unwrap (). name , 639"@types/scope__library" 640); 641 642let registry = url:: Url :: parse ( "https://registry.npmjs.org/" ). unwrap (); 643let mut invalid = lock. clone (); 644 invalid 645. types 646. insert ( source. to_owned (), Some ( "npm:@types/other@*" . to_owned ())); 647assert! ( validate_lock_npm ( & invalid, & registry). is_err ()); 648 649let mut dangling = lock. clone (); 650 dangling 651. packages 652. remove ( "https://registry.npmjs.org/|@types/scope__library|0.9.0" ); 653assert! ( validate_lock_npm ( & dangling, & registry). is_err ()); 654let mut missing_source = lock. clone (); 655 missing_source. packages . remove ( source); 656assert! ( validate_lock_npm ( & missing_source, & registry). is_err ()); 657assert! ( 658validate_lock_npm ( & lock, & url:: Url :: parse ( "https://elsewhere.test/" ). unwrap ()). is_err () 659); 660} 661 662# [ test ] 663fn type_packages_can_have_negative_discovery_records () { 664let identity ="https://registry.npmjs.org/|@types/node|22.0.0" ; 665let lock =NpmLock { 666packages : BTreeMap :: from ([( identity. to_owned (), test_pin ())]), 667types : BTreeMap :: from ([( identity. to_owned (), None )]), 668 ..NpmLock :: default () 669}; 670validate_lock_npm ( 671& lock, 672& url:: Url :: parse ( "https://registry.npmjs.org/" ). unwrap (), 673) 674. unwrap (); 675} 676 677fn test_pin () ->NpmPin { 678NpmPin { 679tarball : "https://registry.npmjs.org/package.tgz" . to_owned (), 680integrity : "sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" . to_owned (), 681manifest : Some ( format! ( "sha256-{}" , "0" . repeat ( 64 ))), 682files : None , 683} 684} 685} 686 687fn locked_satisfies ( version_req : deno_semver:: VersionReq , version : & deno_semver:: Version ) ->bool { 688if version_req. tag (). is_some () || version_req. version_text () =="*" { 689return true ; 690} 691 version_req. matches ( version) 692} 693 694fn validate_npm_pin ( pin : & NpmPin ) ->Result <(), NpmError > { 695npm_tarball_url ( & pin. tarball ) ?; 696select_sri ( & pin. integrity ) ?; 697match ( & pin. manifest , & pin. files ) { 698( Some ( manifest), Some ( files)) =>{ 699manifest_file_name ( manifest) ?; 700validate_files_map ( files) ?; 701if storage:: manifest_digest ( files) ? !=* manifest{ 702return Err ( NpmError :: NpmManifestFilesMismatch ); 703} 704} 705( Some ( manifest), None ) =>{ 706manifest_file_name ( manifest) ?; 707} 708( None , Some ( files)) =>validate_files_map ( files) ?, 709( None , None ) =>return Err ( NpmError :: NpmPinMissingManifest ), 710} 711Ok (()) 712} 713 714pub ( crate ) fn manifest_file_name ( digest : & str ) ->Result < String , NpmError > { 715let hex = digest 716. strip_prefix ( "sha256-" ) 717. ok_or ( NpmError :: InvalidNpmManifestDigest ) ?; 718if hex. len () !=64 || !hex. bytes (). all ( |byte| byte. is_ascii_hexdigit ()) { 719return Err ( NpmError :: InvalidNpmManifestDigest ); 720} 721Ok ( format! ( "{hex}.json" )) 722} 723 724pub ( crate ) fn validate_files_map ( files : & BTreeMap < String , String >) ->Result <(), NpmError > { 725if files. is_empty () || files. len () >MAX_NPM_FILES { 726return Err ( NpmError :: InvalidNpmManifest ); 727} 728for ( path, hash) in files{ 729validate_npm_relpath ( path) ?; 730if !valid_npm_checksum ( hash) { 731return Err ( NpmError :: InvalidNpmFileChecksum { path : path. to_owned () }); 732} 733} 734if !files. contains_key ( "package.json" ) { 735return Err ( NpmError :: NpmPinMissingPackageJson ); 736} 737Ok (()) 738} 739 740fn valid_npm_checksum ( hash : & str ) ->bool { 741 hash. strip_prefix ( "sha256-" ) 742. is_some_and ( |hex| hex. len () ==64 && hex. bytes (). all ( |ch| ch. is_ascii_hexdigit ())) 743} 744 745fn validate_npm_relpath ( path : & str ) ->Result <(), NpmError > { 746if path. is_empty () || path. len () >1024 { 747return Err ( NpmError :: InvalidNpmFilePath { path : path. to_owned () }); 748} 749if path. starts_with ( '/' ) || path. contains ( '\\' ) || path. contains ( '\0' ) { 750return Err ( NpmError :: InvalidNpmFilePath { path : path. to_owned () }); 751} 752if path. bytes (). any ( |byte| byte. is_ascii_control ()) { 753return Err ( NpmError :: InvalidNpmFilePath { path : path. to_owned () }); 754} 755let lower = path. to_ascii_lowercase (); 756if lower. contains ( "%2f" ) || lower. contains ( "%5c" ) { 757return Err ( NpmError :: InvalidNpmFilePath { path : path. to_owned () }); 758} 759for partin path. split ( '/' ) { 760if part. is_empty () || part =="." || part ==".." { 761return Err ( NpmError :: InvalidNpmFilePath { path : path. to_owned () }); 762} 763if part. len () >255 || part. contains ( ':' ) { 764return Err ( NpmError :: InvalidNpmFilePath { path : path. to_owned () }); 765} 766if part. ends_with ( '.' ) || part. ends_with ( ' ' ) { 767return Err ( NpmError :: InvalidNpmFilePath { path : path. to_owned () }); 768} 769} 770Ok (()) 771} 772 773/// Tar entry names arrive as platform paths: one consistent safe top-level 774/// directory is accepted for the whole archive and stripped, harmless `.` 775/// segments beneath it normalize away, while anything else must already be 776/// a clean relative posix path. Returns empty only for ignored root directories. 777fn npm_relpath_from_tar ( 778path : & std:: path:: Path , 779is_dir : bool , 780root : & mut Option < String >, 781) ->Result < String , NpmError > { 782let bytes = path. as_os_str (). as_encoded_bytes (); 783let raw = std:: str:: from_utf8 ( bytes). map_err ( |_|NpmError :: NpmTarballPathNotUtf8 ) ?; 784if raw. contains ( '\\' ) { 785return Err ( NpmError :: InvalidNpmFilePath { path : raw. to_owned () }); 786} 787let ( candidate, stripped) =match raw. find ( '/' ) { 788Some ( slash) =>( & raw[ ..slash], & raw[ slash +1 ..]), 789None =>( raw, "" ), 790}; 791validate_npm_relpath ( candidate) 792. map_err ( |_|NpmError :: InvalidNpmFilePath { path : raw. to_owned () }) ?; 793match root{ 794Some ( bound) if * bound != candidate =>{ 795return Err ( NpmError :: NpmTarballMixedRoots { raw : raw. to_owned () }); 796} 797Some ( _) =>{} 798None =>* root =Some ( candidate. to_owned ()), 799} 800if stripped. len () >1024 { 801return Err ( NpmError :: InvalidNpmFilePath { path : stripped. to_owned () }); 802} 803if is_dir{ 804if stripped. is_empty () { 805return Ok ( String :: new ()); 806} 807let inner = stripped. strip_suffix ( '/' ). unwrap_or ( stripped); 808if inner. is_empty () || inner. split ( '/' ). any ( |part| part. is_empty ()) { 809return Err ( NpmError :: InvalidNpmFilePath { path : stripped. to_owned () }); 810} 811let canonical: Vec < & str > = inner. split ( '/' ). filter ( |part|* part !="." ). collect (); 812let canonical = canonical. join ( "/" ); 813if canonical. is_empty () { 814return Ok ( String :: new ()); 815} 816validate_npm_relpath ( & canonical) ?; 817return Ok ( canonical); 818} 819let canonical: Vec < & str > = stripped. split ( '/' ). filter ( |part|* part !="." ). collect (); 820let canonical = canonical. join ( "/" ); 821validate_npm_relpath ( & canonical) ?; 822Ok ( canonical) 823} 824 825fn npm_tarball_url ( raw : & str ) ->Result < Url , NpmError > { 826let url = 827Url :: parse ( raw). map_err ( |_|NpmError :: InvalidNpmTarballUrl { raw : raw. to_owned () }) ?; 828if !matches! ( url. scheme (), "http" |"https" ) || url. host (). is_none () { 829return Err ( NpmError :: InvalidNpmTarballUrl { raw : raw. to_owned () }); 830} 831if !url. username (). is_empty () || url. password (). is_some () { 832return Err ( NpmError :: NpmTarballCredentials { raw : raw. to_owned () }); 833} 834if url. query (). is_some () || url. fragment (). is_some () { 835return Err ( NpmError :: InvalidNpmTarballUrl { raw : raw. to_owned () }); 836} 837Ok ( url) 838} 839 840fn same_origin ( left : & Url , right : & Url ) ->bool { 841 left. scheme () == right. scheme () 842 && left. host_str () == right. host_str () 843 && left. port_or_known_default () == right. port_or_known_default () 844} 845 846fn is_loopback ( url : & Url ) ->bool { 847matches! ( url. host_str (), Some ( "localhost" |"127.0.0.1" |"[::1]" )) 848} 849 850fn check_https ( url : & Url ) ->Result <(), NpmError > { 851if url. scheme () !="https" && !( url. scheme () =="http" &&is_loopback ( url)) { 852return Err ( NpmError :: NpmUrlRequiresHttps { url : url. to_string () }); 853} 854Ok (()) 855} 856 857fn packument_url ( registry : & Url , name : & str ) ->Result < Url , NpmError > { 858validate_npm_name ( name) ?; 859let mut url = registry. join ( name) ?; 860 url. set_query ( None ); 861 url. set_fragment ( None ); 862Ok ( url) 863} 864 865fn validate_instance ( instance : & str ) ->Result <(), NpmError > { 866if instance. is_empty () 867 || instance. len () >64 868 || instance. starts_with ( '.' ) 869 || instance. bytes (). any ( |byte|{ 870 byte ==b'/' 871 || byte ==b'\\' 872 || byte ==b':' 873 || byte ==b'\0' 874 || byte. is_ascii_control () 875}) 876{ 877return Err ( NpmError :: InvalidNpmInstance { instance : instance. to_owned () }); 878} 879Ok (()) 880} 881 882fn select_sri ( sri : & str ) ->Result <( String , Vec < u8 >), NpmError > { 883let mut best: Option <( u8 , String , Vec < u8 >)> =None ; 884for tokenin sri. split_whitespace () { 885let ( algorithm, encoded) = token. split_once ( '-' ). ok_or ( NpmError :: InvalidNpmIntegrity ) ?; 886let rank: u8 =match algorithm{ 887"sha512" =>3 , 888"sha256" =>2 , 889"sha1" =>1 , 890 _ =>{ 891return Err ( NpmError :: UnsupportedNpmIntegrityAlgorithm { 892algorithm : algorithm. to_owned (), 893}); 894} 895}; 896use base64:: Engine ; 897let digest = base64:: engine:: general_purpose:: STANDARD 898. decode ( encoded) 899. map_err ( |_|NpmError :: InvalidNpmIntegrityEncoding ) ?; 900let expected =match rank{ 9013 =>64 , 9022 =>32 , 903 _ =>20 , 904}; 905if digest. len () != expected{ 906return Err ( NpmError :: InvalidNpmIntegrityLength ); 907} 908if best 909. as_ref () 910. is_some_and ( |( current, _, _) |* current >= rank) 911{ 912continue ; 913} 914let normalized =format! ( 915"{algorithm}-{}" , 916 base64:: engine:: general_purpose:: STANDARD . encode ( & digest) 917); 918 best =Some (( rank, normalized, digest)); 919} 920 best. map ( |( _, normalized, digest) |( normalized, digest)) 921. ok_or ( NpmError :: InvalidNpmIntegrity ) 922} 923 924fn verify_sri ( bytes : & [ u8 ], sri : & str ) ->Result <(), NpmError > { 925let ( normalized, expected) =select_sri ( sri) ?; 926let algorithm = normalized. split_once ( '-' ). unwrap (). 0 ; 927let actual: Vec < u8 > =match algorithm{ 928"sha512" =>{ 929use sha2:: Digest ; 930 sha2:: Sha512 :: digest ( bytes). to_vec () 931} 932"sha256" =>{ 933use sha2:: Digest ; 934 sha2:: Sha256 :: digest ( bytes). to_vec () 935} 936 _ =>{ 937use sha1:: Digest as _; 938 sha1:: Sha1 :: digest ( bytes). to_vec () 939} 940}; 941if actual != expected{ 942return Err ( NpmError :: NpmTarballIntegrityMismatch ); 943} 944Ok (()) 945} 946 947fn normalized_sri ( sri : & str ) ->Result < String , NpmError > { 948let ( normalized, _) =select_sri ( sri) ?; 949Ok ( normalized) 950} 951 952fn sri_from_shasum ( shasum : & str ) ->Result < String , NpmError > { 953if shasum. len () !=40 || !shasum. bytes (). all ( |ch| ch. is_ascii_hexdigit ()) { 954return Err ( NpmError :: InvalidNpmShasum ); 955} 956let mut raw =Vec :: with_capacity ( 20 ); 957for chunkin 0 ..20 { 958let byte = u8:: from_str_radix ( & shasum[ chunk* 2 ..chunk* 2 +2 ], 16 ) 959. map_err ( |_|NpmError :: InvalidNpmShasum ) ?; 960 raw. push ( byte); 961} 962use base64:: Engine ; 963Ok ( format! ( 964"sha1-{}" , 965 base64:: engine:: general_purpose:: STANDARD . encode ( raw ) 966)) 967} 968 969fn pin_integrity ( dist : & NpmDist ) ->Result < String , NpmError > { 970if let Some ( integrity) =& dist. integrity { 971normalized_sri ( integrity) 972} else if let Some ( shasum) =& dist. shasum { 973sri_from_shasum ( shasum) 974} else { 975Err ( NpmError :: NpmVersionMissingIntegrity ) 976} 977} 978 979fn select_npm_version ( 980packument : & NpmPackument , 981version_req : & deno_semver:: VersionReq , 982) ->Result <( String , NpmDist ), NpmError > { 983if let Some ( tag) = version_req. tag () { 984let version = packument 985. dist_tags 986. get ( tag) 987. ok_or_else ( ||NpmError :: NpmTagMissing { 988tag : tag. to_owned (), 989name : packument. name . clone (), 990}) ?; 991validate_npm_version ( version). map_err ( |_|NpmError :: NpmTagInvalidVersion { 992tag : tag. to_owned (), 993version : version. to_owned (), 994}) ?; 995let entry = 996 packument 997. versions 998. get ( version) 999. ok_or_else ( ||NpmError :: NpmTagMissingVersion { 1000tag : tag. to_owned (), 1001version : version. to_owned (), 1002}) ?; 1003return Ok (( version. clone (), entry. dist . clone ())); 1004} 1005if version_req. version_text () =="*" { 1006let version = packument 1007. dist_tags 1008. get ( "latest" ) 1009. ok_or_else ( ||NpmError :: NpmPackageNoLatestTag { name : packument. name . clone () }) ?; 1010validate_npm_version ( version) 1011. map_err ( |_|NpmError :: NpmLatestTagInvalidVersion { version : version. to_owned () }) ?; 1012let entry = packument 1013. versions 1014. get ( version) 1015. ok_or_else ( ||NpmError :: NpmLatestTagMissingVersion { version : version. to_owned () }) ?; 1016return Ok (( version. clone (), entry. dist . clone ())); 1017} 1018let mut best: Option <( deno_semver:: Version , String )> =None ; 1019for version_textin packument. versions . keys () { 1020let Ok ( version) = deno_semver:: Version :: parse_from_npm ( version_text) else { 1021continue ; 1022}; 1023if !version_req. matches ( & version) { 1024continue ; 1025} 1026if best 1027. as_ref () 1028. is_some_and ( |( current, _) | current >=& version) 1029{ 1030continue ; 1031} 1032 best =Some (( version, version_text. clone ())); 1033} 1034let ( _, version_text) = best. ok_or_else ( ||NpmError :: NoNpmVersionSatisfies { 1035version_text : version_req. version_text (). to_owned (), 1036}) ?; 1037let entry =& packument. versions [ & version_text]; 1038Ok (( version_text, entry. dist . clone ())) 1039} 1040 1041/// A pinned package already satisfying a new range is reused without 1042/// consulting mutable packument metadata. 1043fn find_pinned_satisfying ( 1044lock : & super :: Lockfile , 1045registry : & Url , 1046name : & str , 1047version_req : & deno_semver:: VersionReq , 1048) ->Option <( String , NpmPin )> { 1049if version_req. tag (). is_some () || version_req. version_text () =="*" { 1050return None ; 1051} 1052let mut best: Option <( deno_semver:: Version , String )> =None ; 1053for keyin lock. npm . packages . keys () { 1054let Ok (( key_registry, key_name, key_version)) =parse_npm_key ( key) else { 1055continue ; 1056}; 1057if key_registry !=* registry || key_name != name{ 1058continue ; 1059} 1060let Ok ( version) = deno_semver:: Version :: parse_from_npm ( & key_version) else { 1061continue ; 1062}; 1063if !version_req. matches ( & version) { 1064continue ; 1065} 1066if best 1067. as_ref () 1068. is_some_and ( |( current, _) | current >=& version) 1069{ 1070continue ; 1071} 1072 best =Some (( version, key_version)); 1073} 1074 best. and_then ( |( _, version) |{ 1075let key =format! ( "{}|{name}|{version}" , registry. as_str ()); 1076 lock. npm 1077. packages 1078. get ( & key) 1079. map ( |pin|( version, pin. clone ())) 1080}) 1081} 1082 1083struct BoundedTarReader < R > { 1084inner : R , 1085remaining : usize , 1086} 1087 1088impl < R : std:: io:: Read > std:: io:: Read for BoundedTarReader < R > { 1089fn read ( & mut self , buf : & mut [ u8 ]) -> std:: io:: Result < usize > { 1090if self . remaining ==0 { 1091return Err ( std:: io:: Error :: new ( 1092 std:: io:: ErrorKind :: FileTooLarge , 1093"npm tarball expands to too many bytes" , 1094)); 1095} 1096let capped = buf. len (). min ( self . remaining ); 1097let read =self . inner . read ( & mut buf[ ..capped]) ?; 1098self . remaining -= read; 1099Ok ( read) 1100} 1101} 1102 1103fn collect_npm_files ( tarball : & [ u8 ]) ->Result < BTreeMap < String , Vec < u8 >>, NpmError > { 1104use flate2:: read:: GzDecoder ; 1105let decoder =GzDecoder :: new ( tarball); 1106let bounded =BoundedTarReader { 1107inner : decoder, 1108remaining : MAX_NPM_TAR_STREAM , 1109}; 1110let mut archive = tar:: Archive :: new ( bounded); 1111let mut files: BTreeMap < String , Vec < u8 >> =BTreeMap :: new (); 1112let mut root: Option < String > =None ; 1113let mut entries =0usize ; 1114let mut expanded: usize =0 ; 1115let entries_iter = archive. entries (). map_err ( |_|NpmError :: InvalidNpmTarball ) ?; 1116for entryin entries_iter{ 1117let entry = entry. map_err ( |_|NpmError :: InvalidNpmTarball ) ?; 1118 entries +=1 ; 1119if entries >MAX_NPM_FILES { 1120return Err ( NpmError :: NpmTarballTooManyEntries ); 1121} 1122let kind = entry. header (). entry_type (); 1123if kind. is_dir () { 1124let path = entry. path (). map_err ( |_|NpmError :: InvalidNpmTarballPath ) ?; 1125npm_relpath_from_tar ( & path, true , & mut root) ?; 1126continue ; 1127} 1128if !kind. is_file () { 1129return Err ( NpmError :: NpmTarballMustContainRegularFiles ); 1130} 1131let path = entry. path (). map_err ( |_|NpmError :: InvalidNpmTarballPath ) ?; 1132let stripped =npm_relpath_from_tar ( & path, false , & mut root) ?; 1133let mut bytes =Vec :: new (); 1134 entry 1135. take ( MAX_NPM_FILE_BYTES as u64 +1 ) 1136. read_to_end ( & mut bytes) 1137. map_err ( |_|NpmError :: InvalidNpmTarballEntry ) ?; 1138if bytes. len () >MAX_NPM_FILE_BYTES { 1139return Err ( NpmError :: NpmFileTooLarge { path : stripped. to_owned () }); 1140} 1141 expanded = expanded 1142. checked_add ( bytes. len ()) 1143. ok_or ( NpmError :: NpmTarballTooLargeBare ) ?; 1144if expanded >MAX_NPM_TARBALL_EXPANDED { 1145return Err ( NpmError :: NpmTarballExpandsTooMuch ); 1146} 1147// Some published archives repeat the same file under a `./` alias. 1148if let Some ( previous) = files. get ( & stripped) { 1149if * previous != bytes{ 1150return Err ( NpmError :: NpmTarballDuplicate { path : stripped. to_owned () }); 1151} 1152continue ; 1153} 1154if files. len () >=MAX_NPM_FILES { 1155return Err ( NpmError :: NpmTarballTooManyFiles ); 1156} 1157 files. insert ( stripped, bytes); 1158} 1159if files. is_empty () { 1160return Err ( NpmError :: NpmTarballEmpty ); 1161} 1162Ok ( files) 1163} 1164 1165fn bind_npm_files ( 1166data : & BTreeMap < String , Vec < u8 >>, 1167name : & str , 1168version : & str , 1169) ->Result <( BTreeMap < String , String >, serde_json:: Value ), NpmError > { 1170let package_bytes = data 1171. get ( "package.json" ) 1172. ok_or ( NpmError :: NpmTarballMissingPackageJson ) ?; 1173let package: serde_json:: Value = serde_json:: from_slice ( package_bytes) 1174. map_err ( |source|NpmError :: InvalidNpmPackageJson { source : Arc :: new ( source) }) ?; 1175let published_name = package 1176. get ( "name" ) 1177. and_then ( |value| value. as_str ()) 1178. ok_or ( NpmError :: NpmPackageJsonMissingName ) ?; 1179let published_version = package 1180. get ( "version" ) 1181. and_then ( |value| value. as_str ()) 1182. ok_or ( NpmError :: NpmPackageJsonMissingVersion ) ?; 1183if published_name != name || published_version != version{ 1184return Err ( NpmError :: NpmPackageJsonIdentityMismatch ); 1185} 1186let mut hashes =BTreeMap :: new (); 1187for ( path, bytes) in data{ 1188 hashes. insert ( path. clone (), checksum ( bytes)); 1189} 1190Ok (( hashes, package)) 1191} 1192 1193pub ( super ) fn check_npm_url ( 1194authority : Option < & crate :: authority:: SourceAuthority >, 1195url : & Url , 1196) ->Result <(), NpmError > { 1197Ok ( Access :: Module ( url. clone ()). check ( authority) ?) 1198} 1199 1200fn check_npm_exec ( 1201authority : Option < & crate :: authority:: SourceAuthority >, 1202registry : & Url , 1203name : & str , 1204version : & str , 1205files : & BTreeMap < String , String >, 1206) ->Result <(), NpmError > { 1207let version_base =npm_version_base ( registry, name, version) ?; 1208if Access :: Module ( version_base). check ( authority). is_ok () { 1209return Ok (()); 1210} 1211for pathin files. keys () { 1212let url =npm_virtual_module ( registry, name, version, path) ?; 1213if Access :: Module ( url). check ( authority). is_ok () { 1214return Ok (()); 1215} 1216} 1217let version_base =npm_version_base ( registry, name, version) ?; 1218Ok ( Access :: Module ( version_base). check ( authority) ?) 1219} 1220 1221/// Network and policy context for one registry request. Owned, because 1222/// spawned workers carry their own copy rather than borrowing the store. 1223# [ derive ( Clone )] 1224struct Fetcher { 1225options : Options , 1226client : Client < HttpsConnector < HttpConnector >, Empty < Bytes >>, 1227authority : Option < crate :: authority:: SourceAuthority >, 1228progress : Option < super :: ProgressSink >, 1229registry : Url , 1230} 1231 1232/// Packument dedupe and download admission shared by every acquisition. 1233# [ derive ( Clone )] 1234struct Workers { 1235packuments : Arc < Mutex < BTreeMap < String , SharedPackument >>>, 1236downloads : Arc < tokio:: sync:: Semaphore >, 1237runtime : tokio:: runtime:: Handle , 1238} 1239 1240impl Fetcher { 1241fn archive < ' a >( 1242&' a self , 1243name : &' a str , 1244version : &' a str , 1245tarball : &' a str , 1246integrity : &' a str , 1247) ->NpmArchive < ' a > { 1248NpmArchive { 1249registry : & self . registry , 1250 name, 1251 version, 1252 tarball, 1253 integrity, 1254} 1255} 1256 1257async fn fetch_bytes ( 1258& self , 1259mut url : Url , 1260accept : &' static str , 1261max_bytes : usize , 1262what : & str , 1263access : Access , 1264) ->Result < Vec < u8 >, NpmError > { 1265let authority =& self . authority ; 1266let registry =& self . registry ; 1267let client =& self . client ; 1268let download =async { 1269 access. check ( authority. as_ref ()) ?; 1270check_https ( & url) ?; 1271if !same_origin ( & url, registry) { 1272return Err ( NpmError :: NpmOriginOutsideRegistry { url : url. to_string () }); 1273} 1274for _in 0 ..=10 { 1275 access. check ( authority. as_ref ()) ?; 1276let mut request_url = url. clone (); 1277 request_url. set_fragment ( None ); 1278let request =Request :: get ( request_url. as_str ()) 1279. header ( "Accept" , accept) 1280. header ( "User-Agent" , concat! ( "avril/" , env! ( "CARGO_PKG_VERSION" ))) 1281. body ( Empty :: new ()) 1282. map_err ( |detail|NpmError :: NpmHttp { detail : detail. to_string () }) ?; 1283let response = client 1284. request ( request) 1285. await 1286. map_err ( |detail|NpmError :: NpmHttp { detail : detail. to_string () }) ?; 1287 access. check ( authority. as_ref ()) ?; 1288if matches! ( response. status (). as_u16 (), 301 |302 |303 |307 |308 ) { 1289let location = response 1290. headers () 1291. get ( "location" ) 1292. and_then ( |value| value. to_str (). ok ()) 1293. ok_or_else ( ||NpmError :: NpmRedirectNoLocation { url : url. to_string () }) ?; 1294let next = url. join ( location) ?; 1295if !next. username (). is_empty () || next. password (). is_some () { 1296return Err ( NpmError :: NpmRedirectCredentials { next : next. to_string () }); 1297} 1298check_https ( & next) ?; 1299if !same_origin ( & next, registry) { 1300return Err ( NpmError :: NpmRedirectEscapesRegistry { 1301next : next. to_string (), 1302}); 1303} 1304 url = next; 1305continue ; 1306} 1307if !response. status (). is_success () { 1308return Err ( NpmError :: NpmServerStatus { 1309status : response. status (). as_u16 (), 1310url : url. to_string (), 1311}); 1312} 1313if response 1314. headers () 1315. get ( "content-type" ) 1316. and_then ( |value| value. to_str (). ok ()) 1317. is_some_and ( |value| value. starts_with ( "text/html" )) 1318{ 1319return Err ( NpmError :: NpmServerHtml { 1320what : what. to_owned (), 1321url : url. to_string (), 1322}); 1323} 1324let mut body = response. into_body (); 1325let mut bytes =Vec :: new (); 1326while let Some ( frame) = body. frame (). await { 1327if let Ok ( data) = frame 1328. map_err ( |detail|NpmError :: NpmHttp { detail : detail. to_string () }) ? 1329. into_data () 1330{ 1331if bytes. len () + data. len () > max_bytes{ 1332return Err ( NpmError :: NpmResponseTooLarge { 1333what : what. to_owned (), 1334url : url. to_string (), 1335}); 1336} 1337 bytes. extend_from_slice ( & data); 1338} 1339} 1340return Ok ( bytes); 1341} 1342Err ( NpmError :: NpmTooManyRedirects { url : url. to_string () }) 1343}; 1344 tokio:: time:: timeout ( NPM_FETCH_TIMEOUT , download) 1345. await 1346. map_err ( |_|NpmError :: NpmRequestTimeout { url : url. to_string () }) ? 1347} 1348 1349async fn npm_acquire_task ( 1350& self , 1351workers : & Workers , 1352lock_snapshot : super :: Lockfile , 1353instance : String , 1354request : NpmRequest , 1355verification : NpmVerification , 1356) ->Result < NpmArtifact , NpmError > { 1357let options =& self . options ; 1358let authority =& self . authority ; 1359let npm_registry =& self . registry ; 1360let request_key = request. request_key (); 1361let name = request. name ; 1362let version_req = request. version_req ; 1363let package_base =npm_package_base ( npm_registry, & name) ?; 1364Access :: Package ( package_base. clone ()). check ( authority. as_ref ()) ?; 1365// One permit per executing worker; waiter clones only await the shared 1366// result. Metadata fetching takes no permit, so holding one across the 1367// shared packument wait cannot deadlock. Recheck caps after admission 1368// for revocation while queued; later validation rechecks versions. 1369let _download_permit = workers 1370. downloads 1371. clone () 1372. acquire_owned () 1373. await 1374. map_err ( |_|NpmError :: NpmDownloadWorkerStopped ) ?; 1375Access :: Package ( package_base. clone ()). check ( authority. as_ref ()) ?; 1376if let Some ( version_text) = lock_snapshot. npm . specifiers . get ( & request_key) { 1377let version =validate_npm_version ( version_text) ?; 1378if !locked_satisfies ( version_req. clone (), & version) { 1379return Err ( NpmError :: NpmLockedVersionUnsatisfied { 1380version : version_text. clone (), 1381specifier : request_key. clone (), 1382}); 1383} 1384let identity_key =format! ( "{}|{name}|{version_text}" , npm_registry. as_str ()); 1385if let Some ( pin) = lock_snapshot. npm . packages . get ( & identity_key) { 1386return self 1387. pinned_npm ( & instance, & name, version_text, pin, verification) 1388. await ; 1389} 1390} else if !options. update 1391 && !options. frozen 1392 &&let Some (( version_text, pin)) = 1393find_pinned_satisfying ( & lock_snapshot, npm_registry, & name, & version_req) 1394{ 1395return self 1396. pinned_npm ( & instance, & name, & version_text, & pin, verification) 1397. await ; 1398} 1399if options. frozen { 1400return Err ( NpmError :: FrozenLockfileMissing { name : request_key. clone () }); 1401} 1402let packument =self . shared_packument ( & name, workers). await ?; 1403let ( version_text, dist) =select_npm_version ( & packument, & version_req) ?; 1404validate_npm_version ( & version_text) ?; 1405Access :: Package ( npm_version_base ( npm_registry, & name, & version_text) ?) 1406. check ( authority. as_ref ()) ?; 1407let tarball_url =npm_tarball_url ( & dist. tarball ) ?; 1408if !same_origin ( & tarball_url, npm_registry) { 1409return Err ( NpmError :: NpmTarballEscapesRegistry { tarball : dist. tarball . clone () }); 1410} 1411check_https ( & tarball_url) ?; 1412Access :: Package ( package_base. clone ()). check ( authority. as_ref ()) ?; 1413let integrity =pin_integrity ( & dist) ?; 1414if options. offline { 1415let cached = 1416 storage:: read_tarball_cache ( options, & dist. tarball , MAX_NPM_TARBALL_COMPRESSED ) ? 1417. ok_or_else ( ||NpmError :: NpmOfflineCacheMiss { name : request_key. clone () }) ?; 1418verify_sri ( & cached, & integrity) ?; 1419return self 1420. spawn_materialize ( 1421& instance, 1422self . archive ( & name, & version_text, & dist. tarball , & integrity), 1423 cached, 1424None , 1425) 1426. await ; 1427} 1428let bytes =self 1429. fetch_npm_tarball ( & name, & version_text, & dist. tarball , & integrity) 1430. await ?; 1431self . spawn_materialize ( 1432& instance, 1433self . archive ( & name, & version_text, & dist. tarball , & integrity), 1434 bytes, 1435None , 1436) 1437. await 1438} 1439 1440/// Run the blocking stage-publish-verify sequence off the async workers. 1441fn spawn_materialize ( 1442& self , 1443instance : & str , 1444archive : NpmArchive < ' _ >, 1445bytes : Vec < u8 >, 1446expected : Option < NpmPin >, 1447) ->impl std:: future:: Future < Output =Result < NpmArtifact , NpmError >> { 1448let options =self . options . clone (); 1449let authority =self . authority . clone (); 1450let progress =self . progress . clone (); 1451let instance = instance. to_owned (); 1452let registry = archive. registry . clone (); 1453let name = archive. name . to_owned (); 1454let version = archive. version . to_owned (); 1455let tarball = archive. tarball . to_owned (); 1456let integrity = archive. integrity . to_owned (); 1457async move { 1458let access =Access :: Package ( npm_package_base ( & registry, & name) ?); 1459 access. check ( authority. as_ref ()) ?; 1460if let Some ( sink) =& progress{ 1461sink ( & super :: ProgressEvent :: NpmMaterializing { 1462name : name. clone (), 1463version : version. clone (), 1464}); 1465 access. check ( authority. as_ref ()) ?; 1466} 1467let artifact = tokio:: task:: spawn_blocking ( move ||{ 1468 storage:: materialize_npm ( 1469& options, 1470& authority, 1471NpmArchive { 1472registry : & registry, 1473name : & name, 1474version : & version, 1475tarball : & tarball, 1476integrity : & integrity, 1477}, 1478& instance, 1479 bytes, 1480 expected. as_ref (), 1481) 1482}) 1483. await 1484. map_err ( |detail|NpmError :: NpmHttp { detail : detail. to_string () }) ??; 1485if let Some ( sink) =& progress{ 1486sink ( & super :: ProgressEvent :: NpmMaterialized { 1487name : artifact. identity . name . clone (), 1488version : artifact. identity . version . clone (), 1489}); 1490} 1491Ok ( artifact) 1492} 1493} 1494 1495/// Authenticated file map behind a pin: legacy inline maps directly, 1496/// compact pins through the project sidecar, the vendor mirror sidecar, 1497/// or an SRI-verified tarball. Recovery republishes the sidecar; a warm 1498/// tree alone never authenticates. 1499async fn resolve_pin_files ( 1500& self , 1501name : & str , 1502version : & str , 1503pin : & NpmPin , 1504) ->Result < BTreeMap < String , String >, NpmError > { 1505let options =& self . options ; 1506if let Some ( files) =& pin. files { 1507return Ok ( files. clone ()); 1508} 1509let manifest = pin 1510. manifest 1511. as_ref () 1512. ok_or ( NpmError :: NpmPinMissingManifest ) ?; 1513if let Some ( map) = storage:: load_manifest_map ( options, manifest) ?{ 1514return Ok ( map); 1515} 1516if let Some ( map) = storage:: load_vendor_map ( options, manifest) ?{ 1517 storage:: ensure_manifest_sidecar ( options, & map) ?; 1518return Ok ( map); 1519} 1520let bytes = 1521match storage:: read_tarball_cache ( options, & pin. tarball , MAX_NPM_TARBALL_COMPRESSED ) ?{ 1522Some ( bytes) =>{ 1523verify_sri ( & bytes, & pin. integrity ) ?; 1524 bytes 1525} 1526None if options. offline =>{ 1527return Err ( NpmError :: NpmOfflineCacheMiss { 1528name : format! ( "{name}@{version}" ), 1529}); 1530} 1531None =>{ 1532self . fetch_npm_tarball ( name, version, & pin. tarball , & pin. integrity ) 1533. await ? 1534} 1535}; 1536let data =collect_npm_files ( & bytes) ?; 1537let ( derived, _) =bind_npm_files ( & data, name, version) ?; 1538if storage:: manifest_digest ( & derived) ? !=* manifest{ 1539return Err ( NpmError :: NpmArchiveChangedFor { 1540name : name. to_owned (), 1541version : version. to_owned (), 1542}); 1543} 1544 storage:: ensure_manifest_sidecar ( options, & derived) ?; 1545Ok ( derived) 1546} 1547 1548async fn pinned_npm ( 1549& self , 1550instance : & str , 1551name : & str , 1552version : & str , 1553pin : & NpmPin , 1554verification : NpmVerification , 1555) ->Result < NpmArtifact , NpmError > { 1556let options =& self . options ; 1557let authority =& self . authority ; 1558let npm_registry =& self . registry ; 1559validate_npm_version ( version) ?; 1560validate_npm_pin ( pin) ?; 1561let package_base =npm_package_base ( npm_registry, name) ?; 1562let tarball_url =npm_tarball_url ( & pin. tarball ) ?; 1563if !same_origin ( & tarball_url, npm_registry) { 1564return Err ( NpmError :: NpmTarballEscapesRegistry { tarball : pin. tarball . clone () }); 1565} 1566check_https ( & tarball_url) ?; 1567Access :: Package ( package_base). check ( authority. as_ref ()) ?; 1568let files =self . resolve_pin_files ( name, version, pin). await ?; 1569check_npm_exec ( authority. as_ref (), npm_registry, name, version, & files) ?; 1570let dir = storage:: npm_package_dir ( options, instance, name, version); 1571let package =match verification{ 1572NpmVerification :: Full =>{ 1573match storage:: npm_dir_state ( & options. project_root , & dir, & files) ?{ 1574 storage:: NpmDirState :: Verified ( package) =>Some ( package), 1575 storage:: NpmDirState :: Absent =>None , 1576} 1577} 1578NpmVerification :: OnUse =>{ 1579 storage:: verify_npm_metadata ( & options. project_root , & dir, & files) ? 1580} 1581}; 1582if let Some ( package) = package{ 1583if pin. is_legacy () && !options. frozen { 1584 storage:: ensure_manifest_sidecar ( options, & files) ?; 1585} 1586return Ok ( NpmArtifact { 1587identity : NpmIdentity { 1588registry : npm_registry. clone (), 1589name : name. to_owned (), 1590version : version. to_owned (), 1591}, 1592tarball : pin. tarball . clone (), 1593integrity : pin. integrity . clone (), 1594 dir, 1595 files, 1596 package, 1597}); 1598} 1599if let Some ( seeded) = storage:: seed_npm_from_vendor ( 1600 options, 1601 npm_registry, 1602 instance, 1603 name, 1604 version, 1605 pin, 1606& files, 1607) ?{ 1608return Ok ( seeded); 1609} 1610let cached = 1611 storage:: read_tarball_cache ( options, & pin. tarball , MAX_NPM_TARBALL_COMPRESSED ) ?; 1612if let Some ( bytes) = cached{ 1613verify_sri ( & bytes, & pin. integrity ) ?; 1614return self 1615. spawn_materialize ( 1616 instance, 1617self . archive ( name, version, & pin. tarball , & pin. integrity ), 1618 bytes, 1619Some ( pin. clone ()), 1620) 1621. await ; 1622} 1623if options. offline { 1624return Err ( NpmError :: NpmOfflineCacheMiss { 1625name : format! ( "{name}@{version}" ), 1626}); 1627} 1628let bytes =self 1629. fetch_npm_tarball ( name, version, & pin. tarball , & pin. integrity ) 1630. await ?; 1631self . spawn_materialize ( 1632 instance, 1633self . archive ( name, version, & pin. tarball , & pin. integrity ), 1634 bytes, 1635Some ( pin. clone ()), 1636) 1637. await 1638} 1639 1640/// One packument per package shared by selection and acquisition. Completed 1641/// successes stay for this store; malformed fetches evict so retries refetch. 1642async fn shared_packument ( 1643& self , 1644name : & str , 1645workers : & Workers , 1646) ->Result < Arc < NpmPackument >, NpmError > { 1647use futures:: FutureExt ; 1648let name = name. to_owned (); 1649let future ={ 1650let mut guard = workers. packuments . lock (). unwrap (); 1651if let Some ( current) = guard. get ( & name) { 1652 current. clone () 1653} else { 1654let fetcher =self . clone (); 1655let task_name = name. clone (); 1656let ( send, receive) = futures:: channel:: oneshot:: channel (); 1657 workers. runtime . spawn ( async move { 1658let result =async { 1659let bytes = fetcher. fetch_npm_packument ( & task_name). await ?; 1660let packument: NpmPackument = 1661 serde_json:: from_slice ( & bytes). map_err ( |error|{ 1662NpmError :: InvalidNpmPackument { 1663name : task_name. clone (), 1664source : Arc :: new ( error), 1665} 1666}) ?; 1667if packument. name != task_name{ 1668return Err ( NpmError :: NpmPackumentNameMismatch { 1669name : task_name. clone (), 1670}); 1671} 1672Ok ( Arc :: new ( packument)) 1673} 1674. await ; 1675let _ = send. send ( result); 1676}); 1677let shared: SharedPackument =async move { 1678 receive 1679. await 1680. map_err ( |_|NpmError :: NpmMetadataWorkerStopped ) ? 1681} 1682. boxed () 1683. shared (); 1684 guard. insert ( name. clone (), shared. clone ()); 1685 shared 1686} 1687}; 1688let result = future. clone (). await ; 1689if result. is_err () { 1690let mut guard = workers. packuments . lock (). unwrap (); 1691if guard 1692. get ( & name) 1693. is_some_and ( |current| current. ptr_eq ( & future)) 1694{ 1695 guard. remove ( & name); 1696} 1697} 1698 result 1699} 1700 1701async fn fetch_npm_packument ( & self , name : & str ) ->Result < Vec < u8 >, NpmError > { 1702let options =& self . options ; 1703let npm_registry =& self . registry ; 1704let base =npm_package_base ( npm_registry, name) ?; 1705Access :: Package ( base. clone ()). check ( self . authority . as_ref ()) ?; 1706if options. offline { 1707return storage:: read_packument_cache ( options, name, MAX_NPM_PACKUMENT_BYTES ) ? 1708. ok_or ( NpmError :: NpmOfflineMetadataMiss { name : name. to_owned () }); 1709} 1710if let Some ( sink) =& self . progress { 1711sink ( & super :: ProgressEvent :: NpmMetadata { name : name. to_owned () }); 1712} 1713let url =packument_url ( npm_registry, name) ?; 1714let bytes =self 1715. fetch_bytes ( 1716 url, 1717"application/vnd.npm.install-v1+json" , 1718MAX_NPM_PACKUMENT_BYTES , 1719"metadata" , 1720Access :: Package ( base), 1721) 1722. await ?; 1723 serde_json:: from_slice ::< serde_json:: Value >( & bytes). map_err ( |source|{ 1724NpmError :: InvalidNpmPackument { 1725name : name. to_owned (), 1726source : Arc :: new ( source), 1727} 1728}) ?; 1729 storage:: write_packument_cache ( options, name, & bytes) ?; 1730Ok ( bytes) 1731} 1732 1733async fn fetch_npm_tarball ( 1734& self , 1735name : & str , 1736version : & str , 1737tarball : & str , 1738expected : & str , 1739) ->Result < Vec < u8 >, NpmError > { 1740let options =& self . options ; 1741let npm_registry =& self . registry ; 1742let url =npm_tarball_url ( tarball) ?; 1743if !same_origin ( & url, npm_registry) { 1744return Err ( NpmError :: NpmTarballEscapesRegistry { tarball : tarball. to_owned () }); 1745} 1746check_https ( & url) ?; 1747let access =Access :: Package ( npm_package_base ( npm_registry, name) ?); 1748 access. check ( self . authority . as_ref ()) ?; 1749if let Some ( bytes) = 1750 storage:: read_tarball_cache ( options, tarball, MAX_NPM_TARBALL_COMPRESSED ) ? 1751{ 1752verify_sri ( & bytes, expected) ?; 1753return Ok ( bytes); 1754} 1755if let Some ( sink) =& self . progress { 1756sink ( & super :: ProgressEvent :: NpmArchive { 1757name : name. to_owned (), 1758version : version. to_owned (), 1759}); 1760} 1761let bytes =self 1762. fetch_bytes ( 1763 url, 1764"application/octet-stream" , 1765MAX_NPM_TARBALL_COMPRESSED , 1766"tarball" , 1767 access, 1768) 1769. await ?; 1770verify_sri ( & bytes, expected) ?; 1771 storage:: write_tarball_cache ( options, tarball, & bytes) ?; 1772Ok ( bytes) 1773} 1774} 1775 1776impl PackageStore { 1777fn fetcher ( & self ) ->Fetcher { 1778Fetcher { 1779options : self . options . clone (), 1780client : self . client . clone (), 1781authority : self . authority . clone (), 1782progress : self . progress . clone (), 1783registry : self . options . npm_registry . clone (), 1784} 1785} 1786 1787fn workers ( & self ) ->Workers { 1788Workers { 1789packuments : self . npm_packuments . clone (), 1790downloads : self . npm_downloads . clone (), 1791runtime : self . runtime . clone (), 1792} 1793} 1794 1795/// Select and lock automatic DefinitelyTyped discovery for one resolved package. 1796pub async fn npm_types_request ( 1797& self , 1798identity : & NpmIdentity , 1799discover : bool , 1800) ->Result < Option < String >, NpmError > { 1801let key = identity. key (); 1802if identity. registry !=self . options . npm_registry { 1803return Err ( NpmError :: InvalidNpmTypesAssociation { identity : key}); 1804} 1805if identity. name . starts_with ( "@types/" ) { 1806return Ok ( None ); 1807} 1808let replay = ||{ 1809let lock =self . lock . lock (). unwrap (); 1810let mut used =self . used . lock (). unwrap (); 1811if ( !self . options . update || used. npm_types_discovered . contains ( & key)) 1812 &&let Some ( record) = lock. npm . types . get ( & key) 1813{ 1814 used. npm_types . insert ( key. clone ()); 1815Some ( record. clone ()) 1816} else { 1817None 1818} 1819}; 1820if let Some ( record) =replay () { 1821return Ok ( record); 1822} 1823if !discover ||self . options . frozen { 1824return Err ( NpmError :: NpmTypesNotPrepared { identity : key}); 1825} 1826let _gate =self . npm_types_gate . lock (). await ; 1827if let Some ( record) =replay () { 1828return Ok ( record); 1829} 1830 1831let request = package:: types_package_name ( & identity. name ); 1832let library_version =validate_npm_version ( & identity. version ) ?; 1833let major = library_version. major ; 1834let next_major = major 1835. checked_add ( 1 ) 1836. ok_or_else ( ||NpmError :: InvalidNpmVersion { 1837version : identity. version . clone (), 1838}) ?; 1839let range =format! ( ">={major}.0.0 <{next_major}.0.0" ); 1840let type_request =format! ( "npm:{request}@{range}" ); 1841let result =match self . npm_select_version ( & type_request). await { 1842Ok ( _) =>Some ( parse_npm_request ( & type_request) ?. request_key ()), 1843Err ( NpmError :: NpmServerStatus { status : 404 , ..}) 1844 |Err ( NpmError :: NoNpmVersionSatisfies { ..}) =>None , 1845Err ( error) =>return Err ( error), 1846}; 1847 1848let mut lock =self . lock . lock (). unwrap (); 1849let mut used =self . used . lock (). unwrap (); 1850 used. npm_types . insert ( key. clone ()); 1851 used. npm_types_discovered . insert ( key. clone ()); 1852if lock. npm . types . get ( & key) !=Some ( & result) ||self . options . update { 1853 used. pending_npm_types . insert ( key. clone ()); 1854} 1855 lock. npm . types . insert ( key, result. clone ()); 1856Ok ( result) 1857} 1858 1859/// Metadata-only version selection: lock reuse or one shared packument 1860/// fetch, never archives, graphs, pins, or usage marks. 1861pub async fn npm_select_version ( & self , specifier : & str ) ->Result < String , NpmError > { 1862let request =parse_npm_request ( specifier) ?; 1863let package_base =npm_package_base ( & self . options . npm_registry , & request. name ) ?; 1864Access :: Package ( package_base. clone ()). check ( self . authority . as_ref ()) ?; 1865let lock_snapshot =self . lock . lock (). unwrap (). clone (); 1866let version_text = 1867if let Some ( locked) = lock_snapshot. npm . specifiers . get ( & request. request_key ()) { 1868let version =validate_npm_version ( locked) ?; 1869if !locked_satisfies ( request. version_req . clone (), & version) { 1870return Err ( NpmError :: NpmLockedVersionUnsatisfied { 1871version : locked. clone (), 1872specifier : request. request_key (), 1873}); 1874} 1875 locked. clone () 1876} else if !self . options . update 1877 && !self . options . frozen 1878 &&let Some (( pinned, _)) =find_pinned_satisfying ( 1879& lock_snapshot, 1880& self . options . npm_registry , 1881& request. name , 1882& request. version_req , 1883) 1884{ 1885 pinned 1886} else { 1887if self . options . frozen { 1888return Err ( NpmError :: FrozenLockfileMissing { name : request. request_key () }); 1889} 1890let packument =self 1891. fetcher () 1892. shared_packument ( & request. name , & self . workers ()) 1893. await ?; 1894select_npm_version ( & packument, & request. version_req ) ?. 0 1895}; 1896validate_npm_version ( & version_text) ?; 1897Access :: Package ( npm_version_base ( 1898& self . options . npm_registry , 1899& request. name , 1900& version_text, 1901) ?) 1902. check ( self . authority . as_ref ()) ?; 1903Access :: Package ( package_base). check ( self . authority . as_ref ()) ?; 1904Ok ( version_text) 1905} 1906 1907pub async fn npm_acquire ( & self , specifier : & str ) ->Result < NpmArtifact , NpmError > { 1908self . npm_acquire_in ( NPM_DEFAULT_INSTANCE , specifier). await 1909} 1910 1911pub async fn npm_acquire_in ( 1912& self , 1913instance : & str , 1914specifier : & str , 1915) ->Result < NpmArtifact , NpmError > { 1916self . npm_acquire_mode ( instance, specifier, NpmVerification :: Full ) 1917. await 1918} 1919 1920pub ( crate ) async fn npm_acquire_for_resolution ( 1921& self , 1922specifier : & str , 1923) ->Result < NpmArtifact , NpmError > { 1924self . npm_acquire_mode ( NPM_DEFAULT_INSTANCE , specifier, NpmVerification :: OnUse ) 1925. await 1926} 1927 1928/// Materialize an already selected package without introducing a new request 1929/// selection. In particular, frozen replay need not pin an exact-version alias. 1930pub ( crate ) async fn npm_acquire_locked ( 1931& self , 1932identity : & NpmIdentity , 1933pin : & NpmPin , 1934) ->Result < NpmArtifact , NpmError > { 1935if identity. registry !=self . options . npm_registry { 1936return Err ( NpmError :: NpmOriginOutsideRegistry { 1937url : identity. registry . to_string (), 1938}); 1939} 1940let fetcher =self . fetcher (); 1941let downloads =self . npm_downloads . clone (); 1942let identity = identity. clone (); 1943let pin = pin. clone (); 1944let artifact =self 1945. runtime 1946. spawn ( async move { 1947let _permit = downloads 1948. acquire_owned () 1949. await 1950. map_err ( |_|NpmError :: NpmDownloadWorkerStopped ) ?; 1951 fetcher 1952. pinned_npm ( 1953NPM_DEFAULT_INSTANCE , 1954& identity. name , 1955& identity. version , 1956& pin, 1957NpmVerification :: OnUse , 1958) 1959. await 1960}) 1961. await 1962. map_err ( |_|NpmError :: NpmAcquisitionWorkerStopped ) ??; 1963check_npm_exec ( 1964self . authority . as_ref (), 1965& artifact. identity . registry , 1966& artifact. identity . name , 1967& artifact. identity . version , 1968& artifact. files , 1969) ?; 1970self . finish_npm_acquire ( None , & artifact, NpmVerification :: OnUse ) 1971} 1972 1973async fn npm_acquire_mode ( 1974& self , 1975instance : & str , 1976specifier : & str , 1977verification : NpmVerification , 1978) ->Result < NpmArtifact , NpmError > { 1979use futures:: FutureExt ; 1980validate_instance ( instance) ?; 1981let request =parse_npm_request ( specifier) ?; 1982if request. subpath . is_some () { 1983return Err ( NpmError :: NpmSubpathNeedsAcquire ); 1984} 1985let request_key = request. request_key (); 1986let package_base =npm_package_base ( & self . options . npm_registry , & request. name ) ?; 1987Access :: Package ( package_base. clone ()). check ( self . authority . as_ref ()) ?; 1988let key =format! ( "{instance}\0{request_key}\0{verification:?}" ); 1989let future: NpmAcquisition ={ 1990let mut inflight =self . npm_inflight . lock (). unwrap (); 1991 inflight 1992. entry ( key. clone ()) 1993. or_insert_with ( ||{ 1994let fetcher =self . fetcher (); 1995let workers =self . workers (); 1996let lock_snapshot =self . lock . lock (). unwrap (). clone (); 1997let instance = instance. to_owned (); 1998let request = request. clone (); 1999let ( send, receive) = futures:: channel:: oneshot:: channel (); 2000self . runtime . spawn ( async move { 2001let result = fetcher 2002. npm_acquire_task ( 2003& workers, 2004 lock_snapshot, 2005 instance, 2006 request, 2007 verification, 2008) 2009. await 2010. map ( Arc :: new); 2011let _ = send. send ( result); 2012}); 2013async move { 2014 receive 2015. await 2016. map_err ( |_|NpmError :: NpmAcquisitionWorkerStopped ) ? 2017} 2018. boxed () 2019. shared () 2020}) 2021. clone () 2022}; 2023let result = future. clone (). await ; 2024{ 2025let mut inflight =self . npm_inflight . lock (). unwrap (); 2026if inflight 2027. get ( & key) 2028. is_some_and ( |current| current. ptr_eq ( & future)) 2029{ 2030 inflight. remove ( & key); 2031} 2032} 2033let artifact = result?; 2034Access :: Package ( package_base). check ( self . authority . as_ref ()) ?; 2035check_npm_exec ( 2036self . authority . as_ref (), 2037& artifact. identity . registry , 2038& artifact. identity . name , 2039& artifact. identity . version , 2040& artifact. files , 2041) ?; 2042self . finish_npm_acquire ( Some ( & request_key), & artifact, verification) 2043} 2044 2045fn finish_npm_acquire ( 2046& self , 2047request_key : Option < & str >, 2048artifact : & NpmArtifact , 2049verification : NpmVerification , 2050) ->Result < NpmArtifact , NpmError > { 2051if verification ==NpmVerification :: Full { 2052 storage:: verify_and_load_npm_dir ( 2053& self . options . project_root , 2054& artifact. dir , 2055& artifact. files , 2056) ?; 2057} else { 2058 storage:: verify_npm_metadata ( 2059& self . options . project_root , 2060& artifact. dir , 2061& artifact. files , 2062) ? 2063. ok_or ( storage:: StorageError :: NpmManifestMissing ) ?; 2064} 2065let mut lock =self . lock . lock (). unwrap (); 2066let mut used =self . used . lock (). unwrap (); 2067let identity_key = artifact. identity . key (); 2068if let Some ( request_key) = request_key 2069 &&let Some ( existing) = lock. npm . specifiers . get ( request_key) 2070 && existing !=& artifact. identity . version 2071{ 2072return Err ( NpmError :: NpmConflictingVersionSelections { 2073request_key : request_key. to_owned (), 2074}); 2075} 2076let compact =NpmPin { 2077tarball : artifact. tarball . clone (), 2078integrity : artifact. integrity . clone (), 2079manifest : Some ( storage:: manifest_digest ( & artifact. files ) ?), 2080files : None , 2081}; 2082if let Some ( existing) = lock. npm . packages . get ( & identity_key) 2083 && !existing. same_content ( & compact) 2084{ 2085return Err ( NpmError :: LockedNpmPackageChanged { key : identity_key. clone () }); 2086} 2087// A warm frozen legacy hit needs no sidecar; keep its inline map. 2088let pin =if self . options . frozen { 2089 lock. npm 2090. packages 2091. get ( & identity_key) 2092. cloned () 2093. unwrap_or ( compact) 2094} else { 2095 compact 2096}; 2097 used. npm_packages . insert ( identity_key. clone ()); 2098if let Some ( request_key) = request_key{ 2099 used. npm_specifiers . insert ( request_key. to_owned ()); 2100if lock. npm . specifiers . get ( request_key) !=Some ( & artifact. identity . version ) 2101 ||self . options . update 2102{ 2103 used. pending_npm_specifiers . insert ( request_key. to_owned ()); 2104} 2105 lock. npm 2106. specifiers 2107. insert ( request_key. to_owned (), artifact. identity . version . clone ()); 2108} 2109if lock. npm . packages . get ( & identity_key) !=Some ( & pin) { 2110 used. pending_npm_packages . insert ( identity_key. clone ()); 2111} 2112 lock. npm . packages . insert ( identity_key, pin); 2113Ok (( * artifact). clone ()) 2114} 2115}