char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somexperiment: automatic types for npm packages51266f9

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