alpm_repo_db/files/
mod.rs

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