alpm_db/files/
mod.rs

1//! The representation of [alpm-db-files] files.
2//!
3//! [alpm-db-files]: https://alpm.archlinux.page/specifications/alpm-db-files.5.html
4
5#[cfg(feature = "cli")]
6#[doc(hidden)]
7pub mod cli;
8
9mod error;
10mod schema;
11pub mod v1;
12
13use std::{
14    fmt::Display,
15    fs::File,
16    io::Read,
17    path::{Path, PathBuf},
18    str::FromStr,
19};
20
21use alpm_common::{FileFormatSchema, MetadataFile};
22pub use error::Error;
23use fluent_i18n::t;
24pub use schema::DbFilesSchema;
25pub use v1::{BackupEntry, DbFilesV1};
26
27/// The representation of [alpm-db-files] data.
28///
29/// Tracks all known versions of the specification.
30///
31/// [alpm-db-files]: https://alpm.archlinux.page/specifications/alpm-db-files.5.html
32#[derive(Clone, Debug, serde::Serialize)]
33#[serde(untagged)]
34pub enum DbFiles {
35    /// Version 1 of the [alpm-db-files] specification.
36    ///
37    /// [alpm-db-files]: https://alpm.archlinux.page/specifications/alpm-db-files.5.html
38    V1(DbFilesV1),
39}
40
41impl Display for DbFiles {
42    /// Formats the [`DbFiles`] as a string according to its style.
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            DbFiles::V1(files) => files.fmt(f),
46        }
47    }
48}
49
50impl AsRef<[PathBuf]> for DbFiles {
51    /// Returns a reference to the inner [`Vec`] of [`PathBuf`]s.
52    fn as_ref(&self) -> &[PathBuf] {
53        match self {
54            DbFiles::V1(files) => files.as_ref(),
55        }
56    }
57}
58
59impl DbFiles {
60    /// Returns the backup entries associated with this [`DbFiles`].
61    pub fn backups(&self) -> &[BackupEntry] {
62        match self {
63            DbFiles::V1(files) => files.backups(),
64        }
65    }
66}
67
68impl MetadataFile<DbFilesSchema> for DbFiles {
69    type Err = Error;
70
71    /// Creates a new [`DbFiles`] from a file [`Path`] and an optional [`DbFilesSchema`].
72    ///
73    /// # Note
74    ///
75    /// Delegates to [`Self::from_reader_with_schema`] after opening `file` for reading.
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if
80    ///
81    /// - the `file` cannot be opened for reading,
82    /// - or [`Self::from_reader_with_schema`] fails.
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// use std::io::Write;
88    ///
89    /// use alpm_common::MetadataFile;
90    /// use alpm_db::files::{DbFiles, DbFilesSchema};
91    /// use alpm_types::{SchemaVersion, semver_version::Version};
92    /// use tempfile::NamedTempFile;
93    ///
94    /// # fn main() -> testresult::TestResult {
95    /// let data = r#"%FILES%
96    /// usr/
97    /// usr/bin/
98    /// usr/bin/foo
99    /// "#;
100    /// let mut temp_file = NamedTempFile::new()?;
101    /// write!(temp_file, "{data}")?;
102    /// let files = DbFiles::from_file_with_schema(
103    ///     temp_file.path(),
104    ///     Some(DbFilesSchema::V1(SchemaVersion::new(Version::new(1, 0, 0)))),
105    /// )?;
106    /// matches!(files, DbFiles::V1(_));
107    /// assert_eq!(files.as_ref().len(), 3);
108    /// # Ok(())
109    /// # }
110    /// ```
111    fn from_file_with_schema(
112        file: impl AsRef<Path>,
113        schema: Option<DbFilesSchema>,
114    ) -> Result<Self, Self::Err>
115    where
116        Self: Sized,
117    {
118        let path = file.as_ref();
119        Self::from_reader_with_schema(
120            File::open(path).map_err(|source| Error::IoPath {
121                path: path.to_path_buf(),
122                context: t!("error-io-path-context-opening-the-file-for-reading"),
123                source,
124            })?,
125            schema,
126        )
127    }
128
129    /// Creates a new [`DbFiles`] from a [`Read`] implementation and an optional [`DbFilesSchema`].
130    ///
131    /// # Note
132    ///
133    /// Delegates to [`Self::from_str_with_schema`] after reading `reader` to string.
134    ///
135    /// # Errors
136    ///
137    /// Returns an error if
138    ///
139    /// - the `reader` cannot be read to string,
140    /// - or [`Self::from_str_with_schema`] fails.
141    ///
142    /// # Examples
143    ///
144    /// ```
145    /// use std::io::{Seek, SeekFrom, Write};
146    ///
147    /// use alpm_common::MetadataFile;
148    /// use alpm_db::files::{DbFiles, DbFilesSchema};
149    /// use alpm_types::{SchemaVersion, semver_version::Version};
150    /// use tempfile::tempfile;
151    ///
152    /// # fn main() -> testresult::TestResult {
153    /// let data = r#"%FILES%
154    /// usr/
155    /// usr/bin/
156    /// usr/bin/foo
157    /// "#;
158    /// let mut temp_file = tempfile()?;
159    /// write!(temp_file, "{data}")?;
160    /// temp_file.seek(SeekFrom::Start(0))?;
161    /// let files = DbFiles::from_reader_with_schema(
162    ///     temp_file,
163    ///     Some(DbFilesSchema::V1(SchemaVersion::new(Version::new(1, 0, 0)))),
164    /// )?;
165    /// matches!(files, DbFiles::V1(_));
166    /// assert_eq!(files.as_ref().len(), 3);
167    /// # Ok(())
168    /// # }
169    /// ```
170    fn from_reader_with_schema(
171        mut reader: impl Read,
172        schema: Option<DbFilesSchema>,
173    ) -> Result<Self, Self::Err>
174    where
175        Self: Sized,
176    {
177        let mut buf = String::new();
178        reader
179            .read_to_string(&mut buf)
180            .map_err(|source| Error::Io {
181                context: t!("error-io-context-reading-alpm-db-files-data"),
182                source,
183            })?;
184        Self::from_str_with_schema(&buf, schema)
185    }
186
187    /// Creates a new [`DbFiles`] from a string slice and an optional [`DbFilesSchema`].
188    ///
189    /// # Errors
190    ///
191    /// Returns an error if
192    ///
193    /// - `schema` is [`None`] and a [`DbFilesSchema`] cannot be derived from `s`,
194    /// - or a [`DbFilesV1`] cannot be created from `s`.
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// use alpm_common::MetadataFile;
200    /// use alpm_db::files::{DbFiles, DbFilesSchema};
201    /// use alpm_types::{SchemaVersion, semver_version::Version};
202    ///
203    /// # fn main() -> Result<(), alpm_db::files::Error> {
204    /// let data = r#"%FILES%
205    /// usr/
206    /// usr/bin/
207    /// usr/bin/foo
208    /// "#;
209    /// let files = DbFiles::from_str_with_schema(
210    ///     data,
211    ///     Some(DbFilesSchema::V1(SchemaVersion::new(Version::new(1, 0, 0)))),
212    /// )?;
213    /// matches!(files, DbFiles::V1(_));
214    /// assert_eq!(files.as_ref().len(), 3);
215    /// # Ok(())
216    /// # }
217    /// ```
218    fn from_str_with_schema(s: &str, schema: Option<DbFilesSchema>) -> Result<Self, Self::Err>
219    where
220        Self: Sized,
221    {
222        let schema = match schema {
223            Some(schema) => schema,
224            None => DbFilesSchema::derive_from_str(s)?,
225        };
226
227        match schema {
228            DbFilesSchema::V1(_) => Ok(DbFiles::V1(DbFilesV1::from_str(s)?)),
229        }
230    }
231}
232
233impl FromStr for DbFiles {
234    type Err = Error;
235
236    /// Creates a new [`DbFiles`] from string slice.
237    ///
238    /// # Note
239    ///
240    /// Delegates to [`Self::from_str_with_schema`] while not providing a [`DbFilesSchema`].
241    ///
242    /// # Errors
243    ///
244    /// Returns an error if [`Self::from_str_with_schema`] fails.
245    fn from_str(s: &str) -> Result<Self, Self::Err> {
246        Self::from_str_with_schema(s, None)
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use std::{io::Write, str::FromStr};
253
254    use alpm_types::{Md5Checksum, RelativeFilePath, SchemaVersion, semver_version::Version};
255    use rstest::rstest;
256    use tempfile::NamedTempFile;
257    use testresult::TestResult;
258
259    use super::*;
260
261    /// Ensures that [`DbFiles::to_string`] produces the expected output.
262    #[rstest]
263    #[case(
264        vec![
265            PathBuf::from("usr/"),
266            PathBuf::from("usr/bin/"),
267            PathBuf::from("usr/bin/foo"),
268        ],
269        r#"%FILES%
270usr/
271usr/bin/
272usr/bin/foo
273
274"#
275    )]
276    #[case(Vec::new(), "")]
277    fn files_to_string(#[case] input: Vec<PathBuf>, #[case] expected_output: &str) -> TestResult {
278        let files = DbFiles::V1(DbFilesV1::try_from(input)?);
279
280        assert_eq!(files.to_string(), expected_output);
281
282        Ok(())
283    }
284
285    #[test]
286    fn files_to_string_with_backup() -> TestResult {
287        let files = DbFiles::V1(DbFilesV1::try_from((
288            vec![
289                PathBuf::from("usr/"),
290                PathBuf::from("usr/bin/"),
291                PathBuf::from("usr/bin/foo"),
292            ],
293            vec![BackupEntry {
294                path: RelativeFilePath::from_str("usr/bin/foo")?,
295                md5: Md5Checksum::from_str("d41d8cd98f00b204e9800998ecf8427e")?,
296            }],
297        ))?);
298
299        let expected_output = r#"%FILES%
300usr/
301usr/bin/
302usr/bin/foo
303
304%BACKUP%
305usr/bin/foo	d41d8cd98f00b204e9800998ecf8427e
306"#;
307
308        assert_eq!(files.to_string(), expected_output);
309
310        Ok(())
311    }
312
313    #[test]
314    fn files_from_str() -> TestResult {
315        let input = r#"%FILES%
316usr/
317usr/bin/
318usr/bin/foo
319
320"#;
321        let expected_paths = vec![
322            PathBuf::from("usr/"),
323            PathBuf::from("usr/bin/"),
324            PathBuf::from("usr/bin/foo"),
325        ];
326        let files = DbFiles::from_str(input)?;
327
328        assert_eq!(files.as_ref(), expected_paths);
329
330        Ok(())
331    }
332
333    #[test]
334    fn files_from_str_with_backup() -> TestResult {
335        let input = r#"%FILES%
336usr/
337usr/bin/
338usr/bin/foo
339
340%BACKUP%
341usr/bin/foo	d41d8cd98f00b204e9800998ecf8427e
342"#;
343        let files = DbFiles::from_str(input)?;
344
345        let expected_backup = BackupEntry {
346            path: RelativeFilePath::from_str("usr/bin/foo")?,
347            md5: Md5Checksum::from_str("d41d8cd98f00b204e9800998ecf8427e")?,
348        };
349
350        assert_eq!(
351            files.as_ref(),
352            &[
353                PathBuf::from("usr/"),
354                PathBuf::from("usr/bin/"),
355                PathBuf::from("usr/bin/foo")
356            ]
357        );
358
359        assert_eq!(files.backups(), &[expected_backup]);
360
361        Ok(())
362    }
363
364    const ALPM_DB_FILES_FULL: &str = r#"%FILES%
365usr/
366usr/bin/
367usr/bin/foo
368
369"#;
370    const ALPM_DB_FILES_EMPTY: &str = "";
371    const ALPM_REPO_FILES_FULL: &str = r#"%FILES%
372usr/
373usr/bin/
374usr/bin/foo
375"#;
376    const ALPM_REPO_FILES_EMPTY: &str = "%FILES%";
377
378    /// Ensures that different types of full and empty alpm-db-files files can be parsed from file.
379    #[rstest]
380    #[case::alpm_db_files_full(ALPM_DB_FILES_FULL, 3)]
381    #[case::alpm_db_files_empty(ALPM_DB_FILES_EMPTY, 0)]
382    #[case::alpm_repo_files_full(ALPM_REPO_FILES_FULL, 3)]
383    #[case::alpm_repo_files_full(ALPM_REPO_FILES_EMPTY, 0)]
384    fn files_from_file_with_schema_succeeds(#[case] data: &str, #[case] len: usize) -> TestResult {
385        let mut temp_file = NamedTempFile::new()?;
386        write!(temp_file, "{data}")?;
387
388        let files = DbFiles::from_file_with_schema(
389            temp_file.path(),
390            Some(DbFilesSchema::V1(SchemaVersion::new(Version::new(1, 0, 0)))),
391        )?;
392
393        assert!(matches!(files, DbFiles::V1(_)));
394        assert_eq!(files.as_ref().len(), len);
395
396        Ok(())
397    }
398
399    #[test]
400    fn files_from_file_with_backup_section() -> TestResult {
401        let data = r#"%FILES%
402usr/
403usr/bin/
404usr/bin/foo
405
406%BACKUP%
407usr/bin/foo	d41d8cd98f00b204e9800998ecf8427e
408"#;
409        let mut temp_file = NamedTempFile::new()?;
410        write!(temp_file, "{data}")?;
411
412        let files = DbFiles::from_file_with_schema(
413            temp_file.path(),
414            Some(DbFilesSchema::V1(SchemaVersion::new(Version::new(1, 0, 0)))),
415        )?;
416
417        assert!(matches!(files, DbFiles::V1(_)));
418        assert_eq!(files.backups().len(), 1);
419
420        Ok(())
421    }
422}