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