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}