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
7.7 KiB213 linesraw
1use url::Url;
2
3use crate::config::{self, Error, Value, ValueKind, invalid, object};
4
5#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
6pub enum ResolveError {
7    #[error("Import {0:?} is blocked by the import map")]
8    Blocked(String),
9    #[error("Import {0:?} contains an encoded path separator")]
10    EncodedSeparator(String),
11    #[error("Import {0:?} escapes its mapped prefix")]
12    EscapesPrefix(String),
13    #[error("Import subpath must be UTF-8")]
14    SubpathUtf8,
15    #[error("Cannot append a subpath to import target {0}")]
16    CannotAppend(String),
17    #[error(transparent)]
18    Url(#[from] url::ParseError),
19}
20
21type Mappings = Vec<(String, Option<String>)>;
22
23#[derive(Default, Debug)]
24pub struct ImportMap {
25    imports: Mappings,
26    scopes: Vec<(String, Mappings)>,
27}
28
29impl ImportMap {
30    pub fn parse(source: &str, base: &Url) -> Result<Self, Error> {
31        let root = config::parse(source)?;
32        let mut imports = None;
33        let mut scopes = None;
34        for member in object(&root)? {
35            match member.key.as_str() {
36                "imports" => imports = Some(&member.value),
37                "scopes" => scopes = Some(&member.value),
38                _ => {}
39            }
40        }
41        Self::from_fields(imports, scopes, base)
42    }
43
44    pub(crate) fn from_fields(
45        imports: Option<&Value>,
46        scopes: Option<&Value>,
47        base: &Url,
48    ) -> Result<Self, Error> {
49        let mut map = Self::default();
50        if let Some(imports) = imports {
51            map.imports = mappings(imports, base)?;
52        }
53        if let Some(scopes) = scopes {
54            for scope in object(scopes)? {
55                let prefix = base
56                    .join(&scope.key)
57                    .map_err(|_| invalid(scope.key_span.clone(), "Invalid scope URL"))?
58                    .to_string();
59                if map.scopes.iter().any(|(existing, _)| existing == &prefix) {
60                    return Err(invalid(
61                        scope.key_span.clone(),
62                        "Duplicate normalized scope URL",
63                    ));
64                }
65                map.scopes.push((prefix, mappings(&scope.value, base)?));
66            }
67            map.scopes.sort_by_key(|a| std::cmp::Reverse(a.0.len()));
68        }
69        Ok(map)
70    }
71
72    /// Maps each bound package name, and its subpaths, to a `jsr:` package's exports.
73    pub(crate) fn packages(packages: impl IntoIterator<Item = (String, String)>) -> Self {
74        let mut imports: Mappings = packages
75            .into_iter()
76            .flat_map(|(name, package)| {
77                let slash_name = format!("{name}/");
78                let slash_package = format!("{package}/");
79                [(name, Some(package)), (slash_name, Some(slash_package))]
80            })
81            .collect();
82        imports.sort_by_key(|a| std::cmp::Reverse(a.0.len()));
83        Self { imports, scopes: Vec::new() }
84    }
85
86    pub(crate) fn namespace_conflict(&self, name: &str) -> Option<&str> {
87        self.imports
88            .iter()
89            .chain(self.scopes.iter().flat_map(|(_, mappings)| mappings))
90            .map(|(key, _)| key.as_str())
91            .find(|key| {
92                *key == name
93                    || key
94                        .strip_prefix(name)
95                        .is_some_and(|suffix| suffix.starts_with('/'))
96                    || (key.ends_with('/') && name.starts_with(key))
97            })
98    }
99
100    pub fn resolve(&self, specifier: &str, referrer: &Url) -> Result<Option<String>, ResolveError> {
101        let key = normalize(specifier, referrer)?;
102        for (prefix, mappings) in &self.scopes {
103            if (referrer.as_str() == prefix
104                || (prefix.ends_with('/') && referrer.as_str().starts_with(prefix)))
105                && let Some(target) = apply(mappings, &key)?
106            {
107                return Ok(Some(target));
108            }
109        }
110        apply(&self.imports, &key)
111    }
112}
113
114fn normalize(specifier: &str, base: &Url) -> Result<String, ResolveError> {
115    if specifier.starts_with("./") || specifier.starts_with("../") || specifier.starts_with('/') {
116        return base
117            .join(specifier)
118            .map(String::from)
119            .map_err(ResolveError::from);
120    }
121    if let Ok(url) = Url::parse(specifier) {
122        return Ok(url.to_string());
123    }
124    Ok(specifier.to_owned())
125}
126
127fn mappings(value: &Value, base: &Url) -> Result<Vec<(String, Option<String>)>, Error> {
128    let mut mappings = Vec::new();
129    for member in object(value)? {
130        if member.key.is_empty() {
131            return Err(invalid(
132                member.key_span.clone(),
133                "Import keys must not be empty",
134            ));
135        }
136        let key = normalize(&member.key, base)
137            .map_err(|error| invalid(member.key_span.clone(), error.to_string()))?;
138        if mappings.iter().any(|(existing, _)| existing == &key) {
139            return Err(invalid(
140                member.key_span.clone(),
141                "Duplicate normalized import key",
142            ));
143        }
144        let target = match &member.value.kind {
145            ValueKind::Null => None,
146            ValueKind::String(target) => {
147                let target = normalize(target, base)
148                    .map_err(|error| invalid(member.value.span.clone(), error.to_string()))?;
149                if Url::parse(&target).is_err() {
150                    return Err(invalid(
151                        member.value.span.clone(),
152                        "Import targets must be URLs or relative paths",
153                    ));
154                }
155                if key.ends_with('/') && !target.ends_with('/') {
156                    return Err(invalid(
157                        member.value.span.clone(),
158                        "Prefix import targets must end with '/'",
159                    ));
160                }
161                Some(target)
162            }
163            _ => {
164                return Err(invalid(
165                    member.value.span.clone(),
166                    "Import targets must be strings or null",
167                ));
168            }
169        };
170        mappings.push((key, target));
171    }
172    mappings.sort_by_key(|a| std::cmp::Reverse(a.0.len()));
173    Ok(mappings)
174}
175
176fn apply(mappings: &[(String, Option<String>)], key: &str) -> Result<Option<String>, ResolveError> {
177    for (prefix, target) in mappings {
178        if key != prefix && !(prefix.ends_with('/') && key.starts_with(prefix)) {
179            continue;
180        }
181        let target = target
182            .as_ref()
183            .ok_or_else(|| ResolveError::Blocked(key.to_owned()))?;
184        if key == prefix {
185            return Ok(Some(target.clone()));
186        }
187        let suffix = &key[prefix.len()..];
188        let encoded = suffix.to_ascii_lowercase();
189        if encoded.contains("%2f") || encoded.contains("%5c") {
190            return Err(ResolveError::EncodedSeparator(key.to_owned()));
191        }
192        if target.starts_with("jsr:") || target.starts_with("npm:") {
193            let decoded = percent_encoding::percent_decode_str(suffix)
194                .decode_utf8()
195                .map_err(|_| ResolveError::SubpathUtf8)?;
196            if decoded.split('/').any(|part| matches!(part, "." | ".."))
197                || decoded.contains(['?', '#', '\\'])
198            {
199                return Err(ResolveError::EscapesPrefix(key.to_owned()));
200            }
201            return Ok(Some(format!("{target}{suffix}")));
202        }
203        let url = Url::parse(target)?;
204        let resolved = url
205            .join(suffix)
206            .map_err(|_| ResolveError::CannotAppend(target.clone()))?;
207        if !resolved.as_str().starts_with(target) {
208            return Err(ResolveError::EscapesPrefix(key.to_owned()));
209        }
210        return Ok(Some(resolved.to_string()));
211    }
212    Ok(None)
213}