Skip to main content

alpm_package/
package.rs

1//! Representation of [alpm-package] files.
2//!
3//! [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
4
5use std::{
6    fmt::{self, Debug},
7    fs::{File, create_dir_all},
8    io::Read,
9    path::{Path, PathBuf},
10    str::FromStr,
11};
12
13use alpm_buildinfo::BuildInfo;
14use alpm_common::{InputPaths, MetadataFile};
15use alpm_compress::tarball::{TarballBuilder, TarballEntries, TarballEntry, TarballReader};
16use alpm_mtree::Mtree;
17use alpm_pkginfo::PackageInfo;
18use alpm_types::{INSTALL_SCRIPTLET_FILE_NAME, MetadataFileName, PackageError, PackageFileName};
19use fluent_i18n::t;
20use log::debug;
21
22use crate::{OutputDir, PackageCreationConfig};
23
24/// An error that can occur when handling [alpm-package] files.
25///
26/// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
27#[derive(Debug, thiserror::Error)]
28pub enum Error {
29    /// An error occurred while adding files from an input directory to a package.
30    #[error("Error while appending file {from_path} to package archive as {to_path}:\n{source}")]
31    AppendFileToArchive {
32        /// The path to the file that is appended to the archive as `to_path`.
33        from_path: PathBuf,
34        /// The path in the archive that `from_path` is appended as.
35        to_path: PathBuf,
36        /// The source error.
37        source: std::io::Error,
38    },
39
40    /// An error occurred while finishing an uncompressed package.
41    #[error("Error while finishing the creation of uncompressed package {package_path}:\n{source}")]
42    FinishArchive {
43        /// The path of the package file that is being written to
44        package_path: PathBuf,
45        /// The source error.
46        source: std::io::Error,
47    },
48}
49
50/// A path that is guaranteed to be an existing absolute directory.
51#[derive(Clone, Debug)]
52pub struct ExistingAbsoluteDir(PathBuf);
53
54impl ExistingAbsoluteDir {
55    /// Creates a new [`ExistingAbsoluteDir`] from `path`.
56    ///
57    /// Creates a directory at `path` if it does not exist yet.
58    ///
59    /// # Errors
60    ///
61    /// Returns an error if
62    ///
63    /// - `path` is not absolute,
64    /// - `path` does not exist and cannot be created,
65    /// - the metadata of `path` cannot be retrieved,
66    /// - or `path` is not a directory.
67    pub fn new(path: PathBuf) -> Result<Self, crate::Error> {
68        if !path.is_absolute() {
69            return Err(alpm_common::Error::NonAbsolutePaths {
70                paths: vec![path.clone()],
71            }
72            .into());
73        }
74
75        if !path.exists() {
76            create_dir_all(&path).map_err(|source| crate::Error::IoPath {
77                path: path.clone(),
78                context: t!("error-io-create-abs-dir"),
79                source,
80            })?;
81        }
82
83        let metadata = path.metadata().map_err(|source| crate::Error::IoPath {
84            path: path.clone(),
85            context: t!("error-io-get-metadata"),
86            source,
87        })?;
88
89        if !metadata.is_dir() {
90            return Err(alpm_common::Error::NotADirectory { path: path.clone() }.into());
91        }
92
93        Ok(Self(path))
94    }
95
96    /// Coerces to a Path slice.
97    ///
98    /// Delegates to [`PathBuf::as_path`].
99    pub fn as_path(&self) -> &Path {
100        self.0.as_path()
101    }
102
103    /// Converts a Path to an owned PathBuf.
104    ///
105    /// Delegates to [`Path::to_path_buf`].
106    pub fn to_path_buf(&self) -> PathBuf {
107        self.0.to_path_buf()
108    }
109
110    /// Creates an owned PathBuf with path adjoined to self.
111    ///
112    /// Delegates to [`Path::join`].
113    pub fn join(&self, path: impl AsRef<Path>) -> PathBuf {
114        self.0.join(path)
115    }
116}
117
118impl AsRef<Path> for ExistingAbsoluteDir {
119    fn as_ref(&self) -> &Path {
120        &self.0
121    }
122}
123
124impl From<&OutputDir> for ExistingAbsoluteDir {
125    /// Creates an [`ExistingAbsoluteDir`] from an [`OutputDir`].
126    ///
127    /// As [`OutputDir`] provides a more strict set of requirements, this can be infallible.
128    fn from(value: &OutputDir) -> Self {
129        Self(value.to_path_buf())
130    }
131}
132
133impl TryFrom<&Path> for ExistingAbsoluteDir {
134    type Error = crate::Error;
135
136    /// Creates an [`ExistingAbsoluteDir`] from a [`Path`] reference.
137    ///
138    /// Delegates to [`ExistingAbsoluteDir::new`].
139    ///
140    /// # Errors
141    ///
142    /// Returns an error if [`ExistingAbsoluteDir::new`] fails.
143    fn try_from(value: &Path) -> Result<Self, Self::Error> {
144        Self::new(value.to_path_buf())
145    }
146}
147
148/// Appends relative files from an input directory to a [`TarballBuilder`].
149///
150/// Before appending any files, all provided `input_paths` are validated against `mtree` (ALPM-MTREE
151/// data).
152///
153/// # Errors
154///
155/// Returns an error if
156///
157/// - validating any path in `input_paths` using `mtree` fails,
158/// - retrieving files relative to `input_dir` fails,
159/// - or adding one of the relative paths to the `builder` fails.
160// TODO(cleanup): Investigate the arithmetic_side_effects and indexing_slicing
161#[expect(clippy::arithmetic_side_effects, clippy::indexing_slicing)]
162fn append_relative_files<'c>(
163    mut builder: TarballBuilder<'c>,
164    mtree: &Mtree,
165    input_paths: &InputPaths,
166) -> Result<TarballBuilder<'c>, crate::Error> {
167    // Validate all paths using the ALPM-MTREE data before appending them to the builder.
168    let mtree_path = PathBuf::from(MetadataFileName::Mtree.as_ref());
169    let check_paths = {
170        let all_paths = input_paths.paths();
171        // If there is an ALPM-MTREE file, exclude it from the validation, as the ALPM-MTREE data
172        // does not cover it.
173        if let Some(mtree_position) = all_paths.iter().position(|path| path == &mtree_path) {
174            let before = &all_paths[..mtree_position];
175            let after = if all_paths.len() > mtree_position {
176                &all_paths[mtree_position + 1..]
177            } else {
178                &[]
179            };
180            &[before, after].concat()
181        } else {
182            all_paths
183        }
184    };
185    mtree.validate_paths(&InputPaths::new(input_paths.base_dir(), check_paths)?)?;
186
187    // Append all files/directories to the archive.
188    for relative_file in input_paths.paths() {
189        let from_path = input_paths.base_dir().join(relative_file.as_path());
190        builder
191            .inner_mut()
192            .append_path_with_name(from_path.as_path(), relative_file.as_path())
193            .map_err(|source| Error::AppendFileToArchive {
194                from_path,
195                to_path: relative_file.clone(),
196                source,
197            })?
198    }
199
200    Ok(builder)
201}
202
203/// An entry in a package archive.
204///
205/// This can be either a metadata file (such as [PKGINFO], [BUILDINFO], or [ALPM-MTREE]) or an
206/// [alpm-install-scriptlet] file.
207///
208/// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
209/// [BUILDINFO]: https://alpm.archlinux.page/specifications/BUILDINFO.5.html
210/// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
211/// [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
212#[derive(Clone, Debug)]
213pub enum PackageEntry {
214    /// A metadata entry in the package archive.
215    ///
216    /// See [`MetadataEntry`] for the different types of metadata entries.
217    ///
218    /// This variant is boxed to avoid large allocations
219    Metadata(Box<MetadataEntry>),
220
221    /// An [alpm-install-scriptlet] file in the package.
222    ///
223    /// [alpm-install-scriptlet]:
224    /// https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
225    InstallScriptlet(String),
226}
227
228/// Metadata entry contained in an [alpm-package] file.
229///
230/// This is used e.g. in [`PackageReader::metadata_entries`] when iterating over available
231/// metadata files.
232///
233/// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
234#[derive(Clone, Debug)]
235pub enum MetadataEntry {
236    /// The [PKGINFO] data.
237    ///
238    /// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
239    PackageInfo(PackageInfo),
240
241    /// The [BUILDINFO] data.
242    ///
243    /// [BUILDINFO]: https://alpm.archlinux.page/specifications/BUILDINFO.5.html
244    BuildInfo(BuildInfo),
245
246    /// The [ALPM-MTREE] data.
247    ///
248    /// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
249    Mtree(Mtree),
250}
251
252/// All the metadata contained in an [alpm-package] file.
253///
254/// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
255#[derive(Clone, Debug)]
256pub struct Metadata {
257    /// The [PKGINFO] file in the package.
258    ///
259    /// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
260    pub pkginfo: PackageInfo,
261    /// The [BUILDINFO] file in the package.
262    ///
263    /// [BUILDINFO]: https://alpm.archlinux.page/specifications/BUILDINFO.5.html
264    pub buildinfo: BuildInfo,
265    /// The [ALPM-MTREE] file in the package.
266    ///
267    /// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
268    pub mtree: Mtree,
269}
270
271/// An iterator over each [`PackageEntry`] of a package.
272///
273/// Stops early once all package entry files have been found.
274///
275/// # Note
276///
277/// Uses two lifetimes for the underlying [`TarballEntries`]
278pub struct PackageEntryIterator<'a, 'c> {
279    /// The archive entries iterator that contains all of the archive's entries.
280    entries: TarballEntries<'a, 'c>,
281    /// Whether a `.BUILDINFO` file has been found.
282    found_buildinfo: bool,
283    /// Whether a `.MTREE` file has been found.
284    found_mtree: bool,
285    /// Whether a `.PKGINFO` file has been found.
286    found_pkginfo: bool,
287    /// Whether a `.INSTALL` scriptlet has been found or skipped.
288    checked_install_scriptlet: bool,
289}
290
291impl Debug for PackageEntryIterator<'_, '_> {
292    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293        f.debug_struct("PackageEntryIterator")
294            .field("entries", &"TarballEntries")
295            .field("found_buildinfo", &self.found_buildinfo)
296            .field("found_mtree", &self.found_mtree)
297            .field("found_pkginfo", &self.found_pkginfo)
298            .field("checked_install_scriptlet", &self.checked_install_scriptlet)
299            .finish()
300    }
301}
302
303impl<'a, 'c> PackageEntryIterator<'a, 'c> {
304    /// Creates a new [`PackageEntryIterator`] from [`TarballEntries`].
305    pub fn new(entries: TarballEntries<'a, 'c>) -> Self {
306        Self {
307            entries,
308            found_buildinfo: false,
309            found_mtree: false,
310            found_pkginfo: false,
311            checked_install_scriptlet: false,
312        }
313    }
314
315    /// Return the inner [`TarballEntries`] iterator at the current iteration position.
316    pub fn into_inner(self) -> TarballEntries<'a, 'c> {
317        self.entries
318    }
319
320    /// Checks whether all variants of [`PackageEntry`] have been found.
321    ///
322    /// Returns `true` if all variants of [`PackageEntry`] have been found, `false` otherwise.
323    fn all_entries_found(&self) -> bool {
324        self.checked_install_scriptlet
325            && self.found_pkginfo
326            && self.found_mtree
327            && self.found_buildinfo
328    }
329
330    /// A helper function that returns an optional [`PackageEntry`] from a [`TarballEntry`].
331    ///
332    /// Based on the path of `entry` either returns:
333    ///
334    /// - `Ok(Some(PackageEntry))` when a valid [`PackageEntry`] is detected,
335    /// - `Ok(None)` for any other files.
336    ///
337    /// # Errors
338    ///
339    /// Returns an error if
340    ///
341    /// - no path can be retrieved from `entry`,
342    /// - the path of `entry` indicates a [BUILDINFO] file, but a [`BuildInfo`] cannot be created
343    ///   from it,
344    /// - the path of `entry` indicates an [ALPM-MTREE] file, but an [`Mtree`] cannot be created
345    ///   from it,
346    /// - the path of `entry` indicates a [PKGINFO] file, but a [`PackageInfo`] cannot be created
347    ///   from it,
348    /// - or the path of `entry` indicates an [alpm-install-script] file, but it cannot be read to a
349    ///   string.
350    ///
351    /// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
352    /// [BUILDINFO]: https://alpm.archlinux.page/specifications/BUILDINFO.5.html
353    /// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
354    /// [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
355    fn get_package_entry(mut entry: TarballEntry) -> Result<Option<PackageEntry>, crate::Error> {
356        let path = entry.path().to_string_lossy();
357        match path.as_ref() {
358            p if p == MetadataFileName::PackageInfo.as_ref() => {
359                let info = PackageInfo::from_reader(&mut entry)?;
360                Ok(Some(PackageEntry::Metadata(Box::new(
361                    MetadataEntry::PackageInfo(info),
362                ))))
363            }
364            p if p == MetadataFileName::BuildInfo.as_ref() => {
365                let info = BuildInfo::from_reader(&mut entry)?;
366                Ok(Some(PackageEntry::Metadata(Box::new(
367                    MetadataEntry::BuildInfo(info),
368                ))))
369            }
370            p if p == MetadataFileName::Mtree.as_ref() => {
371                let info = Mtree::from_reader(&mut entry)?;
372                Ok(Some(PackageEntry::Metadata(Box::new(
373                    MetadataEntry::Mtree(info),
374                ))))
375            }
376            INSTALL_SCRIPTLET_FILE_NAME => {
377                let mut scriptlet = String::new();
378                entry
379                    .read_to_string(&mut scriptlet)
380                    .map_err(|source| crate::Error::IoRead {
381                        context: t!("error-io-read-install-scriptlet"),
382                        source,
383                    })?;
384                Ok(Some(PackageEntry::InstallScriptlet(scriptlet)))
385            }
386            _ => Ok(None),
387        }
388    }
389}
390
391impl Iterator for PackageEntryIterator<'_, '_> {
392    type Item = Result<PackageEntry, crate::Error>;
393
394    fn next(&mut self) -> Option<Self::Item> {
395        // Return early if we already found all entries.
396        // In that case we don't need to continue iteration.
397        if self.all_entries_found() {
398            return None;
399        }
400
401        for entry_result in &mut self.entries {
402            let entry = match entry_result {
403                Ok(entry) => entry,
404                Err(e) => return Some(Err(e.into())),
405            };
406
407            // Get the package entry and convert `Result<Option<PackageEntry>>` to a
408            // `Option<Result<PackageEntry>>`.
409            let entry = Self::get_package_entry(entry).transpose();
410
411            // Now, if the entry is either an error or a valid PackageEntry, return it.
412            // Otherwise, we look at the next entry.
413            match entry {
414                Some(Ok(ref package_entry)) => {
415                    // Remember each file we found.
416                    // Once all files are found, the iterator can short-circuit and stop early.
417                    match &package_entry {
418                        PackageEntry::Metadata(metadata_entry) => match **metadata_entry {
419                            MetadataEntry::PackageInfo(_) => self.found_pkginfo = true,
420                            MetadataEntry::BuildInfo(_) => self.found_buildinfo = true,
421                            MetadataEntry::Mtree(_) => self.found_mtree = true,
422                        },
423                        PackageEntry::InstallScriptlet(_) => self.checked_install_scriptlet = true,
424                    }
425                    return entry;
426                }
427                Some(Err(e)) => return Some(Err(e)),
428                _ if self.found_buildinfo && self.found_mtree && self.found_pkginfo => {
429                    // Found three required metadata files and hit the first non-metadata file.
430                    // This means that install scriptlet does not exist in the package and we
431                    // can stop iterating.
432                    //
433                    // This logic relies on the ordering of files, where the `.INSTALL` file is
434                    // placed in between `.PKGINFO` and `.MTREE`.
435                    self.checked_install_scriptlet = true;
436                    break;
437                }
438                _ => (),
439            }
440        }
441
442        None
443    }
444}
445
446/// An iterator over each [`MetadataEntry`] of a package.
447///
448/// Stops early once all metadata files have been found.
449///
450/// # Notes
451///
452/// Uses two lifetimes for the underlying [`TarballEntries`] of [`PackageEntryIterator`]
453/// in the `entries` field.
454pub struct MetadataEntryIterator<'a, 'c> {
455    /// The archive entries iterator that contains all archive's entries.
456    entries: PackageEntryIterator<'a, 'c>,
457    /// Whether a `.BUILDINFO` file has been found.
458    found_buildinfo: bool,
459    /// Whether a `.MTREE` file has been found.
460    found_mtree: bool,
461    /// Whether a `.PKGINFO` file has been found.
462    found_pkginfo: bool,
463}
464
465impl Debug for MetadataEntryIterator<'_, '_> {
466    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
467        f.debug_struct("MetadataEntryIterator")
468            .field("entries", &self.entries)
469            .field("found_buildinfo", &self.found_buildinfo)
470            .field("found_mtree", &self.found_mtree)
471            .field("found_pkginfo", &self.found_pkginfo)
472            .finish()
473    }
474}
475
476impl<'a, 'c> MetadataEntryIterator<'a, 'c> {
477    /// Creates a new [`MetadataEntryIterator`] from a [`PackageEntryIterator`].
478    pub fn new(entries: PackageEntryIterator<'a, 'c>) -> Self {
479        Self {
480            entries,
481            found_buildinfo: false,
482            found_mtree: false,
483            found_pkginfo: false,
484        }
485    }
486
487    /// Return the inner [`PackageEntryIterator`] iterator at the current iteration position.
488    pub fn into_inner(self) -> PackageEntryIterator<'a, 'c> {
489        self.entries
490    }
491
492    /// Checks whether all variants of [`MetadataEntry`] have been found.
493    ///
494    /// Returns `true` if all known types of [`MetadataEntry`] have been found, `false` otherwise.
495    fn all_entries_found(&self) -> bool {
496        self.found_pkginfo && self.found_mtree && self.found_buildinfo
497    }
498}
499
500impl Iterator for MetadataEntryIterator<'_, '_> {
501    type Item = Result<MetadataEntry, crate::Error>;
502
503    fn next(&mut self) -> Option<Self::Item> {
504        // Return early if we already found all entries.
505        // In that case we don't need to continue iteration.
506        if self.all_entries_found() {
507            return None;
508        }
509
510        // Now check whether we have any entries left.
511        for entry_result in &mut self.entries {
512            let metadata = match entry_result {
513                Ok(PackageEntry::Metadata(metadata)) => metadata,
514                Ok(PackageEntry::InstallScriptlet(_)) => continue,
515                Err(e) => return Some(Err(e)),
516            };
517
518            match *metadata {
519                MetadataEntry::PackageInfo(_) => self.found_pkginfo = true,
520                MetadataEntry::BuildInfo(_) => self.found_buildinfo = true,
521                MetadataEntry::Mtree(_) => self.found_mtree = true,
522            }
523            return Some(Ok(*metadata));
524        }
525
526        None
527    }
528}
529
530/// A reader for [`Package`] files.
531///
532/// A [`PackageReader`] can be created from a [`Package`] using the
533/// [`Package::into_reader`] or [`PackageReader::try_from`] methods.
534///
535/// # Examples
536///
537/// ```
538/// # use std::fs::{File, Permissions, create_dir_all};
539/// # use std::io::Write;
540/// # use std::os::unix::fs::PermissionsExt;
541/// use std::path::Path;
542///
543/// # use alpm_mtree::create_mtree_v2_from_input_dir;
544/// use alpm_package::{MetadataEntry, Package, PackageReader};
545/// # use alpm_package::{
546/// #     InputDir,
547/// #     OutputDir,
548/// #     PackageCreationConfig,
549/// #     PackageInput,
550/// # };
551/// # use alpm_compress::compression::CompressionSettings;
552/// use alpm_types::MetadataFileName;
553///
554/// # fn main() -> testresult::TestResult {
555/// // A directory for the package file.
556/// let temp_dir = tempfile::tempdir()?;
557/// let path = temp_dir.path();
558/// # let input_dir = path.join("input");
559/// # create_dir_all(&input_dir)?;
560/// # let input_dir = InputDir::new(input_dir)?;
561/// # let output_dir = OutputDir::new(path.join("output"))?;
562/// #
563/// # // Create a valid, but minimal BUILDINFOv2 file.
564/// # let mut file = File::create(&input_dir.join(MetadataFileName::BuildInfo.as_ref()))?;
565/// # write!(file, r#"
566/// # format = 2
567/// # builddate = 1
568/// # builddir = /build
569/// # startdir = /startdir/
570/// # buildtool = devtools
571/// # buildtoolver = 1:1.2.1-1-any
572/// # installed = other-example-1.2.3-1-any
573/// # packager = John Doe <john@example.org>
574/// # pkgarch = any
575/// # pkgbase = example
576/// # pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
577/// # pkgname = example
578/// # pkgver = 1.0.0-1
579/// # "#)?;
580/// #
581/// # // Create a valid, but minimal PKGINFOv2 file.
582/// # let mut file = File::create(&input_dir.join(MetadataFileName::PackageInfo.as_ref()))?;
583/// # write!(file, r#"
584/// # pkgname = example
585/// # pkgbase = example
586/// # xdata = pkgtype=pkg
587/// # pkgver = 1.0.0-1
588/// # pkgdesc = A project that returns true
589/// # url = https://example.org/
590/// # builddate = 1
591/// # packager = John Doe <john@example.org>
592/// # size = 181849963
593/// # arch = any
594/// # license = GPL-3.0-or-later
595/// # depend = bash
596/// # "#)?;
597/// #
598/// # // Create a dummy script as package data.
599/// # create_dir_all(&input_dir.join("usr/bin"))?;
600/// # let mut file = File::create(&input_dir.join("usr/bin/example"))?;
601/// # write!(file, r#"!/bin/bash
602/// # true
603/// # "#)?;
604/// # file.set_permissions(Permissions::from_mode(0o755))?;
605/// #
606/// # // Create a valid ALPM-MTREEv2 file from the input directory.
607/// # create_mtree_v2_from_input_dir(&input_dir)?;
608/// #
609/// # // Create PackageInput and PackageCreationConfig.
610/// # let package_input: PackageInput = input_dir.try_into()?;
611/// # let config = PackageCreationConfig::new(
612/// #     package_input,
613/// #     output_dir,
614/// #     CompressionSettings::default(),
615/// # )?;
616///
617/// # // Create package file.
618/// # let package = Package::try_from(&config)?;
619/// // Assume that the package is created
620/// let package_path = path.join("output/example-1.0.0-1-any.pkg.tar.zst");
621///
622/// // Create a reader for the package.
623/// let mut reader = package.clone().into_reader()?;
624///
625/// // Read all the metadata from the package archive.
626/// let metadata = reader.metadata()?;
627/// let pkginfo = metadata.pkginfo;
628/// let buildinfo = metadata.buildinfo;
629/// let mtree = metadata.mtree;
630///
631/// // Or you can iterate over the metadata entries:
632/// let mut reader = package.clone().into_reader()?;
633/// for entry in reader.metadata_entries()? {
634///     let entry = entry?;
635///     match entry {
636///         MetadataEntry::PackageInfo(pkginfo) => {}
637///         MetadataEntry::BuildInfo(buildinfo) => {}
638///         MetadataEntry::Mtree(mtree) => {}
639///         _ => {}
640///     }
641/// }
642///
643/// // You can also read specific metadata files directly:
644/// let mut reader = package.clone().into_reader()?;
645/// let pkginfo = reader.read_metadata_file(MetadataFileName::PackageInfo)?;
646/// // let buildinfo = reader.read_metadata_file(MetadataFileName::BuildInfo)?;
647/// // let mtree = reader.read_metadata_file(MetadataFileName::Mtree)?;
648///
649/// // Read the install scriptlet, if present:
650/// let mut reader = package.clone().into_reader()?;
651/// let install_scriptlet = reader.read_install_scriptlet()?;
652///
653/// // Iterate over the data entries in the package archive.
654/// let mut reader = package.clone().into_reader()?;
655/// for data_entry in reader.data_entries()? {
656///     let mut data_entry = data_entry?;
657///     let content = data_entry.content()?;
658///     // Note: data_entry also implements `Read`, so you can read from it directly.
659/// }
660/// # Ok(())
661/// # }
662/// ```
663///
664/// # Notes
665///
666/// This API is designed with **streaming** and **single-pass iteration** in mind.
667///
668/// Calling [`Package::into_reader`] creates a new [`PackageReader`] each time,
669/// which consumes the underlying archive in a forward-only manner. This allows
670/// efficient access to package contents without needing to load the entire archive
671/// into memory.
672///
673/// If you need to perform multiple operations on a package, you can call
674/// [`Package::into_reader`] multiple times — each reader starts fresh and ensures
675/// predictable, deterministic access to the archive's contents.
676///
677/// Please note that convenience methods on [`Package`] itself, such as
678/// [`Package::read_pkginfo`], are also provided for better ergonomics
679/// and ease of use.
680///
681/// The lifetimes `'c` is for the [`TarballReader`]
682#[derive(Debug)]
683pub struct PackageReader<'c>(TarballReader<'c>);
684
685impl<'c> PackageReader<'c> {
686    /// Creates a new [`PackageReader`] from an [`TarballReader`].
687    pub fn new(tarball_reader: TarballReader<'c>) -> Self {
688        Self(tarball_reader)
689    }
690
691    fn is_scriplet_file(entry: &TarballEntry) -> bool {
692        let path = entry.path().to_string_lossy();
693        path.as_ref() == INSTALL_SCRIPTLET_FILE_NAME
694    }
695
696    fn is_metadata_file(entry: &TarballEntry) -> bool {
697        let metadata_file_names = [
698            MetadataFileName::PackageInfo.as_ref(),
699            MetadataFileName::BuildInfo.as_ref(),
700            MetadataFileName::Mtree.as_ref(),
701        ];
702        let path = entry.path().to_string_lossy();
703        metadata_file_names.contains(&path.as_ref())
704    }
705
706    fn is_data_file(entry: &TarballEntry) -> bool {
707        !Self::is_scriplet_file(entry) && !Self::is_metadata_file(entry)
708    }
709
710    /// Returns an iterator over the raw entries of the package's tar archive.
711    ///
712    /// The returned [`TarballEntries`] implements an iterator over each [`TarballEntry`],
713    /// which provides direct data access to all entries of the package's tar archive.
714    ///
715    /// # Errors
716    ///
717    /// Returns an error if the [`TarballEntries`] cannot be read from the package's tar archive.
718    pub fn raw_entries<'a>(&'a mut self) -> Result<TarballEntries<'a, 'c>, crate::Error> {
719        Ok(self.0.entries()?)
720    }
721
722    /// Returns an iterator over the known files in the [alpm-package] file.
723    ///
724    /// This iterator yields a set of [`PackageEntry`] variants, which may only contain data
725    /// from metadata files (i.e. [ALPM-MTREE], [BUILDINFO] or [PKGINFO]) or an install scriptlet
726    /// (i.e. [alpm-install-scriplet]).
727    ///
728    /// # Note
729    ///
730    /// The file names of metadata file formats (i.e. [ALPM-MTREE], [BUILDINFO], [PKGINFO])
731    /// and install scriptlets (i.e. [alpm-install-scriptlet]) are prefixed with a dot (`.`)
732    /// in [alpm-package] files.
733    ///
734    /// As [alpm-package] files are assumed to contain a sorted list of entries, these files are
735    /// considered first. The iterator stops as soon as it encounters an entry that does not
736    /// match any known metadata file or install scriptlet file name.
737    ///
738    /// # Errors
739    ///
740    /// Returns an error if
741    ///
742    /// - reading the package archive entries fails,
743    /// - reading a package archive entry fails,
744    /// - reading the contents of a package archive entry fails,
745    /// - or retrieving the path of a package archive entry fails.
746    ///
747    /// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
748    /// [BUILDINFO]: https://alpm.archlinux.page/specifications/BUILDINFO.5.html
749    /// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
750    /// [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
751    /// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
752    pub fn entries<'a>(&'a mut self) -> Result<PackageEntryIterator<'a, 'c>, crate::Error> {
753        let entries = self.raw_entries()?;
754        Ok(PackageEntryIterator::new(entries))
755    }
756
757    /// Returns an iterator over the metadata entries in the package archive.
758    ///
759    /// This iterator yields [`MetadataEntry`]s, which can be either [PKGINFO], [BUILDINFO],
760    /// or [ALPM-MTREE].
761    ///
762    /// The iterator stops when it encounters an entry that does not match any
763    /// known package files.
764    ///
765    /// It is a wrapper around [`PackageReader::entries`] that filters out
766    /// the install scriptlet.
767    ///
768    /// # Errors
769    ///
770    /// Returns an error if [`PackageReader::entries`] fails to read the entries.
771    ///
772    /// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
773    /// [BUILDINFO]: https://alpm.archlinux.page/specifications/BUILDINFO.5.html
774    /// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
775    pub fn metadata_entries<'a>(
776        &'a mut self,
777    ) -> Result<MetadataEntryIterator<'a, 'c>, crate::Error> {
778        let entries = self.entries()?;
779        Ok(MetadataEntryIterator::new(entries))
780    }
781
782    /// Returns an iterator over the data files of the [alpm-package] archive.
783    ///
784    /// This iterator yields the path and content of each data file of a package archive in the form
785    /// of a [`TarballEntry`].
786    ///
787    /// # Notes
788    ///
789    /// This iterator filters out the known metadata files [PKGINFO], [BUILDINFO] and [ALPM-MTREE].
790    /// and the [alpm-install-scriplet] file.
791    ///
792    /// # Errors
793    ///
794    /// Returns an error if
795    ///
796    /// - reading the package archive entries fails,
797    /// - reading a package archive entry fails,
798    /// - reading the contents of a package archive entry fails,
799    /// - or retrieving the path of a package archive entry fails.
800    ///
801    /// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
802    /// [BUILDINFO]: https://alpm.archlinux.page/specifications/BUILDINFO.5.html
803    /// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
804    /// [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
805    /// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
806    pub fn data_entries<'a>(
807        &'a mut self,
808    ) -> Result<impl Iterator<Item = Result<TarballEntry<'a, 'c>, crate::Error>>, crate::Error>
809    {
810        let entries = self.raw_entries()?;
811        Ok(entries.filter_map(move |entry| {
812            let filter = (|| {
813                let entry = entry?;
814                // Filter out known metadata files
815                if !Self::is_data_file(&entry) {
816                    return Ok(None);
817                }
818                Ok(Some(entry))
819            })();
820            filter.transpose()
821        }))
822    }
823
824    /// Reads all metadata from an [alpm-package] file.
825    ///
826    /// This method reads all the metadata entries in the package file and returns a
827    /// [`Metadata`] struct containing the processed data.
828    ///
829    /// # Errors
830    ///
831    /// Returns an error if
832    ///
833    /// - reading the metadata entries fails,
834    /// - parsing a metadata entry fails,
835    /// - or if any of the required metadata files are not found in the package.
836    ///
837    /// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
838    pub fn metadata(&mut self) -> Result<Metadata, crate::Error> {
839        let mut pkginfo = None;
840        let mut buildinfo = None;
841        let mut mtree = None;
842        for entry in self.metadata_entries()? {
843            match entry? {
844                MetadataEntry::PackageInfo(m) => pkginfo = Some(m),
845                MetadataEntry::BuildInfo(m) => buildinfo = Some(m),
846                MetadataEntry::Mtree(m) => mtree = Some(m),
847            }
848        }
849        Ok(Metadata {
850            pkginfo: pkginfo.ok_or(crate::Error::MetadataFileNotFound {
851                name: MetadataFileName::PackageInfo,
852            })?,
853            buildinfo: buildinfo.ok_or(crate::Error::MetadataFileNotFound {
854                name: MetadataFileName::BuildInfo,
855            })?,
856            mtree: mtree.ok_or(crate::Error::MetadataFileNotFound {
857                name: MetadataFileName::Mtree,
858            })?,
859        })
860    }
861
862    /// Reads the data of a specific metadata file from the [alpm-package] file.
863    ///
864    /// This method searches for a metadata file that matches the provided
865    /// [`MetadataFileName`] and returns the corresponding [`MetadataEntry`].
866    ///
867    /// # Errors
868    ///
869    /// Returns an error if
870    ///
871    /// - [`PackageReader::metadata_entries`] fails to retrieve the metadata entries,
872    /// - or a [`MetadataEntry`] is not valid.
873    ///
874    /// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
875    pub fn read_metadata_file(
876        &mut self,
877        file_name: MetadataFileName,
878    ) -> Result<MetadataEntry, crate::Error> {
879        for entry in self.metadata_entries()? {
880            let entry = entry?;
881            match (&entry, &file_name) {
882                (MetadataEntry::PackageInfo(_), MetadataFileName::PackageInfo)
883                | (MetadataEntry::BuildInfo(_), MetadataFileName::BuildInfo)
884                | (MetadataEntry::Mtree(_), MetadataFileName::Mtree) => return Ok(entry),
885                _ => continue,
886            }
887        }
888        Err(crate::Error::MetadataFileNotFound { name: file_name })
889    }
890
891    /// Reads the content of the [alpm-install-scriptlet] from the package archive, if it exists.
892    ///
893    /// # Errors
894    ///
895    /// Returns an error if [`PackageReader::entries`] fails to read the entries.
896    ///
897    /// [alpm-install-scriplet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
898    pub fn read_install_scriptlet(&mut self) -> Result<Option<String>, crate::Error> {
899        for entry in self.entries()? {
900            let entry = entry?;
901            if let PackageEntry::InstallScriptlet(scriptlet) = entry {
902                return Ok(Some(scriptlet));
903            }
904        }
905        Ok(None)
906    }
907
908    /// Reads a [`TarballEntry`] matching a specific path name from the package archive.
909    ///
910    /// Returns [`None`] if no [`TarballEntry`] is found in the package archive that matches `path`.
911    ///
912    /// # Errors
913    ///
914    /// Returns an error if
915    ///
916    /// - [`PackageReader::data_entries`] fails to retrieve the data entries,
917    /// - or retrieving the details of a data entry fails.
918    pub fn read_data_entry<'a, P: AsRef<Path>>(
919        &'a mut self,
920        path: P,
921    ) -> Result<Option<TarballEntry<'a, 'c>>, crate::Error> {
922        for entry in self.data_entries()? {
923            let entry = entry?;
924            if entry.path() == path.as_ref() {
925                return Ok(Some(entry));
926            }
927        }
928        Ok(None)
929    }
930}
931
932impl TryFrom<Package> for PackageReader<'_> {
933    type Error = crate::Error;
934
935    /// Creates a [`PackageReader`] from a [`Package`].
936    ///
937    /// # Errors
938    ///
939    /// Returns an error if:
940    ///
941    /// - the package file cannot be opened,
942    /// - the package file extension cannot be determined,
943    /// - or the compression decoder cannot be created from the file and its extension.
944    fn try_from(package: Package) -> Result<Self, Self::Error> {
945        let path = package.to_path_buf();
946        Ok(Self::new(TarballReader::try_from(path)?))
947    }
948}
949
950impl TryFrom<&Path> for PackageReader<'_> {
951    type Error = crate::Error;
952
953    /// Creates a [`PackageReader`] from a [`Path`].
954    ///
955    /// # Errors
956    ///
957    /// Returns an error if:
958    ///
959    /// - [`Package::try_from`] fails to create a [`Package`] from `path`,
960    /// - or [`PackageReader::try_from`] fails to create a [`PackageReader`] from the package.
961    fn try_from(path: &Path) -> Result<Self, Self::Error> {
962        let package = Package::try_from(path)?;
963        PackageReader::try_from(package)
964    }
965}
966
967/// An [alpm-package] file.
968///
969/// Tracks the [`PackageFileName`] of the [alpm-package] as well as its absolute `parent_dir`.
970///
971/// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
972#[derive(Clone, Debug)]
973pub struct Package {
974    file_name: PackageFileName,
975    parent_dir: ExistingAbsoluteDir,
976}
977
978impl Package {
979    /// Creates a new [`Package`].
980    ///
981    /// # Errors
982    ///
983    /// Returns an error if no file exists at the path defined by `parent_dir` and `filename`.
984    pub fn new(
985        file_name: PackageFileName,
986        parent_dir: ExistingAbsoluteDir,
987    ) -> Result<Self, crate::Error> {
988        let file_path = parent_dir.to_path_buf().join(file_name.to_path_buf());
989        if !file_path.exists() {
990            return Err(crate::Error::PathDoesNotExist { path: file_path });
991        }
992        if !file_path.is_file() {
993            return Err(crate::Error::PathIsNotAFile { path: file_path });
994        }
995
996        Ok(Self {
997            file_name,
998            parent_dir,
999        })
1000    }
1001
1002    /// Returns the absolute path of the [`Package`].
1003    pub fn to_path_buf(&self) -> PathBuf {
1004        self.parent_dir.join(self.file_name.to_path_buf())
1005    }
1006
1007    /// Returns the [`PackageInfo`] of the package.
1008    ///
1009    /// This is a convenience wrapper around [`PackageReader::read_metadata_file`].
1010    ///
1011    /// # Errors
1012    ///
1013    /// Returns an error if
1014    ///
1015    /// - a [`PackageReader`] cannot be created for the package,
1016    /// - the package does not contain a [PKGINFO] file,
1017    /// - or the [PKGINFO] file in the package is not valid.
1018    ///
1019    /// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
1020    pub fn read_pkginfo(&self) -> Result<PackageInfo, crate::Error> {
1021        let mut reader = PackageReader::try_from(self.clone())?;
1022        let metadata = reader.read_metadata_file(MetadataFileName::PackageInfo)?;
1023        match metadata {
1024            MetadataEntry::PackageInfo(pkginfo) => Ok(pkginfo),
1025            _ => Err(crate::Error::MetadataFileNotFound {
1026                name: MetadataFileName::PackageInfo,
1027            }),
1028        }
1029    }
1030
1031    /// Returns the [`BuildInfo`] of the package.
1032    ///
1033    /// This is a convenience wrapper around [`PackageReader::read_metadata_file`].
1034    ///
1035    /// # Errors
1036    ///
1037    /// Returns an error if
1038    ///
1039    /// - a [`PackageReader`] cannot be created for the package,
1040    /// - the package does not contain a [BUILDINFO] file,
1041    /// - or the [BUILDINFO] file in the package is not valid.
1042    ///
1043    /// [BUILDINFO]: https://alpm.archlinux.page/specifications/BUILDINFO.5.html
1044    pub fn read_buildinfo(&self) -> Result<BuildInfo, crate::Error> {
1045        let mut reader = PackageReader::try_from(self.clone())?;
1046        let metadata = reader.read_metadata_file(MetadataFileName::BuildInfo)?;
1047        match metadata {
1048            MetadataEntry::BuildInfo(buildinfo) => Ok(buildinfo),
1049            _ => Err(crate::Error::MetadataFileNotFound {
1050                name: MetadataFileName::BuildInfo,
1051            }),
1052        }
1053    }
1054
1055    /// Returns the [`Mtree`] of the package.
1056    ///
1057    /// This is a convenience wrapper around [`PackageReader::read_metadata_file`].
1058    ///
1059    /// # Errors
1060    ///
1061    /// Returns an error if
1062    ///
1063    /// - a [`PackageReader`] cannot be created for the package,
1064    /// - the package does not contain a [ALPM-MTREE] file,
1065    /// - or the [ALPM-MTREE] file in the package is not valid.
1066    ///
1067    /// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
1068    pub fn read_mtree(&self) -> Result<Mtree, crate::Error> {
1069        let mut reader = PackageReader::try_from(self.clone())?;
1070        let metadata = reader.read_metadata_file(MetadataFileName::Mtree)?;
1071        match metadata {
1072            MetadataEntry::Mtree(mtree) => Ok(mtree),
1073            _ => Err(crate::Error::MetadataFileNotFound {
1074                name: MetadataFileName::Mtree,
1075            }),
1076        }
1077    }
1078
1079    /// Returns the contents of the optional [alpm-install-scriptlet] of the package.
1080    ///
1081    /// Returns [`None`] if the package does not contain an [alpm-install-scriptlet] file.
1082    ///
1083    /// # Errors
1084    ///
1085    /// Returns an error if
1086    ///
1087    /// - a [`PackageReader`] cannot be created for the package,
1088    /// - or reading the entries using [`PackageReader::metadata_entries`].
1089    ///
1090    /// [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
1091    pub fn read_install_scriptlet(&self) -> Result<Option<String>, crate::Error> {
1092        let mut reader = PackageReader::try_from(self.clone())?;
1093        reader.read_install_scriptlet()
1094    }
1095
1096    /// Creates a [`PackageReader`] for the package.
1097    ///
1098    /// Convenience wrapper for [`PackageReader::try_from`].
1099    ///
1100    /// # Errors
1101    ///
1102    /// Returns an error if `self` cannot be converted into a [`PackageReader`].
1103    pub fn into_reader<'c>(self) -> Result<PackageReader<'c>, crate::Error> {
1104        PackageReader::try_from(self)
1105    }
1106}
1107
1108impl TryFrom<&Path> for Package {
1109    type Error = crate::Error;
1110
1111    /// Creates a [`Package`] from a [`Path`] reference.
1112    ///
1113    /// # Errors
1114    ///
1115    /// Returns an error if
1116    ///
1117    /// - no file name can be retrieved from `path`,
1118    /// - `value` has no parent directory,
1119    /// - or [`Package::new`] fails.
1120    fn try_from(value: &Path) -> Result<Self, Self::Error> {
1121        debug!("Attempt to create a package representation from path {value:?}");
1122        let Some(parent_dir) = value.parent() else {
1123            return Err(crate::Error::PathHasNoParent {
1124                path: value.to_path_buf(),
1125            });
1126        };
1127        let Some(filename) = value.file_name().and_then(|name| name.to_str()) else {
1128            return Err(PackageError::InvalidPackageFileNamePath {
1129                path: value.to_path_buf(),
1130            }
1131            .into());
1132        };
1133
1134        Self::new(PackageFileName::from_str(filename)?, parent_dir.try_into()?)
1135    }
1136}
1137
1138impl TryFrom<&PackageCreationConfig> for Package {
1139    type Error = crate::Error;
1140
1141    /// Creates a new [`Package`] from a [`PackageCreationConfig`].
1142    ///
1143    /// Before creating a [`Package`], guarantees the on-disk file consistency with the
1144    /// help of available [`Mtree`] data.
1145    ///
1146    /// # Errors
1147    ///
1148    /// Returns an error if
1149    ///
1150    /// - creating a [`TarballBuilder`] fails,
1151    /// - creating a compressed or uncompressed package file fails,
1152    /// - validating any of the paths using ALPM-MTREE data (available through `value`) fails,
1153    /// - appending files to a compressed or uncompressed package file fails,
1154    /// - finishing a compressed or uncompressed package file fails,
1155    /// - or creating a [`Package`] fails.
1156    fn try_from(value: &PackageCreationConfig) -> Result<Self, Self::Error> {
1157        let filename = PackageFileName::from(value);
1158        let parent_dir: ExistingAbsoluteDir = value.output_dir().into();
1159        let output_path = value.output_dir().join(filename.to_path_buf());
1160
1161        // Create the output file.
1162        let file = File::create(output_path.as_path()).map_err(|source| crate::Error::IoPath {
1163            path: output_path.clone(),
1164            context: t!("error-io-create-package-file"),
1165            source,
1166        })?;
1167
1168        let mut builder = TarballBuilder::new(file, value.compression())?;
1169        builder.inner_mut().follow_symlinks(false);
1170        builder = append_relative_files(
1171            builder,
1172            value.package_input().mtree()?,
1173            &value.package_input().input_paths()?,
1174        )?;
1175        builder.finish()?;
1176
1177        Self::new(filename, parent_dir)
1178    }
1179}
1180
1181#[cfg(test)]
1182mod tests {
1183    use std::fs::create_dir;
1184
1185    use log::{LevelFilter, debug};
1186    use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
1187    use tempfile::{NamedTempFile, TempDir};
1188    use testresult::TestResult;
1189
1190    use super::*;
1191
1192    /// Initializes a global [`TermLogger`].
1193    fn init_logger() {
1194        if TermLogger::init(
1195            LevelFilter::Debug,
1196            Config::default(),
1197            TerminalMode::Mixed,
1198            ColorChoice::Auto,
1199        )
1200        .is_err()
1201        {
1202            debug!("Not initializing another logger, as one is initialized already.");
1203        }
1204    }
1205
1206    /// Ensures that [`ExistingAbsoluteDir::new`] creates non-existing, absolute paths.
1207    #[test]
1208    fn absolute_dir_new_creates_dir() -> TestResult {
1209        init_logger();
1210
1211        let temp_dir = TempDir::new()?;
1212        let path = temp_dir.path().join("additional");
1213
1214        if let Err(error) = ExistingAbsoluteDir::new(path) {
1215            panic!("Failed although it should have succeeded: {error}");
1216        }
1217
1218        Ok(())
1219    }
1220
1221    /// Ensures that [`ExistingAbsoluteDir::new`] fails on non-absolute paths and those representing
1222    /// a file.
1223    #[test]
1224    fn absolute_dir_new_fails() -> TestResult {
1225        init_logger();
1226
1227        if let Err(error) = ExistingAbsoluteDir::new(PathBuf::from("test")) {
1228            assert!(matches!(
1229                error,
1230                crate::Error::AlpmCommon(alpm_common::Error::NonAbsolutePaths { paths: _ })
1231            ));
1232        } else {
1233            panic!("Succeeded although it should have failed");
1234        }
1235
1236        let temp_file = NamedTempFile::new()?;
1237        let path = temp_file.path();
1238        if let Err(error) = ExistingAbsoluteDir::new(path.to_path_buf()) {
1239            assert!(matches!(
1240                error,
1241                crate::Error::AlpmCommon(alpm_common::Error::NotADirectory { path: _ })
1242            ));
1243        } else {
1244            panic!("Succeeded although it should have failed");
1245        }
1246
1247        Ok(())
1248    }
1249
1250    /// Ensures that utility methods of [`ExistingAbsoluteDir`] are functional.
1251    #[test]
1252    fn absolute_dir_utilities() -> TestResult {
1253        let temp_dir = TempDir::new()?;
1254        let path = temp_dir.path();
1255
1256        // Create from &Path
1257        let absolute_dir: ExistingAbsoluteDir = path.try_into()?;
1258
1259        assert_eq!(absolute_dir.as_path(), path);
1260        assert_eq!(absolute_dir.as_ref(), path);
1261
1262        Ok(())
1263    }
1264
1265    /// Ensure that [`Package::new`] can succeeds.
1266    #[test]
1267    fn package_new() -> TestResult {
1268        let temp_dir = TempDir::new()?;
1269        let path = temp_dir.path();
1270        let absolute_dir = ExistingAbsoluteDir::new(path.to_path_buf())?;
1271        let package_name = "example-1.0.0-1-x86_64.pkg.tar.zst";
1272        File::create(absolute_dir.join(package_name))?;
1273
1274        let Ok(_package) = Package::new(package_name.parse()?, absolute_dir.clone()) else {
1275            panic!("Failed although it should have succeeded");
1276        };
1277
1278        Ok(())
1279    }
1280
1281    /// Ensure that [`Package::new`] fails on a non-existent file and on paths that are not a file.
1282    #[test]
1283    fn package_new_fails() -> TestResult {
1284        let temp_dir = TempDir::new()?;
1285        let path = temp_dir.path();
1286        let absolute_dir = ExistingAbsoluteDir::new(path.to_path_buf())?;
1287        let package_name = "example-1.0.0-1-x86_64.pkg.tar.zst";
1288
1289        // The file does not exist.
1290        if let Err(error) = Package::new(package_name.parse()?, absolute_dir.clone()) {
1291            assert!(matches!(error, crate::Error::PathDoesNotExist { path: _ }))
1292        } else {
1293            panic!("Succeeded although it should have failed");
1294        }
1295
1296        // The file is a directory.
1297        create_dir(absolute_dir.join(package_name))?;
1298        if let Err(error) = Package::new(package_name.parse()?, absolute_dir.clone()) {
1299            assert!(matches!(error, crate::Error::PathIsNotAFile { path: _ }))
1300        } else {
1301            panic!("Succeeded although it should have failed");
1302        }
1303
1304        Ok(())
1305    }
1306
1307    /// Ensure that [`Package::try_from`] fails on paths not providing a file name and paths not
1308    /// providing a parent directory.
1309    #[test]
1310    fn package_try_from_path_fails() -> TestResult {
1311        init_logger();
1312
1313        // Fail on trying to use a directory without a file name as a package.
1314        assert!(Package::try_from(PathBuf::from("/").as_path()).is_err());
1315
1316        // Fail on trying to use a file without a parent
1317        assert!(
1318            Package::try_from(
1319                PathBuf::from("/something_very_unlikely_to_ever_exist_in_a_filesystem").as_path()
1320            )
1321            .is_err()
1322        );
1323
1324        Ok(())
1325    }
1326
1327    /// Ensure that the Debug implementation of [`PackageEntryIterator`] and
1328    /// [`MetadataEntryIterator`] works as expected.
1329    #[test]
1330    fn package_entry_iterators_debug() -> TestResult {
1331        init_logger();
1332
1333        let temp_dir = TempDir::new()?;
1334        let path = temp_dir.path();
1335        let absolute_dir = ExistingAbsoluteDir::new(path.to_path_buf())?;
1336        let package_name = "example-1.0.0-1-x86_64.pkg.tar.zst";
1337        File::create(absolute_dir.join(package_name))?;
1338        let package = Package::new(package_name.parse()?, absolute_dir.clone())?;
1339
1340        // Create iterators
1341        let mut reader = PackageReader::try_from(package.clone())?;
1342        let entry_iter = reader.entries()?;
1343
1344        let mut reader = PackageReader::try_from(package.clone())?;
1345        let metadata_iter = reader.metadata_entries()?;
1346
1347        assert_eq!(
1348            format!("{entry_iter:?}"),
1349            "PackageEntryIterator { entries: \"TarballEntries\", found_buildinfo: false, \
1350                found_mtree: false, found_pkginfo: false, checked_install_scriptlet: false }"
1351        );
1352        assert_eq!(
1353            format!("{metadata_iter:?}"),
1354            "MetadataEntryIterator { entries: PackageEntryIterator { entries: \"TarballEntries\", \
1355                found_buildinfo: false, found_mtree: false, found_pkginfo: false, checked_install_scriptlet: false }, \
1356                found_buildinfo: false, found_mtree: false, found_pkginfo: false }"
1357        );
1358
1359        Ok(())
1360    }
1361}