alpm_mtree/mtree/
v2.rs

1//! Interpreter for ALPM-MTREE v1 and v2.
2
3use std::{fs::Metadata, io::Read, os::linux::fs::MetadataExt, path::PathBuf};
4
5use alpm_common::InputPath;
6use alpm_types::{Checksum, Digest, Md5Checksum, Sha256Checksum};
7use log::trace;
8use serde::{Serialize, Serializer, ser::Error as SerdeError}; // codespell:ignore ser
9use winnow::Parser;
10
11#[cfg(doc)]
12use crate::Mtree;
13pub use crate::parser::PathType;
14use crate::{
15    Error,
16    mtree::path_validation_error::PathValidationError,
17    parser::{self, SetProperty, UnsetProperty},
18};
19
20/// The prefix that is used in all ALPM-MTREE paths.
21pub const MTREE_PATH_PREFIX: &str = "./";
22
23/// Represents a `/set` line in an MTREE file.
24///
25/// This struct also internally serves as the representation of default values
26/// that're passed to all following path type lines.
27#[derive(Clone, Debug, Default)]
28pub struct PathDefaults {
29    uid: Option<u32>,
30    gid: Option<u32>,
31    mode: Option<String>,
32    path_type: Option<PathType>,
33}
34
35impl PathDefaults {
36    /// Apply a parsed `/set` statement's properties onto the current set of [PathDefaults].
37    fn apply_set(&mut self, properties: Vec<SetProperty>) {
38        for property in properties {
39            match property {
40                SetProperty::Uid(uid) => self.uid = Some(uid),
41                SetProperty::Gid(gid) => self.gid = Some(gid),
42                SetProperty::Mode(mode) => self.mode = Some(mode.to_string()),
43                SetProperty::Type(path_type) => self.path_type = Some(path_type),
44            }
45        }
46    }
47
48    /// Apply a parsed `/unset` statement's properties onto the current set of [PathDefaults].
49    fn apply_unset(&mut self, properties: Vec<UnsetProperty>) {
50        for property in properties {
51            match property {
52                UnsetProperty::Uid => self.uid = None,
53                UnsetProperty::Gid => self.gid = None,
54                UnsetProperty::Mode => self.mode = None,
55                UnsetProperty::Type => self.path_type = None,
56            }
57        }
58    }
59}
60
61/// Validates common path features against relevant [`Mtree`] data.
62///
63/// Returns a list of zero or more [`PathValidationError`]s.
64/// Checks that
65///
66/// - `mtree_time` matches the modification time available in `metadata`,
67/// - `mtree_uid` matches the UID available in the `metadata`,
68/// - `mtree_gid` matches the GID available in the `metadata`,
69/// - and the mode available in `metadata` ends in `mtree_mode`.
70fn validate_path_common(
71    mtree_path: impl AsRef<std::path::Path>,
72    mtree_time: i64,
73    mtree_uid: u32,
74    mtree_gid: u32,
75    mtree_mode: &str,
76    path: impl AsRef<std::path::Path>,
77    metadata: &Metadata,
78) -> Vec<PathValidationError> {
79    let mtree_path = mtree_path.as_ref();
80    let path = path.as_ref();
81    let mut errors = Vec::new();
82
83    // Ensure that the path modification time recorded in the ALPM-MTREE data matches the
84    // on-disk file.
85    if mtree_time != metadata.st_mtime() {
86        errors.push(PathValidationError::PathTimeMismatch {
87            mtree_path: mtree_path.to_path_buf(),
88            mtree_time,
89            path: path.to_path_buf(),
90            path_time: metadata.st_mtime(),
91        });
92    }
93
94    // Ensure that the path UID recorded in the ALPM-MTREE data matches the
95    // on-disk file.
96    if mtree_uid != metadata.st_uid() {
97        errors.push(PathValidationError::PathUidMismatch {
98            mtree_path: mtree_path.to_path_buf(),
99            mtree_uid,
100            path: path.to_path_buf(),
101            path_uid: metadata.st_uid(),
102        });
103    }
104
105    // Ensure that the path GID recorded in the ALPM-MTREE data matches the
106    // on-disk file.
107    if mtree_gid != metadata.st_gid() {
108        errors.push(PathValidationError::PathGidMismatch {
109            mtree_path: mtree_path.to_path_buf(),
110            mtree_gid,
111            path: path.to_path_buf(),
112            path_gid: metadata.st_gid(),
113        });
114    }
115
116    // Ensure that the path mode recorded in the ALPM-MTREE data matches the
117    // on-disk file.
118    let path_mode = format!("{:o}", metadata.st_mode());
119    if !path_mode.ends_with(mtree_mode) {
120        errors.push(PathValidationError::PathModeMismatch {
121            mtree_path: mtree_path.to_path_buf(),
122            mtree_mode: mtree_mode.to_string(),
123            path: path.to_path_buf(),
124            path_mode: path_mode.to_string(),
125        });
126    }
127
128    errors
129}
130
131/// Normalizes a [`std::path::Path`] by stripping the prefix [`MTREE_PATH_PREFIX`].
132///
133/// # Errors
134///
135/// Returns an [`alpm_common::Error`] if the prefix can not be stripped.
136fn normalize_mtree_path(path: &std::path::Path) -> Result<&std::path::Path, alpm_common::Error> {
137    path.strip_prefix(MTREE_PATH_PREFIX)
138        .map_err(|source| alpm_common::Error::PathStripPrefix {
139            prefix: PathBuf::from(MTREE_PATH_PREFIX),
140            path: path.to_path_buf(),
141            source,
142        })
143}
144
145/// Returns the [`Metadata`] of a [`std::path::Path`].
146///
147/// Uses [`Path::symlink_metadata`][`std::path::Path::symlink_metadata`] if `is_symlink` is `true`,
148/// else uses [`Path::metadata`][`std::path::Path::metadata`] to retrieve the [`Metadata`] of
149/// `path`.
150///
151/// # Errors
152///
153/// Returns a [`PathValidationError`] if the metadata of `path` cannot be retrieved.
154fn path_metadata(
155    path: impl AsRef<std::path::Path>,
156    is_symlink: bool,
157) -> Result<Metadata, PathValidationError> {
158    let path = path.as_ref();
159
160    if is_symlink {
161        path.symlink_metadata()
162            .map_err(|source| PathValidationError::PathMetadata {
163                path: path.to_path_buf(),
164                source,
165            })
166    } else {
167        path.metadata()
168            .map_err(|source| PathValidationError::PathMetadata {
169                path: path.to_path_buf(),
170                source,
171            })
172    }
173}
174
175/// A directory type path statement in an mtree file.
176#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
177pub struct Directory {
178    /// The path of the directory.
179    pub path: PathBuf,
180    /// The user ID of the directory.
181    pub uid: u32,
182    /// The group ID of the directory.
183    pub gid: u32,
184    /// The file mode of the directory.
185    pub mode: String,
186    /// The modification time of the directory in seconds since the epoch.
187    pub time: i64,
188}
189
190impl Directory {
191    /// Checks whether [`InputPath`] equals `self`.
192    ///
193    /// More specifically, checks that
194    ///
195    /// - [`MTREE_PATH_PREFIX`] can be stripped from `self.path`,
196    /// - [`InputPath::path`] and the stripped `self.path` match,
197    /// - [`InputPath::to_path_buf`] exists,
198    /// - metadata can be retrieved for [`InputPath::to_path_buf`],
199    /// - [`InputPath::to_path_buf`] is a directory,
200    /// - the modification time of [`InputPath::to_path_buf`] matches that of `self.time`,
201    /// - the UID of [`InputPath::to_path_buf`] matches that of `self.uid`,
202    /// - the GID of [`InputPath::to_path_buf`] matches that of `self.gid`,
203    /// - the mode of [`InputPath::to_path_buf`] matches that of `self.mode`.
204    ///
205    /// # Errors
206    ///
207    /// Returns a list of [`PathValidationError`]s if issues have been found during validation of
208    /// `input_path`.
209    pub fn equals_path(&self, input_path: &InputPath) -> Result<(), Vec<PathValidationError>> {
210        let base_dir = input_path.base_dir();
211        let path = input_path.path();
212        let mut errors = Vec::new();
213
214        trace!(
215            "Comparing ALPM-MTREE directory path {self:?} with path {path:?} below {base_dir:?}"
216        );
217
218        // Normalize the ALPM-MTREE path.
219        let mtree_path = match normalize_mtree_path(self.path.as_path()) {
220            Ok(mtree_path) => mtree_path,
221            Err(error) => {
222                errors.push(error.into());
223                // Return early, as the ALPM-MTREE data is not as it should be.
224                return Err(errors);
225            }
226        };
227
228        // Ensure `self.path` and `path` match.
229        if mtree_path != path {
230            errors.push(PathValidationError::PathMismatch {
231                mtree_path: self.path.clone(),
232                path: path.to_path_buf(),
233            });
234            // Return early as the paths mismatch.
235            return Err(errors);
236        }
237
238        let path = input_path.to_path_buf();
239
240        // Ensure path exists.
241        if !path.exists() {
242            errors.push(PathValidationError::PathMissing {
243                mtree_path: self.path.clone(),
244                path: path.clone(),
245            });
246            // Return early, as there is no reason to continue doing file checks.
247            return Err(errors);
248        }
249
250        // Retrieve metadata of directory.
251        let metadata = match path_metadata(path.as_path(), false) {
252            Ok(metadata) => metadata,
253            Err(error) => {
254                errors.push(error);
255                // Return early, as the following checks are based on metadata.
256                return Err(errors);
257            }
258        };
259
260        // Ensure that the on-disk path is a directory.
261        if !metadata.is_dir() {
262            errors.push(PathValidationError::PathNotADir {
263                mtree_path: mtree_path.to_path_buf(),
264                path: path.to_path_buf(),
265            });
266            // Return early, because further checks are (mostly) based on whether this is a
267            // directory.
268            return Err(errors);
269        }
270
271        let mut common_errors = validate_path_common(
272            mtree_path,
273            self.time,
274            self.uid,
275            self.gid,
276            &self.mode,
277            path.as_path(),
278            &metadata,
279        );
280        errors.append(&mut common_errors);
281
282        if errors.is_empty() {
283            Ok(())
284        } else {
285            Err(errors)
286        }
287    }
288}
289
290/// A file type path statement in an mtree file.
291///
292/// The md5_digest is accepted for backwards compatibility reasons in v2 as well.
293#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
294pub struct File {
295    /// The path of the file.
296    pub path: PathBuf,
297    /// The user ID of the file.
298    pub uid: u32,
299    /// The group ID of the file.
300    pub gid: u32,
301    /// The file mode of the file.
302    pub mode: String,
303    /// The size of the file in bytes.
304    pub size: u64,
305    /// The modification time of the file in seconds since the epoch.
306    pub time: i64,
307    #[serde(
308        skip_serializing_if = "Option::is_none",
309        serialize_with = "serialize_optional_checksum_as_hex"
310    )]
311    /// The optional MD-5 hash digest of the file.
312    pub md5_digest: Option<Md5Checksum>,
313    /// The SHA-256 hash digest of the file.
314    #[serde(serialize_with = "serialize_checksum_as_hex")]
315    pub sha256_digest: Sha256Checksum,
316}
317
318impl File {
319    /// Checks whether [`InputPath`] equals `self`.
320    ///
321    /// More specifically, checks that
322    ///
323    /// - [`MTREE_PATH_PREFIX`] can be stripped from `self.path`,
324    /// - [`InputPath::path`] and the stripped `self.path` match,
325    /// - [`InputPath::to_path_buf`] exists,
326    /// - metadata can be retrieved for [`InputPath::to_path_buf`],
327    /// - [`InputPath::to_path_buf`] is a file,
328    /// - the size of [`InputPath::to_path_buf`] matches that of `self.size`,
329    /// - the SHA-256 hash digest of [`InputPath::to_path_buf`] matches that of
330    ///   `self.sha256_digest`,
331    /// - the modification time of [`InputPath::to_path_buf`] matches that of `self.time`,
332    /// - the UID of [`InputPath::to_path_buf`] matches that of `self.uid`,
333    /// - the GID of [`InputPath::to_path_buf`] matches that of `self.gid`,
334    /// - the mode of [`InputPath::to_path_buf`] matches that of `self.mode`.
335    ///
336    /// # Errors
337    ///
338    /// Returns a list of [`PathValidationError`]s if issues have been found during validation of
339    /// `input_path`.
340    pub fn equals_path(&self, input_path: &InputPath) -> Result<(), Vec<PathValidationError>> {
341        let base_dir = input_path.base_dir();
342        let path = input_path.path();
343        let mut errors = Vec::new();
344
345        trace!("Comparing ALPM-MTREE file path {self:?} with path {path:?} below {base_dir:?}");
346
347        // Normalize the ALPM-MTREE path.
348        let mtree_path = match normalize_mtree_path(self.path.as_path()) {
349            Ok(mtree_path) => mtree_path,
350            Err(error) => {
351                errors.push(error.into());
352                // Return early, as the ALPM-MTREE data is not as it should be.
353                return Err(errors);
354            }
355        };
356
357        // Ensure `self.path` and `path` match.
358        if mtree_path != path {
359            errors.push(PathValidationError::PathMismatch {
360                mtree_path: self.path.clone(),
361                path: path.to_path_buf(),
362            });
363            // Return early as the paths mismatch.
364            return Err(errors);
365        }
366
367        let path = input_path.to_path_buf();
368
369        // Ensure path exists.
370        if !path.exists() {
371            errors.push(PathValidationError::PathMissing {
372                mtree_path: self.path.clone(),
373                path: path.clone(),
374            });
375            // Return early, as there is no reason to continue doing file checks.
376            return Err(errors);
377        }
378
379        // Retrieve metadata of file.
380        let metadata = match path_metadata(path.as_path(), false) {
381            Ok(metadata) => metadata,
382            Err(error) => {
383                errors.push(error);
384                // Return early, as the following checks are based on metadata.
385                return Err(errors);
386            }
387        };
388
389        // Ensure that the on-disk path is a file.
390        if !metadata.is_file() {
391            errors.push(PathValidationError::PathNotAFile {
392                mtree_path: mtree_path.to_path_buf(),
393                path: path.to_path_buf(),
394            });
395            // Return early, because further checks are (mostly) based on whether this is a file.
396            return Err(errors);
397        }
398
399        // Create the hash digest.
400        let path_digest = {
401            let mut file = match std::fs::File::open(path.as_path()) {
402                Ok(file) => file,
403                Err(source) => {
404                    errors.push(PathValidationError::CreateHashDigest {
405                        path: path.to_path_buf(),
406                        source,
407                    });
408                    // Return early, because not being able to open the file points at file system
409                    // issues.
410                    return Err(errors);
411                }
412            };
413
414            let mut buf = Vec::new();
415            match file.read_to_end(&mut buf) {
416                Ok(_) => {}
417                Err(source) => {
418                    errors.push(PathValidationError::CreateHashDigest {
419                        path: path.to_path_buf(),
420                        source,
421                    });
422                    // Return early, because not being able to read the file points at file system
423                    // issues.
424                    return Err(errors);
425                }
426            }
427
428            Sha256Checksum::calculate_from(buf)
429        };
430
431        // Compare the file size.
432        if metadata.st_size() != self.size {
433            errors.push(PathValidationError::PathSizeMismatch {
434                mtree_path: self.path.clone(),
435                mtree_size: self.size,
436                path: path.to_path_buf(),
437                path_size: metadata.st_size(),
438            });
439        }
440
441        // Compare the hash digests.
442        if self.sha256_digest != path_digest {
443            errors.push(PathValidationError::PathDigestMismatch {
444                mtree_path: mtree_path.to_path_buf(),
445                mtree_digest: self.sha256_digest.clone(),
446                path: path.to_path_buf(),
447                path_digest,
448            });
449        }
450
451        let mut common_errors = validate_path_common(
452            mtree_path,
453            self.time,
454            self.uid,
455            self.gid,
456            &self.mode,
457            path.as_path(),
458            &metadata,
459        );
460        errors.append(&mut common_errors);
461
462        if errors.is_empty() {
463            Ok(())
464        } else {
465            Err(errors)
466        }
467    }
468}
469
470/// Serialize an `Option<Checksum<D>>` as a HexString.
471///
472/// # Errors
473///
474/// Returns an error if the `checksum` can not be serialized using the `serializer`.
475fn serialize_checksum_as_hex<S, D>(checksum: &Checksum<D>, serializer: S) -> Result<S::Ok, S::Error>
476where
477    S: Serializer,
478    D: Digest,
479{
480    let hex_string = checksum.to_string();
481    serializer.serialize_str(&hex_string)
482}
483
484/// Serialize an `Option<Checksum<D>>`
485///
486/// Sadly this is needed in addition to the function above, even though we know that it won't be
487/// called due to the `skip_serializing_if` check above.
488fn serialize_optional_checksum_as_hex<S, D>(
489    checksum: &Option<Checksum<D>>,
490    serializer: S,
491) -> Result<S::Ok, S::Error>
492where
493    S: Serializer,
494    D: Digest,
495{
496    let hex_string = checksum
497        .as_ref()
498        .ok_or_else(|| S::Error::custom("Empty checksums won't be serialized"))?
499        .to_string();
500    serializer.serialize_str(&hex_string)
501}
502
503/// A link type path in an mtree file that points to a file somewhere on the system.
504#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
505pub struct Link {
506    /// The path of the symlink.
507    pub path: PathBuf,
508    /// The user ID of the symlink.
509    pub uid: u32,
510    /// The group ID of the symlink.
511    pub gid: u32,
512    /// The file mode of the symlink.
513    pub mode: String,
514    /// The modification time of the symlink in seconds since the epoch.
515    pub time: i64,
516    /// The target path of the symlink.
517    pub link_path: PathBuf,
518}
519
520impl Link {
521    /// Checks whether [`InputPath`] equals `self`.
522    ///
523    /// More specifically, checks that
524    ///
525    /// - [`MTREE_PATH_PREFIX`] can be stripped from `self.path`,
526    /// - [`InputPath::path`] and the stripped `self.path` match,
527    /// - [`InputPath::to_path_buf`] exists and is a symlink,
528    /// - metadata can be retrieved for [`InputPath::to_path_buf`],
529    /// - the link path of [`InputPath::to_path_buf`] matches that of `self.link_path`,
530    /// - the modification time of [`InputPath::to_path_buf`] matches that of `self.time`,
531    /// - the UID of [`InputPath::to_path_buf`] matches that of `self.uid`,
532    /// - the GID of [`InputPath::to_path_buf`] matches that of `self.gid`,
533    /// - the mode of [`InputPath::to_path_buf`] matches that of `self.mode`.
534    ///
535    /// # Errors
536    ///
537    /// Returns a list of [`PathValidationError`]s if issues have been found during validation of
538    /// `input_path`.
539    pub fn equals_path(&self, input_path: &InputPath) -> Result<(), Vec<PathValidationError>> {
540        let base_dir = input_path.base_dir();
541        let path = input_path.path();
542        let mut errors = Vec::new();
543
544        trace!("Comparing ALPM-MTREE symlink path {self:?} with path {path:?} below {base_dir:?}");
545
546        // Normalize the ALPM-MTREE path.
547        let mtree_path = match normalize_mtree_path(self.path.as_path()) {
548            Ok(mtree_path) => mtree_path,
549            Err(error) => {
550                errors.push(error.into());
551                // Return early, as the ALPM-MTREE data is not as it should be.
552                return Err(errors);
553            }
554        };
555
556        // Ensure `self.path` and `path` match.
557        if mtree_path != path {
558            errors.push(PathValidationError::PathMismatch {
559                mtree_path: self.path.clone(),
560                path: path.to_path_buf(),
561            });
562            // Return early as the paths mismatch.
563            return Err(errors);
564        }
565
566        let path = input_path.to_path_buf();
567
568        // Get the target path of the symlink and ensure it matches.
569        match path.read_link() {
570            Ok(link_path) => {
571                if self.link_path != link_path.as_path() {
572                    errors.push(PathValidationError::PathSymlinkMismatch {
573                        mtree_path: mtree_path.to_path_buf(),
574                        mtree_link_path: self.link_path.clone(),
575                        path: path.clone(),
576                        link_path,
577                    });
578                }
579            }
580            Err(source) => {
581                // Here we know the path is either not a symlink or does not exist.
582                errors.push(PathValidationError::ReadLink {
583                    path: path.clone(),
584                    mtree_path: mtree_path.to_path_buf(),
585                    source,
586                });
587                // Return early, as there is no reason to continue doing file checks.
588                return Err(errors);
589            }
590        }
591
592        // Retrieve metadata of symlink.
593        let metadata = match path_metadata(path.as_path(), true) {
594            Ok(metadata) => metadata,
595            Err(error) => {
596                errors.push(error);
597                // Return early, as the following checks are based on metadata.
598                return Err(errors);
599            }
600        };
601
602        let mut common_errors = validate_path_common(
603            mtree_path,
604            self.time,
605            self.uid,
606            self.gid,
607            &self.mode,
608            path.as_path(),
609            &metadata,
610        );
611        errors.append(&mut common_errors);
612
613        if errors.is_empty() {
614            Ok(())
615        } else {
616            Err(errors)
617        }
618    }
619}
620
621/// Represents the three possible types inside a path type line of an MTREE file.
622///
623/// While serializing, the type is converted into a `type` field on the inner struct.
624/// This means that `Vec<Path>` will be serialized to a list of maps where each map has a `type`
625/// entry with the respective name.
626#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
627#[serde(tag = "type")]
628pub enum Path {
629    /// A directory.
630    #[serde(rename = "dir")]
631    Directory(Directory),
632
633    /// A file.
634    #[serde(rename = "file")]
635    File(File),
636
637    /// A symlink.
638    #[serde(rename = "link")]
639    Link(Link),
640}
641
642impl Path {
643    /// Checks whether an [`InputPath`] equals `self`.
644    ///
645    /// Depending on type of [`Path`], delegates to [`Directory::equals_path`],
646    /// [`File::equals_path`] or [`Link::equals_path`].
647    ///
648    /// # Errors
649    ///
650    /// Returns a list of [`PathValidationError`]s if issues have been found during validation of
651    /// `input_path`.
652    pub fn equals_path(&self, input_path: &InputPath) -> Result<(), Vec<PathValidationError>> {
653        match self {
654            Self::Directory(directory) => directory.equals_path(input_path),
655            Self::File(file) => file.equals_path(input_path),
656            Self::Link(link) => link.equals_path(input_path),
657        }
658    }
659
660    /// Returns the [`PathBuf`] of the [`Path`].
661    pub fn to_path_buf(&self) -> PathBuf {
662        match self {
663            Self::Directory(directory) => directory.path.clone(),
664            Self::File(file) => file.path.clone(),
665            Self::Link(link) => link.path.clone(),
666        }
667    }
668
669    /// Returns the [`std::path::Path`] of the [`Path`].
670    pub fn as_path(&self) -> &std::path::Path {
671        match self {
672            Self::Directory(directory) => directory.path.as_path(),
673            Self::File(file) => file.path.as_path(),
674            Self::Link(link) => link.path.as_path(),
675        }
676    }
677
678    /// Returns the normalized [`std::path::Path`] of the [`Path`].
679    ///
680    /// Normalization strips the prefix [`MTREE_PATH_PREFIX`].
681    ///
682    /// # Errors
683    ///
684    /// Returns an [`alpm_common::Error`] if the prefix can not be stripped.
685    pub fn as_normalized_path(&self) -> Result<&std::path::Path, alpm_common::Error> {
686        normalize_mtree_path(self.as_path())
687    }
688}
689
690impl Ord for Path {
691    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
692        let path = match self {
693            Path::Directory(dir) => dir.path.as_path(),
694            Path::File(file) => file.path.as_path(),
695            Path::Link(link) => link.path.as_path(),
696        };
697        let other_path = match other {
698            Path::Directory(dir) => dir.path.as_path(),
699            Path::File(file) => file.path.as_path(),
700            Path::Link(link) => link.path.as_path(),
701        };
702        path.cmp(other_path)
703    }
704}
705
706impl PartialOrd for Path {
707    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
708        Some(self.cmp(other))
709    }
710}
711
712/// Parse the content of an MTREE v2 file.
713///
714/// This parser is backwards compatible to `v1`, in the sense that it allows `md5` checksums, but
715/// doesn't require them.
716///
717/// # Example
718///
719/// ```
720/// use alpm_mtree::mtree::v2::parse_mtree_v2;
721///
722/// # fn main() -> Result<(), alpm_mtree::Error> {
723/// let content = r#"
724/// /set uid=0 gid=0 mode=644 type=link
725/// ./some_link link=/etc time=1706086640.0
726/// "#;
727/// let paths = parse_mtree_v2(content.to_string())?;
728/// # Ok(())
729/// # }
730/// ```
731///
732/// # Errors
733///
734/// - `Error::ParseError` if a malformed MTREE file is encountered.
735/// - `Error::InterpreterError` if there's missing fields or logical error in the parsed contents of
736///   the MTREE file.
737pub fn parse_mtree_v2(content: String) -> Result<Vec<Path>, Error> {
738    let parsed_contents = parser::mtree
739        .parse(&content)
740        .map_err(|err| Error::ParseError(format!("{err}")))?;
741
742    paths_from_parsed_content(&content, parsed_contents)
743}
744
745/// Take unsanitized parsed content and convert it to a list of sorted paths with properties.
746///
747/// This is effectively the interpreter step for mtree's declaration language.
748fn paths_from_parsed_content(
749    content: &str,
750    parsed_content: Vec<parser::Statement>,
751) -> Result<Vec<Path>, Error> {
752    let mut paths = Vec::new();
753    // Track the current default properties for paths.
754    let mut path_defaults = PathDefaults::default();
755
756    for (line_nr, parsed) in parsed_content.into_iter().enumerate() {
757        match parsed {
758            parser::Statement::Ignored => continue,
759            parser::Statement::Path { path, properties } => {
760                // Create a [Path] instance from a given path statement.
761                // Pass the content and line-nr through.
762                // The line nr is incremented by one due to `#mtree` being the first line.
763                let path = path_from_parsed(content, line_nr, &path_defaults, path, properties)?;
764                paths.push(path);
765            }
766            parser::Statement::Set(properties) => {
767                // Incorporate a new `/set` command into the current set of defaults.
768                path_defaults.apply_set(properties);
769            }
770            parser::Statement::Unset(properties) => {
771                // Incorporate a new `/unset` command into the current set of defaults.
772                path_defaults.apply_unset(properties);
773            }
774        }
775    }
776
777    // Sort paths to ensure that ALPM-MTREE paths can be compared to file system paths.
778    // Paths in a package file, as well as the input to `bsdtar` when creating an ALPM-MTREE file
779    // are also sorted.
780    // Without this the reproducibility of the data can not be guaranteed.
781    paths.sort_unstable();
782
783    Ok(paths)
784}
785
786/// Return the nth line of a given file's content.
787///
788/// # Panics
789///
790/// Panics if `line_nr` refers to a line, that does not exist in `content`.
791/// This is unlikely to ever happen, as the `content` is derived from a parsed file and therefore it
792/// is known that the specific line referenced by `line_nr` exists.
793fn content_line(content: &str, line_nr: usize) -> String {
794    let line = content.lines().nth(line_nr);
795    let Some(line) = line else {
796        unreachable!(
797            "Failed to read {line_nr} while handling an error. This should not happen, please report it as an issue."
798        );
799    };
800
801    line.to_string()
802}
803
804/// Take any given property and ensure that it's set.
805///
806/// # Errors
807///
808/// - `Error::InterpreterError` if the expected property is `None`.
809fn ensure_property<T>(
810    content: &str,
811    line_nr: usize,
812    property: Option<T>,
813    property_name: &str,
814) -> Result<T, Error> {
815    // Ensure that we know the type of the given path.
816    let Some(property) = property else {
817        return Err(Error::InterpreterError(
818            line_nr,
819            content_line(content, line_nr),
820            format!("Couldn't find property {property_name} for path."),
821        ));
822    };
823
824    Ok(property)
825}
826
827/// Create the actual final MTREE path representation from the parsed input.
828///
829/// This is the core of the mtree interpreter logic and does a few critical things:
830/// - Incorporate default properties specified by previous `/set` and `/unset` statements.
831/// - Ensure all paths have all necessary properties for the given path type.
832///
833/// The way this works is as follows:
834/// Go through all given properties and collect them in local `Option<T>` variables.
835/// Afterwards, look at the `path_type` and ensure that all necessary properties for the given
836/// path type are set.
837/// If all properties are there, initialize the respective [Path] type and return it.
838///
839/// The original content (`content`), as well as line number (`line_nr`) are passed in as well to
840/// provide detailed error messages.
841///
842/// # Errors
843///
844/// - `Error::InterpreterError` if expected properties for a given type aren't set.
845fn path_from_parsed(
846    content: &str,
847    line_nr: usize,
848    defaults: &PathDefaults,
849    path: PathBuf,
850    properties: Vec<parser::PathProperty>,
851) -> Result<Path, Error> {
852    // Copy any possible default values over.
853    let mut uid: Option<u32> = defaults.uid;
854    let mut gid: Option<u32> = defaults.gid;
855    let mut mode: Option<String> = defaults.mode.clone();
856    let mut path_type: Option<PathType> = defaults.path_type;
857
858    let mut link: Option<PathBuf> = None;
859    let mut size: Option<u64> = None;
860    let mut md5_digest: Option<Md5Checksum> = None;
861    let mut sha256_digest: Option<Sha256Checksum> = None;
862    let mut time: Option<i64> = None;
863
864    // Read all properties and set them accordingly.
865    for property in properties {
866        match property {
867            parser::PathProperty::Uid(inner) => uid = Some(inner),
868            parser::PathProperty::Gid(inner) => gid = Some(inner),
869            parser::PathProperty::Mode(inner) => mode = Some(inner.to_string()),
870            parser::PathProperty::Type(inner) => path_type = Some(inner),
871            parser::PathProperty::Size(inner) => size = Some(inner),
872            parser::PathProperty::Link(inner) => link = Some(inner),
873            parser::PathProperty::Md5Digest(checksum) => md5_digest = Some(checksum),
874            parser::PathProperty::Sha256Digest(checksum) => sha256_digest = Some(checksum),
875            parser::PathProperty::Time(inner) => time = Some(inner),
876        }
877    }
878
879    // Ensure that we know the type of the given path.
880    let Some(path_type) = path_type else {
881        return Err(Error::InterpreterError(
882            line_nr,
883            content_line(content, line_nr),
884            "Found no type for path.".to_string(),
885        ));
886    };
887
888    // Build the path based on the path type.
889    // While doing so, ensure that all required properties are set.
890    let path = match path_type {
891        PathType::Dir => Path::Directory(Directory {
892            path,
893            uid: ensure_property(content, line_nr, uid, "uid")?,
894            gid: ensure_property(content, line_nr, gid, "gid")?,
895            mode: ensure_property(content, line_nr, mode, "mode")?,
896            time: ensure_property(content, line_nr, time, "time")?,
897        }),
898        PathType::File => Path::File(File {
899            path,
900            uid: ensure_property(content, line_nr, uid, "uid")?,
901            gid: ensure_property(content, line_nr, gid, "gid")?,
902            mode: ensure_property(content, line_nr, mode, "mode")?,
903            size: ensure_property(content, line_nr, size, "size")?,
904            time: ensure_property(content, line_nr, time, "time")?,
905            md5_digest,
906            sha256_digest: ensure_property(content, line_nr, sha256_digest, "sha256_digest")?,
907        }),
908        PathType::Link => Path::Link(Link {
909            path,
910            uid: ensure_property(content, line_nr, uid, "uid")?,
911            gid: ensure_property(content, line_nr, gid, "gid")?,
912            mode: ensure_property(content, line_nr, mode, "mode")?,
913            link_path: ensure_property(content, line_nr, link, "link")?,
914            time: ensure_property(content, line_nr, time, "time")?,
915        }),
916    };
917
918    Ok(path)
919}
920
921#[cfg(test)]
922mod tests {
923    use std::{fs::create_dir, os::unix::fs::symlink};
924
925    use rstest::rstest;
926    use tempfile::tempdir;
927    use testresult::TestResult;
928
929    use super::*;
930
931    /// Succeeds to normalize a [`std::path::Path`].
932    #[rstest]
933    #[case("./test", "test")]
934    #[case("./test/foo/bar", "test/foo/bar")]
935    fn test_normalize_mtree_path_success(#[case] path: &str, #[case] expected: &str) -> TestResult {
936        let path = PathBuf::from(path);
937        let expected = PathBuf::from(expected);
938
939        assert_eq!(&expected, &normalize_mtree_path(&path)?);
940        Ok(())
941    }
942
943    /// Fails to normalize a [`std::path::Path`].
944    #[rstest]
945    #[case("test")]
946    #[case("test/foo/bar")]
947    fn test_normalize_mtree_path_failure(#[case] path: &str) -> TestResult {
948        let path = PathBuf::from(path);
949
950        match normalize_mtree_path(&path) {
951            Ok(output) => return Err(format!(
952                "Succeeded to normalize path {path:?} as {output:?}, but this should have failed!"
953            )
954            .into()),
955            Err(error) => {
956                if !matches!(
957                    error,
958                    alpm_common::Error::PathStripPrefix {
959                        prefix: _,
960                        path: _,
961                        source: _
962                    }
963                ) {
964                    return Err("Did not raise the correct error".into());
965                }
966            }
967        }
968
969        Ok(())
970    }
971
972    /// Succeeds to retrieve [`Metadata`] of a file.
973    #[test]
974    fn test_path_metadata_success() -> TestResult {
975        let tmp_dir = tempdir()?;
976        let tmp_path = tmp_dir.path();
977
978        let test_dir = tmp_path.join("dir");
979        let test_file = tmp_path.join("file.txt");
980        let test_symlink = tmp_path.join("link_file.txt");
981
982        create_dir(&test_dir)?;
983        std::fs::File::create(&test_file)?;
984        symlink(&test_file, &test_symlink)?;
985
986        if let Err(error) = path_metadata(&test_dir, false) {
987            return Err(format!(
988                "Retrieving metadata of {test_dir:?} should have succeeded, but failed:\n{error}"
989            )
990            .into());
991        }
992
993        if let Err(error) = path_metadata(&test_file, false) {
994            return Err(format!(
995                "Retrieving metadata of {test_file:?} should have succeeded, but failed:\n{error}"
996            )
997            .into());
998        }
999
1000        if let Err(error) = path_metadata(&test_symlink, false) {
1001            return Err(format!(
1002                "Retrieving metadata of {test_symlink:?} should have succeeded, but failed:\n{error}"
1003            )
1004            .into());
1005        }
1006
1007        Ok(())
1008    }
1009
1010    /// Fails to retrieve [`Metadata`] of a file.
1011    #[test]
1012    fn test_path_metadata_failure() -> TestResult {
1013        let tmp_dir = tempdir()?;
1014        let tmp_path = tmp_dir.path();
1015
1016        let test_dir = tmp_path.join("dir");
1017        let test_file = tmp_path.join("file.txt");
1018        let test_symlink = tmp_path.join("link_file.txt");
1019
1020        if let Ok(metadata) = path_metadata(&test_dir, false) {
1021            return Err(format!(
1022                "Retrieving metadata of {test_dir:?} should have failed, but succeeded:\n{metadata:?}"
1023            )
1024            .into());
1025        }
1026
1027        if let Ok(metadata) = path_metadata(&test_file, false) {
1028            return Err(format!(
1029                "Retrieving metadata of {test_file:?} should have failed, but succeeded:\n{metadata:?}"
1030            )
1031            .into());
1032        }
1033
1034        if let Ok(metadata) = path_metadata(&test_symlink, true) {
1035            return Err(format!(
1036                "Retrieving metadata of {test_symlink:?} should have failed, but succeeded:\n{metadata:?}"
1037            )
1038            .into());
1039        }
1040
1041        Ok(())
1042    }
1043}