alpm_repo_db/desc/file.rs
1//! File handling for [alpm-repo-desc] files.
2//!
3//! [alpm-repo-desc]: https://alpm.archlinux.page/specifications/alpm-repo-desc.5.html
4
5use std::{
6 fmt::Display,
7 fs::File,
8 path::{Path, PathBuf},
9 str::FromStr,
10};
11
12use alpm_common::{FileFormatSchema, MetadataFile};
13use fluent_i18n::t;
14
15use crate::{
16 Error,
17 desc::{RepoDescFileV1, RepoDescFileV2, RepoDescSchema},
18};
19
20/// A representation of the [alpm-repo-desc] file format.
21///
22/// Tracks all supported schema versions (`v1` and `v2`) of the package repository description file.
23/// Each variant corresponds to a distinct layout of the format.
24///
25/// [alpm-repo-desc]: https://alpm.archlinux.page/specifications/alpm-repo-desc.5.html
26#[derive(Clone, Debug, PartialEq, serde::Serialize)]
27#[serde(untagged)]
28pub enum RepoDescFile {
29 /// The [alpm-repo-descv1] file format.
30 ///
31 /// [alpm-repo-descv1]: https://alpm.archlinux.page/specifications/alpm-repo-descv1.5.html
32 V1(RepoDescFileV1),
33 /// The [alpm-repo-descv2] file format.
34 ///
35 /// This revision of the file format, removes %MD5SUM% and makes the %PGPSIG% section optional.
36 ///
37 /// [alpm-repo-descv2]: https://alpm.archlinux.page/specifications/alpm-repo-descv2.5.html
38 V2(RepoDescFileV2),
39}
40
41impl MetadataFile<RepoDescSchema> for RepoDescFile {
42 type Err = Error;
43
44 /// Creates a [`RepoDescFile`] from a file on disk, optionally validated using a
45 /// [`RepoDescSchema`].
46 ///
47 /// Opens the file and defers to [`RepoDescFile::from_reader_with_schema`].
48 ///
49 /// # Examples
50 ///
51 /// ```
52 /// use std::{fs::File, io::Write};
53 ///
54 /// use alpm_common::{FileFormatSchema, MetadataFile};
55 /// use alpm_repo_db::desc::{RepoDescFile, RepoDescSchema};
56 /// use alpm_types::{SchemaVersion, semver_version::Version};
57 ///
58 /// # fn main() -> testresult::TestResult {
59 /// // Prepare a file with package repository desc data (v1)
60 /// let (file, desc_data) = {
61 /// let desc_data = r#"%FILENAME%
62 /// example-meta-1.0.0-1-any.pkg.tar.zst
63 ///
64 /// %NAME%
65 /// example-meta
66 ///
67 /// %BASE%
68 /// example-meta
69 ///
70 /// %VERSION%
71 /// 1.0.0-1
72 ///
73 /// %DESC%
74 /// An example meta package
75 ///
76 /// %CSIZE%
77 /// 4634
78 ///
79 /// %ISIZE%
80 /// 0
81 ///
82 /// %MD5SUM%
83 /// d3b07384d113edec49eaa6238ad5ff00
84 ///
85 /// %SHA256SUM%
86 /// b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
87 ///
88 /// %PGPSIG%
89 /// iHUEABYKAB0WIQRizHP4hOUpV7L92IObeih9mi7GCAUCaBZuVAAKCRCbeih9mi7GCIlMAP9ws/jU4f580ZRQlTQKvUiLbAZOdcB7mQQj83hD1Nc/GwD/WIHhO1/OQkpMERejUrLo3AgVmY3b4/uGhx9XufWEbgE=
90 ///
91 /// %URL%
92 /// https://example.org/
93 ///
94 /// %LICENSE%
95 /// GPL-3.0-or-later
96 ///
97 /// %ARCH%
98 /// any
99 ///
100 /// %BUILDDATE%
101 /// 1729181726
102 ///
103 /// %PACKAGER%
104 /// Foobar McFooface <foobar@mcfooface.org>
105 ///
106 /// "#;
107 /// let file = tempfile::NamedTempFile::new()?;
108 /// let mut output = File::create(&file)?;
109 /// write!(output, "{}", desc_data)?;
110 /// (file, desc_data)
111 /// };
112 ///
113 /// let repo_desc = RepoDescFile::from_file_with_schema(
114 /// file.path(),
115 /// Some(RepoDescSchema::V1(SchemaVersion::new(Version::new(
116 /// 1, 0, 0,
117 /// )))),
118 /// )?;
119 /// assert_eq!(repo_desc.to_string(), desc_data);
120 /// # Ok(())
121 /// # }
122 /// ```
123 ///
124 /// # Errors
125 ///
126 /// Returns an error if:
127 ///
128 /// - the file cannot be opened for reading,
129 /// - the contents cannot be parsed into any known [`RepoDescFile`] variant,
130 /// - or the provided [`RepoDescSchema`] does not match the contents of the file.
131 fn from_file_with_schema(
132 file: impl AsRef<Path>,
133 schema: Option<RepoDescSchema>,
134 ) -> Result<Self, Error> {
135 let file = file.as_ref();
136 Self::from_reader_with_schema(
137 File::open(file).map_err(|source| Error::IoPathError {
138 path: PathBuf::from(file),
139 context: t!("error-io-path-open-file"),
140 source,
141 })?,
142 schema,
143 )
144 }
145
146 /// Creates a [`RepoDescFile`] from any readable stream, optionally validated using a
147 /// [`RepoDescSchema`].
148 ///
149 /// Reads the `reader` to a string buffer and defers to [`RepoDescFile::from_str_with_schema`].
150 ///
151 /// # Examples
152 ///
153 /// ```
154 /// use std::{fs::File, io::Write};
155 ///
156 /// use alpm_common::MetadataFile;
157 /// use alpm_repo_db::desc::{RepoDescFile, RepoDescSchema};
158 /// use alpm_types::{SchemaVersion, semver_version::Version};
159 ///
160 /// # fn main() -> testresult::TestResult {
161 /// // Prepare a reader with package repository desc data (v2)
162 /// let (reader, desc_data) = {
163 /// let desc_data = r#"%FILENAME%
164 /// example-meta-1.0.0-1-any.pkg.tar.zst
165 ///
166 /// %NAME%
167 /// example-meta
168 ///
169 /// %BASE%
170 /// example-meta
171 ///
172 /// %VERSION%
173 /// 1.0.0-1
174 ///
175 /// %DESC%
176 /// An example meta package
177 ///
178 /// %CSIZE%
179 /// 4634
180 ///
181 /// %ISIZE%
182 /// 0
183 ///
184 /// %SHA256SUM%
185 /// b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
186 ///
187 /// %URL%
188 /// https://example.org/
189 ///
190 /// %LICENSE%
191 /// GPL-3.0-or-later
192 ///
193 /// %ARCH%
194 /// any
195 ///
196 /// %BUILDDATE%
197 /// 1729181726
198 ///
199 /// %PACKAGER%
200 /// Foobar McFooface <foobar@mcfooface.org>
201 ///
202 /// "#;
203 /// let file = tempfile::NamedTempFile::new()?;
204 /// let mut output = File::create(&file)?;
205 /// write!(output, "{}", desc_data)?;
206 /// (File::open(&file.path())?, desc_data)
207 /// };
208 ///
209 /// let repo_desc = RepoDescFile::from_reader_with_schema(
210 /// reader,
211 /// Some(RepoDescSchema::V2(SchemaVersion::new(Version::new(
212 /// 2, 0, 0,
213 /// )))),
214 /// )?;
215 /// assert_eq!(repo_desc.to_string(), desc_data);
216 /// # Ok(())
217 /// # }
218 /// ```
219 ///
220 /// # Errors
221 ///
222 /// Returns an error if:
223 ///
224 /// - the `reader` cannot be read to string,
225 /// - the data cannot be parsed into a known [`RepoDescFile`] variant,
226 /// - or the provided [`RepoDescSchema`] does not match the parsed content.
227 fn from_reader_with_schema(
228 mut reader: impl std::io::Read,
229 schema: Option<RepoDescSchema>,
230 ) -> Result<Self, Error> {
231 let mut buf = String::new();
232 reader
233 .read_to_string(&mut buf)
234 .map_err(|source| Error::IoReadError {
235 context: t!("error-io-read-repo-desc"),
236 source,
237 })?;
238 Self::from_str_with_schema(&buf, schema)
239 }
240
241 /// Creates a [`RepoDescFile`] from a string slice, optionally validated using a
242 /// [`RepoDescSchema`].
243 ///
244 /// If `schema` is [`None`], automatically infers the schema version by inspecting the input
245 /// (`v1` if `%MD5SUM%` is present, `v2` otherwise).
246 ///
247 /// # Examples
248 ///
249 /// ```
250 /// use alpm_common::MetadataFile;
251 /// use alpm_repo_db::desc::{RepoDescFile, RepoDescSchema};
252 /// use alpm_types::{SchemaVersion, semver_version::Version};
253 ///
254 /// # fn main() -> testresult::TestResult {
255 /// let v1_data = r#"%FILENAME%
256 /// example-meta-1.0.0-1-any.pkg.tar.zst
257 ///
258 /// %NAME%
259 /// example-meta
260 ///
261 /// %BASE%
262 /// example-meta
263 ///
264 /// %VERSION%
265 /// 1.0.0-1
266 ///
267 /// %DESC%
268 /// An example meta package
269 ///
270 /// %CSIZE%
271 /// 4634
272 ///
273 /// %ISIZE%
274 /// 0
275 ///
276 /// %MD5SUM%
277 /// d3b07384d113edec49eaa6238ad5ff00
278 ///
279 /// %SHA256SUM%
280 /// b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
281 ///
282 /// %PGPSIG%
283 /// iHUEABYKAB0WIQRizHP4hOUpV7L92IObeih9mi7GCAUCaBZuVAAKCRCbeih9mi7GCIlMAP9ws/jU4f580ZRQlTQKvUiLbAZOdcB7mQQj83hD1Nc/GwD/WIHhO1/OQkpMERejUrLo3AgVmY3b4/uGhx9XufWEbgE=
284 ///
285 /// %URL%
286 /// https://example.org/
287 ///
288 /// %LICENSE%
289 /// GPL-3.0-or-later
290 ///
291 /// %ARCH%
292 /// any
293 ///
294 /// %BUILDDATE%
295 /// 1729181726
296 ///
297 /// %PACKAGER%
298 /// Foobar McFooface <foobar@mcfooface.org>
299 ///
300 /// "#;
301 ///
302 /// let repo_desc_v1 = RepoDescFile::from_str_with_schema(
303 /// v1_data,
304 /// Some(RepoDescSchema::V1(SchemaVersion::new(Version::new(
305 /// 1, 0, 0,
306 /// )))),
307 /// )?;
308 /// assert_eq!(repo_desc_v1.to_string(), v1_data);
309 ///
310 /// let v2_data = r#"%FILENAME%
311 /// example-meta-1.0.0-1-any.pkg.tar.zst
312 ///
313 /// %NAME%
314 /// example-meta
315 ///
316 /// %BASE%
317 /// example-meta
318 ///
319 /// %VERSION%
320 /// 1.0.0-1
321 ///
322 /// %DESC%
323 /// An example meta package
324 ///
325 /// %CSIZE%
326 /// 4634
327 ///
328 /// %ISIZE%
329 /// 0
330 ///
331 /// %SHA256SUM%
332 /// b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
333 ///
334 /// %URL%
335 /// https://example.org/
336 ///
337 /// %LICENSE%
338 /// GPL-3.0-or-later
339 ///
340 /// %ARCH%
341 /// any
342 ///
343 /// %BUILDDATE%
344 /// 1729181726
345 ///
346 /// %PACKAGER%
347 /// Foobar McFooface <foobar@mcfooface.org>
348 ///
349 /// "#;
350 ///
351 /// let repo_desc_v2 = RepoDescFile::from_str_with_schema(
352 /// v2_data,
353 /// Some(RepoDescSchema::V2(SchemaVersion::new(Version::new(
354 /// 2, 0, 0,
355 /// )))),
356 /// )?;
357 /// assert_eq!(repo_desc_v2.to_string(), v2_data);
358 /// # Ok(())
359 /// # }
360 /// ```
361 ///
362 /// # Errors
363 ///
364 /// Returns an error if:
365 ///
366 /// - the input cannot be parsed into a valid [`RepoDescFile`],
367 /// - or the derived or provided schema does not match the detected format.
368 fn from_str_with_schema(s: &str, schema: Option<RepoDescSchema>) -> Result<Self, Error> {
369 let schema = match schema {
370 Some(schema) => schema,
371 None => RepoDescSchema::derive_from_str(s)?,
372 };
373
374 match schema {
375 RepoDescSchema::V1(_) => Ok(RepoDescFile::V1(RepoDescFileV1::from_str(s)?)),
376 RepoDescSchema::V2(_) => Ok(RepoDescFile::V2(RepoDescFileV2::from_str(s)?)),
377 }
378 }
379}
380
381impl Display for RepoDescFile {
382 /// Returns the textual representation of the [`RepoDescFile`] in its corresponding
383 /// [alpm-repo-desc] format.
384 ///
385 /// [alpm-repo-desc]: https://alpm.archlinux.page/specifications/alpm-repo-desc.5.html
386 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387 match self {
388 Self::V1(file) => write!(f, "{file}"),
389 Self::V2(file) => write!(f, "{file}"),
390 }
391 }
392}
393
394impl FromStr for RepoDescFile {
395 type Err = Error;
396
397 /// Creates a [`RepoDescFile`] from a string slice.
398 ///
399 /// Internally calls [`RepoDescFile::from_str_with_schema`] with `schema` set to [`None`].
400 ///
401 /// # Errors
402 ///
403 /// Returns an error if [`RepoDescFile::from_str_with_schema`] fails.
404 fn from_str(s: &str) -> Result<Self, Self::Err> {
405 Self::from_str_with_schema(s, None)
406 }
407}