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