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}