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