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}