alpm_db/desc/
file.rs

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