1use 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}; use 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
20pub const MTREE_PATH_PREFIX: &str = "./";
22
23#[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 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 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
61fn 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 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 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 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 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
131fn 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
145fn 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#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
177pub struct Directory {
178 pub path: PathBuf,
180 pub uid: u32,
182 pub gid: u32,
184 pub mode: String,
186 pub time: i64,
188}
189
190impl Directory {
191 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 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 Err(errors);
225 }
226 };
227
228 if mtree_path != path {
230 errors.push(PathValidationError::PathMismatch {
231 mtree_path: self.path.clone(),
232 path: path.to_path_buf(),
233 });
234 return Err(errors);
236 }
237
238 let path = input_path.to_path_buf();
239
240 if !path.exists() {
242 errors.push(PathValidationError::PathMissing {
243 mtree_path: self.path.clone(),
244 path: path.clone(),
245 });
246 return Err(errors);
248 }
249
250 let metadata = match path_metadata(path.as_path(), false) {
252 Ok(metadata) => metadata,
253 Err(error) => {
254 errors.push(error);
255 return Err(errors);
257 }
258 };
259
260 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 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#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
294pub struct File {
295 pub path: PathBuf,
297 pub uid: u32,
299 pub gid: u32,
301 pub mode: String,
303 pub size: u64,
305 pub time: i64,
307 #[serde(
308 skip_serializing_if = "Option::is_none",
309 serialize_with = "serialize_optional_checksum_as_hex"
310 )]
311 pub md5_digest: Option<Md5Checksum>,
313 #[serde(serialize_with = "serialize_checksum_as_hex")]
315 pub sha256_digest: Sha256Checksum,
316}
317
318impl File {
319 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 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 Err(errors);
354 }
355 };
356
357 if mtree_path != path {
359 errors.push(PathValidationError::PathMismatch {
360 mtree_path: self.path.clone(),
361 path: path.to_path_buf(),
362 });
363 return Err(errors);
365 }
366
367 let path = input_path.to_path_buf();
368
369 if !path.exists() {
371 errors.push(PathValidationError::PathMissing {
372 mtree_path: self.path.clone(),
373 path: path.clone(),
374 });
375 return Err(errors);
377 }
378
379 let metadata = match path_metadata(path.as_path(), false) {
381 Ok(metadata) => metadata,
382 Err(error) => {
383 errors.push(error);
384 return Err(errors);
386 }
387 };
388
389 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 Err(errors);
397 }
398
399 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 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 Err(errors);
425 }
426 }
427
428 Sha256Checksum::calculate_from(buf)
429 };
430
431 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 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
470fn 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
484fn 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#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
505pub struct Link {
506 pub path: PathBuf,
508 pub uid: u32,
510 pub gid: u32,
512 pub mode: String,
514 pub time: i64,
516 pub link_path: PathBuf,
518}
519
520impl Link {
521 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 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 Err(errors);
553 }
554 };
555
556 if mtree_path != path {
558 errors.push(PathValidationError::PathMismatch {
559 mtree_path: self.path.clone(),
560 path: path.to_path_buf(),
561 });
562 return Err(errors);
564 }
565
566 let path = input_path.to_path_buf();
567
568 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 errors.push(PathValidationError::ReadLink {
583 path: path.clone(),
584 mtree_path: mtree_path.to_path_buf(),
585 source,
586 });
587 return Err(errors);
589 }
590 }
591
592 let metadata = match path_metadata(path.as_path(), true) {
594 Ok(metadata) => metadata,
595 Err(error) => {
596 errors.push(error);
597 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#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
627#[serde(tag = "type")]
628pub enum Path {
629 #[serde(rename = "dir")]
631 Directory(Directory),
632
633 #[serde(rename = "file")]
635 File(File),
636
637 #[serde(rename = "link")]
639 Link(Link),
640}
641
642impl Path {
643 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 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 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 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
712pub 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
745fn 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 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 let path = path_from_parsed(content, line_nr, &path_defaults, path, properties)?;
764 paths.push(path);
765 }
766 parser::Statement::Set(properties) => {
767 path_defaults.apply_set(properties);
769 }
770 parser::Statement::Unset(properties) => {
771 path_defaults.apply_unset(properties);
773 }
774 }
775 }
776
777 paths.sort_unstable();
782
783 Ok(paths)
784}
785
786fn 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
804fn ensure_property<T>(
810 content: &str,
811 line_nr: usize,
812 property: Option<T>,
813 property_name: &str,
814) -> Result<T, Error> {
815 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
827fn 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 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 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 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 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 #[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 #[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 #[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 #[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}