Skip to main content

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