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