alpm_package/
input.rs

1//! Facilities for creating a package file from input.
2
3use 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/// A single key-value pair from a type of [alpm-package] metadata file.
30///
31/// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
32#[derive(Clone, Debug)]
33pub struct MetadataKeyValue {
34    /// The file name of the metadata type.
35    pub file_name: MetadataFileName,
36    /// The key of one piece of metadata in `file_name`.
37    pub key: String,
38    /// The value associated with the `key` of one piece of metadata in `file_name`.
39    pub value: String,
40}
41
42/// A mismatch between metadata of two types of [alpm-package] metadata files.
43///
44/// Tracks two [`MetadataKeyValue`] instances that describe a mismatch in a key-value pair.
45///
46/// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
47#[derive(Clone, Debug)]
48pub struct MetadataMismatch {
49    /// A [`MetadataKeyValue`].
50    pub first: MetadataKeyValue,
51    /// Another [`MetadataKeyValue`] that differs from the `first`.
52    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/// An error that can occur when dealing with package input directories and package files.
71#[derive(Debug, thiserror::Error)]
72pub enum Error {
73    /// The hash digest of a file in an input directory no longer matches.
74    #[error(
75        "The hash digest {initial_digest} of {path:?} in package input directory {input_dir:?} has changed to {current_digest}"
76    )]
77    FileHashDigestChanged {
78        /// The relative path of a file for which the hash digest does not match.
79        path: PathBuf,
80        /// The current hash digest of the file.
81        current_digest: Sha256Checksum,
82        /// The initial hash digest of the file.
83        initial_digest: Sha256Checksum,
84        /// The path to the package input directory in which the file resides.
85        input_dir: PathBuf,
86    },
87
88    /// A file is missing in a package input directory.
89    #[error("The file {path:?} in package input directory {input_dir:?} is missing")]
90    FileIsMissing {
91        /// The relative path of the missing file.
92        path: PathBuf,
93        /// The path to the package input directory.
94        input_dir: PathBuf,
95    },
96
97    /// Two metadata files have mismatching entries.
98    #[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        /// A list of mismatches.
107        mismatches: Vec<MetadataMismatch>,
108    },
109}
110
111/// An input directory that is guaranteed to be an absolute directory.
112#[derive(Clone, Debug)]
113pub struct InputDir(PathBuf);
114
115impl InputDir {
116    /// Creates a new [`InputDir`] from `path`.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if
121    ///
122    /// - `path` is not absolute,
123    /// - `path` does not exist,
124    /// - the metadata of `path` cannot be retrieved,
125    /// - or `path` is not a directory.
126    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    /// Coerces to a Path slice.
146    ///
147    /// Delegates to [`PathBuf::as_path`].
148    pub fn as_path(&self) -> &Path {
149        self.0.as_path()
150    }
151
152    /// Converts a Path to an owned PathBuf.
153    ///
154    /// Delegates to [`Path::to_path_buf`].
155    pub fn to_path_buf(&self) -> PathBuf {
156        self.0.to_path_buf()
157    }
158
159    /// Creates an owned PathBuf with path adjoined to self.
160    ///
161    /// Delegates to [`Path::join`].
162    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
173/// Compares the hash digest of a file with the recorded data in an [`Mtree`].
174///
175/// Takes an `mtree` against which a `file_name` in `input_dir` is checked.
176/// Returns the absolute path to the file and a byte buffer that represents the contents of the
177/// file.
178///
179/// # Errors
180///
181/// Returns an error if
182///
183/// - the file path (`input_dir` + `file_name`) does not exist,
184/// - the file can not be read,
185/// - the hash digest of the file does not match that initially recorded in `mtree`,
186/// - or the file can not be found in `mtree`.
187fn 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    // Read the file to a buffer.
203    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    // Create a custom file name for searching in ALPM-MTREE entries, as they are prefixed with
210    // MTREE_PATH_PREFIX.
211    let mtree_file_name = PathBuf::from(MTREE_PATH_PREFIX).join(file_name);
212
213    // Create a SHA-256 hash digest for the file.
214    let current_digest = Sha256Checksum::calculate_from(&buf);
215
216    // Check if the initial hash digest of the file - recorded in ALPM-MTREE data - matches.
217    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
248/// Returns whether an [alpm-install-scriptlet] exists in an input directory.
249///
250/// # Errors
251///
252/// Returns an error if
253///
254/// - the file contents cannot be read to a buffer,
255/// - the hash digest of the file does not match that initially recorded in `mtree`,
256/// - or the file contents do not represent valid [alpm-install-scriptlet] data.
257///
258/// [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
259fn 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    // Validate the scriptlet.
272    check_scriptlet(&path)?;
273
274    Ok(Some(path))
275}
276
277/// Returns a [`BuildInfo`] from a BUILDINFO file in an input directory.
278///
279/// # Errors
280///
281/// Returns an error if
282///
283/// - the file does not exist,
284/// - the file contents cannot be read to a buffer,
285/// - the hash digest of the file does not match that initially recorded in `mtree`,
286/// - or the file contents do not represent valid [`BuildInfo`] data.
287fn 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
295/// Returns a [`PackageInfo`] from a PKGINFO file in an input directory.
296///
297/// # Errors
298///
299/// Returns an error if
300///
301/// - the file does not exist,
302/// - the file contents cannot be read to a buffer,
303/// - the hash digest of the file does not match that initially recorded in `mtree`,
304/// - or the file contents do not represent valid [`PackageInfo`] data.
305fn 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
313/// Returns an [`Mtree`] and its file hash digest.
314///
315/// # Errors
316///
317/// Returns an error if
318///
319/// - the file does not exist,
320/// - the file contents cannot be read to a buffer,
321/// - or the file contents do not represent valid [`Mtree`] data.
322fn 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    // Read the file to a buffer.
335    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    // Validate the metadata.
341    let mtree = Mtree::from_reader(buf.as_slice()).map_err(crate::Error::AlpmMtree)?;
342    debug!(".MTREE data:\n{mtree}");
343    // Create a hash digest for the file.
344    let mtree_digest = Sha256Checksum::calculate_from(buf);
345
346    Ok((mtree, mtree_digest))
347}
348
349/// The comparison intersection between two different types of metadata files.
350///
351/// This is used to allow for a basic data comparison between [`BuildInfo`] and [`PackageInfo`].
352#[derive(Clone, Debug)]
353pub struct MetadataComparison<'a> {
354    /// The [alpm-package-name] encoded in the metadata file.
355    ///
356    /// [alpm-package-name]: https://alpm.archlinux.page/specifications/alpm-package-name.7.html
357    pub package_name: &'a Name,
358    /// The alpm-package-base encoded in the metadata file.
359    pub package_base: &'a Name,
360    /// The [alpm-package-version] encoded in the metadata file.
361    ///
362    /// [alpm-package-version]: https://alpm.archlinux.page/specifications/alpm-package-version.7.html
363    pub version: &'a FullVersion,
364    /// The [alpm-architecture] encoded in the metadata file.
365    ///
366    /// [alpm-architecture]: https://alpm.archlinux.page/specifications/alpm-architecture.7.html
367    pub architecture: &'a Architecture,
368    /// The packager encoded in the metadata file.
369    pub packager: &'a Packager,
370    /// The date in seconds since the epoch when the package has been built as encoded in the
371    /// metadata file.
372    pub build_date: i64,
373}
374
375impl<'a> From<&'a BuildInfo> for MetadataComparison<'a> {
376    /// Creates a [`MetadataComparison`] from a [`BuildInfo`].
377    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    /// Creates a [`MetadataComparison`] from a [`PackageInfo`].
401    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
423/// Compares overlapping data of a [`BuildInfo`] and a [`PackageInfo`].
424///
425/// # Errors
426///
427/// Returns an error if there are one or more mismatches in the data provided by `build_info`
428/// and `package_info`.
429fn 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/// A package input directory.
488///
489/// An input directory must contain
490///
491/// - a valid [ALPM-MTREE] file,
492/// - a valid [BUILDINFO] file,
493/// - a valid [PKGINFO] file,
494///
495/// Further, the input directory may contain an [alpm-install-scriptlet] file and zero or more
496/// package data files (see [alpm-package]).
497///
498/// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
499/// [BUILDINFO]: https://alpm.archlinux.page/specifications/BUILDINFO.5.html
500/// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
501/// [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
502/// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
503#[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    /// Returns the input directory of the [`PackageInput`] as [`Path`] reference.
516    pub fn input_dir(&self) -> &Path {
517        self.input_dir.as_ref()
518    }
519
520    /// Returns a reference to the [`BuildInfo`] data of the [`PackageInput`].
521    ///
522    /// # Note
523    ///
524    /// The [`BuildInfo`] data relates directly to an on-disk file tracked by the
525    /// [`PackageInput`]. This method provides access to the data as present during the creation
526    /// of the [`PackageInput`]. While the data can be guaranteed to be correct, the on-disk
527    /// file may have changed between creation of the [`PackageInput`] and the call of this method.
528    pub fn build_info(&self) -> &BuildInfo {
529        &self.build_info
530    }
531
532    /// Returns a reference to the [`PackageInfo`] data of the [`PackageInput`].
533    ///
534    /// # Note
535    ///
536    /// The [`PackageInfo`] data relates directly to an on-disk file tracked by the
537    /// [`PackageInput`]. This method provides access to the data as present during the creation
538    /// of the [`PackageInput`]. While the data can be guaranteed to be correct, the on-disk
539    /// file may have changed between creation of the [`PackageInput`] and the call of this method.
540    pub fn package_info(&self) -> &PackageInfo {
541        &self.package_info
542    }
543
544    /// Returns a reference to the [`Mtree`] data of the [`PackageInput`].
545    ///
546    /// Compares the stored hash digest of the file with that of the file on disk.
547    ///
548    /// # Errors
549    ///
550    /// Returns an error if
551    ///
552    /// - the file on disk can no longer be read,
553    /// - or the file on disk has a changed hash digest.
554    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    /// Returns the optional [alpm-install-scriptlet] of the [`PackageInput`] as [`Path`] reference.
577    ///
578    /// # Note
579    ///
580    /// The [alpm-install-scriptlet] path relates directly to an on-disk file tracked by the
581    /// [`PackageInput`]. This method provides access to the data as present during the creation
582    /// of the [`PackageInput`]. While the data can be guaranteed to be correct, the on-disk
583    /// file may have changed between creation of the [`PackageInput`] and the call of this method.
584    ///
585    /// [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
586    pub fn install_scriptlet(&self) -> Option<&Path> {
587        self.scriptlet.as_deref()
588    }
589
590    /// Returns all paths relative to the [`PackageInput`]'s input directory.
591    pub fn relative_paths(&self) -> &[PathBuf] {
592        &self.relative_paths
593    }
594
595    /// Returns an [`InputPaths`] for the input directory and all relative paths contained in it.
596    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    /// Creates a [`PackageInput`] from input directory `path`.
608    ///
609    /// This function reads [ALPM-MTREE], [BUILDINFO] and [PKGINFO] files in `path`, collects the
610    /// path of an existing [alpm-install-scriptlet] and validates them.
611    /// All data files below `path` are then checked against the [ALPM-MTREE] data.
612    ///
613    /// # Errors
614    ///
615    /// Returns an error if
616    ///
617    /// - `value` is not a valid [`InputDir`],
618    /// - there is no valid [BUILDINFO] file,
619    /// - there is no valid [ALPM-MTREE] file,
620    /// - there is no valid [PKGINFO] file,
621    /// - or one of the files below `dir` does not match the [ALPM-MTREE] data.
622    ///
623    /// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
624    /// [BUILDINFO]: https://alpm.archlinux.page/specifications/BUILDINFO.5.html
625    /// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
626    /// [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
627    fn try_from(value: InputDir) -> Result<Self, Self::Error> {
628        debug!("Create PackageInput from path {value:?}");
629
630        // Get Mtree data and file digest.
631        let (mtree, mtree_digest) = get_mtree(&value)?;
632
633        // Get all relative paths in value.
634        let relative_paths = relative_files(&value, &[])?;
635        trace!("Relative files:\n{relative_paths:?}");
636
637        // When comparing with ALPM-MTREE data, exclude the ALPM-MTREE file.
638        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        // Get PackageInfo data and file digest.
646        let package_info = get_package_info(&value, &mtree)?;
647        // Get BuildInfo data and file digest.
648        let build_info = get_build_info(&value, &mtree)?;
649
650        // Compare overlapping metadata of BuildInfo and PackageInfo data.
651        compare_build_info_package_info(&build_info, &package_info)?;
652
653        // Get optional scriptlet file.
654        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    /// Ensures that a [`MetadataMismatch`] has mismatching values.
679    ///
680    /// This test is mostly here for coverage improvement.
681    #[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    /// Ensures that [`InputDir::new`] fails on relative paths, non-existing paths and non-directory
702    /// paths.
703    #[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    /// Ensures that [`InputDir::to_path_buf`] works.
732    #[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    /// Ensures that [`compare_build_info_package_info`] fails on mismatches in [`BuildInfo`] and
918    /// [`PackageInfo`].
919    #[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}