alpm_repo_db/desc/
schema.rs

1//! Schema definition for the [alpm-repo-desc] file format.
2//!
3//! [alpm-repo-desc]: https://alpm.archlinux.page/specifications/alpm-repo-desc.5.html
4
5use std::{
6    fmt::{Display, Formatter},
7    fs::File,
8    path::{Path, PathBuf},
9    str::FromStr,
10};
11
12use alpm_common::FileFormatSchema;
13use alpm_types::{SchemaVersion, semver_version::Version};
14use fluent_i18n::t;
15
16use crate::Error;
17
18/// An enum describing all valid [alpm-repo-desc] schemas.
19///
20/// Each variant corresponds to a specific revision of the
21/// specification.
22///
23/// [alpm-repo-desc]: https://alpm.archlinux.page/specifications/alpm-repo-desc.5.html
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub enum RepoDescSchema {
26    /// Schema for the [alpm-repo-descv1] file format.
27    ///
28    /// [alpm-repo-descv1]: https://alpm.archlinux.page/specifications/alpm-repo-descv1.5.html
29    V1(SchemaVersion),
30    /// Schema for the [alpm-repo-descv2] file format.
31    ///
32    /// [alpm-repo-descv2]: https://alpm.archlinux.page/specifications/alpm-repo-descv2.5.html
33    V2(SchemaVersion),
34}
35
36impl FileFormatSchema for RepoDescSchema {
37    type Err = Error;
38
39    /// Returns a reference to the inner [`SchemaVersion`].
40    fn inner(&self) -> &SchemaVersion {
41        match self {
42            RepoDescSchema::V1(v) => v,
43            RepoDescSchema::V2(v) => v,
44        }
45    }
46
47    /// Derives a [`RepoDescSchema`] from an [alpm-repo-desc] file on disk.
48    ///
49    /// Opens the `file` and defers to [`RepoDescSchema::derive_from_reader`].
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if:
54    ///
55    /// - the file cannot be opened for reading,
56    /// - or deriving a [`RepoDescSchema`] from its contents fails.
57    ///
58    /// [alpm-repo-desc]: https://alpm.archlinux.page/specifications/alpm-repo-desc.5.html
59    fn derive_from_file(file: impl AsRef<Path>) -> Result<Self, Error>
60    where
61        Self: Sized,
62    {
63        let file = file.as_ref();
64        Self::derive_from_reader(File::open(file).map_err(|source| Error::IoPathError {
65            path: PathBuf::from(file),
66            context: t!("error-io-path-schema-file"),
67            source,
68        })?)
69    }
70
71    /// Derives a [`RepoDescSchema`] from [alpm-repo-desc] data in a reader.
72    ///
73    /// Reads the `reader` to a string and defers to [`RepoDescSchema::derive_from_str`].
74    ///
75    /// # Errors
76    ///
77    /// Returns an error if:
78    ///
79    /// - reading from `reader` fails,
80    /// - or deriving a [`RepoDescSchema`] from its contents fails.
81    ///
82    /// [alpm-repo-desc]: https://alpm.archlinux.page/specifications/alpm-repo-desc.5.html
83    fn derive_from_reader(reader: impl std::io::Read) -> Result<Self, Error>
84    where
85        Self: Sized,
86    {
87        let mut buf = String::new();
88        let mut reader = reader;
89        reader
90            .read_to_string(&mut buf)
91            .map_err(|source| Error::IoReadError {
92                context: t!("error-io-read-schema-data"),
93                source,
94            })?;
95        Self::derive_from_str(&buf)
96    }
97
98    /// Derives a [`RepoDescSchema`] from a string slice containing [alpm-repo-desc] data.
99    ///
100    /// The parser uses a simple heuristic:
101    ///
102    /// - v1 → `%MD5SUM%` section present
103    /// - v2 → no `%MD5SUM%` section present
104    ///
105    /// This approach avoids relying on explicit version metadata, as the package repository desc
106    /// format itself is not self-describing.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use alpm_common::FileFormatSchema;
112    /// use alpm_repo_db::desc::RepoDescSchema;
113    /// use alpm_types::{SchemaVersion, semver_version::Version};
114    ///
115    /// # fn main() -> Result<(), alpm_repo_db::Error> {
116    /// let v1_data = r#"%FILENAME%
117    /// example-meta-1.0.0-1-any.pkg.tar.zst
118    ///
119    /// %NAME%
120    /// example-meta
121    ///
122    /// %BASE%
123    /// example-meta
124    ///
125    /// %VERSION%
126    /// 1.0.0-1
127    ///
128    /// %DESC%
129    /// An example meta package
130    ///
131    /// %CSIZE%
132    /// 4634
133    ///
134    /// %ISIZE%
135    /// 0
136    ///
137    /// %MD5SUM%
138    /// d3b07384d113edec49eaa6238ad5ff00
139    ///
140    /// %SHA256SUM%
141    /// b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
142    ///
143    /// %PGPSIG%
144    /// iHUEABYKAB0WIQRizHP4hOUpV7L92IObeih9mi7GCAUCaBZuVAAKCRCbeih9mi7GCIlMAP9ws/jU4f580ZRQlTQKvUiLbAZOdcB7mQQj83hD1Nc/GwD/WIHhO1/OQkpMERejUrLo3AgVmY3b4/uGhx9XufWEbgE=
145    ///
146    /// %URL%
147    /// https://example.org/
148    ///
149    /// %LICENSE%
150    /// GPL-3.0-or-later
151    ///
152    /// %ARCH%
153    /// any
154    ///
155    /// %BUILDDATE%
156    /// 1729181726
157    ///
158    /// %PACKAGER%
159    /// Foobar McFooface <foobar@mcfooface.org>
160    ///
161    /// "#;
162    ///
163    /// assert_eq!(
164    ///     RepoDescSchema::V1(SchemaVersion::new(Version::new(1, 0, 0))),
165    ///     RepoDescSchema::derive_from_str(v1_data)?
166    /// );
167    ///
168    /// let v2_data = r#"%FILENAME%
169    /// example-meta-1.0.0-1-any.pkg.tar.zst
170    ///
171    /// %NAME%
172    /// example-meta
173    ///
174    /// %BASE%
175    /// example-meta
176    ///
177    /// %VERSION%
178    /// 1.0.0-1
179    ///
180    /// %DESC%
181    /// An example meta package
182    ///
183    /// %CSIZE%
184    /// 4634
185    ///
186    /// %ISIZE%
187    /// 0
188    ///
189    /// %SHA256SUM%
190    /// b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
191    ///
192    /// %URL%
193    /// https://example.org/
194    ///
195    /// %LICENSE%
196    /// GPL-3.0-or-later
197    ///
198    /// %ARCH%
199    /// any
200    ///
201    /// %BUILDDATE%
202    /// 1729181726
203    ///
204    /// %PACKAGER%
205    /// Foobar McFooface <foobar@mcfooface.org>
206    ///
207    /// "#;
208    ///
209    /// assert_eq!(
210    ///     RepoDescSchema::V2(SchemaVersion::new(Version::new(2, 0, 0))),
211    ///     RepoDescSchema::derive_from_str(v2_data)?
212    /// );
213    /// # Ok(())
214    /// # }
215    /// ```
216    ///
217    /// # Errors
218    ///
219    /// Returns an error only if internal conversion or string handling fails.
220    ///
221    /// [alpm-repo-desc]: https://alpm.archlinux.page/specifications/alpm-repo-desc.5.html
222    fn derive_from_str(s: &str) -> Result<RepoDescSchema, Error> {
223        // Instead of an explicit "format" key, we use a heuristic:
224        // presence of `%MD5SUM%` implies version 1.
225        if s.contains("%MD5SUM%") {
226            Ok(RepoDescSchema::V1(SchemaVersion::new(Version::new(
227                1, 0, 0,
228            ))))
229        } else {
230            Ok(RepoDescSchema::V2(SchemaVersion::new(Version::new(
231                2, 0, 0,
232            ))))
233        }
234    }
235}
236
237impl Default for RepoDescSchema {
238    /// Returns the default schema variant ([`RepoDescSchema::V2`]).
239    fn default() -> Self {
240        Self::V2(SchemaVersion::new(Version::new(2, 0, 0)))
241    }
242}
243
244impl FromStr for RepoDescSchema {
245    type Err = Error;
246
247    /// Parses a [`RepoDescSchema`] from a version string.
248    ///
249    /// # Errors
250    ///
251    /// Returns an error if:
252    ///
253    /// - the input string is not a valid version,
254    /// - or the version does not correspond to a known schema variant.
255    fn from_str(s: &str) -> Result<RepoDescSchema, Self::Err> {
256        match SchemaVersion::from_str(s) {
257            Ok(version) => Self::try_from(version),
258            Err(_) => Err(Error::UnsupportedSchemaVersion(s.to_string())),
259        }
260    }
261}
262
263impl TryFrom<SchemaVersion> for RepoDescSchema {
264    type Error = Error;
265
266    /// Converts a [`SchemaVersion`] into a corresponding [`RepoDescSchema`].
267    ///
268    /// # Errors
269    ///
270    /// Returns an error if the major version of [`SchemaVersion`] does not
271    /// correspond to a known [`RepoDescSchema`] variant.
272    fn try_from(value: SchemaVersion) -> Result<Self, Self::Error> {
273        match value.inner().major {
274            1 => Ok(RepoDescSchema::V1(value)),
275            2 => Ok(RepoDescSchema::V2(value)),
276            _ => Err(Error::UnsupportedSchemaVersion(value.to_string())),
277        }
278    }
279}
280
281impl Display for RepoDescSchema {
282    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
283        write!(
284            fmt,
285            "{}",
286            match self {
287                RepoDescSchema::V1(version) | RepoDescSchema::V2(version) => version.inner().major,
288            }
289        )
290    }
291}