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}