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