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}