alpm_pkginfo/
schema.rs

1//! Schemas for PKGINFO data.
2
3use std::{
4    collections::HashMap,
5    fmt::{Display, Formatter},
6    fs::File,
7    path::{Path, PathBuf},
8    str::FromStr,
9};
10
11use alpm_common::FileFormatSchema;
12use alpm_parsers::custom_ini::parser::Item;
13use alpm_types::{SchemaVersion, semver_version::Version};
14use fluent_i18n::t;
15
16use crate::Error;
17
18/// An enum tracking all available [PKGINFO] schemas.
19///
20/// The schema of a PKGINFO refers to its available fields in a specific version.
21///
22/// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub enum PackageInfoSchema {
25    /// Schema for the [PKGINFOv1] file format.
26    ///
27    /// [PKGINFOv1]: https://alpm.archlinux.page/specifications/PKGINFOv1.5.html
28    V1(SchemaVersion),
29    /// Schema for the [PKGINFOv2] file format.
30    ///
31    /// [PKGINFOv2]: https://alpm.archlinux.page/specifications/PKGINFOv2.5.html
32    V2(SchemaVersion),
33}
34
35impl FileFormatSchema for PackageInfoSchema {
36    type Err = Error;
37
38    /// Returns a reference to the inner [`SchemaVersion`].
39    fn inner(&self) -> &SchemaVersion {
40        match self {
41            PackageInfoSchema::V1(v) | PackageInfoSchema::V2(v) => v,
42        }
43    }
44
45    /// Derives a [`PackageInfoSchema`] from a PKGINFO file.
46    ///
47    /// Opens the `file` and defers to [`PackageInfoSchema::derive_from_reader`].
48    ///
49    /// # Errors
50    ///
51    /// Returns an error if
52    /// - opening `file` for reading fails
53    /// - or deriving a [`PackageInfoSchema`] from the contents of `file` fails.
54    fn derive_from_file(file: impl AsRef<Path>) -> Result<Self, Error>
55    where
56        Self: Sized,
57    {
58        let file = file.as_ref();
59        Self::derive_from_reader(File::open(file).map_err(|source| Error::IoPathError {
60            path: PathBuf::from(file),
61            context: t!("error-io-derive-schema-from-pkginfo"),
62            source,
63        })?)
64    }
65
66    /// Derives a [`PackageInfoSchema`] from PKGINFO data in a `reader`.
67    ///
68    /// Reads the `reader` to string and defers to [`PackageInfoSchema::derive_from_str`].
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if
73    /// - reading a [`String`] from `reader` fails
74    /// - or deriving a [`PackageInfoSchema`] from the contents of `reader` fails.
75    fn derive_from_reader(reader: impl std::io::Read) -> Result<Self, Error>
76    where
77        Self: Sized,
78    {
79        let mut buf = String::new();
80        let mut reader = reader;
81        reader
82            .read_to_string(&mut buf)
83            .map_err(|source| Error::IoReadError {
84                context: t!("error-io-derive-schema-from-pkginfo"),
85                source,
86            })?;
87        Self::derive_from_str(&buf)
88    }
89
90    /// Derives a [`PackageInfoSchema`] from a string slice containing PKGINFO data.
91    ///
92    /// Since the PKGINFO format does not carry any version information, this function looks for the
93    /// first `xdata` field (if any) to determine whether the input may be [PKGINFOv2].
94    /// If no `xdata` field is found, [PKGINFOv1] is assumed.
95    ///
96    /// # Examples
97    ///
98    /// ```
99    /// use alpm_common::FileFormatSchema;
100    /// use alpm_pkginfo::PackageInfoSchema;
101    /// use alpm_types::{SchemaVersion, semver_version::Version};
102    ///
103    /// # fn main() -> Result<(), alpm_pkginfo::Error> {
104    /// let pkginfo_v2 = r#"
105    /// pkgname = example
106    /// pkgbase = example
107    /// pkgver = 1:1.0.0-1
108    /// pkgdesc = A project that does something
109    /// url = https://example.org/
110    /// builddate = 1729181726
111    /// packager = John Doe <john@example.org>
112    /// size = 181849963
113    /// arch = any
114    /// xdata = pkgtype=pkg
115    /// "#;
116    /// assert_eq!(
117    ///     PackageInfoSchema::V2(SchemaVersion::new(Version::new(2, 0, 0))),
118    ///     PackageInfoSchema::derive_from_str(pkginfo_v2)?
119    /// );
120    ///
121    /// let pkginfo_v1 = r#"
122    /// pkgname = example
123    /// pkgbase = example
124    /// pkgver = 1:1.0.0-1
125    /// pkgdesc = A project that does something
126    /// url = https://example.org/
127    /// builddate = 1729181726
128    /// packager = John Doe <john@example.org>
129    /// size = 181849963
130    /// arch = any
131    /// "#;
132    /// assert_eq!(
133    ///     PackageInfoSchema::V1(SchemaVersion::new(Version::new(1, 0, 0))),
134    ///     PackageInfoSchema::derive_from_str(pkginfo_v1)?
135    /// );
136    /// # Ok(())
137    /// # }
138    /// ```
139    ///
140    /// # Errors
141    ///
142    /// Returns an error if
143    /// - the first `xdata` keyword is assigned an empty string,
144    /// - or the first `xdata` keyword does not assign "pkgtype".
145    ///
146    /// [PKGINFOv1]: https://alpm.archlinux.page/specifications/PKGINFOv1.5.html
147    /// [PKGINFOv2]: https://alpm.archlinux.page/specifications/PKGINFOv2.5.html
148    fn derive_from_str(s: &str) -> Result<PackageInfoSchema, Error> {
149        // Deserialize the file into a simple map, so we can take a look at whether there is a
150        // `xdata` string that indicates PKGINFOv2.
151        let raw: HashMap<String, Item> = alpm_parsers::custom_ini::from_str(s)?;
152        let value = match raw.get("xdata") {
153            Some(Item::Value(value)) => Some(value),
154            Some(Item::List(values)) => {
155                if !values.is_empty() {
156                    values.iter().next()
157                } else {
158                    return Err(Error::ExtraDataEmpty);
159                }
160            }
161            None => return Ok(Self::V1(SchemaVersion::new(Version::new(1, 0, 0)))),
162        };
163
164        if let Some(value) = value {
165            if value.starts_with("pkgtype") {
166                Ok(Self::V2(SchemaVersion::new(Version::new(2, 0, 0))))
167            } else {
168                Err(Error::FirstExtraDataNotPkgType)
169            }
170        } else {
171            Err(Error::ExtraDataEmpty)
172        }
173    }
174}
175
176impl Default for PackageInfoSchema {
177    /// Returns the default [`PackageInfoSchema`] variant ([`PackageInfoSchema::V2`]).
178    fn default() -> Self {
179        Self::V2(SchemaVersion::new(Version::new(2, 0, 0)))
180    }
181}
182
183impl FromStr for PackageInfoSchema {
184    type Err = Error;
185
186    /// Creates a [`PackageInfoSchema`] from string slice `s`.
187    ///
188    /// Relies on [`SchemaVersion::from_str`] to create a corresponding [`PackageInfoSchema`] from
189    /// `s`.
190    ///
191    /// # Errors
192    ///
193    /// Returns an error if
194    /// - no [`SchemaVersion`] can be created from `s`,
195    /// - or the conversion from [`SchemaVersion`] to [`PackageInfoSchema`] fails.
196    fn from_str(s: &str) -> Result<PackageInfoSchema, Self::Err> {
197        match SchemaVersion::from_str(s) {
198            Ok(version) => Self::try_from(version),
199            Err(_) => Err(Error::UnsupportedSchemaVersion(s.to_string())),
200        }
201    }
202}
203
204impl TryFrom<SchemaVersion> for PackageInfoSchema {
205    type Error = Error;
206
207    /// Converts a [`SchemaVersion`] to a [`PackageInfoSchema`].
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if the [`SchemaVersion`]'s inner [`Version`] does not provide a major
212    /// version that corresponds to a [`PackageInfoSchema`] variant.
213    fn try_from(value: SchemaVersion) -> Result<Self, Self::Error> {
214        match value.inner().major {
215            1 => Ok(PackageInfoSchema::V1(value)),
216            2 => Ok(PackageInfoSchema::V2(value)),
217            _ => Err(Error::UnsupportedSchemaVersion(value.to_string())),
218        }
219    }
220}
221
222impl Display for PackageInfoSchema {
223    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
224        write!(
225            fmt,
226            "{}",
227            match self {
228                PackageInfoSchema::V1(version) | PackageInfoSchema::V2(version) =>
229                    version.inner().major,
230            }
231        )
232    }
233}