alpm_mtree/mtree/mod.rs
1//! Handling for the ALPM-MTREE file format.
2
3pub mod path_validation_error;
4pub mod v2;
5use std::{
6 collections::HashSet,
7 fmt::{Display, Write},
8 fs::File,
9 io::{BufReader, Read},
10 path::{Path, PathBuf},
11 str::FromStr,
12};
13
14use alpm_common::{FileFormatSchema, InputPath, InputPaths, MetadataFile};
15use path_validation_error::{PathValidationError, PathValidationErrors};
16#[cfg(doc)]
17use v2::MTREE_PATH_PREFIX;
18
19use crate::{Error, MtreeSchema, mtree_buffer_to_string, parse_mtree_v2};
20
21/// A representation of the [ALPM-MTREE] file format.
22///
23/// Tracks all available versions of the file format.
24///
25/// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
26#[derive(Clone, Debug, PartialEq, serde::Serialize)]
27#[serde(untagged)]
28pub enum Mtree {
29 /// The [ALPM-MTREEv1] file format.
30 ///
31 /// [ALPM-MTREEv1]: https://alpm.archlinux.page/specifications/ALPM-MTREEv1.5.html
32 V1(Vec<crate::mtree::v2::Path>),
33 /// The [ALPM-MTREEv2] file format.
34 ///
35 /// [ALPM-MTREEv2]: https://alpm.archlinux.page/specifications/ALPM-MTREEv2.5.html
36 V2(Vec<crate::mtree::v2::Path>),
37}
38
39impl Mtree {
40 /// Validates an [`InputPaths`].
41 ///
42 /// With `input_paths`a set of relative paths and a common base directory is provided.
43 ///
44 /// Each member of [`InputPaths::paths`] is compared with the data available in `self` by
45 /// retrieving metadata from the on-disk files below [`InputPaths::base_dir`].
46 /// For this, [`MTREE_PATH_PREFIX`] is stripped from each [`Path`][`crate::mtree::v2::Path`]
47 /// tracked by the [`Mtree`] and afterwards each [`Path`][`crate::mtree::v2::Path`] is
48 /// compared with the respective file in [`InputPaths::base_dir`].
49 /// This includes checking if
50 ///
51 /// - each relative path in [`InputPaths::paths`] matches a record in the [ALPM-MTREE] data,
52 /// - each relative path in [`InputPaths::paths`] relates to an existing file, directory or
53 /// symlink in [`InputPaths::base_dir`],
54 /// - the target of each symlink in the [ALPM-MTREE] data matches that of the corresponding
55 /// on-disk file,
56 /// - size and SHA-256 hash digest of each file in the [ALPM-MTREE] data matches that of the
57 /// corresponding on-disk file,
58 /// - the [ALPM-MTREE] data file itself is included in the [ALPM-MTREE] data,
59 /// - and the creation time, UID, GID and file mode of each file in the [ALPM-MTREE] data
60 /// matches that of the corresponding on-disk file.
61 ///
62 /// # Errors
63 ///
64 /// Returns an error if
65 ///
66 /// - [`InputPaths::paths`] contains duplicates,
67 /// - or one of the [ALPM-MTREE] data entries
68 /// - does not have a matching on-disk file, directory or symlink (depending on type),
69 /// - has a mismatching symlink target from that of a corresponding on-disk file,
70 /// - has a mismatching size or SHA-256 hash digest from that of a corresponding on-disk file,
71 /// - is the [ALPM-MTREE] file,
72 /// - or has a mismatching creation time, UID, GID or file mode from that of a corresponding
73 /// on-disk file,
74 /// - or one of the file system paths in [`InputPaths::paths`] has no matching [ALPM-MTREE]
75 /// entry.
76 ///
77 /// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
78 pub fn validate_paths(&self, input_paths: &InputPaths) -> Result<(), Error> {
79 let base_dir = input_paths.base_dir();
80 // Use paths in a HashSet for easier handling later.
81 let mut hashed_paths = HashSet::new();
82 let mut duplicates = HashSet::new();
83 for path in input_paths.paths() {
84 if hashed_paths.contains(path.as_path()) {
85 duplicates.insert(path.to_path_buf());
86 }
87 hashed_paths.insert(path.as_path());
88 }
89 // If there are duplicate paths, return early.
90 if !duplicates.is_empty() {
91 return Err(Error::DuplicatePaths { paths: duplicates });
92 }
93
94 let mtree_paths = match self {
95 Mtree::V1(mtree) | Mtree::V2(mtree) => mtree,
96 };
97 let mut errors = PathValidationErrors::new(base_dir.to_path_buf());
98 let mut unmatched_paths = Vec::new();
99
100 for mtree_path in mtree_paths.iter() {
101 // Normalize the ALPM-MTREE path.
102 let normalized_path = match mtree_path.as_normalized_path() {
103 Ok(mtree_path) => mtree_path,
104 Err(source) => {
105 let mut normalize_errors: Vec<PathValidationError> = vec![source.into()];
106 errors.append(&mut normalize_errors);
107 // Continue, as the ALPM-MTREE data is not as it should be.
108 continue;
109 }
110 };
111
112 // If the normalized path exists in the hashed input paths, compare.
113 if hashed_paths.remove(normalized_path) {
114 if let Err(mut comparison_errors) =
115 mtree_path.equals_path(&InputPath::new(base_dir, normalized_path)?)
116 {
117 errors.append(&mut comparison_errors);
118 }
119 } else {
120 unmatched_paths.push(mtree_path);
121 }
122 }
123
124 // Add dedicated error, if some file system paths are not covered by ALPM-MTREE data.
125 if !hashed_paths.is_empty() {
126 errors.append(&mut vec![PathValidationError::UnmatchedFileSystemPaths {
127 paths: hashed_paths.iter().map(|path| path.to_path_buf()).collect(),
128 }])
129 }
130
131 // Add dedicated error, if some ALPM-MTREE paths have no matching file system paths.
132 if !unmatched_paths.is_empty() {
133 errors.append(&mut vec![PathValidationError::UnmatchedMtreePaths {
134 paths: unmatched_paths
135 .iter()
136 .map(|path| path.to_path_buf())
137 .collect(),
138 }])
139 }
140
141 // Emit all error messages on stderr and fail if there are any errors.
142 errors.check()?;
143
144 Ok(())
145 }
146}
147
148impl MetadataFile<MtreeSchema> for Mtree {
149 type Err = Error;
150
151 /// Creates a [`Mtree`] from `file`, optionally validated using a [`MtreeSchema`].
152 ///
153 /// Opens the `file` and defers to [`Mtree::from_reader_with_schema`].
154 ///
155 /// # Note
156 ///
157 /// To automatically derive the [`MtreeSchema`], use [`Mtree::from_file`].
158 ///
159 /// # Examples
160 ///
161 /// ```
162 /// use std::{fs::File, io::Write};
163 ///
164 /// use alpm_common::{FileFormatSchema, MetadataFile};
165 /// use alpm_mtree::{Mtree, MtreeSchema};
166 /// use alpm_types::{SchemaVersion, semver_version::Version};
167 ///
168 /// # fn main() -> testresult::TestResult {
169 /// // Prepare a file with ALPM-MTREE data
170 /// let file = {
171 /// let mtree_data = r#"#mtree
172 /// /set mode=644 uid=0 gid=0 type=file
173 /// ./some_file time=1700000000.0 size=1337 sha256digest=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
174 /// ./some_link type=link link=some_file time=1700000000.0
175 /// ./some_dir type=dir time=1700000000.0
176 /// "#;
177 /// let mtree_file = tempfile::NamedTempFile::new()?;
178 /// let mut output = File::create(&mtree_file)?;
179 /// write!(output, "{}", mtree_data)?;
180 /// mtree_file
181 /// };
182 ///
183 /// let mtree = Mtree::from_file_with_schema(
184 /// file.path().to_path_buf(),
185 /// Some(MtreeSchema::V2(SchemaVersion::new(Version::new(2, 0, 0)))),
186 /// )?;
187 /// # let mtree_version = match mtree {
188 /// # Mtree::V1(_) => "1",
189 /// # Mtree::V2(_) => "2",
190 /// # };
191 /// # assert_eq!("2", mtree_version);
192 /// # Ok(())
193 /// # }
194 /// ```
195 ///
196 /// # Errors
197 ///
198 /// Returns an error if
199 /// - the `file` cannot be opened for reading,
200 /// - no variant of [`Mtree`] can be constructed from the contents of `file`,
201 /// - or `schema` is [`Some`] and the [`MtreeSchema`] does not match the contents of `file`.
202 fn from_file_with_schema(
203 file: impl AsRef<Path>,
204 schema: Option<MtreeSchema>,
205 ) -> Result<Self, Error> {
206 let file = file.as_ref();
207 Self::from_reader_with_schema(
208 File::open(file).map_err(|source| {
209 Error::IoPath(PathBuf::from(file), "opening the file for reading", source)
210 })?,
211 schema,
212 )
213 }
214
215 /// Creates a [`Mtree`] from a `reader`, optionally validated using a
216 /// [`MtreeSchema`].
217 ///
218 /// Reads the `reader` to string (and decompresses potentially gzip compressed data on-the-fly).
219 /// Then defers to [`Mtree::from_str_with_schema`].
220 ///
221 /// # Note
222 ///
223 /// To automatically derive the [`MtreeSchema`], use [`Mtree::from_reader`].
224 ///
225 /// # Examples
226 ///
227 /// ```
228 /// use std::{fs::File, io::Write};
229 ///
230 /// use alpm_common::MetadataFile;
231 /// use alpm_mtree::{Mtree, MtreeSchema};
232 /// use alpm_types::{SchemaVersion, semver_version::Version};
233 ///
234 /// # fn main() -> testresult::TestResult {
235 /// // Prepare a reader with ALPM-MTREE data
236 /// let reader = {
237 /// let mtree_data = r#"#mtree
238 /// /set mode=644 uid=0 gid=0 type=file
239 /// ./some_file time=1700000000.0 size=1337 sha256digest=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
240 /// ./some_link type=link link=some_file time=1700000000.0
241 /// ./some_dir type=dir time=1700000000.0
242 /// "#;
243 /// let mtree_file = tempfile::NamedTempFile::new()?;
244 /// let mut output = File::create(&mtree_file)?;
245 /// write!(output, "{}", mtree_data)?;
246 /// File::open(&mtree_file.path())?
247 /// };
248 ///
249 /// let mtree = Mtree::from_reader_with_schema(
250 /// reader,
251 /// Some(MtreeSchema::V2(SchemaVersion::new(Version::new(2, 0, 0)))),
252 /// )?;
253 /// # let mtree_version = match mtree {
254 /// # Mtree::V1(_) => "1",
255 /// # Mtree::V2(_) => "2",
256 /// # };
257 /// # assert_eq!("2", mtree_version);
258 /// # Ok(())
259 /// # }
260 /// ```
261 ///
262 /// # Errors
263 ///
264 /// Returns an error if
265 /// - the `reader` cannot be read to string,
266 /// - no variant of [`Mtree`] can be constructed from the contents of the `reader`,
267 /// - or `schema` is [`Some`] and the [`MtreeSchema`] does not match the contents of the
268 /// `reader`.
269 fn from_reader_with_schema(
270 reader: impl std::io::Read,
271 schema: Option<MtreeSchema>,
272 ) -> Result<Self, Error> {
273 let mut buffer = Vec::new();
274 let mut buf_reader = BufReader::new(reader);
275 buf_reader
276 .read_to_end(&mut buffer)
277 .map_err(|source| Error::Io("reading ALPM-MTREE data", source))?;
278 Self::from_str_with_schema(&mtree_buffer_to_string(buffer)?, schema)
279 }
280
281 /// Creates a [`Mtree`] from string slice, optionally validated using a
282 /// [`MtreeSchema`].
283 ///
284 /// If `schema` is [`None`] attempts to detect the [`MtreeSchema`] from `s`.
285 /// Attempts to create a [`Mtree`] variant that corresponds to the [`MtreeSchema`].
286 ///
287 /// # Note
288 ///
289 /// To automatically derive the [`MtreeSchema`], use [`Mtree::from_str`].
290 ///
291 /// # Examples
292 ///
293 /// ```
294 /// use std::{fs::File, io::Write};
295 ///
296 /// use alpm_common::MetadataFile;
297 /// use alpm_mtree::{Mtree, MtreeSchema};
298 /// use alpm_types::{SchemaVersion, semver_version::Version};
299 ///
300 /// # fn main() -> testresult::TestResult {
301 /// let mtree_v2 = r#"
302 /// #mtree
303 /// /set mode=644 uid=0 gid=0 type=file
304 /// ./some_file time=1700000000.0 size=1337 sha256digest=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
305 /// ./some_link type=link link=some_file time=1700000000.0
306 /// ./some_dir type=dir time=1700000000.0
307 /// "#;
308 /// let mtree = Mtree::from_str_with_schema(
309 /// mtree_v2,
310 /// Some(MtreeSchema::V2(SchemaVersion::new(Version::new(2, 0, 0)))),
311 /// )?;
312 /// # let mtree_version = match mtree {
313 /// # Mtree::V1(_) => "1",
314 /// # Mtree::V2(_) => "2",
315 /// # };
316 /// # assert_eq!("2", mtree_version);
317 ///
318 /// let mtree_v1 = r#"
319 /// #mtree
320 /// /set mode=644 uid=0 gid=0 type=file
321 /// ./some_file time=1700000000.0 size=1337 sha256digest=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef md5digest=d3b07384d113edec49eaa6238ad5ff00
322 /// ./some_link type=link link=some_file time=1700000000.0
323 /// ./some_dir type=dir time=1700000000.0
324 /// "#;
325 /// let mtree = Mtree::from_str_with_schema(
326 /// mtree_v1,
327 /// Some(MtreeSchema::V1(SchemaVersion::new(Version::new(1, 0, 0)))),
328 /// )?;
329 /// # let mtree_version = match mtree {
330 /// # Mtree::V1(_) => "1",
331 /// # Mtree::V2(_) => "2",
332 /// # };
333 /// # assert_eq!("1", mtree_version);
334 /// # Ok(())
335 /// # }
336 /// ```
337 ///
338 /// # Errors
339 ///
340 /// Returns an error if
341 /// - `schema` is [`Some`] and the specified variant of [`Mtree`] cannot be constructed from
342 /// `s`,
343 /// - `schema` is [`None`] and
344 /// - a [`MtreeSchema`] cannot be derived from `s`,
345 /// - or the detected variant of [`Mtree`] cannot be constructed from `s`.
346 fn from_str_with_schema(s: &str, schema: Option<MtreeSchema>) -> Result<Self, Error> {
347 let schema = match schema {
348 Some(schema) => schema,
349 None => MtreeSchema::derive_from_str(s)?,
350 };
351
352 match schema {
353 MtreeSchema::V1(_) => Ok(Mtree::V1(parse_mtree_v2(s.to_string())?)),
354 MtreeSchema::V2(_) => Ok(Mtree::V2(parse_mtree_v2(s.to_string())?)),
355 }
356 }
357}
358
359impl Display for Mtree {
360 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361 write!(
362 f,
363 "{}",
364 match self {
365 Self::V1(paths) | Self::V2(paths) => {
366 paths.iter().fold(String::new(), |mut output, path| {
367 let _ = write!(output, "{path:?}");
368 output
369 })
370 }
371 },
372 )
373 }
374}
375
376impl FromStr for Mtree {
377 type Err = Error;
378
379 /// Creates a [`Mtree`] from string slice `s`.
380 ///
381 /// Calls [`Mtree::from_str_with_schema`] with `schema` set to [`None`].
382 ///
383 /// # Errors
384 ///
385 /// Returns an error if
386 /// - a [`MtreeSchema`] cannot be derived from `s`,
387 /// - or the detected variant of [`Mtree`] cannot be constructed from `s`.
388 fn from_str(s: &str) -> Result<Self, Self::Err> {
389 Self::from_str_with_schema(s, None)
390 }
391}