char/avril

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

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

Charlotte Sommass: run cargo fmt05d50bd

main
22.8 KiB613 linesraw
1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use deno_semver::{Version, jsr::JsrPackageReqReference};
6use serde::Deserialize;
7use url::Url;
8
9use super::http::http_url;
10use super::{Access, MAX_ARTIFACT_SIZE, PackageStore, checksum, valid_checksum, verify};
11
12/// Failures resolving and fetching JSR packages.
13#[derive(Clone, Debug, thiserror::Error)]
14pub enum JsrError {
15    #[error("Not a registry module: {name}")]
16    NotRegistryModule { name: String },
17    #[error("Invalid registry module URL: {name}")]
18    InvalidRegistryModuleUrl { name: String },
19    #[error("Unsupported remote module: {name}")]
20    UnsupportedRemoteModule { name: String },
21    #[error("Invalid registry module version: {name}")]
22    InvalidRegistryModuleVersion { name: String },
23    #[error("Expected a registry module file: {name}")]
24    ExpectedRegistryModuleFile { name: String },
25    #[error("Conflicting version selections for {key}")]
26    ConflictingVersionSelections { key: String },
27    #[error("Package is not prepared for synchronous resolution: {key}")]
28    PackageNotPreparedSync { key: String },
29    #[error("Package metadata is not prepared for synchronous resolution: {key}")]
30    PackageMetadataNotPreparedSync { key: String },
31    #[error("{detail}")]
32    JsrRequest { detail: String },
33    #[error("JSR version tags are not supported; use a semver range")]
34    JsrVersionTagsUnsupported,
35    #[error("Package {key} has no export {export}")]
36    PackageMissingExport { key: String, export: String },
37    #[error("Export {export} is not in the package manifest")]
38    ExportNotInManifest { export: String },
39    #[error("Invalid locked package version")]
40    InvalidLockedPackageVersion,
41    #[error("Locked version {version} does not satisfy {key}")]
42    LockedVersionUnsatisfied { version: String, key: String },
43    #[error("Frozen lockfile is missing {name}")]
44    FrozenLockfileMissing { name: String },
45    #[error("No non-yanked version satisfies {key}")]
46    NoMatchingVersion { key: String },
47    #[error("Invalid package index {url}: {source}")]
48    InvalidPackageIndex {
49        url: String,
50        source: Arc<serde_json::Error>,
51    },
52
53    #[error("Invalid version metadata {url}: {source}")]
54    InvalidVersionMetadata {
55        url: String,
56        source: Arc<serde_json::Error>,
57    },
58    #[error("Invalid file manifest in {url}")]
59    InvalidFileManifest { url: String },
60    #[error("Module is not in the package manifest: {name}")]
61    ModuleNotInManifest { name: String },
62    #[error("Manifest size mismatch: {name}")]
63    ManifestSizeMismatch { name: String },
64    #[error("JSR packages must be scoped: {name}")]
65    JsrPackageUnscoped { name: String },
66    #[error("Invalid JSR package name: {name}")]
67    InvalidJsrPackageName { name: String },
68    #[error("Package export must be a relative file path")]
69    PackageExportNotRelative,
70    #[error("Registry module queries and fragments are not supported")]
71    RegistryModuleQueryFragment,
72    #[error("Registry module URL is not canonical")]
73    RegistryModuleNotCanonical,
74    #[error("Module escapes its registry package")]
75    ModuleEscapesRegistryPackage,
76    #[error("Invalid registry module path")]
77    InvalidRegistryModulePath,
78    #[error("Module path must be UTF-8")]
79    ModulePathNotUtf8,
80    #[error("Vendor artifact is outside the registry")]
81    VendorArtifactOutsideRegistry,
82    #[error("Vendor path must be UTF-8")]
83    VendorPathNotUtf8,
84    #[error("Invalid vendor artifact path")]
85    InvalidVendorArtifactPath,
86    #[error("Module is not UTF-8: {name}")]
87    ModuleNotUtf8 { name: String },
88    #[error(transparent)]
89    Registry(Box<super::RegistryError>),
90    #[error(transparent)]
91    Http(#[from] super::http::HttpError),
92    #[error(transparent)]
93    Caps(#[from] crate::caps::CapabilitiesError),
94    #[error(transparent)]
95    Url(#[from] url::ParseError),
96}
97
98/// A parsed `jsr:` request. Unlike `JsrPackageReqReference`, the specifier
99/// string is retained verbatim: the canonical key drops a leading `jsr:/`
100/// slash that import maps and metadata must reproduce byte-for-byte.
101#[derive(Clone, Debug, PartialEq, Eq)]
102pub struct JsrRequest {
103    pub name: String,
104    pub range: Option<String>,
105    pub version_req: deno_semver::VersionReq,
106    pub subpath: Option<String>,
107    pub specifier: String,
108}
109
110impl JsrRequest {
111    /// The canonical lock and specifier key: `jsr:{name}` or `jsr:{name}@{range}`.
112    pub fn request_key(&self) -> String {
113        match &self.range {
114            None => format!("jsr:{}", self.name),
115            Some(range) => format!("jsr:{}@{range}", self.name),
116        }
117    }
118
119    /// The normalized export name (`./sub` or `.`), mirroring deno's resolution.
120    pub fn export_name(&self) -> String {
121        deno_semver::jsr::normalized_export_name(self.subpath.as_deref()).into_owned()
122    }
123}
124
125/// Parses a `jsr:` specifier once; validation beyond the grammar
126/// (whole-package shape, version tags) happens at each call site.
127pub fn parse_jsr_request(specifier: &str) -> Result<JsrRequest, JsrError> {
128    let reference = JsrPackageReqReference::from_str(specifier)
129        .map_err(|detail| JsrError::JsrRequest { detail: detail.to_string() })?;
130    let req = reference.req();
131    let range =
132        (req.version_req.version_text() != "*").then(|| req.version_req.version_text().to_owned());
133    Ok(JsrRequest {
134        name: req.name.to_string(),
135        range,
136        version_req: req.version_req.clone(),
137        subpath: reference.sub_path().map(str::to_owned),
138        specifier: specifier.to_owned(),
139    })
140}
141
142impl From<super::RegistryError> for JsrError {
143    fn from(error: super::RegistryError) -> Self {
144        Self::Registry(Box::new(error))
145    }
146}
147
148#[derive(Deserialize)]
149pub(super) struct PackageIndex {
150    versions: BTreeMap<String, IndexVersion>,
151}
152
153#[derive(Deserialize)]
154struct IndexVersion {
155    #[serde(default)]
156    yanked: bool,
157}
158
159#[derive(Clone, Deserialize)]
160pub(super) struct ManifestEntry {
161    size: usize,
162    pub(super) checksum: String,
163}
164
165#[derive(Deserialize)]
166pub(super) struct VersionMetadata {
167    pub(super) manifest: BTreeMap<String, ManifestEntry>,
168    exports: BTreeMap<String, String>,
169    #[serde(default, rename = "moduleGraph2")]
170    graph: serde_json::Value,
171}
172
173impl PackageStore {
174    pub fn module_base(&self, name: &str) -> Result<Url, JsrError> {
175        let url =
176            Url::parse(name).map_err(|_| JsrError::NotRegistryModule { name: name.to_owned() })?;
177        if url.query().is_some()
178            || url.fragment().is_some()
179            || url.as_str() != name
180            || crate::caps::canonical_remote_url(&url)? != url
181        {
182            return Err(JsrError::InvalidRegistryModuleUrl { name: name.to_owned() });
183        }
184        let relative = name
185            .strip_prefix(self.options.jsr_registry.as_str())
186            .ok_or_else(|| JsrError::UnsupportedRemoteModule { name: name.to_owned() })?;
187        let mut parts = relative.splitn(4, '/');
188        let scope = parts.next().unwrap_or_default();
189        let package = parts.next().unwrap_or_default();
190        let version = parts.next().unwrap_or_default();
191        let path = parts.next().unwrap_or_default();
192        validate_name(&format!("{scope}/{package}"))?;
193        Version::parse_standard(version)
194            .map_err(|_| JsrError::InvalidRegistryModuleVersion { name: name.to_owned() })?;
195        if path.is_empty() {
196            return Err(JsrError::ExpectedRegistryModuleFile { name: name.to_owned() });
197        }
198        self.options
199            .jsr_registry
200            .join(&format!("{scope}/{package}/{version}/"))
201            .map_err(JsrError::from)
202    }
203
204    pub async fn resolve(&self, specifier: &str) -> Result<String, JsrError> {
205        let request = parse_jsr_request(specifier)?;
206        self.resolve_request(&request).await
207    }
208
209    pub async fn resolve_request(&self, request: &JsrRequest) -> Result<String, JsrError> {
210        self.check_package(request)?;
211        let key = request.request_key();
212        let version = self.select(request).await?;
213        let base = self
214            .options
215            .jsr_registry
216            .join(&format!("{}/{version}/", request.name))?;
217        let metadata = self.version_metadata(&base).await?;
218        let name = self.export_target(request, &base, &metadata)?;
219        let mut lock = self.lock.lock().unwrap();
220        if let Some(existing) = lock.specifiers.get(&key)
221            && existing != &version
222        {
223            return Err(JsrError::ConflictingVersionSelections { key: key.to_owned() });
224        }
225        let mut used = self.used.lock().unwrap();
226        used.specifiers.insert(key.clone());
227        if lock.specifiers.get(&key) != Some(&version) || self.options.update {
228            used.pending_specifiers.insert(key.clone());
229        }
230        lock.specifiers.insert(key, version);
231        Ok(name.to_string())
232    }
233
234    fn check_package(&self, request: &JsrRequest) -> Result<(), JsrError> {
235        validate_name(&request.name)?;
236        let package = self
237            .options
238            .jsr_registry
239            .join(&format!("{}/", request.name))?;
240        Access::Package(package).check(self.authority.as_ref())?;
241        if request.version_req.tag().is_some() {
242            return Err(JsrError::JsrVersionTagsUnsupported);
243        }
244        Ok(())
245    }
246
247    pub fn resolve_import_meta(&self, specifier: &str) -> Result<String, JsrError> {
248        self.resolve_import_meta_request(&parse_jsr_request(specifier)?)
249    }
250
251    pub fn resolve_import_meta_request(&self, request: &JsrRequest) -> Result<String, JsrError> {
252        self.check_package(request)?;
253        let key = request.request_key();
254        let version = self
255            .locked_version(request)?
256            .ok_or_else(|| JsrError::PackageNotPreparedSync { key: key.to_owned() })?;
257        let base = self
258            .options
259            .jsr_registry
260            .join(&format!("{}/{version}/", request.name))?;
261        let metadata_url = format!("{}_meta.json", base.as_str().trim_end_matches('/'));
262        let metadata = self
263            .metadata
264            .lock()
265            .unwrap()
266            .get(metadata_url.as_str())
267            .cloned()
268            .ok_or_else(|| JsrError::PackageMetadataNotPreparedSync { key: key.to_owned() })?;
269        self.export_target(request, &base, &metadata)
270            .map(String::from)
271    }
272
273    fn export_target(
274        &self,
275        request: &JsrRequest,
276        base: &Url,
277        metadata: &VersionMetadata,
278    ) -> Result<Url, JsrError> {
279        let key = request.request_key();
280        let export = request.export_name();
281        let target = metadata.exports.get(export.as_str()).ok_or_else(|| {
282            JsrError::PackageMissingExport {
283                key: key.to_owned(),
284                export: format!("{export:?}"),
285            }
286        })?;
287        let name = exported_file(base, target)?;
288        if !metadata.manifest.contains_key(&manifest_path(base, &name)?) {
289            return Err(JsrError::ExportNotInManifest { export: format!("{export:?}") });
290        }
291        Access::Module(name.clone()).check(self.authority.as_ref())?;
292        Ok(name)
293    }
294
295    fn locked_version(&self, request: &JsrRequest) -> Result<Option<String>, JsrError> {
296        let key = request.request_key();
297        let selected = self.lock.lock().unwrap().specifiers.get(&key).cloned();
298        if let Some(version) = &selected {
299            let parsed = Version::parse_standard(version)
300                .map_err(|_| JsrError::InvalidLockedPackageVersion)?;
301            if !request.version_req.matches(&parsed) {
302                return Err(JsrError::LockedVersionUnsatisfied {
303                    version: version.to_owned(),
304                    key: key.to_owned(),
305                });
306            }
307        }
308        Ok(selected)
309    }
310
311    /// The newest non-yanked version of a package, excluding prereleases unless requested.
312    pub async fn latest(&self, name: &str, prerelease: bool) -> Result<Option<String>, JsrError> {
313        validate_name(name)?;
314        Ok(self
315            .index(name)
316            .await?
317            .versions
318            .iter()
319            .filter(|(_, data)| !data.yanked)
320            .filter_map(|(version, _)| Version::parse_standard(version).ok())
321            .filter(|version| prerelease || version.pre.is_empty())
322            .max()
323            .map(|version| version.to_string()))
324    }
325
326    /// The export keys of the version a `jsr:` specifier selects.
327    pub async fn exports(&self, specifier: &str) -> Result<Vec<String>, JsrError> {
328        let request = parse_jsr_request(specifier)?;
329        validate_name(&request.name)?;
330        if request.version_req.tag().is_some() {
331            return Err(JsrError::JsrVersionTagsUnsupported);
332        }
333        let version = self.select(&request).await?;
334        let base = self
335            .options
336            .jsr_registry
337            .join(&format!("{}/{version}/", request.name))?;
338        Ok(self
339            .version_metadata(&base)
340            .await?
341            .exports
342            .keys()
343            .cloned()
344            .collect())
345    }
346
347    async fn select(&self, request: &JsrRequest) -> Result<String, JsrError> {
348        let key = request.request_key();
349        Ok(match self.locked_version(request)? {
350            Some(version) => version,
351            None => {
352                if self.options.frozen {
353                    return Err(JsrError::FrozenLockfileMissing { name: key.clone() });
354                }
355                let index = self.index(request.name.as_str()).await?;
356                index
357                    .versions
358                    .iter()
359                    .filter(|(_, data)| !data.yanked)
360                    .filter_map(|(version, _)| Version::parse_standard(version).ok())
361                    .filter(|version| request.version_req.matches(version))
362                    .max()
363                    .ok_or_else(|| JsrError::NoMatchingVersion { key: key.clone() })?
364                    .to_string()
365            }
366        })
367    }
368
369    async fn index(&self, name: &str) -> Result<Arc<PackageIndex>, JsrError> {
370        let url = self
371            .options
372            .jsr_registry
373            .join(&format!("{name}/meta.json"))?;
374        let cached = self.indexes.lock().unwrap().get(url.as_str()).cloned();
375        Ok(match cached {
376            Some(index) => index,
377            None => {
378                let package = self.options.jsr_registry.join(&format!("{name}/"))?;
379                let bytes = self
380                    .bytes(
381                        url.clone(),
382                        None,
383                        !self.options.offline,
384                        Access::Package(package),
385                    )
386                    .await?;
387                let index: PackageIndex = serde_json::from_slice(&bytes).map_err(|source| {
388                    JsrError::InvalidPackageIndex {
389                        url: url.to_string(),
390                        source: std::sync::Arc::new(source),
391                    }
392                })?;
393                let index = Arc::new(index);
394                self.indexes
395                    .lock()
396                    .unwrap()
397                    .insert(url.to_string(), index.clone());
398                index
399            }
400        })
401    }
402
403    pub(super) async fn version_metadata(
404        &self,
405        base: &Url,
406    ) -> Result<Arc<VersionMetadata>, JsrError> {
407        Access::Package(base.clone()).check(self.authority.as_ref())?;
408        let metadata_url = Url::parse(&format!(
409            "{}_meta.json",
410            base.as_str().trim_end_matches('/')
411        ))?;
412        let cached = self
413            .metadata
414            .lock()
415            .unwrap()
416            .get(metadata_url.as_str())
417            .cloned();
418        if let Some(metadata) = cached {
419            let mut used = self.used.lock().unwrap();
420            if used.packages.insert(metadata_url.to_string()) {
421                used.pending_packages.insert(metadata_url.to_string());
422            }
423            return Ok(metadata);
424        }
425        let expected = self
426            .lock
427            .lock()
428            .unwrap()
429            .packages
430            .get(metadata_url.as_str())
431            .cloned();
432        if expected.is_none() && self.options.frozen {
433            return Err(JsrError::FrozenLockfileMissing { name: metadata_url.to_string() });
434        }
435        let bytes = self
436            .bytes(
437                metadata_url.clone(),
438                expected.clone(),
439                expected.is_none() && !self.options.offline,
440                Access::Package(base.clone()),
441            )
442            .await?;
443        let metadata: VersionMetadata =
444            serde_json::from_slice(&bytes).map_err(|source| JsrError::InvalidVersionMetadata {
445                url: metadata_url.to_string(),
446                source: std::sync::Arc::new(source),
447            })?;
448        for entry in metadata.manifest.values() {
449            if entry.size > MAX_ARTIFACT_SIZE || !valid_checksum(&entry.checksum) {
450                return Err(JsrError::InvalidFileManifest { url: metadata_url.to_string() });
451            }
452        }
453        let checksum = checksum(&bytes);
454        let mut lock = self.lock.lock().unwrap();
455        if let Some(existing) = lock.packages.get(metadata_url.as_str()) {
456            verify(&bytes, existing)?;
457        }
458        let mut used = self.used.lock().unwrap();
459        used.packages.insert(metadata_url.to_string());
460        if lock.packages.get(metadata_url.as_str()) != Some(&checksum) {
461            used.pending_packages.insert(metadata_url.to_string());
462        }
463        lock.packages.insert(metadata_url.to_string(), checksum);
464        let metadata = Arc::new(metadata);
465        self.metadata
466            .lock()
467            .unwrap()
468            .insert(metadata_url.to_string(), metadata.clone());
469        Ok(metadata)
470    }
471
472    pub fn resolve_relative(&self, specifier: &str, referrer: &str) -> Result<String, JsrError> {
473        let base = self.module_base(referrer)?;
474        let url = Url::parse(referrer)?.join(specifier)?;
475        manifest_path(&base, &url)?;
476        Ok(url.to_string())
477    }
478
479    pub async fn source(&self, name: &str) -> Result<String, JsrError> {
480        let base = self.module_base(name)?;
481        let url = Url::parse(name)?;
482        Access::Module(url.clone()).check(self.authority.as_ref())?;
483        let metadata = self.version_metadata(&base).await?;
484        let path = manifest_path(&base, &url)?;
485        let entry = metadata
486            .manifest
487            .get(&path)
488            .ok_or_else(|| JsrError::ModuleNotInManifest { name: name.to_owned() })?;
489        let bytes = self
490            .bytes(
491                url.clone(),
492                Some(entry.checksum.clone()),
493                false,
494                Access::Module(url),
495            )
496            .await?;
497        if bytes.len() != entry.size {
498            return Err(JsrError::ManifestSizeMismatch { name: name.to_owned() });
499        }
500        let source = String::from_utf8(bytes)
501            .map_err(|_| JsrError::ModuleNotUtf8 { name: name.to_owned() })?;
502        self.used.lock().unwrap().sources.insert(name.to_owned());
503        Ok(source)
504    }
505
506    pub async fn hints(&self, name: &str) -> Result<Vec<String>, JsrError> {
507        if !name.starts_with(self.options.jsr_registry.as_str()) {
508            http_url(name)?;
509            return Ok(vec![]);
510        }
511        let base = self.module_base(name)?;
512        let metadata = self.version_metadata(&base).await?;
513        let url = Url::parse(name)?;
514        let path = manifest_path(&base, &url)?;
515        let Some(dependencies) = metadata
516            .graph
517            .get(&path)
518            .and_then(|info| info.get("dependencies"))
519            .and_then(|value| value.as_array())
520        else {
521            return Ok(vec![]);
522        };
523        Ok(dependencies
524            .iter()
525            .filter(|dep| {
526                dep.get("type").and_then(|value| value.as_str()) == Some("static")
527                    && dep.get("kind").and_then(|value| value.as_str()) == Some("import")
528            })
529            .filter_map(|dep| dep.get("specifier").and_then(|value| value.as_str()))
530            .filter(|specifier| specifier.starts_with("./") || specifier.starts_with("../"))
531            .filter_map(|specifier| url.join(specifier).ok())
532            .filter(|url| {
533                manifest_path(&base, url).is_ok_and(|path| metadata.manifest.contains_key(&path))
534            })
535            .filter(|url| {
536                matches!(
537                    Path::new(url.path())
538                        .extension()
539                        .and_then(|ext| ext.to_str()),
540                    Some("js" | "mjs" | "ts" | "mts")
541                ) && !url.path().ends_with(".d.ts")
542                    && !url.path().ends_with(".d.mts")
543            })
544            .take(32)
545            .map(String::from)
546            .collect())
547    }
548}
549
550pub(crate) fn validate_name(name: &str) -> Result<(), JsrError> {
551    let Some((scope, package)) = name.strip_prefix('@').and_then(|name| name.split_once('/'))
552    else {
553        return Err(JsrError::JsrPackageUnscoped { name: name.to_owned() });
554    };
555    if [scope, package].iter().any(|part| {
556        part.is_empty()
557            || !part
558                .chars()
559                .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-' || ch == '_')
560    }) {
561        return Err(JsrError::InvalidJsrPackageName { name: name.to_owned() });
562    }
563    Ok(())
564}
565
566fn exported_file(base: &Url, target: &str) -> Result<Url, JsrError> {
567    let target = target
568        .strip_prefix("./")
569        .or_else(|| target.strip_prefix('/'))
570        .ok_or(JsrError::PackageExportNotRelative)?;
571    let url = base.join(target)?;
572    manifest_path(base, &url)?;
573    Ok(url)
574}
575
576pub(super) fn manifest_path(base: &Url, url: &Url) -> Result<String, JsrError> {
577    if url.query().is_some() || url.fragment().is_some() {
578        return Err(JsrError::RegistryModuleQueryFragment);
579    }
580    if crate::caps::canonical_remote_url(url)? != *url {
581        return Err(JsrError::RegistryModuleNotCanonical);
582    }
583    let suffix = url
584        .as_str()
585        .strip_prefix(base.as_str())
586        .ok_or(JsrError::ModuleEscapesRegistryPackage)?;
587    let lower = suffix.to_ascii_lowercase();
588    if lower.contains("%2f") || lower.contains("%5c") || suffix.is_empty() {
589        return Err(JsrError::InvalidRegistryModulePath);
590    }
591    let decoded = percent_encoding::percent_decode_str(suffix)
592        .decode_utf8()
593        .map_err(|_| JsrError::ModulePathNotUtf8)?;
594    Ok(format!("/{decoded}"))
595}
596
597pub(super) fn vendor_path(registry: &Url, url: &Url) -> Result<PathBuf, JsrError> {
598    let suffix = url
599        .as_str()
600        .strip_prefix(registry.as_str())
601        .ok_or(JsrError::VendorArtifactOutsideRegistry)?;
602    let decoded = percent_encoding::percent_decode_str(suffix)
603        .decode_utf8()
604        .map_err(|_| JsrError::VendorPathNotUtf8)?;
605    let path = PathBuf::from(decoded.as_ref());
606    if path
607        .components()
608        .any(|component| !matches!(component, std::path::Component::Normal(_)))
609    {
610        return Err(JsrError::InvalidVendorArtifactPath);
611    }
612    Ok(path)
613}