alpm_repo_db/files/mod.rs
1//! The representation of [alpm-repo-files] files.
2//!
3//! [alpm-repo-files]: https://alpm.archlinux.page/specifications/alpm-repo-files.5.html
4
5#[cfg(feature = "cli")]
6#[doc(hidden)]
7pub mod cli;
8
9mod error;
10mod schema;
11pub mod v1;
12
13use std::{
14 fmt::Display,
15 fs::File,
16 io::Read,
17 path::{Path, PathBuf},
18 str::FromStr,
19};
20
21use alpm_common::{FileFormatSchema, MetadataFile};
22pub use error::Error;
23use fluent_i18n::t;
24pub use schema::RepoFilesSchema;
25pub use v1::RepoFilesV1;
26
27/// The representation of [alpm-repo-files] data.
28///
29/// Tracks all known versions of the specification.
30///
31/// [alpm-repo-files]: https://alpm.archlinux.page/specifications/alpm-repo-files.5.html
32#[derive(Clone, Debug, serde::Serialize)]
33#[serde(untagged)]
34pub enum RepoFiles {
35 /// Version 1 of the [alpm-repo-files] specification.
36 ///
37 /// [alpm-repo-files]: https://alpm.archlinux.page/specifications/alpm-repo-files.5.html
38 V1(RepoFilesV1),
39}
40
41impl Display for RepoFiles {
42 /// Formats the [`RepoFiles`] as a string.
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 RepoFiles::V1(files) => files.fmt(f),
46 }
47 }
48}
49
50impl AsRef<[PathBuf]> for RepoFiles {
51 /// Returns a reference to the inner [`Vec`] of [`PathBuf`]s.
52 fn as_ref(&self) -> &[PathBuf] {
53 match self {
54 RepoFiles::V1(files) => files.as_ref(),
55 }
56 }
57}
58
59impl MetadataFile<RepoFilesSchema> for RepoFiles {
60 type Err = Error;
61
62 /// Creates a new [`RepoFiles`] from a file [`Path`] and an optional [`RepoFilesSchema`].
63 ///
64 /// # Note
65 ///
66 /// Delegates to [`Self::from_reader_with_schema`] after opening `file` for reading.
67 ///
68 /// # Errors
69 ///
70 /// Returns an error if
71 ///
72 /// - the `file` cannot be opened for reading,
73 /// - or [`Self::from_reader_with_schema`] fails.
74 ///
75 /// # Examples
76 ///
77 /// ```
78 /// use std::io::Write;
79 ///
80 /// use alpm_common::MetadataFile;
81 /// use alpm_repo_db::files::{RepoFiles, RepoFilesSchema};
82 /// use alpm_types::{SchemaVersion, semver_version::Version};
83 /// use tempfile::NamedTempFile;
84 ///
85 /// # fn main() -> testresult::TestResult {
86 /// let data = r#"%FILES%
87 /// usr/
88 /// usr/bin/
89 /// usr/bin/foo
90 /// "#;
91 /// let mut temp_file = NamedTempFile::new()?;
92 /// write!(temp_file, "{data}")?;
93 /// let files = RepoFiles::from_file_with_schema(
94 /// temp_file.path(),
95 /// Some(RepoFilesSchema::V1(SchemaVersion::new(Version::new(
96 /// 1, 0, 0,
97 /// )))),
98 /// )?;
99 /// matches!(files, RepoFiles::V1(_));
100 /// assert_eq!(files.as_ref().len(), 3);
101 /// # Ok(())
102 /// # }
103 /// ```
104 fn from_file_with_schema(
105 file: impl AsRef<Path>,
106 schema: Option<RepoFilesSchema>,
107 ) -> Result<Self, Self::Err>
108 where
109 Self: Sized,
110 {
111 let path = file.as_ref();
112 Self::from_reader_with_schema(
113 File::open(path).map_err(|source| Error::IoPath {
114 path: path.to_path_buf(),
115 context: t!("error-io-path-context-opening-the-file-for-reading"),
116 source,
117 })?,
118 schema,
119 )
120 }
121
122 /// Creates a new [`RepoFiles`] from a [`Read`] implementation and an optional
123 /// [`RepoFilesSchema`].
124 ///
125 /// # Note
126 ///
127 /// Delegates to [`Self::from_str_with_schema`] after reading `reader` to string.
128 ///
129 /// # Errors
130 ///
131 /// Returns an error if
132 ///
133 /// - the `reader` cannot be read to string,
134 /// - or [`Self::from_str_with_schema`] fails.
135 ///
136 /// # Examples
137 ///
138 /// ```
139 /// use std::io::{Seek, SeekFrom, Write};
140 ///
141 /// use alpm_common::MetadataFile;
142 /// use alpm_repo_db::files::{RepoFiles, RepoFilesSchema};
143 /// use alpm_types::{SchemaVersion, semver_version::Version};
144 /// use tempfile::tempfile;
145 ///
146 /// # fn main() -> testresult::TestResult {
147 /// let data = r#"%FILES%
148 /// usr/
149 /// usr/bin/
150 /// usr/bin/foo
151 /// "#;
152 /// let mut temp_file = tempfile()?;
153 /// write!(temp_file, "{data}")?;
154 /// temp_file.seek(SeekFrom::Start(0))?;
155 /// let files = RepoFiles::from_reader_with_schema(
156 /// temp_file,
157 /// Some(RepoFilesSchema::V1(SchemaVersion::new(Version::new(
158 /// 1, 0, 0,
159 /// )))),
160 /// )?;
161 /// matches!(files, RepoFiles::V1(_));
162 /// assert_eq!(files.as_ref().len(), 3);
163 /// # Ok(())
164 /// # }
165 /// ```
166 fn from_reader_with_schema(
167 mut reader: impl Read,
168 schema: Option<RepoFilesSchema>,
169 ) -> Result<Self, Self::Err>
170 where
171 Self: Sized,
172 {
173 let mut buf = String::new();
174 reader
175 .read_to_string(&mut buf)
176 .map_err(|source| Error::Io {
177 context: t!("error-io-context-reading-alpm-repo-files-data"),
178 source,
179 })?;
180 Self::from_str_with_schema(&buf, schema)
181 }
182
183 /// Creates a new [`RepoFiles`] from a string slice and an optional [`RepoFilesSchema`].
184 ///
185 /// # Errors
186 ///
187 /// Returns an error if
188 ///
189 /// - `schema` is [`None`] and a [`RepoFilesSchema`] cannot be derived from `s`,
190 /// - or a [`RepoFilesV1`] cannot be created from `s`.
191 ///
192 /// # Examples
193 ///
194 /// ```
195 /// use alpm_common::MetadataFile;
196 /// use alpm_repo_db::files::{RepoFiles, RepoFilesSchema};
197 /// use alpm_types::{SchemaVersion, semver_version::Version};
198 ///
199 /// # fn main() -> Result<(), alpm_repo_db::files::Error> {
200 /// let data = r#"%FILES%
201 /// usr/
202 /// usr/bin/
203 /// usr/bin/foo
204 /// "#;
205 /// let files = RepoFiles::from_str_with_schema(
206 /// data,
207 /// Some(RepoFilesSchema::V1(SchemaVersion::new(Version::new(
208 /// 1, 0, 0,
209 /// )))),
210 /// )?;
211 /// matches!(files, RepoFiles::V1(_));
212 /// assert_eq!(files.as_ref().len(), 3);
213 /// # Ok(())
214 /// # }
215 /// ```
216 fn from_str_with_schema(s: &str, schema: Option<RepoFilesSchema>) -> Result<Self, Self::Err>
217 where
218 Self: Sized,
219 {
220 let schema = match schema {
221 Some(schema) => schema,
222 None => RepoFilesSchema::derive_from_str(s)?,
223 };
224
225 match schema {
226 RepoFilesSchema::V1(_) => Ok(RepoFiles::V1(RepoFilesV1::from_str(s)?)),
227 }
228 }
229}
230
231impl FromStr for RepoFiles {
232 type Err = Error;
233
234 /// Creates a new [`RepoFiles`] from string slice.
235 ///
236 /// # Note
237 ///
238 /// Delegates to [`Self::from_str_with_schema`] while not providing a [`RepoFilesSchema`].
239 ///
240 /// # Errors
241 ///
242 /// Returns an error if [`Self::from_str_with_schema`] fails.
243 fn from_str(s: &str) -> Result<Self, Self::Err> {
244 Self::from_str_with_schema(s, None)
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use std::io::Write;
251
252 use alpm_types::{SchemaVersion, semver_version::Version};
253 use rstest::rstest;
254 use tempfile::NamedTempFile;
255 use testresult::TestResult;
256
257 use super::*;
258
259 /// Ensures that [`RepoFiles::to_string`] produces the expected output.
260 #[rstest]
261 #[case(
262 vec![
263 PathBuf::from("usr/"),
264 PathBuf::from("usr/bin/"),
265 PathBuf::from("usr/bin/foo"),
266 ],
267 r#"%FILES%
268usr/
269usr/bin/
270usr/bin/foo
271"#
272 )]
273 #[case(Vec::new(), "%FILES%\n")]
274 fn files_to_string(#[case] input: Vec<PathBuf>, #[case] expected_output: &str) -> TestResult {
275 let files = RepoFiles::V1(RepoFilesV1::try_from(input)?);
276
277 assert_eq!(files.to_string(), expected_output);
278
279 Ok(())
280 }
281
282 #[test]
283 fn files_from_str() -> TestResult {
284 let input = r#"%FILES%
285usr/
286usr/bin/
287usr/bin/foo
288
289"#;
290 let expected_paths = vec![
291 PathBuf::from("usr/"),
292 PathBuf::from("usr/bin/"),
293 PathBuf::from("usr/bin/foo"),
294 ];
295 let files = RepoFiles::from_str(input)?;
296
297 assert_eq!(files.as_ref(), expected_paths);
298
299 Ok(())
300 }
301
302 /// Ensures that missing section headers are rejected when deriving the schema.
303 #[test]
304 fn files_from_str_fails_without_header() {
305 let result = RepoFiles::from_str("");
306
307 assert!(matches!(result, Err(Error::UnknownSchemaVersion)));
308 }
309
310 const ALPM_REPO_FILES_FULL: &str = r#"%FILES%
311usr/
312usr/bin/
313usr/bin/foo
314"#;
315 const ALPM_REPO_FILES_EMPTY: &str = "%FILES%\n";
316 const ALPM_REPO_FILES_EMPTY_NO_HEADER: &str = "";
317
318 /// Ensures that full and empty alpm-repo-files files can be parsed from file.
319 #[rstest]
320 #[case::alpm_repo_files_full(ALPM_REPO_FILES_FULL, 3)]
321 #[case::alpm_repo_files_empty(ALPM_REPO_FILES_EMPTY, 0)]
322 fn files_from_file_with_schema_succeeds(#[case] data: &str, #[case] len: usize) -> TestResult {
323 let mut temp_file = NamedTempFile::new()?;
324 write!(temp_file, "{data}")?;
325
326 let files = RepoFiles::from_file_with_schema(
327 temp_file.path(),
328 Some(RepoFilesSchema::V1(SchemaVersion::new(Version::new(
329 1, 0, 0,
330 )))),
331 )?;
332
333 assert!(matches!(files, RepoFiles::V1(_)));
334 assert_eq!(files.as_ref().len(), len);
335
336 Ok(())
337 }
338
339 /// Ensures that missing headers prevent parsing alpm-repo-files files.
340 #[test]
341 fn files_from_file_with_schema_fails_without_header() -> TestResult {
342 let mut temp_file = NamedTempFile::new()?;
343 write!(temp_file, "{ALPM_REPO_FILES_EMPTY_NO_HEADER}")?;
344
345 let result = RepoFiles::from_file_with_schema(
346 temp_file.path(),
347 Some(RepoFilesSchema::V1(SchemaVersion::new(Version::new(
348 1, 0, 0,
349 )))),
350 );
351
352 assert!(matches!(result, Err(Error::ParseError(_))));
353
354 Ok(())
355 }
356}