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}