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}