1use std::{
4 fmt::Display,
5 fs::{File, read},
6 path::{Path, PathBuf},
7};
8
9use alpm_buildinfo::BuildInfo;
10use alpm_common::{InputPaths, MetadataFile, relative_files};
11use alpm_mtree::{Mtree, mtree::v2::MTREE_PATH_PREFIX};
12use alpm_pkginfo::PackageInfo;
13use alpm_types::{
14 Architecture,
15 FullVersion,
16 INSTALL_SCRIPTLET_FILE_NAME,
17 MetadataFileName,
18 Name,
19 Packager,
20 Sha256Checksum,
21};
22use fluent_i18n::t;
23use log::{debug, trace};
24
25#[cfg(doc)]
26use crate::Package;
27use crate::scriptlet::check_scriptlet;
28
29#[derive(Clone, Debug)]
33pub struct MetadataKeyValue {
34 pub file_name: MetadataFileName,
36 pub key: String,
38 pub value: String,
40}
41
42#[derive(Clone, Debug)]
48pub struct MetadataMismatch {
49 pub first: MetadataKeyValue,
51 pub second: MetadataKeyValue,
53}
54
55impl Display for MetadataMismatch {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 write!(
58 f,
59 "{}: {} => {}\n{}: {} => {}",
60 self.first.file_name,
61 self.first.key,
62 self.first.value,
63 self.second.file_name,
64 self.second.key,
65 self.second.value
66 )
67 }
68}
69
70#[derive(Debug, thiserror::Error)]
72pub enum Error {
73 #[error(
75 "The hash digest {initial_digest} of {path:?} in package input directory {input_dir:?} has changed to {current_digest}"
76 )]
77 FileHashDigestChanged {
78 path: PathBuf,
80 current_digest: Sha256Checksum,
82 initial_digest: Sha256Checksum,
84 input_dir: PathBuf,
86 },
87
88 #[error("The file {path:?} in package input directory {input_dir:?} is missing")]
90 FileIsMissing {
91 path: PathBuf,
93 input_dir: PathBuf,
95 },
96
97 #[error(
99 "The following metadata entries are not matching:\n{}",
100 mismatches.iter().map(
101 |mismatch|
102 mismatch.to_string()
103 ).collect::<Vec<String>>().join("\n")
104 )]
105 MetadataMismatch {
106 mismatches: Vec<MetadataMismatch>,
108 },
109}
110
111#[derive(Clone, Debug)]
113pub struct InputDir(PathBuf);
114
115impl InputDir {
116 pub fn new(path: PathBuf) -> Result<Self, crate::Error> {
127 if !path.is_absolute() {
128 return Err(alpm_common::Error::NonAbsolutePaths {
129 paths: vec![path.clone()],
130 }
131 .into());
132 }
133
134 if !path.exists() {
135 return Err(crate::Error::PathDoesNotExist { path: path.clone() });
136 }
137
138 if !path.is_dir() {
139 return Err(alpm_common::Error::NotADirectory { path: path.clone() }.into());
140 }
141
142 Ok(Self(path))
143 }
144
145 pub fn as_path(&self) -> &Path {
149 self.0.as_path()
150 }
151
152 pub fn to_path_buf(&self) -> PathBuf {
156 self.0.to_path_buf()
157 }
158
159 pub fn join(&self, path: impl AsRef<Path>) -> PathBuf {
163 self.0.join(path)
164 }
165}
166
167impl AsRef<Path> for InputDir {
168 fn as_ref(&self) -> &Path {
169 &self.0
170 }
171}
172
173fn compare_digests(
188 mtree: &Mtree,
189 input_dir: &InputDir,
190 file_name: &str,
191) -> Result<(PathBuf, Vec<u8>), crate::Error> {
192 let path = input_dir.join(file_name);
193
194 if !path.exists() {
195 return Err(Error::FileIsMissing {
196 path: PathBuf::from(file_name),
197 input_dir: input_dir.to_path_buf(),
198 }
199 .into());
200 }
201
202 let buf = read(path.as_path()).map_err(|source| crate::Error::IoPath {
204 path: path.clone(),
205 context: t!("error-io-read-file"),
206 source,
207 })?;
208
209 let mtree_file_name = PathBuf::from(MTREE_PATH_PREFIX).join(file_name);
212
213 let current_digest = Sha256Checksum::calculate_from(&buf);
215
216 if let Some(initial_digest) = match mtree {
218 Mtree::V1(paths) => paths.as_slice(),
219 Mtree::V2(paths) => paths.as_slice(),
220 }
221 .iter()
222 .find_map(|path| match path {
223 alpm_mtree::mtree::v2::Path::File(file) if file.path == mtree_file_name => {
224 Some(file.sha256_digest.clone())
225 }
226 _ => None,
227 }) {
228 if initial_digest != current_digest {
229 return Err(Error::FileHashDigestChanged {
230 path: PathBuf::from(file_name),
231 current_digest,
232 initial_digest,
233 input_dir: input_dir.to_path_buf(),
234 }
235 .into());
236 }
237 } else {
238 return Err(Error::FileIsMissing {
239 path: PathBuf::from(file_name),
240 input_dir: input_dir.to_path_buf(),
241 }
242 .into());
243 };
244
245 Ok((path, buf))
246}
247
248fn get_install_scriptlet(
260 input_dir: &InputDir,
261 mtree: &Mtree,
262) -> Result<Option<PathBuf>, crate::Error> {
263 debug!("Check that an alpm-install-scriptlet is valid if it exists in {input_dir:?}.");
264
265 let path = match compare_digests(mtree, input_dir, INSTALL_SCRIPTLET_FILE_NAME) {
266 Err(crate::Error::Input(Error::FileIsMissing { .. })) => return Ok(None),
267 Err(error) => return Err(error),
268 Ok((path, _buf)) => path,
269 };
270
271 check_scriptlet(&path)?;
273
274 Ok(Some(path))
275}
276
277fn get_build_info(input_dir: &InputDir, mtree: &Mtree) -> Result<BuildInfo, crate::Error> {
288 debug!("Check that a valid BUILDINFO file exists in {input_dir:?}.");
289
290 let (_path, buf) = compare_digests(mtree, input_dir, MetadataFileName::BuildInfo.as_ref())?;
291
292 BuildInfo::from_reader(buf.as_slice()).map_err(crate::Error::AlpmBuildInfo)
293}
294
295fn get_package_info(input_dir: &InputDir, mtree: &Mtree) -> Result<PackageInfo, crate::Error> {
306 debug!("Check that a valid PKGINFO file exists in {input_dir:?}.");
307
308 let (_path, buf) = compare_digests(mtree, input_dir, MetadataFileName::PackageInfo.as_ref())?;
309
310 PackageInfo::from_reader(buf.as_slice()).map_err(crate::Error::AlpmPackageInfo)
311}
312
313fn get_mtree(input_dir: &InputDir) -> Result<(Mtree, Sha256Checksum), crate::Error> {
323 debug!("Check that a valid .MTREE file exists in {input_dir:?}.");
324 let file_name = PathBuf::from(MetadataFileName::Mtree.as_ref());
325 let path = input_dir.join(file_name.as_path());
326
327 if !path.exists() {
328 return Err(Error::FileIsMissing {
329 path: file_name,
330 input_dir: input_dir.to_path_buf(),
331 }
332 .into());
333 }
334 let buf = read(path.as_path()).map_err(|source| crate::Error::IoPath {
336 path,
337 context: t!("error-io-read-mtree"),
338 source,
339 })?;
340 let mtree = Mtree::from_reader(buf.as_slice()).map_err(crate::Error::AlpmMtree)?;
342 debug!(".MTREE data:\n{mtree}");
343 let mtree_digest = Sha256Checksum::calculate_from(buf);
345
346 Ok((mtree, mtree_digest))
347}
348
349#[derive(Clone, Debug)]
353pub struct MetadataComparison<'a> {
354 pub package_name: &'a Name,
358 pub package_base: &'a Name,
360 pub version: &'a FullVersion,
364 pub architecture: &'a Architecture,
368 pub packager: &'a Packager,
370 pub build_date: i64,
373}
374
375impl<'a> From<&'a BuildInfo> for MetadataComparison<'a> {
376 fn from(value: &'a BuildInfo) -> Self {
378 match value {
379 BuildInfo::V1(inner) => MetadataComparison {
380 package_name: &inner.pkgname,
381 package_base: &inner.pkgbase,
382 version: &inner.pkgver,
383 architecture: &inner.pkgarch,
384 packager: &inner.packager,
385 build_date: inner.builddate,
386 },
387 BuildInfo::V2(inner) => MetadataComparison {
388 package_name: &inner.pkgname,
389 package_base: &inner.pkgbase,
390 version: &inner.pkgver,
391 architecture: &inner.pkgarch,
392 packager: &inner.packager,
393 build_date: inner.builddate,
394 },
395 }
396 }
397}
398
399impl<'a> From<&'a PackageInfo> for MetadataComparison<'a> {
400 fn from(value: &'a PackageInfo) -> Self {
402 match value {
403 PackageInfo::V1(inner) => MetadataComparison {
404 package_name: &inner.pkgname,
405 package_base: &inner.pkgbase,
406 version: &inner.pkgver,
407 architecture: &inner.arch,
408 packager: &inner.packager,
409 build_date: inner.builddate,
410 },
411 PackageInfo::V2(inner) => MetadataComparison {
412 package_name: &inner.pkgname,
413 package_base: &inner.pkgbase,
414 version: &inner.pkgver,
415 architecture: &inner.arch,
416 packager: &inner.packager,
417 build_date: inner.builddate,
418 },
419 }
420 }
421}
422
423fn compare_build_info_package_info(
430 build_info: &BuildInfo,
431 package_info: &PackageInfo,
432) -> Result<(), crate::Error> {
433 let build_info_compare: MetadataComparison<'_> = build_info.into();
434 let package_info_compare: MetadataComparison<'_> = package_info.into();
435 let mut mismatches = Vec::new();
436
437 let comparisons = [
438 (
439 (build_info_compare.package_name.to_string(), "pkgname"),
440 (package_info_compare.package_name.to_string(), "pkgname"),
441 ),
442 (
443 (build_info_compare.package_base.to_string(), "pkgbase"),
444 (package_info_compare.package_base.to_string(), "pkgbase"),
445 ),
446 (
447 (build_info_compare.version.to_string(), "pkgver"),
448 (package_info_compare.version.to_string(), "pkgver"),
449 ),
450 (
451 (build_info_compare.architecture.to_string(), "pkgarch"),
452 (package_info_compare.architecture.to_string(), "arch"),
453 ),
454 (
455 (build_info_compare.packager.to_string(), "packager"),
456 (package_info_compare.packager.to_string(), "packager"),
457 ),
458 (
459 (build_info_compare.build_date.to_string(), "builddate"),
460 (package_info_compare.build_date.to_string(), "builddate"),
461 ),
462 ];
463 for comparison in comparisons {
464 if comparison.0.0 != comparison.1.0 {
465 mismatches.push(MetadataMismatch {
466 first: MetadataKeyValue {
467 file_name: MetadataFileName::BuildInfo,
468 key: comparison.0.1.to_string(),
469 value: comparison.0.0,
470 },
471 second: MetadataKeyValue {
472 file_name: MetadataFileName::PackageInfo,
473 key: comparison.1.1.to_string(),
474 value: comparison.1.0,
475 },
476 })
477 }
478 }
479
480 if !mismatches.is_empty() {
481 return Err(Error::MetadataMismatch { mismatches }.into());
482 }
483
484 Ok(())
485}
486
487#[derive(Clone, Debug)]
504pub struct PackageInput {
505 build_info: BuildInfo,
506 package_info: PackageInfo,
507 mtree: Mtree,
508 mtree_digest: Sha256Checksum,
509 input_dir: InputDir,
510 scriptlet: Option<PathBuf>,
511 relative_paths: Vec<PathBuf>,
512}
513
514impl PackageInput {
515 pub fn input_dir(&self) -> &Path {
517 self.input_dir.as_ref()
518 }
519
520 pub fn build_info(&self) -> &BuildInfo {
529 &self.build_info
530 }
531
532 pub fn package_info(&self) -> &PackageInfo {
541 &self.package_info
542 }
543
544 pub fn mtree(&self) -> Result<&Mtree, crate::Error> {
555 let file_name = PathBuf::from(MetadataFileName::Mtree.as_ref());
556 let path = self.input_dir.join(file_name.as_path());
557 let file = File::open(&path).map_err(|source| crate::Error::IoPath {
558 path: path.clone(),
559 context: t!("error-io-read-mtree"),
560 source,
561 })?;
562 let current_digest =
563 Sha256Checksum::calculate_from_reader(file).map_err(|source| crate::Error::IoPath {
564 path: path.clone(),
565 context: t!("error-io-read-mtree"),
566 source,
567 })?;
568 if current_digest != self.mtree_digest {
569 return Err(Error::FileHashDigestChanged {
570 path: file_name,
571 current_digest,
572 initial_digest: self.mtree_digest.clone(),
573 input_dir: self.input_dir.to_path_buf(),
574 }
575 .into());
576 }
577
578 Ok(&self.mtree)
579 }
580
581 pub fn install_scriptlet(&self) -> Option<&Path> {
592 self.scriptlet.as_deref()
593 }
594
595 pub fn relative_paths(&self) -> &[PathBuf] {
597 &self.relative_paths
598 }
599
600 pub fn input_paths(&self) -> Result<InputPaths<'_, '_>, crate::Error> {
602 Ok(InputPaths::new(
603 self.input_dir.as_path(),
604 &self.relative_paths,
605 )?)
606 }
607}
608
609impl TryFrom<InputDir> for PackageInput {
610 type Error = crate::Error;
611
612 fn try_from(value: InputDir) -> Result<Self, Self::Error> {
633 debug!("Create PackageInput from path {value:?}");
634
635 let (mtree, mtree_digest) = get_mtree(&value)?;
637
638 let relative_paths = relative_files(&value, &[])?;
640 trace!("Relative files:\n{relative_paths:?}");
641
642 let relative_mtree_paths: Vec<PathBuf> = relative_paths
644 .iter()
645 .filter(|path| path.as_os_str() != MetadataFileName::Mtree.as_ref())
646 .cloned()
647 .collect();
648 mtree.validate_paths(&InputPaths::new(value.as_ref(), &relative_mtree_paths)?)?;
649
650 let package_info = get_package_info(&value, &mtree)?;
652 let build_info = get_build_info(&value, &mtree)?;
654
655 compare_build_info_package_info(&build_info, &package_info)?;
657
658 let scriptlet = get_install_scriptlet(&value, &mtree)?;
660
661 Ok(Self {
662 build_info,
663 package_info,
664 mtree,
665 mtree_digest,
666 input_dir: value,
667 scriptlet,
668 relative_paths,
669 })
670 }
671}
672
673#[cfg(test)]
674mod tests {
675 use std::{fs::File, str::FromStr};
676
677 use rstest::rstest;
678 use tempfile::tempdir;
679 use testresult::TestResult;
680
681 use super::*;
682
683 #[test]
687 fn metadata_mismatch() -> TestResult {
688 let mismatch = MetadataMismatch {
689 first: MetadataKeyValue {
690 file_name: MetadataFileName::BuildInfo,
691 key: "pkgname".to_string(),
692 value: "example".to_string(),
693 },
694 second: MetadataKeyValue {
695 file_name: MetadataFileName::PackageInfo,
696 key: "pkgname".to_string(),
697 value: "other-example".to_string(),
698 },
699 };
700
701 assert_eq!(mismatch.first.key, mismatch.second.key);
702 assert_ne!(mismatch.first.value, mismatch.second.value);
703 Ok(())
704 }
705
706 #[test]
709 fn input_dir_new_fails() -> TestResult {
710 assert!(matches!(
711 InputDir::new(PathBuf::from("test")),
712 Err(crate::Error::AlpmCommon(
713 alpm_common::Error::NonAbsolutePaths { paths: _ }
714 ))
715 ));
716
717 let temp_dir = tempdir()?;
718 let non_existing_path = temp_dir.path().join("non-existing");
719 assert!(matches!(
720 InputDir::new(non_existing_path),
721 Err(crate::Error::PathDoesNotExist { path: _ })
722 ));
723
724 let file_path = temp_dir.path().join("non-existing");
725 let _file = File::create(&file_path)?;
726 assert!(matches!(
727 InputDir::new(file_path),
728 Err(crate::Error::AlpmCommon(
729 alpm_common::Error::NotADirectory { path: _ }
730 ))
731 ));
732
733 Ok(())
734 }
735
736 #[test]
738 fn input_dir_to_path_buf() -> TestResult {
739 let temp_dir = tempdir()?;
740 let dir = temp_dir.path();
741 let input_dir = InputDir::new(dir.to_path_buf())?;
742
743 assert_eq!(input_dir.to_path_buf(), dir.to_path_buf());
744
745 Ok(())
746 }
747
748 const PKGNAME_MISMATCH: &[&str; 2] = &[
749 r#"
750format = 2
751builddate = 1
752builddir = /build
753startdir = /startdir/
754buildtool = devtools
755buildtoolver = 1:1.2.1-1-any
756packager = John Doe <john@example.org>
757pkgarch = any
758pkgbase = example
759pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
760pkgname = example
761pkgver = 1:1.0.0-1
762"#,
763 r#"
764pkgname = example-different
765pkgbase = example
766xdata = pkgtype=pkg
767pkgver = 1:1.0.0-1
768pkgdesc = A project that does something
769url = https://example.org/
770builddate = 1
771packager = John Doe <john@example.org>
772size = 181849963
773arch = any
774"#,
775 ];
776
777 const PKGBASE_MISMATCH: &[&str; 2] = &[
778 r#"
779format = 2
780builddate = 1
781builddir = /build
782startdir = /startdir/
783buildtool = devtools
784buildtoolver = 1:1.2.1-1-any
785packager = John Doe <john@example.org>
786pkgarch = any
787pkgbase = example
788pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
789pkgname = example
790pkgver = 1:1.0.0-1
791"#,
792 r#"
793pkgname = example
794pkgbase = example-different
795xdata = pkgtype=pkg
796pkgver = 1:1.0.0-1
797pkgdesc = A project that does something
798url = https://example.org/
799builddate = 1
800packager = John Doe <john@example.org>
801size = 181849963
802arch = any
803"#,
804 ];
805
806 const VERSION_MISMATCH: &[&str; 2] = &[
807 r#"
808format = 2
809builddate = 1
810builddir = /build
811startdir = /startdir/
812buildtool = devtools
813buildtoolver = 1:1.2.1-1-any
814packager = John Doe <john@example.org>
815pkgarch = any
816pkgbase = example
817pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
818pkgname = example
819pkgver = 1:1.0.0-1
820"#,
821 r#"
822pkgname = example
823pkgbase = example
824xdata = pkgtype=pkg
825pkgver = 1.0.0-1
826pkgdesc = A project that does something
827url = https://example.org/
828builddate = 1
829packager = John Doe <john@example.org>
830size = 181849963
831arch = any
832"#,
833 ];
834
835 const ARCHITECTURE_MISMATCH: &[&str; 2] = &[
836 r#"
837format = 2
838builddate = 1
839builddir = /build
840startdir = /startdir/
841buildtool = devtools
842buildtoolver = 1:1.2.1-1-any
843packager = John Doe <john@example.org>
844pkgarch = any
845pkgbase = example
846pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
847pkgname = example
848pkgver = 1:1.0.0-1
849"#,
850 r#"
851pkgname = example
852pkgbase = example
853xdata = pkgtype=pkg
854pkgver = 1:1.0.0-1
855pkgdesc = A project that does something
856url = https://example.org/
857builddate = 1
858packager = John Doe <john@example.org>
859size = 181849963
860arch = x86_64
861"#,
862 ];
863
864 const PACKAGER_MISMATCH: &[&str; 2] = &[
865 r#"
866format = 2
867builddate = 1
868builddir = /build
869startdir = /startdir/
870buildtool = devtools
871buildtoolver = 1:1.2.1-1-any
872packager = John Doe <john@example.org>
873pkgarch = any
874pkgbase = example
875pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
876pkgname = example
877pkgver = 1:1.0.0-1
878"#,
879 r#"
880pkgname = example
881pkgbase = example
882xdata = pkgtype=pkg
883pkgver = 1:1.0.0-1
884pkgdesc = A project that does something
885url = https://example.org/
886builddate = 1
887packager = Jane Doe <jane@example.org>
888size = 181849963
889arch = any
890"#,
891 ];
892
893 const BUILD_DATE_MISMATCH: &[&str; 2] = &[
894 r#"
895format = 2
896builddate = 1
897builddir = /build
898startdir = /startdir/
899buildtool = devtools
900buildtoolver = 1:1.2.1-1-any
901packager = John Doe <john@example.org>
902pkgarch = any
903pkgbase = example
904pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
905pkgname = example
906pkgver = 1:1.0.0-1
907"#,
908 r#"
909pkgname = example
910pkgbase = example
911xdata = pkgtype=pkg
912pkgver = 1:1.0.0-1
913pkgdesc = A project that does something
914url = https://example.org/
915builddate = 2
916packager = John Doe <john@example.org>
917size = 181849963
918arch = any
919"#,
920 ];
921
922 #[rstest]
925 #[case::pkgname_mismatch(PKGNAME_MISMATCH, ("pkgname", "pkgname"))]
926 #[case::pkgbase_mismatch(PKGBASE_MISMATCH, ("pkgbase", "pkgbase"))]
927 #[case::version_mismatch(VERSION_MISMATCH, ("pkgver", "pkgver"))]
928 #[case::architecture_mismatch(ARCHITECTURE_MISMATCH, ("pkgarch", "arch"))]
929 #[case::packager_mismatch(PACKAGER_MISMATCH, ("packager", "packager"))]
930 #[case::build_date_mismatch(BUILD_DATE_MISMATCH, ("builddate", "builddate"))]
931 fn test_compare_build_info_package_info_fails(
932 #[case] metadata: &[&str; 2],
933 #[case] expected: (&str, &str),
934 ) -> TestResult {
935 let build_info = BuildInfo::from_str(metadata[0])?;
936 let package_info = PackageInfo::from_str(metadata[1])?;
937
938 if let Err(error) = compare_build_info_package_info(&build_info, &package_info) {
939 match error {
940 crate::Error::Input(crate::input::Error::MetadataMismatch { mismatches }) => {
941 if mismatches.len() != 1 {
942 panic!("There should be exactly one metadata mismatch");
943 }
944 let Some(mismatch) = mismatches.first() else {
945 panic!("There should be at least one metadata mismatch");
946 };
947 assert_eq!(mismatch.first.key, expected.0);
948 assert_eq!(mismatch.second.key, expected.1);
949 }
950 _ => panic!("Did not return the correct error variant"),
951 }
952 } else {
953 panic!("Should have returned an error but succeeded");
954 }
955
956 Ok(())
957 }
958}