alpm_files/
schema.rs

1//! Schema definition for the [alpm-files] format.
2//!
3//! [alpm-files]: https://alpm.archlinux.page/specifications/alpm-files.5.html
4
5use std::{fs::File, io::Read, path::Path};
6
7use alpm_common::FileFormatSchema;
8use alpm_types::{SchemaVersion, semver_version::Version};
9use fluent_i18n::t;
10
11use crate::{Error, files::v1::FilesSection};
12
13/// A schema for the [alpm-files] format.
14///
15/// [alpm-files]: https://alpm.archlinux.page/specifications/alpm-files.5.html
16#[derive(Clone, Debug, Eq, PartialEq)]
17pub enum FilesSchema {
18    /// Version 1 of the [alpm-files] specification.
19    ///
20    /// [alpm-files]: https://alpm.archlinux.page/specifications/alpm-files.5.html
21    V1(SchemaVersion),
22}
23
24impl FileFormatSchema for FilesSchema {
25    type Err = Error;
26
27    /// Returns a reference to the inner [`SchemaVersion`].
28    fn inner(&self) -> &SchemaVersion {
29        match self {
30            FilesSchema::V1(v) => v,
31        }
32    }
33
34    /// Creates a new [`FilesSchema`] from a file [`Path`].
35    ///
36    /// # Note
37    ///
38    /// Delegates to [`Self::derive_from_reader`] after opening `file` for reading.
39    ///
40    /// # Errors
41    ///
42    /// Returns an error if
43    ///
44    /// - `file` cannot be opened for reading,
45    /// - or [`Self::derive_from_reader`] fails.
46    ///
47    /// # Examples
48    ///
49    /// ```
50    /// use std::io::Write;
51    ///
52    /// use alpm_common::FileFormatSchema;
53    /// use alpm_files::FilesSchema;
54    /// use tempfile::NamedTempFile;
55    ///
56    /// # fn main() -> testresult::TestResult {
57    /// let data = r#"%FILES%
58    /// usr/
59    /// usr/bin/
60    /// usr/bin/foo
61    /// "#;
62    /// let mut temp_file = NamedTempFile::new()?;
63    /// write!(temp_file, "{data}");
64    /// let schema = FilesSchema::derive_from_file(temp_file.path())?;
65    /// matches!(schema, FilesSchema::V1(_));
66    ///
67    /// // Empty inputs may also be considered as a schema
68    /// let mut temp_file = NamedTempFile::new()?;
69    /// let schema = FilesSchema::derive_from_file(temp_file.path())?;
70    /// matches!(schema, FilesSchema::V1(_));
71    /// # Ok(())
72    /// # }
73    /// ```
74    fn derive_from_file(file: impl AsRef<Path>) -> Result<Self, Self::Err>
75    where
76        Self: Sized,
77    {
78        let file = file.as_ref();
79        Self::derive_from_reader(File::open(file).map_err(|source| Error::IoPath {
80            path: file.to_path_buf(),
81            context: t!("error-io-path-context-deriving-schema-version-from-alpm-files-file"),
82            source,
83        })?)
84    }
85
86    /// Creates a new [`FilesSchema`] from a [`Read`] implementation.
87    ///
88    /// # Note
89    ///
90    /// Delegates to [`Self::derive_from_str`] after reading the `reader` to string.
91    ///
92    /// # Errors
93    ///
94    /// Returns an error if
95    ///
96    /// - `reader` cannot be read to string,
97    /// - or [`Self::derive_from_str`] fails.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use std::io::{Seek, SeekFrom, Write};
103    ///
104    /// use alpm_common::FileFormatSchema;
105    /// use alpm_files::FilesSchema;
106    /// use tempfile::tempfile;
107    ///
108    /// # fn main() -> testresult::TestResult {
109    /// let data = r#"%FILES%
110    /// usr/
111    /// usr/bin/
112    /// usr/bin/foo
113    /// "#;
114    /// let mut temp_file = tempfile()?;
115    /// write!(temp_file, "{data}");
116    /// temp_file.seek(SeekFrom::Start(0))?;
117    ///
118    /// let schema = FilesSchema::derive_from_reader(temp_file)?;
119    /// matches!(schema, FilesSchema::V1(_));
120    ///
121    /// // Empty inputs may also be considered as a schema
122    /// let mut temp_file = tempfile()?;
123    /// let schema = FilesSchema::derive_from_reader(temp_file)?;
124    /// matches!(schema, FilesSchema::V1(_));
125    /// # Ok(())
126    /// # }
127    /// ```
128    fn derive_from_reader(mut reader: impl Read) -> Result<Self, Self::Err>
129    where
130        Self: Sized,
131    {
132        let mut buf = String::new();
133        reader
134            .read_to_string(&mut buf)
135            .map_err(|source| Error::Io {
136                context: t!("error-io-context-deriving-a-schema-version-from-alpm-files-data"),
137                source,
138            })?;
139        Self::derive_from_str(&buf)
140    }
141
142    /// Creates a new [`FilesSchema`] from a string slice.
143    ///
144    /// # Errors
145    ///
146    /// Returns an error if
147    ///
148    /// - a [`FilesSchema`] cannot be derived from `s`,
149    /// - or a [`FilesV1`][`crate::FilesV1`] cannot be created from `s`.
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// use alpm_common::FileFormatSchema;
155    /// use alpm_files::FilesSchema;
156    ///
157    /// # fn main() -> Result<(), alpm_files::Error> {
158    /// let data = r#"%FILES%
159    /// usr/
160    /// usr/bin/
161    /// usr/bin/foo
162    /// "#;
163    /// let schema = FilesSchema::derive_from_str(data)?;
164    /// matches!(schema, FilesSchema::V1(_));
165    ///
166    /// // Empty inputs may also be considered as a schema
167    /// let data = "";
168    /// let schema = FilesSchema::derive_from_str(data)?;
169    /// matches!(schema, FilesSchema::V1(_));
170    /// # Ok(())
171    /// # }
172    /// ```
173    fn derive_from_str(s: &str) -> Result<Self, Self::Err>
174    where
175        Self: Sized,
176    {
177        // Return an error if there is a first line, but it doesn't contain the expected section
178        // header.
179        if s.lines()
180            .next()
181            .is_some_and(|line| line != FilesSection::SECTION_KEYWORD)
182        {
183            return Err(Error::SchemaVersionIsUnknown);
184        }
185
186        // If there are no lines (empty file) or the first line contains the expected section
187        // header, we can assume to deal with a version 1.
188        Ok(Self::V1(SchemaVersion::new(Version::new(1, 0, 0))))
189    }
190}
191
192impl Default for FilesSchema {
193    /// Returns the default schema variant ([`FilesSchema::V1`]).
194    fn default() -> Self {
195        Self::V1(SchemaVersion::new(Version::new(1, 0, 0)))
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use std::io::Write;
202
203    use rstest::rstest;
204    use tempfile::NamedTempFile;
205    use testresult::TestResult;
206
207    use super::*;
208
209    const ALPM_DB_FILES_FULL: &str = r#"%FILES%
210usr/
211usr/bin/
212usr/bin/foo
213"#;
214    const ALPM_DB_FILES_EMPTY: &str = "";
215    const ALPM_REPO_FILES_FULL: &str = r#"%FILES%
216usr/
217usr/bin/
218usr/bin/foo"#;
219    const ALPM_REPO_FILES_EMPTY: &str = "%FILES%";
220
221    /// Ensures that different types of full and empty alpm-files files can be parsed from file.
222    #[rstest]
223    #[case::alpm_db_files_full(ALPM_DB_FILES_FULL)]
224    #[case::alpm_db_files_empty(ALPM_DB_FILES_EMPTY)]
225    #[case::alpm_repo_files_full(ALPM_REPO_FILES_FULL)]
226    #[case::alpm_repo_files_full(ALPM_REPO_FILES_EMPTY)]
227    fn files_schema_derive_from_file_succeeds(#[case] data: &str) -> TestResult {
228        let mut temp_file = NamedTempFile::new()?;
229        write!(temp_file, "{data}")?;
230
231        let schema = FilesSchema::derive_from_file(temp_file.path())?;
232
233        assert!(matches!(schema, FilesSchema::V1(_)));
234
235        Ok(())
236    }
237
238    /// Ensures that different types of full and empty alpm-files files can be parsed from file.
239    #[test]
240    fn files_schema_derive_from_file_fails() -> TestResult {
241        let mut temp_file = NamedTempFile::new()?;
242        write!(temp_file, "%WRONG%")?;
243
244        let result = FilesSchema::derive_from_file(temp_file.path());
245        match result {
246            Ok(schema) => {
247                return Err(format!(
248                    "Expected to fail with an Error::SchemaVersionIsUnknown but succeeded: {schema:?}"
249                ).into());
250            }
251            Err(Error::SchemaVersionIsUnknown) => {}
252            Err(error) => {
253                return Err(format!(
254                    "Expected to fail with an Error::SchemaVersionIsUnknown but got another error instead: {error}"
255                ).into());
256            }
257        }
258
259        Ok(())
260    }
261
262    /// Ensures that [`FilesSchema::inner`] returns the correct schema.
263    #[test]
264    fn files_schema_inner() {
265        let schema_version = SchemaVersion::new(Version::new(1, 0, 0));
266        let schema = FilesSchema::V1(schema_version.clone());
267        assert_eq!(schema.inner(), &schema_version)
268    }
269
270    /// Ensures that [`FilesSchema::V1`] is the default.
271    #[test]
272    fn files_schema_default() {
273        assert_eq!(
274            FilesSchema::default(),
275            FilesSchema::V1(SchemaVersion::new(Version::new(1, 0, 0)))
276        )
277    }
278}