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