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
28.2 KiB767 linesraw
1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet, HashMap};
3use std::io::Read;
4use std::path::{Path, PathBuf};
5use std::rc::Rc;
6
7use crate::authority::SourceAuthority;
8use crate::caps::PathTarget;
9use crate::package_config::{PackageConfig, Source};
10use avril_core::Error;
11
12/// Which effects a resolution step may perform. `Acquire` may bind
13/// packages, load remote configs, and prepare npm roots; `PreparedOnly`
14/// uses only already-prepared metadata and never performs I/O.
15/// Shared by `LocalPackages`, `RemotePackages`, and `ModuleGraph` so the
16/// decision tree branches on one mode type at the edges only.
17#[derive(Clone, Copy, PartialEq, Eq, Debug)]
18pub enum ResolveMode {
19    Acquire,
20    PreparedOnly,
21}
22
23impl ResolveMode {
24    /// `resolve_target` and export probing allow missing files exactly in
25    /// prepared-only metadata resolution.
26    pub fn allow_missing(self) -> bool {
27        matches!(self, Self::PreparedOnly)
28    }
29}
30
31struct Package {
32    target: PathTarget,
33    config: PackageConfig,
34    bindings: RefCell<Option<BTreeMap<String, Source>>>,
35    source: String,
36}
37
38/// Shared prefix of local classification: the owning package plus what its
39/// own `package_mapping` selects, before any binding acquisition.
40enum LocalStep {
41    Done(Resolution),
42    Export(Rc<Package>, String),
43    Bindings(Rc<Package>),
44}
45
46pub enum Resolution {
47    Specifier(String),
48    Module(String),
49    Remote(String),
50}
51
52pub(crate) enum PackageMapping {
53    Specifier(String),
54    Export(String),
55}
56
57pub(crate) fn package_mapping(
58    config: &PackageConfig,
59    specifier: &str,
60    referrer: &url::Url,
61) -> Result<Option<PackageMapping>, String> {
62    if let Some(target) = config
63        .import_map
64        .resolve(specifier, referrer)
65        .map_err(|error| error.to_string())?
66    {
67        return Ok(Some(PackageMapping::Specifier(target)));
68    }
69    if let Some(name) = &config.name
70        && let Some(key) = export_key(specifier, name)
71    {
72        return Ok(Some(PackageMapping::Export(key)));
73    }
74    Ok(config
75        .registry_map
76        .resolve(specifier, referrer)
77        .map_err(|error| error.to_string())?
78        .map(PackageMapping::Specifier))
79}
80
81pub(crate) fn select_binding(
82    bindings: Option<&BTreeMap<String, Source>>,
83    specifier: &str,
84) -> Option<(String, Source)> {
85    bindings
86        .into_iter()
87        .flatten()
88        .find_map(|(binding, source)| {
89            export_key(specifier, binding).map(|key| (key, source.clone()))
90        })
91}
92
93pub struct LocalPackages {
94    pub(crate) config_inputs: RefCell<BTreeSet<PathBuf>>,
95    authority: SourceAuthority,
96    directories: RefCell<HashMap<PathBuf, Option<Rc<Package>>>>,
97    tsconfigs: RefCell<HashMap<PathBuf, crate::project::JsxConfig>>,
98}
99
100impl LocalPackages {
101    pub fn new(authority: SourceAuthority) -> Self {
102        Self {
103            authority,
104            config_inputs: RefCell::default(),
105            directories: RefCell::new(HashMap::new()),
106            tsconfigs: RefCell::new(HashMap::new()),
107        }
108    }
109
110    fn package(&self, referrer: &str) -> Result<Option<Rc<Package>>, Error> {
111        let referrer = Path::new(referrer);
112        let mut directory = referrer
113            .parent()
114            .ok_or_else(|| Error::Module("Expected an absolute referrer path".into()))?;
115        let package = loop {
116            // The reserved `.avril/npm` tree is never a config authority,
117            // even if a published archive plants an avril.conf inside it.
118            // Other `.avril` data (editor output, gitignore) keeps the
119            // existing discovery behavior.
120            if under_npm_tree(directory) {
121                match directory.parent() {
122                    Some(parent) => {
123                        directory = parent;
124                        continue;
125                    }
126                    None => break None,
127                }
128            }
129            let target = PathTarget::resolve(directory)
130                .map_err(|_| Error::Module("Cannot discover package".into()))?;
131            if target.as_path() != directory || self.authority.check_local(&target).is_err() {
132                break None;
133            }
134            let handle = crate::sources::open_directory(directory)
135                .map_err(|_| Error::Module("Cannot discover package".into()))?;
136            if !trusted(
137                &handle
138                    .dir_metadata()
139                    .map_err(|_| Error::Module("Cannot inspect package directory".into()))?,
140            ) {
141                break None;
142            }
143            let path = directory.join("avril.conf");
144            let target = PathTarget::for_entry(&path)
145                .map_err(|_| Error::Module("Cannot discover package".into()))?;
146            // A denial must not disclose whether the config (or a symlink referent) exists.
147            if self.authority.check_local(&target).is_err() {
148                break None;
149            }
150            let package = self.load(directory, &handle, target, false)?;
151            if package.is_some() {
152                break package;
153            }
154            match directory.parent() {
155                Some(parent) => directory = parent,
156                None => break None,
157            }
158        };
159        Ok(package)
160    }
161
162    fn load(
163        &self,
164        directory: &Path,
165        handle: &cap_std::fs::Dir,
166        target: PathTarget,
167        required: bool,
168    ) -> Result<Option<Rc<Package>>, Error> {
169        if target.as_path().parent() != Some(directory) {
170            return Err(Error::Module(
171                "Config directory changed during discovery".into(),
172            ));
173        }
174        let cached = self.directories.borrow().get(directory).cloned();
175        if let Some(Some(package)) = cached {
176            self.authority
177                .check_local(&package.target)
178                .map_err(|error| Error::Module(error.to_string()))?;
179            return Ok(Some(package));
180        }
181        if matches!(cached, Some(None)) {
182            return if required {
183                Err(Error::Module(format!(
184                    "Dependency directory must contain avril.conf: {}",
185                    directory.display()
186                )))
187            } else {
188                Ok(None)
189            };
190        }
191        let path = target.as_path();
192        self.authority
193            .check_local(&target)
194            .map_err(|error| Error::Module(error.to_string()))?;
195        let package =
196            match read_config(handle, path).map_err(|error| Error::Module(error.to_string()))? {
197                Some(source) => {
198                    self.authority
199                        .check_local(&target)
200                        .map_err(|error| Error::Module(error.to_string()))?;
201                    let base = url::Url::from_file_path(path)
202                        .map_err(|_| Error::Module("Config paths must be absolute".into()))?;
203                    let render = |error: crate::config::Error| {
204                        Error::Module(error.render(&path.to_string_lossy(), &source))
205                    };
206                    let config = PackageConfig::parse(&source, &base).map_err(render)?;
207                    self.authority
208                        .check_local(&target)
209                        .map_err(|error| Error::Module(error.to_string()))?;
210                    Some(Rc::new(Package {
211                        target,
212                        config,
213                        bindings: RefCell::default(),
214                        source,
215                    }))
216                }
217                None if required => {
218                    return Err(Error::Module(format!(
219                        "Dependency directory must contain avril.conf: {}",
220                        directory.display()
221                    )));
222                }
223                None => None,
224            };
225        self.directories
226            .borrow_mut()
227            .insert(directory.to_owned(), package.clone());
228        Ok(package)
229    }
230
231    fn dependency(&self, directory: &Path, specifier: &str) -> Result<Rc<Package>, Error> {
232        let target =
233            PathTarget::resolve(directory).map_err(|error| self.path_error(specifier, error))?;
234        self.authority
235            .check_local(&target)
236            .map_err(|error| self.path_error(specifier, error))?;
237        let directory = target.as_path();
238        let handle = crate::sources::open_directory(directory)
239            .map_err(|error| self.path_error(specifier, error))?;
240        if !trusted(
241            &handle
242                .dir_metadata()
243                .map_err(|error| self.path_error(specifier, error))?,
244        ) {
245            return Err(Error::Module(format!(
246                "Untrusted dependency directory: {specifier}"
247            )));
248        }
249        let target = PathTarget::for_entry(directory.join("avril.conf"))
250            .map_err(|error| self.path_error(specifier, error))?;
251        self.authority
252            .check_local(&target)
253            .map_err(|error| self.path_error(specifier, error))?;
254        let package = self.load(directory, &handle, target, true)?.unwrap();
255        if package.config.exports.is_empty() {
256            return Err(Error::Module(format!(
257                "Dependency {specifier:?} has no exports: {}",
258                package.target.as_path().display()
259            )));
260        }
261        Ok(package)
262    }
263
264    fn path_error(&self, specifier: &str, error: impl std::fmt::Display) -> Error {
265        if self.authority.unrestricted_local() {
266            Error::Module(format!("{specifier}: {error}"))
267        } else if self.authority.is_read() {
268            Error::Module(format!("PermissionDenied: read {specifier}"))
269        } else {
270            Error::Module(format!("PermissionDenied: js-exec {specifier}"))
271        }
272    }
273
274    /// Loads the package owning `referrer`, binding its dependencies before any guest code runs.
275    pub async fn initialize(
276        &self,
277        referrer: &str,
278        remote: &crate::remote_packages::RemotePackages,
279    ) -> Result<(), Error> {
280        if let Some(package) = self.package(referrer)? {
281            self.bind(&package, remote).await?;
282        }
283        Ok(())
284    }
285
286    async fn bind(
287        &self,
288        package: &Package,
289        remote: &crate::remote_packages::RemotePackages,
290    ) -> Result<(), Error> {
291        if package.bindings.borrow().is_none() {
292            let bindings = remote.bindings(&package.config).await.map_err(|error| {
293                Error::Module(
294                    error.render(&package.target.as_path().to_string_lossy(), &package.source),
295                )
296            })?;
297            self.authority
298                .check_local(&package.target)
299                .map_err(|error| Error::Module(error.to_string()))?;
300            *package.bindings.borrow_mut() = Some(bindings);
301        }
302        Ok(())
303    }
304
305    pub fn type_override(
306        &self,
307        specifier: &str,
308        referrer: &str,
309        owner: &str,
310    ) -> Result<Option<String>, Error> {
311        let Some(package) = self.package(owner)? else {
312            return Ok(None);
313        };
314        self.authority
315            .check_local(&package.target)
316            .map_err(|error| Error::Module(error.to_string()))?;
317        self.config_inputs
318            .borrow_mut()
319            .insert(package.target.as_path().to_owned());
320        let referrer_url = url::Url::from_file_path(referrer)
321            .map_err(|_| Error::Module("Referrers must be absolute paths".into()))?;
322        let result = package
323            .config
324            .types
325            .imports
326            .resolve(specifier, &referrer_url)
327            .map_err(|error| Error::Module(error.to_string()))?;
328        self.authority
329            .check_local(&package.target)
330            .map_err(|error| Error::Module(error.to_string()))?;
331        Ok(result)
332    }
333
334    pub fn type_globals(&self, referrer: &str) -> Result<Vec<String>, Error> {
335        let Some(package) = self.package(referrer)? else {
336            return Ok(Vec::new());
337        };
338        self.authority
339            .check_local(&package.target)
340            .map_err(|error| Error::Module(error.to_string()))?;
341        self.config_inputs
342            .borrow_mut()
343            .insert(package.target.as_path().to_owned());
344        let globals = package.config.types.globals.clone();
345        self.authority
346            .check_local(&package.target)
347            .map_err(|error| Error::Module(error.to_string()))?;
348        Ok(globals)
349    }
350
351    pub fn jsx_config(&self, referrer: &str) -> Result<crate::project::JsxConfig, Error> {
352        let package = self.package(referrer)?;
353        let boundary = package
354            .as_ref()
355            .and_then(|package| package.target.as_path().parent());
356        crate::project::jsx_config(
357            &self.authority,
358            Path::new(referrer),
359            boundary,
360            &package
361                .as_ref()
362                .map(|package| package.config.compiler_options.clone())
363                .unwrap_or_default(),
364            &self.config_inputs,
365            &self.tsconfigs,
366        )
367    }
368
369    pub fn root(&self, referrer: &str) -> Result<PathBuf, Error> {
370        if self.authority.is_read() {
371            self.root_for_repl(referrer)
372        } else {
373            self.discover_root(referrer, false)
374        }
375    }
376
377    pub fn root_for_repl(&self, referrer: &str) -> Result<PathBuf, Error> {
378        let fallback = Path::new(referrer)
379            .parent()
380            .ok_or_else(|| Error::Module("Expected a REPL directory".into()))?;
381        Ok(self
382            .package(referrer)?
383            .and_then(|package| package.target.as_path().parent().map(Path::to_owned))
384            .unwrap_or_else(|| fallback.to_owned()))
385    }
386
387    /// The trusted config a REPL session belongs to, through the same
388    /// discovery and trust checks package loading applies.
389    pub fn config_for_repl(&self, referrer: &str) -> Result<Option<PathBuf>, Error> {
390        Ok(self
391            .package(referrer)?
392            .map(|package| package.target.as_path().to_owned()))
393    }
394
395    pub fn root_for_add(&self, referrer: &str) -> Result<PathBuf, Error> {
396        self.discover_root(referrer, true)
397    }
398
399    fn discover_root(&self, referrer: &str, strict: bool) -> Result<PathBuf, Error> {
400        let fallback = Path::new(referrer)
401            .parent()
402            .ok_or_else(|| Error::Module("Expected an entry directory".into()))?;
403        let mut directory = Some(fallback);
404        while let Some(path) = directory {
405            if under_npm_tree(path) {
406                directory = path.parent();
407                continue;
408            }
409            let handle = crate::sources::open_directory(path)
410                .map_err(|error| Error::Module(error.to_string()))?;
411            if !trusted(
412                &handle
413                    .dir_metadata()
414                    .map_err(|error| Error::Module(error.to_string()))?,
415            ) {
416                break;
417            }
418            match handle.symlink_metadata("avril.conf") {
419                Ok(metadata) if metadata.is_file() && trusted(&metadata) => {
420                    return Ok(path.to_owned());
421                }
422                Ok(_) if strict => {
423                    return Err(Error::Module(format!(
424                        "Untrusted config: {}",
425                        path.join("avril.conf").display()
426                    )));
427                }
428                Ok(_) => break,
429                Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
430                Err(error) => return Err(Error::Module(error.to_string())),
431            }
432            directory = path.parent();
433        }
434        Ok(fallback.to_owned())
435    }
436
437    /// The `npm:` dependency targets declared by the package owning
438    /// `referrer`, for joint graph preparation. Empty without a config.
439    pub fn npm_dependencies(&self, referrer: &str) -> Result<Vec<String>, Error> {
440        let Some(package) = self.package(referrer)? else {
441            return Ok(Vec::new());
442        };
443        self.authority
444            .check_local(&package.target)
445            .map_err(|error| Error::Module(error.to_string()))?;
446        Ok(package
447            .config
448            .dependencies
449            .iter()
450            .filter_map(|dependency| match &dependency.source {
451                Source::Npm(request) => Some(request.request_key()),
452                _ => None,
453            })
454            .collect())
455    }
456
457    pub fn remote_dependencies(&self, referrer: &str) -> Result<Vec<url::Url>, Error> {
458        let Some(package) = self.package(referrer)? else {
459            return Ok(Vec::new());
460        };
461        self.authority
462            .check_local(&package.target)
463            .map_err(|error| Error::Module(error.to_string()))?;
464        Ok(package
465            .config
466            .dependencies
467            .iter()
468            .filter_map(|dependency| match &dependency.source {
469                Source::Http(directory) => Some(directory.clone()),
470                _ => None,
471            })
472            .collect())
473    }
474
475    /// Shared synchronous prefix of both modes: the owning package and
476    /// what its own imports/scopes/registry map select. Binding lookup
477    /// and remote exports stay mode-specific below.
478    fn step(&self, specifier: &str, referrer: &str) -> Result<Option<LocalStep>, Error> {
479        let Some(package) = self.package(referrer)? else {
480            return Ok(None);
481        };
482        self.authority
483            .check_local(&package.target)
484            .map_err(|error| Error::Module(error.to_string()))?;
485        let url = url::Url::from_file_path(referrer)
486            .map_err(|_| Error::Module("Expected an absolute referrer path".into()))?;
487        match package_mapping(&package.config, specifier, &url).map_err(|error| {
488            Error::Module(format!("{}: {error}", package.target.as_path().display()))
489        })? {
490            Some(PackageMapping::Specifier(target)) => {
491                Ok(Some(LocalStep::Done(Resolution::Specifier(target))))
492            }
493            Some(PackageMapping::Export(key)) => Ok(Some(LocalStep::Export(package, key))),
494            None => Ok(Some(LocalStep::Bindings(package))),
495        }
496    }
497
498    /// Prepared bindings as-is: shared by both modes once acquisition (if
499    /// any) has run.
500    fn selected(&self, package: &Package, specifier: &str) -> Option<(String, Source)> {
501        select_binding(package.bindings.borrow().as_ref(), specifier)
502    }
503
504    pub async fn resolve(
505        &self,
506        specifier: &str,
507        referrer: &str,
508        remote: &crate::remote_packages::RemotePackages,
509    ) -> Result<Option<Resolution>, Error> {
510        let step = self.step(specifier, referrer)?;
511        match step {
512            None => Ok(None),
513            Some(LocalStep::Done(resolution)) => Ok(Some(resolution)),
514            Some(LocalStep::Export(package, key)) => self
515                .export(&package, &key, specifier, ResolveMode::Acquire)
516                .map(Some),
517            Some(LocalStep::Bindings(package)) => {
518                self.bind(&package, remote).await?;
519                match self.selected(&package, specifier) {
520                    Some((key, Source::Local(directory))) => {
521                        let target = self.dependency(&directory, specifier)?;
522                        self.export(&target, &key, specifier, ResolveMode::Acquire)
523                            .map(Some)
524                    }
525                    Some((key, Source::Http(directory))) => {
526                        remote.export(&directory, &key, specifier).await.map(Some)
527                    }
528                    _ => Ok(None),
529                }
530            }
531        }
532    }
533
534    pub fn resolve_import_meta(
535        &self,
536        specifier: &str,
537        referrer: &str,
538        remote: &crate::remote_packages::RemotePackages,
539    ) -> Result<Option<Resolution>, Error> {
540        let step = self.step(specifier, referrer)?;
541        match step {
542            None => Ok(None),
543            Some(LocalStep::Done(resolution)) => Ok(Some(resolution)),
544            Some(LocalStep::Export(package, key)) => self
545                .export(&package, &key, specifier, ResolveMode::PreparedOnly)
546                .map(Some),
547            Some(LocalStep::Bindings(package)) => match self.selected(&package, specifier) {
548                Some((key, Source::Local(directory))) => {
549                    let target = self.dependency(&directory, specifier)?;
550                    self.export(&target, &key, specifier, ResolveMode::PreparedOnly)
551                        .map(Some)
552                }
553                Some((key, Source::Http(directory))) => remote
554                    .resolve_export_meta(&directory, &key, specifier)
555                    .map(Some),
556                _ => Ok(None),
557            },
558        }
559    }
560
561    fn export(
562        &self,
563        package: &Package,
564        key: &str,
565        specifier: &str,
566        mode: ResolveMode,
567    ) -> Result<Resolution, Error> {
568        let export = package.config.exports.get(key).ok_or_else(|| {
569            Error::Module(format!(
570                "Package has no export {key:?} for {specifier:?}: {}",
571                package.target.as_path().display()
572            ))
573        })?;
574        let path = export
575            .target
576            .to_file_path()
577            .map_err(|_| Error::Module("Expected a local export".into()))?;
578        let target = if mode.allow_missing() {
579            PathTarget::for_creation(path)
580        } else {
581            PathTarget::resolve(path)
582        }
583        .map_err(|error| self.path_error(specifier, error))?;
584        self.authority
585            .check_local(&target)
586            .map_err(|error| self.path_error(specifier, error))?;
587        let directory = package.target.as_path().parent().unwrap();
588        if !target.as_path().starts_with(directory) {
589            return Err(Error::Module(
590                crate::config::Error {
591                    kind: crate::config::ErrorKind::Syntax,
592                    message: "Export target escapes its package".into(),
593                    span: export.span.clone(),
594                    previous: None,
595                }
596                .render(&package.target.as_path().to_string_lossy(), &package.source),
597            ));
598        }
599        let name = target
600            .into_path()
601            .into_os_string()
602            .into_string()
603            .map_err(|_| Error::Module("Module paths must be valid UTF-8".into()))?;
604        self.authority
605            .check_local(&package.target)
606            .map_err(|error| self.path_error(specifier, error))?;
607        Ok(Resolution::Module(name))
608    }
609
610    pub fn metadata(&self, referrer: &str) -> Result<Option<serde_json::Value>, Error> {
611        let Some(package) = self.package(referrer)? else {
612            return Ok(None);
613        };
614        self.authority
615            .check_local(&package.target)
616            .map_err(|error| Error::Module(error.to_string()))?;
617        let exports: std::collections::BTreeMap<_, _> = package
618            .config
619            .exports
620            .iter()
621            .map(|(key, export)| (key, export.target.to_file_path().unwrap()))
622            .collect();
623        let bindings = package.bindings.borrow();
624        let dependencies: BTreeMap<_, _> = bindings
625            .as_ref()
626            .into_iter()
627            .flatten()
628            .map(|(name, source)| match source {
629                Source::Local(directory) => (name, serde_json::json!(directory)),
630                Source::Jsr(request) => (name, serde_json::json!(request.specifier)),
631                Source::Npm(request) => (name, serde_json::json!(request.request_key())),
632                Source::Http(directory) => (name, serde_json::json!(directory.as_str())),
633            })
634            .collect();
635        Ok(Some(
636            serde_json::json!({"config": url::Url::from_file_path(package.target.as_path()).ok().map(String::from), "name": package.config.name, "version": package.config.version, "description": package.config.description, "exports": exports, "dependencies": dependencies}),
637        ))
638    }
639}
640
641/// Reads a dependency's declared name as host configuration: guest execution grants do not
642/// apply, but trust checks do. The config is neither bound nor cached, so later guest loads
643/// still pass through `LocalPackages::load` and its capability checks.
644/// Read mode must authorize the config itself, including diagnostic excerpts.
645pub(crate) fn declared_name(
646    directory: &Path,
647    authority: &SourceAuthority,
648) -> Result<Option<String>, String> {
649    let target = if authority.is_read() {
650        let target = PathTarget::for_entry(directory.join("avril.conf"))
651            .map_err(|error| error.to_string())?;
652        authority
653            .check_local(&target)
654            .map_err(|error| error.to_string())?;
655        Some(target)
656    } else {
657        None
658    };
659    let path = match &target {
660        Some(target) => target.as_path().to_owned(),
661        None => PathTarget::resolve(directory)
662            .map_err(|error| error.to_string())?
663            .into_path()
664            .join("avril.conf"),
665    };
666    let directory = path.parent().unwrap();
667    let handle = crate::sources::open_directory(directory).map_err(|error| error.to_string())?;
668    if !trusted(&handle.dir_metadata().map_err(|error| error.to_string())?) {
669        return Err(format!(
670            "Untrusted dependency directory: {}",
671            directory.display()
672        ));
673    }
674    let source = read_config(&handle, &path)?.ok_or_else(|| {
675        format!(
676            "Dependency directory must contain avril.conf: {}",
677            directory.display()
678        )
679    })?;
680    let base = url::Url::from_file_path(&path).map_err(|_| "Config paths must be absolute")?;
681    let parsed = PackageConfig::parse(&source, &base);
682    if let Some(target) = &target {
683        authority
684            .check_local(target)
685            .map_err(|error| error.to_string())?;
686    }
687    parsed
688        .map(|config| config.name)
689        .map_err(|error| error.render(&path.to_string_lossy(), &source))
690}
691
692fn read_config(handle: &cap_std::fs::Dir, path: &Path) -> Result<Option<String>, String> {
693    match handle.symlink_metadata("avril.conf") {
694        Ok(metadata) if metadata.is_symlink() || !metadata.is_file() => {
695            return Err(format!(
696                "Config must be a regular non-symlink file: {}",
697                path.display()
698            ));
699        }
700        Ok(metadata) if !trusted(&metadata) => {
701            return Err(format!("Untrusted config: {}", path.display()));
702        }
703        Ok(_) => {}
704        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
705        Err(_) => return Err(format!("Cannot discover config {}", path.display())),
706    }
707    let file = crate::sources::open_regular(handle, "avril.conf")
708        .map_err(|_| format!("Cannot read config {}", path.display()))?;
709    let metadata = file
710        .metadata()
711        .map_err(|_| "Cannot inspect config".to_owned())?;
712    if !trusted(&metadata) {
713        return Err(format!("Untrusted config: {}", path.display()));
714    }
715    let mut source = String::new();
716    file.take(32 * 1024 * 1024 + 1)
717        .read_to_string(&mut source)
718        .map_err(|_| format!("Cannot read config {}", path.display()))?;
719    if source.len() > 32 * 1024 * 1024 {
720        return Err("Config is too large".into());
721    }
722    Ok(Some(source))
723}
724
725pub(crate) fn trusted(metadata: &cap_std::fs::Metadata) -> bool {
726    #[cfg(unix)]
727    {
728        use cap_std::fs::MetadataExt;
729        // Root-owned installs are trusted; shared writable directories are not config authorities.
730        (metadata.uid() == 0 || metadata.uid() == unsafe { libc::geteuid() })
731            && metadata.mode() & 0o002 == 0
732    }
733    #[cfg(not(unix))]
734    {
735        let _ = metadata;
736        true
737    }
738}
739
740/// Whether `path` lies at or below a reserved `.avril/npm` tree. Only the
741/// npm instance tree is skipped during config discovery; every other
742/// `.avril` path keeps its existing behavior.
743fn under_npm_tree(path: &Path) -> bool {
744    let mut parts = path.components().filter_map(|part| match part {
745        std::path::Component::Normal(name) => name.to_str(),
746        _ => None,
747    });
748    let mut previous = parts.next();
749    for current in parts {
750        if previous == Some(".avril") && current == "npm" {
751            return true;
752        }
753        previous = Some(current);
754    }
755    false
756}
757
758pub(crate) fn export_key(specifier: &str, binding: &str) -> Option<String> {
759    if specifier == binding {
760        Some(".".into())
761    } else {
762        specifier
763            .strip_prefix(binding)
764            .and_then(|suffix| suffix.strip_prefix('/'))
765            .map(|suffix| format!("./{suffix}"))
766    }
767}