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}