alpm_files/files/
mod.rs

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