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