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}