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
11.6 KiB305 linesraw
1use std::io::Read;
2use std::path::Path;
3
4use crate::config::ValueKind;
5use serde_fjson::{Document, PathSegment};
6
7/// What `add` writes: an alias in `imports`, or a whole `jsr:` package in `deps`, bound
8/// to its package name unless `name` renames it.
9#[derive(Debug, Clone, Copy)]
10pub enum Addition<'a> {
11    Import {
12        alias: &'a str,
13        target: &'a str,
14    },
15    Dependency {
16        name: Option<&'a str>,
17        package: &'a str,
18    },
19}
20
21impl Addition<'_> {
22    /// The specifier, or namespace of specifiers, that the addition binds.
23    pub fn binding(&self) -> Result<String, String> {
24        match *self {
25            Self::Import { alias, .. } => Ok(alias.to_owned()),
26            Self::Dependency { name: Some(name), .. } => Ok(name.to_owned()),
27            Self::Dependency { name: None, package } => package_name(package),
28        }
29    }
30
31    pub fn target(&self) -> &str {
32        match *self {
33            Self::Import { target, .. } => target,
34            Self::Dependency { package, .. } => package,
35        }
36    }
37}
38
39fn package_name(target: &str) -> Result<String, String> {
40    if target.starts_with("npm:") {
41        return crate::registry::npm::parse_npm_request(target)
42            .map(|request| request.name)
43            .map_err(|error| error.to_string());
44    }
45    crate::registry::parse_jsr_request(target)
46        .map(|request| request.name)
47        .map_err(|error| error.to_string())
48}
49
50/// Whether a registry target names a whole package, without any subpath or prefix.
51pub fn selects_whole_package(target: &str) -> Result<bool, String> {
52    if target.starts_with("npm:") {
53        let request =
54            crate::registry::npm::parse_npm_request(target).map_err(|error| error.to_string())?;
55        return Ok(request.subpath.is_none() && !target.ends_with('/'));
56    }
57    let request = crate::registry::parse_jsr_request(target).map_err(|error| error.to_string())?;
58    Ok(request.subpath.is_none() && !target.ends_with('/'))
59}
60
61/// The whole package a registry target selects from, keeping its version requirement.
62pub fn whole_package(target: &str) -> Result<String, String> {
63    if target.starts_with("npm:") {
64        let request =
65            crate::registry::npm::parse_npm_request(target).map_err(|error| error.to_string())?;
66        return Ok(request.request_key());
67    }
68    let request = crate::registry::parse_jsr_request(target).map_err(|error| error.to_string())?;
69    let name = request.name.clone();
70    let version = after_name(target, &name)
71        .strip_prefix('@')
72        .and_then(|rest| rest.split('/').next())
73        .map_or(String::new(), |version| format!("@{version}"));
74    Ok(format!("jsr:{name}{version}"))
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum Change {
79    Added,
80    /// The alias previously mapped elsewhere, or was blocked when `previous` is `None`.
81    Updated {
82        previous: Option<String>,
83    },
84    Unchanged,
85}
86
87/// The package a registry target names without any version, which `add` fills in.
88pub fn unversioned(target: &str) -> Result<Option<String>, String> {
89    if target.starts_with("npm:") {
90        let request =
91            crate::registry::npm::parse_npm_request(target).map_err(|error| error.to_string())?;
92        return Ok(request.range.is_none().then(|| request.name.clone()));
93    }
94    let request = crate::registry::parse_jsr_request(target).map_err(|error| error.to_string())?;
95    let name = request.name.clone();
96    Ok((!after_name(target, &name).starts_with('@')).then_some(name))
97}
98
99/// Gives a target `unversioned` named `name` a caret range from `version`, keeping any
100/// subpath, as `deno add` does.
101pub fn with_caret(target: &str, name: &str, version: &str) -> String {
102    if target.starts_with("npm:") {
103        let request = crate::registry::npm::parse_npm_request(target)
104            .map(|request| request.subpath)
105            .unwrap_or_default();
106        let subpath = request.map_or(String::new(), |path| format!("/{path}"));
107        return format!("npm:{name}@^{version}{subpath}");
108    }
109    format!("jsr:{name}@^{version}{}", after_name(target, name))
110}
111
112fn after_name<'a>(target: &'a str, name: &str) -> &'a str {
113    let rest = &target["jsr:".len()..];
114    &rest.strip_prefix('/').unwrap_or(rest)[name.len()..]
115}
116
117pub fn add(source: &str, addition: &Addition) -> Result<(String, Change), String> {
118    match *addition {
119        Addition::Import { alias, target } => {
120            if alias.is_empty() || alias.starts_with(['.', '/']) || alias.contains([':', '\\']) {
121                return Err("Expected a bare import alias".into());
122            }
123            package_name(target)?;
124            if alias.ends_with('/') != target.ends_with('/') {
125                return Err("Slash-prefix aliases and targets must both end with '/'".into());
126            }
127        }
128        Addition::Dependency { package, .. } => {
129            if !selects_whole_package(package)? {
130                return Err("Dependencies are whole packages, without subpaths".into());
131            }
132        }
133    }
134    let mut document =
135        Document::parse_object(source).map_err(|error| error.render("avril.conf", source))?;
136    let root = document.root();
137    let base = url::Url::parse(if cfg!(windows) {
138        "file:///C:/package/avril.conf"
139    } else {
140        "file:///package/avril.conf"
141    })
142    .unwrap();
143    crate::package_config::PackageConfig::from_value(root, &base)
144        .map_err(|error| error.render("avril.conf", source))?;
145    let ValueKind::Object(members) = &root.kind else {
146        unreachable!()
147    };
148    let field = |key: &str| members.iter().find(|member| member.key == key);
149    let target = addition.target();
150    let (existing, path, value) = match *addition {
151        Addition::Import { alias, .. } => {
152            let imports = field("imports");
153            let existing = imports.and_then(|imports| {
154                let ValueKind::Object(entries) = &imports.value.kind else {
155                    unreachable!()
156                };
157                entries
158                    .iter()
159                    .find(|entry| entry.key == alias)
160                    .map(|entry| (&entry.value, false))
161            });
162            if imports.is_some() {
163                (
164                    existing,
165                    vec!["imports".into(), alias.into()],
166                    serde_json::json!(target),
167                )
168            } else {
169                (
170                    None,
171                    vec!["imports".into()],
172                    serde_json::json!({(alias): target}),
173                )
174            }
175        }
176        Addition::Dependency { name, package } => {
177            let implicit = package_name(package)?;
178            // Naming a package after itself is the implicit form.
179            let name = name.filter(|name| *name != implicit);
180            let binding = name.unwrap_or(&implicit).to_owned();
181            let deps = field("deps");
182            let entries = match deps.map(|deps| &deps.value.kind) {
183                Some(ValueKind::Array(entries)) => entries.as_slice(),
184                None => &[],
185                _ => unreachable!(),
186            };
187            let existing =
188                entries
189                    .iter()
190                    .enumerate()
191                    .find_map(|(index, entry)| match &entry.kind {
192                        // A renamed replacement keeps this binding only as {from, name}.
193                        ValueKind::String(from) => ((from.starts_with("jsr:")
194                            || from.starts_with("npm:"))
195                            && package_name(from).as_deref() == Ok(binding.as_str()))
196                        .then_some((entry, name.is_some(), vec!["deps".into(), index.into()])),
197                        ValueKind::Object(fields) => {
198                            let field = |key: &str| fields.iter().find(|field| field.key == key);
199                            field("name")
200                                .is_some_and(|name| {
201                                    name.value.kind == ValueKind::String(binding.clone())
202                                })
203                                .then(|| {
204                                    field("from").map(|from| {
205                                        (
206                                            &from.value,
207                                            false,
208                                            vec!["deps".into(), index.into(), "from".into()],
209                                        )
210                                    })
211                                })
212                                .flatten()
213                        }
214                        _ => None,
215                    });
216            let entry = match name {
217                Some(name) => serde_json::json!({"from": package, "name": name}),
218                None => serde_json::json!(package),
219            };
220            if let Some((existing, whole, path)) = existing {
221                let value = if whole {
222                    entry
223                } else {
224                    serde_json::json!(target)
225                };
226                (Some((existing, whole)), path, value)
227            } else if deps.is_some() {
228                (
229                    None,
230                    vec!["deps".into(), PathSegment::Index(entries.len())],
231                    entry,
232                )
233            } else {
234                (None, vec!["deps".into()], serde_json::json!([entry]))
235            }
236        }
237    };
238    let change = if let Some((existing, whole)) = existing {
239        let previous = match &existing.kind {
240            ValueKind::String(previous) => Some(previous.clone()),
241            _ => None,
242        };
243        if !whole && previous.as_deref() == Some(target) {
244            return Ok((document.into_source(), Change::Unchanged));
245        }
246        Change::Updated { previous }
247    } else {
248        Change::Added
249    };
250    document
251        .set(&path, &value)
252        .map_err(|error| error.render("avril.conf", source))?;
253    let output = document.into_source();
254    crate::package_config::PackageConfig::parse(&output, &base)
255        .map_err(|error| error.render("avril.conf", &output))?;
256    Ok((output, change))
257}
258
259pub fn add_file(path: &Path, addition: &Addition) -> Result<Change, String> {
260    let parent = path.parent().ok_or("Config needs a parent directory")?;
261    std::fs::create_dir_all(parent).map_err(|error| error.to_string())?;
262    let mut change = Change::Unchanged;
263    crate::registry::update_file(
264        path,
265        || read(path),
266        |source| {
267            let source = source.as_deref().unwrap_or_default();
268            let edited;
269            (edited, change) = add(source, addition)?;
270            Ok((edited != source).then(|| edited.into_bytes()))
271        },
272    )?;
273    Ok(change)
274}
275
276fn read(path: &Path) -> Result<Option<String>, String> {
277    let mut options = std::fs::OpenOptions::new();
278    options.read(true);
279    #[cfg(unix)]
280    {
281        use std::os::unix::fs::OpenOptionsExt;
282        options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
283    }
284    match options.open(path) {
285        Ok(file) => {
286            if !file
287                .metadata()
288                .map_err(|error| error.to_string())?
289                .is_file()
290            {
291                return Err("Config must be a regular file".into());
292            }
293            let mut source = String::new();
294            file.take(32 * 1024 * 1024 + 1)
295                .read_to_string(&mut source)
296                .map_err(|error| error.to_string())?;
297            if source.len() > 32 * 1024 * 1024 {
298                return Err("Config is too large".into());
299            }
300            Ok(Some(source))
301        }
302        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
303        Err(error) => Err(error.to_string()),
304    }
305}