Skip to main content

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