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