alpm_repo_db/files/
schema.rs

1//! Schema definition for the [alpm-repo-files] format.
2//!
3//! [alpm-repo-files]: https://alpm.archlinux.page/specifications/alpm-repo-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::files::{Error, v1::FilesSection};
12
13/// A schema for the [alpm-repo-files] format.
14///
15/// [alpm-repo-files]: https://alpm.archlinux.page/specifications/alpm-repo-files.5.html
16#[derive(Clone, Debug, Eq, PartialEq)]
17pub enum RepoFilesSchema {
18    /// Version 1 of the [alpm-repo-files] specification.
19    ///
20    /// [alpm-repo-files]: https://alpm.archlinux.page/specifications/alpm-repo-files.5.html
21    V1(SchemaVersion),
22}
23
24impl FileFormatSchema for RepoFilesSchema {
25    type Err = Error;
26
27    /// Returns a reference to the inner [`SchemaVersion`].
28    fn inner(&self) -> &SchemaVersion {
29        match self {
30            RepoFilesSchema::V1(v) => v,
31        }
32    }
33
34    /// Creates a new [`RepoFilesSchema`] 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_repo_db::files::RepoFilesSchema;
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 = RepoFilesSchema::derive_from_file(temp_file.path())?;
65    /// matches!(schema, RepoFilesSchema::V1(_));
66    /// # Ok(())
67    /// # }
68    /// ```
69    fn derive_from_file(file: impl AsRef<Path>) -> Result<Self, Self::Err>
70    where
71        Self: Sized,
72    {
73        let file = file.as_ref();
74        Self::derive_from_reader(File::open(file).map_err(|source| Error::IoPath {
75            path: file.to_path_buf(),
76            context: t!("error-io-path-context-deriving-schema-version-from-alpm-repo-files-file"),
77            source,
78        })?)
79    }
80
81    /// Creates a new [`RepoFilesSchema`] from a [`Read`] implementation.
82    ///
83    /// # Note
84    ///
85    /// Delegates to [`Self::derive_from_str`] after reading the `reader` to string.
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if
90    ///
91    /// - `reader` cannot be read to string,
92    /// - or [`Self::derive_from_str`] fails.
93    ///
94    /// # Examples
95    ///
96    /// ```
97    /// use std::io::{Seek, SeekFrom, Write};
98    ///
99    /// use alpm_common::FileFormatSchema;
100    /// use alpm_repo_db::files::RepoFilesSchema;
101    /// use tempfile::tempfile;
102    ///
103    /// # fn main() -> testresult::TestResult {
104    /// let data = r#"%FILES%
105    /// usr/
106    /// usr/bin/
107    /// usr/bin/foo
108    /// "#;
109    /// let mut temp_file = tempfile()?;
110    /// write!(temp_file, "{data}");
111    /// temp_file.seek(SeekFrom::Start(0))?;
112    ///
113    /// let schema = RepoFilesSchema::derive_from_reader(temp_file)?;
114    /// matches!(schema, RepoFilesSchema::V1(_));
115    /// # Ok(())
116    /// # }
117    /// ```
118    fn derive_from_reader(mut reader: impl Read) -> Result<Self, Self::Err>
119    where
120        Self: Sized,
121    {
122        let mut buf = String::new();
123        reader
124            .read_to_string(&mut buf)
125            .map_err(|source| Error::Io {
126                context: t!("error-io-context-deriving-a-schema-version-from-alpm-repo-files-data"),
127                source,
128            })?;
129        Self::derive_from_str(&buf)
130    }
131
132    /// Creates a new [`RepoFilesSchema`] from a string slice.
133    ///
134    /// # Errors
135    ///
136    /// Returns an error if
137    ///
138    /// - a [`RepoFilesSchema`] cannot be derived from `s`,
139    /// - or a [`RepoFilesV1`][`crate::files::RepoFilesV1`] cannot be created from `s`.
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// use alpm_common::FileFormatSchema;
145    /// use alpm_repo_db::files::RepoFilesSchema;
146    ///
147    /// # fn main() -> Result<(), alpm_repo_db::files::Error> {
148    /// let data = r#"%FILES%
149    /// usr/
150    /// usr/bin/
151    /// usr/bin/foo
152    /// "#;
153    /// let schema = RepoFilesSchema::derive_from_str(data)?;
154    /// matches!(schema, RepoFilesSchema::V1(_));
155    /// # Ok(())
156    /// # }
157    /// ```
158    fn derive_from_str(s: &str) -> Result<Self, Self::Err>
159    where
160        Self: Sized,
161    {
162        match s.lines().next() {
163            Some(line) if line == FilesSection::SECTION_KEYWORD => {
164                Ok(Self::V1(SchemaVersion::new(Version::new(1, 0, 0))))
165            }
166            _ => Err(Error::UnknownSchemaVersion),
167        }
168    }
169}
170
171impl Default for RepoFilesSchema {
172    /// Returns the default schema variant ([`RepoFilesSchema::V1`]).
173    fn default() -> Self {
174        Self::V1(SchemaVersion::new(Version::new(1, 0, 0)))
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use std::io::Write;
181
182    use rstest::rstest;
183    use tempfile::NamedTempFile;
184    use testresult::TestResult;
185
186    use super::*;
187
188    const ALPM_REPO_FILES_FULL: &str = r#"%FILES%
189usr/
190usr/bin/
191usr/bin/foo
192"#;
193    const ALPM_REPO_FILES_EMPTY: &str = "%FILES%\n";
194    const ALPM_REPO_FILES_EMPTY_NO_HEADER: &str = "";
195
196    /// Ensures that different types of full and empty alpm-repo-files files can be parsed from
197    /// file.
198    #[rstest]
199    #[case::alpm_repo_files_full(ALPM_REPO_FILES_FULL)]
200    #[case::alpm_repo_files_empty(ALPM_REPO_FILES_EMPTY)]
201    fn files_schema_derive_from_file_succeeds(#[case] data: &str) -> TestResult {
202        let mut temp_file = NamedTempFile::new()?;
203        write!(temp_file, "{data}")?;
204
205        let schema = RepoFilesSchema::derive_from_file(temp_file.path())?;
206
207        assert!(matches!(schema, RepoFilesSchema::V1(_)));
208
209        Ok(())
210    }
211
212    /// Ensures that files with wrong headers fail to derive the schema.
213    #[rstest]
214    #[case::wrong_header("%WRONG%")]
215    #[case::missing_header(ALPM_REPO_FILES_EMPTY_NO_HEADER)]
216    fn files_schema_derive_from_file_fails(#[case] data: &str) -> TestResult {
217        let mut temp_file = NamedTempFile::new()?;
218        write!(temp_file, "{data}")?;
219
220        let result = RepoFilesSchema::derive_from_file(temp_file.path());
221        match result {
222            Ok(schema) => {
223                panic!(
224                    "Expected to fail with an Error::UnknownSchemaVersion but succeeded: {schema:?}"
225                );
226            }
227            Err(Error::UnknownSchemaVersion) => {}
228            Err(error) => {
229                panic!(
230                    "Expected to fail with an Error::UnknownSchemaVersion but got another error instead: {error}"
231                );
232            }
233        }
234
235        Ok(())
236    }
237
238    /// Ensures that [`RepoFilesSchema::inner`] returns the correct schema.
239    #[test]
240    fn files_schema_inner() {
241        let schema_version = SchemaVersion::new(Version::new(1, 0, 0));
242        let schema = RepoFilesSchema::V1(schema_version.clone());
243        assert_eq!(schema.inner(), &schema_version)
244    }
245
246    /// Ensures that [`RepoFilesSchema::V1`] is the default.
247    #[test]
248    fn files_schema_default() {
249        assert_eq!(
250            RepoFilesSchema::default(),
251            RepoFilesSchema::V1(SchemaVersion::new(Version::new(1, 0, 0)))
252        )
253    }
254}