pkg/
package.rs

1use std::{
2    borrow::Borrow,
3    collections::{HashMap, 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
16#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd)]
17pub struct Package {
18    pub name: PackageName,
19    #[serde(default, skip_serializing_if = "String::is_empty")]
20    pub version: String,
21    pub target: String,
22    //pub summary: String,
23    //pub description: String,
24    #[serde(default)]
25    pub depends: Vec<PackageName>,
26}
27
28impl Package {
29    pub fn new(name: &PackageName) -> Result<Self, PackageError> {
30        let dir = find(name.as_str()).ok_or_else(|| PackageError::PackageNotFound(name.clone()))?;
31        let target = env::var("TARGET").map_err(|_| PackageError::TargetInvalid)?;
32
33        let file = dir.join("target").join(target).join("stage.toml");
34        if !file.is_file() {
35            return Err(PackageError::FileMissing(file));
36        }
37
38        let toml = fs::read_to_string(&file)
39            .map_err(|err| PackageError::Parse(DeError::custom(err), Some(file.clone())))?;
40        toml::from_str(&toml).map_err(|err| PackageError::Parse(DeError::custom(err), Some(file)))
41    }
42
43    pub fn new_recursive(
44        names: &[PackageName],
45        recursion: usize,
46    ) -> Result<Vec<Self>, PackageError> {
47        if recursion == 0 {
48            return Err(PackageError::Recursion(Default::default()));
49        }
50
51        let mut packages = Vec::new();
52        for name in names {
53            let package = Self::new(name)?;
54
55            let dependencies =
56                Self::new_recursive(&package.depends, recursion - 1).map_err(|mut err| {
57                    err.append_recursion(name);
58                    err
59                })?;
60
61            for dependency in dependencies {
62                if !packages.contains(&dependency) {
63                    packages.push(dependency);
64                }
65            }
66
67            if !packages.contains(&package) {
68                packages.push(package);
69            }
70        }
71
72        Ok(packages)
73    }
74
75    pub fn from_toml(text: &str) -> Result<Self, toml::de::Error> {
76        from_str(text)
77    }
78
79    #[allow(dead_code)]
80    pub fn to_toml(&self) -> String {
81        // to_string *should* be safe to unwrap for this struct
82        // use error handling callbacks for this
83        to_string(self).unwrap()
84    }
85}
86
87#[derive(Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd, Deserialize, Serialize)]
88#[serde(into = "String")]
89#[serde(try_from = "String")]
90pub struct PackageName(String);
91
92impl PackageName {
93    pub fn new(name: impl Into<String>) -> Result<Self, PackageError> {
94        let name = name.into();
95        //TODO: are there any other characters that should be invalid?
96        if name.is_empty() || name.contains(['.', '/', '\0']) {
97            return Err(PackageError::PackageNameInvalid(name));
98        }
99        Ok(Self(name))
100    }
101
102    pub fn as_str(&self) -> &str {
103        self.0.as_str()
104    }
105}
106
107impl From<PackageName> for String {
108    fn from(package_name: PackageName) -> Self {
109        package_name.0
110    }
111}
112
113impl TryFrom<String> for PackageName {
114    type Error = PackageError;
115    fn try_from(name: String) -> Result<Self, Self::Error> {
116        Self::new(name)
117    }
118}
119
120impl TryFrom<&str> for PackageName {
121    type Error = PackageError;
122    fn try_from(name: &str) -> Result<Self, Self::Error> {
123        Self::new(name)
124    }
125}
126
127impl TryFrom<&OsStr> for PackageName {
128    type Error = PackageError;
129    fn try_from(name: &OsStr) -> Result<Self, Self::Error> {
130        let name = name
131            .to_str()
132            .ok_or_else(|| PackageError::PackageNameInvalid(name.to_string_lossy().to_string()))?;
133        Self::new(name)
134    }
135}
136
137impl TryFrom<OsString> for PackageName {
138    type Error = PackageError;
139    fn try_from(name: OsString) -> Result<Self, Self::Error> {
140        name.as_os_str().try_into()
141    }
142}
143
144impl fmt::Display for PackageName {
145    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
146        write!(f, "{}", self.0)
147    }
148}
149
150impl Borrow<str> for PackageName {
151    fn borrow(&self) -> &str {
152        self.as_str()
153    }
154}
155
156#[derive(Debug)]
157pub struct PackageInfo {
158    pub installed: bool,
159    pub version: String,
160    pub target: String,
161
162    pub download_size: String,
163    // pub install_size: String,
164    pub depends: Vec<PackageName>,
165}
166
167#[derive(Debug, serde::Deserialize)]
168pub struct Repository {
169    pub packages: HashMap<String, String>,
170}
171
172impl Repository {
173    pub fn from_toml(text: &str) -> Result<Self, toml::de::Error> {
174        from_str(text)
175    }
176}
177
178/// Errors that occur while opening or parsing [`Package`]s.
179///
180/// These errors are unrecoverable but useful for reporting.
181#[derive(Debug, thiserror::Error)]
182pub enum PackageError {
183    #[error("Missing package file {0:?}")]
184    FileMissing(PathBuf),
185    #[error("Package {0:?} name invalid")]
186    PackageNameInvalid(String),
187    #[error("Package {0:?} not found")]
188    PackageNotFound(PackageName),
189    #[error("Failed parsing package: {0}; file: {1:?}")]
190    Parse(serde::de::value::Error, Option<PathBuf>),
191    #[error("Recursion limit reached while processing dependencies; tree: {0:?}")]
192    Recursion(VecDeque<PackageName>),
193    #[error("TARGET triplet env var unset or invalid")]
194    TargetInvalid,
195}
196
197impl PackageError {
198    /// Append [`PackageName`] if the error is a recursion error.
199    ///
200    /// The [`PackageError::Recursion`] variant is a stack of package names that caused
201    /// the recursion limit to be reached. This functions conditionally pushes a package
202    /// name if the error is a recursion error to make it easier to build the stack.
203    pub fn append_recursion(&mut self, name: &PackageName) {
204        if let PackageError::Recursion(ref mut packages) = self {
205            packages.push_front(name.clone());
206        }
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::{Package, PackageName};
213
214    const WORKING_DEPENDS: &str = r#"
215    name = "gzdoom"
216    version = "TODO"
217    target = "x86_64-unknown-redox"
218    depends = ["gtk3", "sdl2", "zmusic"]
219    "#;
220
221    const WORKING_NO_DEPENDS: &str = r#"
222    name = "kmquake2"
223    version = "TODO"
224    target = "x86_64-unknown-redox"
225    "#;
226
227    const WORKING_EMPTY_DEPENDS: &str = r#"
228    name = "iodoom3"
229    version = "TODO"
230    target = "x86_64-unknown-redox"
231    depends = []
232    "#;
233
234    const WORKING_EMPTY_VERSION: &str = r#"
235    name = "dev-essentials"
236    target = "x86_64-unknown-redox"
237    depends = ["gcc13"]
238    "#;
239
240    const INVALID_NAME: &str = r#"
241    name = "dolphin.emulator"
242    version = "TODO"
243    target = "x86_64-unknown-redox"
244    depends = ["qt5"]
245    "#;
246
247    const INVALID_NAME_DEPENDS: &str = r#"
248    name = "mgba"
249    version = "TODO"
250    target = "x86_64-unknown-redox"
251    depends = ["ffmpeg.latest"]
252    "#;
253
254    #[test]
255    fn deserialize_with_depends() -> Result<(), toml::de::Error> {
256        let actual = Package::from_toml(WORKING_DEPENDS)?;
257        let expected = Package {
258            name: PackageName("gzdoom".into()),
259            version: "TODO".into(),
260            target: "x86_64-unknown-redox".into(),
261            depends: vec![
262                PackageName("gtk3".into()),
263                PackageName("sdl2".into()),
264                PackageName("zmusic".into()),
265            ],
266        };
267
268        assert_eq!(expected, actual);
269        Ok(())
270    }
271
272    #[test]
273    fn deserialize_no_depends() -> Result<(), toml::de::Error> {
274        let actual = Package::from_toml(WORKING_NO_DEPENDS)?;
275        let expected = Package {
276            name: PackageName("kmquake2".into()),
277            version: "TODO".into(),
278            target: "x86_64-unknown-redox".into(),
279            depends: vec![],
280        };
281
282        assert_eq!(expected, actual);
283        Ok(())
284    }
285
286    #[test]
287    fn deserialize_empty_depends() -> Result<(), toml::de::Error> {
288        let actual = Package::from_toml(WORKING_EMPTY_DEPENDS)?;
289        let expected = Package {
290            name: PackageName("iodoom3".into()),
291            version: "TODO".into(),
292            target: "x86_64-unknown-redox".into(),
293            depends: vec![],
294        };
295
296        assert_eq!(expected, actual);
297        Ok(())
298    }
299
300    #[test]
301    fn deserialize_empty_version() -> Result<(), toml::de::Error> {
302        let actual = Package::from_toml(WORKING_EMPTY_VERSION)?;
303        let expected = Package {
304            name: PackageName("dev-essentials".into()),
305            version: "".into(),
306            target: "x86_64-unknown-redox".into(),
307            depends: vec![PackageName("gcc13".into())],
308        };
309
310        assert_eq!(expected, actual);
311        Ok(())
312    }
313
314    #[test]
315    #[should_panic]
316    fn deserialize_with_invalid_name_fails() {
317        Package::from_toml(INVALID_NAME).unwrap();
318    }
319
320    #[test]
321    #[should_panic]
322    fn deserialize_with_invalid_dependency_name_fails() {
323        Package::from_toml(INVALID_NAME_DEPENDS).unwrap();
324    }
325
326    #[test]
327    fn roundtrip() -> Result<(), toml::de::Error> {
328        let package = Package::from_toml(WORKING_DEPENDS)?;
329        let package_roundtrip = Package::from_toml(&package.to_toml())?;
330
331        assert_eq!(package, package_roundtrip);
332        Ok(())
333    }
334}