Skip to main content

alpm_package/
input.rs

1//! Facilities for creating a package file from input.
2
3use std::{
4    fmt::Display,
5    fs::{File, read},
6    path::{Path, PathBuf},
7};
8
9use alpm_buildinfo::BuildInfo;
10use alpm_common::{InputPaths, MetadataFile, relative_files};
11use alpm_mtree::{Mtree, mtree::v2::MTREE_PATH_PREFIX};
12use alpm_pkginfo::PackageInfo;
13use alpm_types::{
14    Architecture,
15    FullVersion,
16    INSTALL_SCRIPTLET_FILE_NAME,
17    MetadataFileName,
18    Name,
19    Packager,
20    Sha256Checksum,
21};
22use fluent_i18n::t;
23use log::{debug, trace};
24
25#[cfg(doc)]
26use crate::Package;
27use crate::scriptlet::check_scriptlet;
28
29/// 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 file = File::open(&path).map_err(|source| crate::Error::IoPath {
558            path: path.clone(),
559            context: t!("error-io-read-mtree"),
560            source,
561        })?;
562        let current_digest =
563            Sha256Checksum::calculate_from_reader(file).map_err(|source| crate::Error::IoPath {
564                path: path.clone(),
565                context: t!("error-io-read-mtree"),
566                source,
567            })?;
568        if current_digest != self.mtree_digest {
569            return Err(Error::FileHashDigestChanged {
570                path: file_name,
571                current_digest,
572                initial_digest: self.mtree_digest.clone(),
573                input_dir: self.input_dir.to_path_buf(),
574            }
575            .into());
576        }
577
578        Ok(&self.mtree)
579    }
580
581    /// Returns the optional [alpm-install-scriptlet] of the [`PackageInput`] as [`Path`] reference.
582    ///
583    /// # Note
584    ///
585    /// The [alpm-install-scriptlet] path relates directly to an on-disk file tracked by the
586    /// [`PackageInput`]. This method provides access to the data as present during the creation
587    /// of the [`PackageInput`]. While the data can be guaranteed to be correct, the on-disk
588    /// file may have changed between creation of the [`PackageInput`] and the call of this method.
589    ///
590    /// [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
591    pub fn install_scriptlet(&self) -> Option<&Path> {
592        self.scriptlet.as_deref()
593    }
594
595    /// Returns all paths relative to the [`PackageInput`]'s input directory.
596    pub fn relative_paths(&self) -> &[PathBuf] {
597        &self.relative_paths
598    }
599
600    /// Returns an [`InputPaths`] for the input directory and all relative paths contained in it.
601    pub fn input_paths(&self) -> Result<InputPaths<'_, '_>, crate::Error> {
602        Ok(InputPaths::new(
603            self.input_dir.as_path(),
604            &self.relative_paths,
605        )?)
606    }
607}
608
609impl TryFrom<InputDir> for PackageInput {
610    type Error = crate::Error;
611
612    /// Creates a [`PackageInput`] from input directory `path`.
613    ///
614    /// This function reads [ALPM-MTREE], [BUILDINFO] and [PKGINFO] files in `path`, collects the
615    /// path of an existing [alpm-install-scriptlet] and validates them.
616    /// All data files below `path` are then checked against the [ALPM-MTREE] data.
617    ///
618    /// # Errors
619    ///
620    /// Returns an error if
621    ///
622    /// - `value` is not a valid [`InputDir`],
623    /// - there is no valid [BUILDINFO] file,
624    /// - there is no valid [ALPM-MTREE] file,
625    /// - there is no valid [PKGINFO] file,
626    /// - or one of the files below `dir` does not match the [ALPM-MTREE] data.
627    ///
628    /// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
629    /// [BUILDINFO]: https://alpm.archlinux.page/specifications/BUILDINFO.5.html
630    /// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
631    /// [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
632    fn try_from(value: InputDir) -> Result<Self, Self::Error> {
633        debug!("Create PackageInput from path {value:?}");
634
635        // Get Mtree data and file digest.
636        let (mtree, mtree_digest) = get_mtree(&value)?;
637
638        // Get all relative paths in value.
639        let relative_paths = relative_files(&value, &[])?;
640        trace!("Relative files:\n{relative_paths:?}");
641
642        // When comparing with ALPM-MTREE data, exclude the ALPM-MTREE file.
643        let relative_mtree_paths: Vec<PathBuf> = relative_paths
644            .iter()
645            .filter(|path| path.as_os_str() != MetadataFileName::Mtree.as_ref())
646            .cloned()
647            .collect();
648        mtree.validate_paths(&InputPaths::new(value.as_ref(), &relative_mtree_paths)?)?;
649
650        // Get PackageInfo data and file digest.
651        let package_info = get_package_info(&value, &mtree)?;
652        // Get BuildInfo data and file digest.
653        let build_info = get_build_info(&value, &mtree)?;
654
655        // Compare overlapping metadata of BuildInfo and PackageInfo data.
656        compare_build_info_package_info(&build_info, &package_info)?;
657
658        // Get optional scriptlet file.
659        let scriptlet = get_install_scriptlet(&value, &mtree)?;
660
661        Ok(Self {
662            build_info,
663            package_info,
664            mtree,
665            mtree_digest,
666            input_dir: value,
667            scriptlet,
668            relative_paths,
669        })
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use std::{fs::File, str::FromStr};
676
677    use rstest::rstest;
678    use tempfile::tempdir;
679    use testresult::TestResult;
680
681    use super::*;
682
683    /// Ensures that a [`MetadataMismatch`] has mismatching values.
684    ///
685    /// This test is mostly here for coverage improvement.
686    #[test]
687    fn metadata_mismatch() -> TestResult {
688        let mismatch = MetadataMismatch {
689            first: MetadataKeyValue {
690                file_name: MetadataFileName::BuildInfo,
691                key: "pkgname".to_string(),
692                value: "example".to_string(),
693            },
694            second: MetadataKeyValue {
695                file_name: MetadataFileName::PackageInfo,
696                key: "pkgname".to_string(),
697                value: "other-example".to_string(),
698            },
699        };
700
701        assert_eq!(mismatch.first.key, mismatch.second.key);
702        assert_ne!(mismatch.first.value, mismatch.second.value);
703        Ok(())
704    }
705
706    /// Ensures that [`InputDir::new`] fails on relative paths, non-existing paths and non-directory
707    /// paths.
708    #[test]
709    fn input_dir_new_fails() -> TestResult {
710        assert!(matches!(
711            InputDir::new(PathBuf::from("test")),
712            Err(crate::Error::AlpmCommon(
713                alpm_common::Error::NonAbsolutePaths { paths: _ }
714            ))
715        ));
716
717        let temp_dir = tempdir()?;
718        let non_existing_path = temp_dir.path().join("non-existing");
719        assert!(matches!(
720            InputDir::new(non_existing_path),
721            Err(crate::Error::PathDoesNotExist { path: _ })
722        ));
723
724        let file_path = temp_dir.path().join("non-existing");
725        let _file = File::create(&file_path)?;
726        assert!(matches!(
727            InputDir::new(file_path),
728            Err(crate::Error::AlpmCommon(
729                alpm_common::Error::NotADirectory { path: _ }
730            ))
731        ));
732
733        Ok(())
734    }
735
736    /// Ensures that [`InputDir::to_path_buf`] works.
737    #[test]
738    fn input_dir_to_path_buf() -> TestResult {
739        let temp_dir = tempdir()?;
740        let dir = temp_dir.path();
741        let input_dir = InputDir::new(dir.to_path_buf())?;
742
743        assert_eq!(input_dir.to_path_buf(), dir.to_path_buf());
744
745        Ok(())
746    }
747
748    const PKGNAME_MISMATCH: &[&str; 2] = &[
749        r#"
750format = 2
751builddate = 1
752builddir = /build
753startdir = /startdir/
754buildtool = devtools
755buildtoolver = 1:1.2.1-1-any
756packager = John Doe <john@example.org>
757pkgarch = any
758pkgbase = example
759pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
760pkgname = example
761pkgver = 1:1.0.0-1
762"#,
763        r#"
764pkgname = example-different
765pkgbase = example
766xdata = pkgtype=pkg
767pkgver = 1:1.0.0-1
768pkgdesc = A project that does something
769url = https://example.org/
770builddate = 1
771packager = John Doe <john@example.org>
772size = 181849963
773arch = any
774"#,
775    ];
776
777    const PKGBASE_MISMATCH: &[&str; 2] = &[
778        r#"
779format = 2
780builddate = 1
781builddir = /build
782startdir = /startdir/
783buildtool = devtools
784buildtoolver = 1:1.2.1-1-any
785packager = John Doe <john@example.org>
786pkgarch = any
787pkgbase = example
788pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
789pkgname = example
790pkgver = 1:1.0.0-1
791"#,
792        r#"
793pkgname = example
794pkgbase = example-different
795xdata = pkgtype=pkg
796pkgver = 1:1.0.0-1
797pkgdesc = A project that does something
798url = https://example.org/
799builddate = 1
800packager = John Doe <john@example.org>
801size = 181849963
802arch = any
803"#,
804    ];
805
806    const VERSION_MISMATCH: &[&str; 2] = &[
807        r#"
808format = 2
809builddate = 1
810builddir = /build
811startdir = /startdir/
812buildtool = devtools
813buildtoolver = 1:1.2.1-1-any
814packager = John Doe <john@example.org>
815pkgarch = any
816pkgbase = example
817pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
818pkgname = example
819pkgver = 1:1.0.0-1
820"#,
821        r#"
822pkgname = example
823pkgbase = example
824xdata = pkgtype=pkg
825pkgver = 1.0.0-1
826pkgdesc = A project that does something
827url = https://example.org/
828builddate = 1
829packager = John Doe <john@example.org>
830size = 181849963
831arch = any
832"#,
833    ];
834
835    const ARCHITECTURE_MISMATCH: &[&str; 2] = &[
836        r#"
837format = 2
838builddate = 1
839builddir = /build
840startdir = /startdir/
841buildtool = devtools
842buildtoolver = 1:1.2.1-1-any
843packager = John Doe <john@example.org>
844pkgarch = any
845pkgbase = example
846pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
847pkgname = example
848pkgver = 1:1.0.0-1
849"#,
850        r#"
851pkgname = example
852pkgbase = example
853xdata = pkgtype=pkg
854pkgver = 1:1.0.0-1
855pkgdesc = A project that does something
856url = https://example.org/
857builddate = 1
858packager = John Doe <john@example.org>
859size = 181849963
860arch = x86_64
861"#,
862    ];
863
864    const PACKAGER_MISMATCH: &[&str; 2] = &[
865        r#"
866format = 2
867builddate = 1
868builddir = /build
869startdir = /startdir/
870buildtool = devtools
871buildtoolver = 1:1.2.1-1-any
872packager = John Doe <john@example.org>
873pkgarch = any
874pkgbase = example
875pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
876pkgname = example
877pkgver = 1:1.0.0-1
878"#,
879        r#"
880pkgname = example
881pkgbase = example
882xdata = pkgtype=pkg
883pkgver = 1:1.0.0-1
884pkgdesc = A project that does something
885url = https://example.org/
886builddate = 1
887packager = Jane Doe <jane@example.org>
888size = 181849963
889arch = any
890"#,
891    ];
892
893    const BUILD_DATE_MISMATCH: &[&str; 2] = &[
894        r#"
895format = 2
896builddate = 1
897builddir = /build
898startdir = /startdir/
899buildtool = devtools
900buildtoolver = 1:1.2.1-1-any
901packager = John Doe <john@example.org>
902pkgarch = any
903pkgbase = example
904pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
905pkgname = example
906pkgver = 1:1.0.0-1
907"#,
908        r#"
909pkgname = example
910pkgbase = example
911xdata = pkgtype=pkg
912pkgver = 1:1.0.0-1
913pkgdesc = A project that does something
914url = https://example.org/
915builddate = 2
916packager = John Doe <john@example.org>
917size = 181849963
918arch = any
919"#,
920    ];
921
922    /// Ensures that [`compare_build_info_package_info`] fails on mismatches in [`BuildInfo`] and
923    /// [`PackageInfo`].
924    #[rstest]
925    #[case::pkgname_mismatch(PKGNAME_MISMATCH, ("pkgname", "pkgname"))]
926    #[case::pkgbase_mismatch(PKGBASE_MISMATCH, ("pkgbase", "pkgbase"))]
927    #[case::version_mismatch(VERSION_MISMATCH, ("pkgver", "pkgver"))]
928    #[case::architecture_mismatch(ARCHITECTURE_MISMATCH, ("pkgarch", "arch"))]
929    #[case::packager_mismatch(PACKAGER_MISMATCH, ("packager", "packager"))]
930    #[case::build_date_mismatch(BUILD_DATE_MISMATCH, ("builddate", "builddate"))]
931    fn test_compare_build_info_package_info_fails(
932        #[case] metadata: &[&str; 2],
933        #[case] expected: (&str, &str),
934    ) -> TestResult {
935        let build_info = BuildInfo::from_str(metadata[0])?;
936        let package_info = PackageInfo::from_str(metadata[1])?;
937
938        if let Err(error) = compare_build_info_package_info(&build_info, &package_info) {
939            match error {
940                crate::Error::Input(crate::input::Error::MetadataMismatch { mismatches }) => {
941                    if mismatches.len() != 1 {
942                        panic!("There should be exactly one metadata mismatch");
943                    }
944                    let Some(mismatch) = mismatches.first() else {
945                        panic!("There should be at least one metadata mismatch");
946                    };
947                    assert_eq!(mismatch.first.key, expected.0);
948                    assert_eq!(mismatch.second.key, expected.1);
949                }
950                _ => panic!("Did not return the correct error variant"),
951            }
952        } else {
953            panic!("Should have returned an error but succeeded");
954        }
955
956        Ok(())
957    }
958}