pkg/
package.rs

1use std::{
2    borrow::Borrow,
3    collections::{BTreeMap, VecDeque},
4    env,
5    ffi::{OsStr, OsString},
6    fmt, fs,
7    path::PathBuf,
8};
9
10use serde::de::{value::Error as DeError, Error as DeErrorT};
11use serde_derive::{Deserialize, Serialize};
12use toml::{self, from_str, to_string};
13
14use crate::recipes::find;
15
16fn is_zero(n: &u64) -> bool {
17    *n == 0
18}
19
20#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, PartialOrd)]
21#[serde(default)]
22pub struct Package {
23    /// package name
24    pub name: PackageName,
25    /// package version
26    #[serde(skip_serializing_if = "String::is_empty")]
27    pub version: String,
28    /// platform target
29    pub target: String,
30    /// hash in pkgar head
31    #[serde(skip_serializing_if = "String::is_empty")]
32    pub blake3: String,
33    /// size of files (uncompressed)
34    #[serde(skip_serializing_if = "is_zero")]
35    pub storage_size: u64,
36    /// size of pkgar (maybe compressed)
37    #[serde(skip_serializing_if = "is_zero")]
38    pub network_size: u64,
39    /// dependencies
40    pub depends: Vec<PackageName>,
41}
42
43impl Package {
44    pub fn new(name: &PackageName) -> Result<Self, PackageError> {
45        let dir = find(name.name()).ok_or_else(|| PackageError::PackageNotFound(name.clone()))?;
46        let target = env::var("TARGET").map_err(|_| PackageError::TargetInvalid)?;
47
48        let file = dir.join("target").join(target).join("stage.toml");
49        if !file.is_file() {
50            return Err(PackageError::FileMissing(file));
51        }
52
53        let toml = fs::read_to_string(&file)
54            .map_err(|err| PackageError::Parse(DeError::custom(err), Some(file.clone())))?;
55        toml::from_str(&toml).map_err(|err| PackageError::Parse(DeError::custom(err), Some(file)))
56    }
57
58    pub fn new_recursive(
59        names: &[PackageName],
60        nonstop: bool,
61        recursion: usize,
62    ) -> Result<Vec<Self>, PackageError> {
63        if recursion == 0 {
64            return Err(PackageError::Recursion(Default::default()));
65        }
66
67        let mut packages = Vec::new();
68        let mut last_err = None;
69        for name in names {
70            let package = match Self::new(name) {
71                Ok(p) => p,
72                Err(e) => {
73                    if nonstop {
74                        last_err = Some(e);
75                        continue;
76                    } else {
77                        return Err(e);
78                    }
79                }
80            };
81
82            let dependencies = match Self::new_recursive(&package.depends, nonstop, recursion - 1) {
83                Ok(p) => p,
84                Err(mut e) => {
85                    e.append_recursion(name);
86                    if nonstop {
87                        last_err = Some(e);
88                        continue;
89                    } else {
90                        return Err(e);
91                    }
92                }
93            };
94
95            for dependency in dependencies {
96                if !packages.contains(&dependency) {
97                    packages.push(dependency);
98                }
99            }
100
101            if !packages.contains(&package) {
102                packages.push(package);
103            }
104        }
105
106        if packages.len() == 0 && last_err.is_some() {
107            return Err(last_err.unwrap());
108        }
109
110        Ok(packages)
111    }
112
113    pub fn from_toml(text: &str) -> Result<Self, toml::de::Error> {
114        from_str(text)
115    }
116
117    #[allow(dead_code)]
118    pub fn to_toml(&self) -> String {
119        // to_string *should* be safe to unwrap for this struct
120        // use error handling callbacks for this
121        to_string(self).unwrap()
122    }
123}
124
125/// A package name is valid in these formats:
126///
127/// + `recipe` A recipe on mandatory package
128/// + `recipe.pkg` A recipe on "pkg" optional package
129/// + `host:recipe` A recipe with host target on mandatory package
130/// + `host:recipe.pkg` A recipe with host target on "pkg" optional package
131#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Ord, PartialOrd, Deserialize, Serialize)]
132#[serde(into = "String")]
133#[serde(try_from = "String")]
134pub struct PackageName(String);
135
136impl PackageName {
137    pub fn new(name: impl Into<String>) -> Result<Self, PackageError> {
138        let name = name.into();
139        //TODO: are there any other characters that should be invalid?
140        if name.is_empty() {
141            return Err(PackageError::PackageNameInvalid(name));
142        }
143        let mut separators = 0;
144        let mut has_host_prefix = false;
145        for c in name.chars() {
146            if "/\0".contains(c) {
147                return Err(PackageError::PackageNameInvalid(name));
148            }
149            if c == '.' {
150                separators += 1;
151                if separators > 1 {
152                    return Err(PackageError::PackageNameInvalid(name));
153                }
154            }
155            if c == ':' {
156                if has_host_prefix {
157                    return Err(PackageError::PackageNameInvalid(name));
158                }
159                has_host_prefix = true;
160            }
161        }
162        let r = Self(name);
163        if has_host_prefix && !r.is_host() {
164            return Err(PackageError::PackageNameInvalid(r.0));
165        }
166        Ok(r)
167    }
168
169    pub fn as_str(&self) -> &str {
170        self.0.as_str()
171    }
172
173    /// Check if "host:" prefix exists
174    pub fn is_host(&self) -> bool {
175        self.0.starts_with("host:")
176    }
177
178    /// Get the name between "host:" prefix and ".pkg" suffix
179    pub fn name(&self) -> &str {
180        let mut s = self.0.as_str();
181        if self.is_host() {
182            s = &s[5..]
183        }
184        if let Some(pos) = s.find('.') {
185            s = &s[..pos]
186        }
187        s
188    }
189
190    /// Get ".pkg" suffix
191    pub fn suffix(&self) -> Option<&str> {
192        let mut s = self.0.as_str();
193        if self.is_host() {
194            s = &s[5..]
195        }
196        if let Some(pos) = s.find('.') {
197            Some(&s[pos + 1..])
198        } else {
199            None
200        }
201    }
202
203    /// Strip "host:" prefix if exists
204    pub fn without_host(&self) -> PackageName {
205        let name = if self.is_host() {
206            &self.as_str()["host:".len()..]
207        } else {
208            self.as_str()
209        };
210
211        Self(name.to_string())
212    }
213
214    /// Add "host:" prefix if not exists
215    pub fn with_host(&self) -> PackageName {
216        let name = if self.is_host() {
217            self.as_str().to_string()
218        } else {
219            format!("host:{}", self.as_str())
220        };
221
222        Self(name)
223    }
224
225    /// Add or replace suffix. Does not retain "host:" prefix
226    pub fn with_suffix(&self, suffix: Option<&str>) -> PackageName {
227        let mut name = self.name().to_string();
228        if let Some(suffix) = suffix {
229            name.push('.');
230            name.push_str(suffix);
231        }
232
233        Self(name)
234    }
235}
236
237impl From<PackageName> for String {
238    fn from(package_name: PackageName) -> Self {
239        package_name.0
240    }
241}
242
243impl TryFrom<String> for PackageName {
244    type Error = PackageError;
245    fn try_from(name: String) -> Result<Self, Self::Error> {
246        Self::new(name)
247    }
248}
249
250impl TryFrom<&str> for PackageName {
251    type Error = PackageError;
252    fn try_from(name: &str) -> Result<Self, Self::Error> {
253        Self::new(name)
254    }
255}
256
257impl TryFrom<&OsStr> for PackageName {
258    type Error = PackageError;
259    fn try_from(name: &OsStr) -> Result<Self, Self::Error> {
260        let name = name
261            .to_str()
262            .ok_or_else(|| PackageError::PackageNameInvalid(name.to_string_lossy().to_string()))?;
263        Self::new(name)
264    }
265}
266
267impl TryFrom<OsString> for PackageName {
268    type Error = PackageError;
269    fn try_from(name: OsString) -> Result<Self, Self::Error> {
270        name.as_os_str().try_into()
271    }
272}
273
274impl fmt::Display for PackageName {
275    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
276        write!(f, "{}", self.0)
277    }
278}
279
280impl Borrow<str> for PackageName {
281    fn borrow(&self) -> &str {
282        self.as_str()
283    }
284}
285
286#[derive(Debug)]
287pub struct PackageInfo {
288    pub installed: bool,
289    pub package: Package,
290}
291
292#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
293pub struct Repository {
294    pub packages: BTreeMap<String, String>,
295}
296
297impl Repository {
298    pub fn from_toml(text: &str) -> Result<Self, toml::de::Error> {
299        from_str(text)
300    }
301}
302
303/// Errors that occur while opening or parsing [`Package`]s.
304///
305/// These errors are unrecoverable but useful for reporting.
306#[derive(Debug, thiserror::Error)]
307pub enum PackageError {
308    #[error("Missing package file {0:?}")]
309    FileMissing(PathBuf),
310    #[error("Package {0:?} name invalid")]
311    PackageNameInvalid(String),
312    #[error("Package {0:?} not found")]
313    PackageNotFound(PackageName),
314    #[error("Failed parsing package: {0}; file: {1:?}")]
315    Parse(serde::de::value::Error, Option<PathBuf>),
316    #[error("Recursion limit reached while processing dependencies; tree: {0:?}")]
317    Recursion(VecDeque<PackageName>),
318    #[error("TARGET triplet env var unset or invalid")]
319    TargetInvalid,
320}
321
322impl PackageError {
323    /// Append [`PackageName`] if the error is a recursion error.
324    ///
325    /// The [`PackageError::Recursion`] variant is a stack of package names that caused
326    /// the recursion limit to be reached. This functions conditionally pushes a package
327    /// name if the error is a recursion error to make it easier to build the stack.
328    pub fn append_recursion(&mut self, name: &PackageName) {
329        if let PackageError::Recursion(ref mut packages) = self {
330            packages.push_front(name.clone());
331        }
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use std::collections::BTreeMap;
338
339    use crate::package::Repository;
340
341    use super::{Package, PackageName};
342
343    const WORKING_DEPENDS: &str = r#"
344    name = "gzdoom"
345    version = "TODO"
346    target = "x86_64-unknown-redox"
347    depends = ["gtk3", "sdl2", "zmusic"]
348    "#;
349
350    const WORKING_NO_DEPENDS: &str = r#"
351    name = "kmquake2"
352    version = "TODO"
353    target = "x86_64-unknown-redox"
354    "#;
355
356    const WORKING_EMPTY_DEPENDS: &str = r#"
357    name = "iodoom3"
358    version = "TODO"
359    target = "x86_64-unknown-redox"
360    depends = []
361    "#;
362
363    const WORKING_EMPTY_VERSION: &str = r#"
364    name = "dev-essentials"
365    target = "x86_64-unknown-redox"
366    depends = ["gcc13"]
367    "#;
368
369    const WORKING_REPOSITORY: &str = r#"
370    [packages]
371    foo = "bar"
372    "#;
373
374    const INVALID_NAME: &str = r#"
375    name = "dolphin.emu.lator"
376    version = "TODO"
377    target = "x86_64-unknown-redox"
378    depends = ["qt5"]
379    "#;
380
381    const INVALID_NAME_DEPENDS: &str = r#"
382    name = "mgba"
383    version = "TODO"
384    target = "x86_64-unknown-redox"
385    depends = ["ffmpeg:latest"]
386    "#;
387
388    #[test]
389    fn package_name_split() -> Result<(), toml::de::Error> {
390        let name1 = PackageName::new("foo").unwrap();
391        let name2 = PackageName::new("foo.bar").unwrap();
392        let name3 = PackageName::new("host:foo").unwrap();
393        let name4 = PackageName::new("host:foo.").unwrap();
394        assert_eq!(
395            (name1.name(), name1.is_host(), name1.suffix()),
396            ("foo", false, None)
397        );
398        assert_eq!(
399            (name2.name(), name2.is_host(), name2.suffix()),
400            ("foo", false, Some("bar"))
401        );
402        assert_eq!(
403            (name3.name(), name3.is_host(), name3.suffix()),
404            ("foo", true, None)
405        );
406        assert_eq!(
407            (name4.name(), name4.is_host(), name4.suffix()),
408            ("foo", true, Some(""))
409        );
410        Ok(())
411    }
412
413    #[test]
414    fn deserialize_with_depends() -> Result<(), toml::de::Error> {
415        let actual = Package::from_toml(WORKING_DEPENDS)?;
416        let expected = Package {
417            name: PackageName("gzdoom".into()),
418            version: "TODO".into(),
419            target: "x86_64-unknown-redox".into(),
420            depends: vec![
421                PackageName("gtk3".into()),
422                PackageName("sdl2".into()),
423                PackageName("zmusic".into()),
424            ],
425            ..Default::default()
426        };
427
428        assert_eq!(expected, actual);
429        Ok(())
430    }
431
432    #[test]
433    fn deserialize_no_depends() -> Result<(), toml::de::Error> {
434        let actual = Package::from_toml(WORKING_NO_DEPENDS)?;
435        let expected = Package {
436            name: PackageName("kmquake2".into()),
437            version: "TODO".into(),
438            target: "x86_64-unknown-redox".into(),
439            ..Default::default()
440        };
441
442        assert_eq!(expected, actual);
443        Ok(())
444    }
445
446    #[test]
447    fn deserialize_empty_depends() -> Result<(), toml::de::Error> {
448        let actual = Package::from_toml(WORKING_EMPTY_DEPENDS)?;
449        let expected = Package {
450            name: PackageName("iodoom3".into()),
451            version: "TODO".into(),
452            target: "x86_64-unknown-redox".into(),
453            depends: vec![],
454            ..Default::default()
455        };
456
457        assert_eq!(expected, actual);
458        Ok(())
459    }
460
461    #[test]
462    fn deserialize_empty_version() -> Result<(), toml::de::Error> {
463        let actual = Package::from_toml(WORKING_EMPTY_VERSION)?;
464        let expected = Package {
465            name: PackageName("dev-essentials".into()),
466            target: "x86_64-unknown-redox".into(),
467            depends: vec![PackageName("gcc13".into())],
468            ..Default::default()
469        };
470
471        assert_eq!(expected, actual);
472        Ok(())
473    }
474
475    #[test]
476    fn deserialize_repository() -> Result<(), toml::de::Error> {
477        let actual = Repository::from_toml(WORKING_REPOSITORY)?;
478        let expected = Repository {
479            packages: BTreeMap::from([("foo".into(), "bar".into())]),
480        };
481
482        assert_eq!(expected, actual);
483        Ok(())
484    }
485
486    #[test]
487    #[should_panic]
488    fn deserialize_with_invalid_name_fails() {
489        Package::from_toml(INVALID_NAME).unwrap();
490    }
491
492    #[test]
493    #[should_panic]
494    fn deserialize_with_invalid_dependency_name_fails() {
495        Package::from_toml(INVALID_NAME_DEPENDS).unwrap();
496    }
497
498    #[test]
499    fn roundtrip() -> Result<(), toml::de::Error> {
500        let package = Package::from_toml(WORKING_DEPENDS)?;
501        let package_roundtrip = Package::from_toml(&package.to_toml())?;
502
503        assert_eq!(package, package_roundtrip);
504        Ok(())
505    }
506}