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}