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