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 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 buf = read(path.as_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 = Sha256Checksum::calculate_from(buf);
563 if current_digest != self.mtree_digest {
564 return Err(Error::FileHashDigestChanged {
565 path: file_name,
566 current_digest,
567 initial_digest: self.mtree_digest.clone(),
568 input_dir: self.input_dir.to_path_buf(),
569 }
570 .into());
571 }
572
573 Ok(&self.mtree)
574 }
575
576 pub fn install_scriptlet(&self) -> Option<&Path> {
587 self.scriptlet.as_deref()
588 }
589
590 pub fn relative_paths(&self) -> &[PathBuf] {
592 &self.relative_paths
593 }
594
595 pub fn input_paths(&self) -> Result<InputPaths<'_, '_>, crate::Error> {
597 Ok(InputPaths::new(
598 self.input_dir.as_path(),
599 &self.relative_paths,
600 )?)
601 }
602}
603
604impl TryFrom<InputDir> for PackageInput {
605 type Error = crate::Error;
606
607 fn try_from(value: InputDir) -> Result<Self, Self::Error> {
628 debug!("Create PackageInput from path {value:?}");
629
630 let (mtree, mtree_digest) = get_mtree(&value)?;
632
633 let relative_paths = relative_files(&value, &[])?;
635 trace!("Relative files:\n{relative_paths:?}");
636
637 let relative_mtree_paths: Vec<PathBuf> = relative_paths
639 .iter()
640 .filter(|path| path.as_os_str() != MetadataFileName::Mtree.as_ref())
641 .cloned()
642 .collect();
643 mtree.validate_paths(&InputPaths::new(value.as_ref(), &relative_mtree_paths)?)?;
644
645 let package_info = get_package_info(&value, &mtree)?;
647 let build_info = get_build_info(&value, &mtree)?;
649
650 compare_build_info_package_info(&build_info, &package_info)?;
652
653 let scriptlet = get_install_scriptlet(&value, &mtree)?;
655
656 Ok(Self {
657 build_info,
658 package_info,
659 mtree,
660 mtree_digest,
661 input_dir: value,
662 scriptlet,
663 relative_paths,
664 })
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use std::{fs::File, str::FromStr};
671
672 use rstest::rstest;
673 use tempfile::tempdir;
674 use testresult::TestResult;
675
676 use super::*;
677
678 #[test]
682 fn metadata_mismatch() -> TestResult {
683 let mismatch = MetadataMismatch {
684 first: MetadataKeyValue {
685 file_name: MetadataFileName::BuildInfo,
686 key: "pkgname".to_string(),
687 value: "example".to_string(),
688 },
689 second: MetadataKeyValue {
690 file_name: MetadataFileName::PackageInfo,
691 key: "pkgname".to_string(),
692 value: "other-example".to_string(),
693 },
694 };
695
696 assert_eq!(mismatch.first.key, mismatch.second.key);
697 assert_ne!(mismatch.first.value, mismatch.second.value);
698 Ok(())
699 }
700
701 #[test]
704 fn input_dir_new_fails() -> TestResult {
705 assert!(matches!(
706 InputDir::new(PathBuf::from("test")),
707 Err(crate::Error::AlpmCommon(
708 alpm_common::Error::NonAbsolutePaths { paths: _ }
709 ))
710 ));
711
712 let temp_dir = tempdir()?;
713 let non_existing_path = temp_dir.path().join("non-existing");
714 assert!(matches!(
715 InputDir::new(non_existing_path),
716 Err(crate::Error::PathDoesNotExist { path: _ })
717 ));
718
719 let file_path = temp_dir.path().join("non-existing");
720 let _file = File::create(&file_path)?;
721 assert!(matches!(
722 InputDir::new(file_path),
723 Err(crate::Error::AlpmCommon(
724 alpm_common::Error::NotADirectory { path: _ }
725 ))
726 ));
727
728 Ok(())
729 }
730
731 #[test]
733 fn input_dir_to_path_buf() -> TestResult {
734 let temp_dir = tempdir()?;
735 let dir = temp_dir.path();
736 let input_dir = InputDir::new(dir.to_path_buf())?;
737
738 assert_eq!(input_dir.to_path_buf(), dir.to_path_buf());
739
740 Ok(())
741 }
742
743 const PKGNAME_MISMATCH: &[&str; 2] = &[
744 r#"
745format = 2
746builddate = 1
747builddir = /build
748startdir = /startdir/
749buildtool = devtools
750buildtoolver = 1:1.2.1-1-any
751packager = John Doe <john@example.org>
752pkgarch = any
753pkgbase = example
754pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
755pkgname = example
756pkgver = 1:1.0.0-1
757"#,
758 r#"
759pkgname = example-different
760pkgbase = example
761xdata = pkgtype=pkg
762pkgver = 1:1.0.0-1
763pkgdesc = A project that does something
764url = https://example.org/
765builddate = 1
766packager = John Doe <john@example.org>
767size = 181849963
768arch = any
769"#,
770 ];
771
772 const PKGBASE_MISMATCH: &[&str; 2] = &[
773 r#"
774format = 2
775builddate = 1
776builddir = /build
777startdir = /startdir/
778buildtool = devtools
779buildtoolver = 1:1.2.1-1-any
780packager = John Doe <john@example.org>
781pkgarch = any
782pkgbase = example
783pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
784pkgname = example
785pkgver = 1:1.0.0-1
786"#,
787 r#"
788pkgname = example
789pkgbase = example-different
790xdata = pkgtype=pkg
791pkgver = 1:1.0.0-1
792pkgdesc = A project that does something
793url = https://example.org/
794builddate = 1
795packager = John Doe <john@example.org>
796size = 181849963
797arch = any
798"#,
799 ];
800
801 const VERSION_MISMATCH: &[&str; 2] = &[
802 r#"
803format = 2
804builddate = 1
805builddir = /build
806startdir = /startdir/
807buildtool = devtools
808buildtoolver = 1:1.2.1-1-any
809packager = John Doe <john@example.org>
810pkgarch = any
811pkgbase = example
812pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
813pkgname = example
814pkgver = 1:1.0.0-1
815"#,
816 r#"
817pkgname = example
818pkgbase = example
819xdata = pkgtype=pkg
820pkgver = 1.0.0-1
821pkgdesc = A project that does something
822url = https://example.org/
823builddate = 1
824packager = John Doe <john@example.org>
825size = 181849963
826arch = any
827"#,
828 ];
829
830 const ARCHITECTURE_MISMATCH: &[&str; 2] = &[
831 r#"
832format = 2
833builddate = 1
834builddir = /build
835startdir = /startdir/
836buildtool = devtools
837buildtoolver = 1:1.2.1-1-any
838packager = John Doe <john@example.org>
839pkgarch = any
840pkgbase = example
841pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
842pkgname = example
843pkgver = 1:1.0.0-1
844"#,
845 r#"
846pkgname = example
847pkgbase = example
848xdata = pkgtype=pkg
849pkgver = 1:1.0.0-1
850pkgdesc = A project that does something
851url = https://example.org/
852builddate = 1
853packager = John Doe <john@example.org>
854size = 181849963
855arch = x86_64
856"#,
857 ];
858
859 const PACKAGER_MISMATCH: &[&str; 2] = &[
860 r#"
861format = 2
862builddate = 1
863builddir = /build
864startdir = /startdir/
865buildtool = devtools
866buildtoolver = 1:1.2.1-1-any
867packager = John Doe <john@example.org>
868pkgarch = any
869pkgbase = example
870pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
871pkgname = example
872pkgver = 1:1.0.0-1
873"#,
874 r#"
875pkgname = example
876pkgbase = example
877xdata = pkgtype=pkg
878pkgver = 1:1.0.0-1
879pkgdesc = A project that does something
880url = https://example.org/
881builddate = 1
882packager = Jane Doe <jane@example.org>
883size = 181849963
884arch = any
885"#,
886 ];
887
888 const BUILD_DATE_MISMATCH: &[&str; 2] = &[
889 r#"
890format = 2
891builddate = 1
892builddir = /build
893startdir = /startdir/
894buildtool = devtools
895buildtoolver = 1:1.2.1-1-any
896packager = John Doe <john@example.org>
897pkgarch = any
898pkgbase = example
899pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
900pkgname = example
901pkgver = 1:1.0.0-1
902"#,
903 r#"
904pkgname = example
905pkgbase = example
906xdata = pkgtype=pkg
907pkgver = 1:1.0.0-1
908pkgdesc = A project that does something
909url = https://example.org/
910builddate = 2
911packager = John Doe <john@example.org>
912size = 181849963
913arch = any
914"#,
915 ];
916
917 #[rstest]
920 #[case::pkgname_mismatch(PKGNAME_MISMATCH, ("pkgname", "pkgname"))]
921 #[case::pkgbase_mismatch(PKGBASE_MISMATCH, ("pkgbase", "pkgbase"))]
922 #[case::version_mismatch(VERSION_MISMATCH, ("pkgver", "pkgver"))]
923 #[case::architecture_mismatch(ARCHITECTURE_MISMATCH, ("pkgarch", "arch"))]
924 #[case::packager_mismatch(PACKAGER_MISMATCH, ("packager", "packager"))]
925 #[case::build_date_mismatch(BUILD_DATE_MISMATCH, ("builddate", "builddate"))]
926 fn test_compare_build_info_package_info_fails(
927 #[case] metadata: &[&str; 2],
928 #[case] expected: (&str, &str),
929 ) -> TestResult {
930 let build_info = BuildInfo::from_str(metadata[0])?;
931 let package_info = PackageInfo::from_str(metadata[1])?;
932
933 if let Err(error) = compare_build_info_package_info(&build_info, &package_info) {
934 match error {
935 crate::Error::Input(crate::input::Error::MetadataMismatch { mismatches }) => {
936 if mismatches.len() != 1 {
937 panic!("There should be exactly one metadata mismatch");
938 }
939 let Some(mismatch) = mismatches.first() else {
940 panic!("There should be at least one metadata mismatch");
941 };
942 assert_eq!(mismatch.first.key, expected.0);
943 assert_eq!(mismatch.second.key, expected.1);
944 }
945 _ => panic!("Did not return the correct error variant"),
946 }
947 } else {
948 panic!("Should have returned an error but succeeded");
949 }
950
951 Ok(())
952 }
953}