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