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}