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(&'a self) -> &'a 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 if let Some(Ok(package_entry)) = &entry {
573 // Remember each file we found.
574 // Once all files are found, the iterator can short-circuit and stop early.
575 match &package_entry {
576 PackageEntry::Metadata(metadata_entry) => match **metadata_entry {
577 MetadataEntry::PackageInfo(_) => self.found_pkginfo = true,
578 MetadataEntry::BuildInfo(_) => self.found_buildinfo = true,
579 MetadataEntry::Mtree(_) => self.found_mtree = true,
580 },
581 PackageEntry::InstallScriptlet(_) => self.checked_install_scriptlet = true,
582 }
583 return entry;
584 } else if self.found_buildinfo && self.found_mtree && self.found_pkginfo {
585 // Found three required metadata files and hit the first non-metadata file.
586 // This means that install scriptlet does not exist in the package and we
587 // can stop iterating.
588 //
589 // This logic relies on the ordering of files, where the `.INSTALL` file is placed
590 // in between `.PKGINFO` and `.MTREE`.
591 self.checked_install_scriptlet = true;
592 break;
593 }
594 }
595
596 None
597 }
598}
599
600/// An iterator over each [`MetadataEntry`] of a package.
601///
602/// Stops early once all metadata files have been found.
603///
604/// # Notes
605///
606/// Uses two lifetimes for the `entries` field:
607///
608/// - `'a` for the nested internal reference of the [`Archive`] in [`Entries::fields`] in
609/// [`PackageEntryIterator`]
610/// - `'c` for the [`CompressionDecoder`]
611pub struct MetadataEntryIterator<'a, 'c> {
612 /// The archive entries iterator that contains all archive's entries.
613 entries: PackageEntryIterator<'a, 'c>,
614 /// Whether a `.BUILDINFO` file has been found.
615 found_buildinfo: bool,
616 /// Whether a `.MTREE` file has been found.
617 found_mtree: bool,
618 /// Whether a `.PKGINFO` file has been found.
619 found_pkginfo: bool,
620}
621
622impl Debug for MetadataEntryIterator<'_, '_> {
623 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
624 f.debug_struct("MetadataEntryIterator")
625 .field("entries", &self.entries)
626 .field("found_buildinfo", &self.found_buildinfo)
627 .field("found_mtree", &self.found_mtree)
628 .field("found_pkginfo", &self.found_pkginfo)
629 .finish()
630 }
631}
632
633impl<'a, 'c> MetadataEntryIterator<'a, 'c> {
634 /// Creates a new [`MetadataEntryIterator`] from a [`PackageEntryIterator`].
635 pub fn new(entries: PackageEntryIterator<'a, 'c>) -> Self {
636 Self {
637 entries,
638 found_buildinfo: false,
639 found_mtree: false,
640 found_pkginfo: false,
641 }
642 }
643
644 /// Return the inner [`PackageEntryIterator`] iterator at the current iteration position.
645 pub fn into_inner(self) -> PackageEntryIterator<'a, 'c> {
646 self.entries
647 }
648
649 /// Checks whether all variants of [`MetadataEntry`] have been found.
650 ///
651 /// Returns `true` if all known types of [`MetadataEntry`] have been found, `false` otherwise.
652 fn all_entries_found(&self) -> bool {
653 self.found_pkginfo && self.found_mtree && self.found_buildinfo
654 }
655}
656
657impl Iterator for MetadataEntryIterator<'_, '_> {
658 type Item = Result<MetadataEntry, crate::Error>;
659
660 fn next(&mut self) -> Option<Self::Item> {
661 // Return early if we already found all entries.
662 // In that case we don't need to continue iteration.
663 if self.all_entries_found() {
664 return None;
665 }
666
667 // Now check whether we have any entries left.
668 for entry_result in &mut self.entries {
669 let metadata = match entry_result {
670 Ok(PackageEntry::Metadata(metadata)) => metadata,
671 Ok(PackageEntry::InstallScriptlet(_)) => continue,
672 Err(e) => return Some(Err(e)),
673 };
674
675 match *metadata {
676 MetadataEntry::PackageInfo(_) => self.found_pkginfo = true,
677 MetadataEntry::BuildInfo(_) => self.found_buildinfo = true,
678 MetadataEntry::Mtree(_) => self.found_mtree = true,
679 }
680 return Some(Ok(*metadata));
681 }
682
683 None
684 }
685}
686
687/// A reader for [`Package`] files.
688///
689/// A [`PackageReader`] can be created from a [`Package`] using the
690/// [`Package::into_reader`] or [`PackageReader::try_from`] methods.
691///
692/// # Examples
693///
694/// ```
695/// # use std::fs::{File, Permissions, create_dir_all};
696/// # use std::io::Write;
697/// # use std::os::unix::fs::PermissionsExt;
698/// use std::path::Path;
699///
700/// # use alpm_mtree::create_mtree_v2_from_input_dir;
701/// use alpm_package::{MetadataEntry, Package, PackageReader};
702/// # use alpm_package::{
703/// # CompressionSettings,
704/// # InputDir,
705/// # OutputDir,
706/// # PackageCreationConfig,
707/// # PackageInput,
708/// # };
709/// use alpm_types::MetadataFileName;
710///
711/// # fn main() -> testresult::TestResult {
712/// // A directory for the package file.
713/// let temp_dir = tempfile::tempdir()?;
714/// let path = temp_dir.path();
715/// # let input_dir = path.join("input");
716/// # create_dir_all(&input_dir)?;
717/// # let input_dir = InputDir::new(input_dir)?;
718/// # let output_dir = OutputDir::new(path.join("output"))?;
719/// #
720/// # // Create a valid, but minimal BUILDINFOv2 file.
721/// # let mut file = File::create(&input_dir.join(MetadataFileName::BuildInfo.as_ref()))?;
722/// # write!(file, r#"
723/// # builddate = 1
724/// # builddir = /build
725/// # startdir = /startdir/
726/// # buildtool = devtools
727/// # buildtoolver = 1:1.2.1-1-any
728/// # format = 2
729/// # installed = other-example-1.2.3-1-any
730/// # packager = John Doe <john@example.org>
731/// # pkgarch = any
732/// # pkgbase = example
733/// # pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
734/// # pkgname = example
735/// # pkgver = 1.0.0-1
736/// # "#)?;
737/// #
738/// # // Create a valid, but minimal PKGINFOv2 file.
739/// # let mut file = File::create(&input_dir.join(MetadataFileName::PackageInfo.as_ref()))?;
740/// # write!(file, r#"
741/// # pkgname = example
742/// # pkgbase = example
743/// # xdata = pkgtype=pkg
744/// # pkgver = 1.0.0-1
745/// # pkgdesc = A project that returns true
746/// # url = https://example.org/
747/// # builddate = 1
748/// # packager = John Doe <john@example.org>
749/// # size = 181849963
750/// # arch = any
751/// # license = GPL-3.0-or-later
752/// # depend = bash
753/// # "#)?;
754/// #
755/// # // Create a dummy script as package data.
756/// # create_dir_all(&input_dir.join("usr/bin"))?;
757/// # let mut file = File::create(&input_dir.join("usr/bin/example"))?;
758/// # write!(file, r#"!/bin/bash
759/// # true
760/// # "#)?;
761/// # file.set_permissions(Permissions::from_mode(0o755))?;
762/// #
763/// # // Create a valid ALPM-MTREEv2 file from the input directory.
764/// # create_mtree_v2_from_input_dir(&input_dir)?;
765/// #
766/// # // Create PackageInput and PackageCreationConfig.
767/// # let package_input: PackageInput = input_dir.try_into()?;
768/// # let config = PackageCreationConfig::new(
769/// # package_input,
770/// # output_dir,
771/// # Some(CompressionSettings::default()),
772/// # )?;
773///
774/// # // Create package file.
775/// # let package = Package::try_from(&config)?;
776/// // Assume that the package is created
777/// let package_path = path.join("output/example-1.0.0-1-any.pkg.tar.zst");
778///
779/// // Create a reader for the package.
780/// let mut reader = package.clone().into_reader()?;
781///
782/// // Read all the metadata from the package archive.
783/// let metadata = reader.metadata()?;
784/// let pkginfo = metadata.pkginfo;
785/// let buildinfo = metadata.buildinfo;
786/// let mtree = metadata.mtree;
787///
788/// // Or you can iterate over the metadata entries:
789/// let mut reader = package.clone().into_reader()?;
790/// for entry in reader.metadata_entries()? {
791/// let entry = entry?;
792/// match entry {
793/// MetadataEntry::PackageInfo(pkginfo) => {}
794/// MetadataEntry::BuildInfo(buildinfo) => {}
795/// MetadataEntry::Mtree(mtree) => {}
796/// _ => {}
797/// }
798/// }
799///
800/// // You can also read specific metadata files directly:
801/// let mut reader = package.clone().into_reader()?;
802/// let pkginfo = reader.read_metadata_file(MetadataFileName::PackageInfo)?;
803/// // let buildinfo = reader.read_metadata_file(MetadataFileName::BuildInfo)?;
804/// // let mtree = reader.read_metadata_file(MetadataFileName::Mtree)?;
805///
806/// // Read the install scriptlet, if present:
807/// let mut reader = package.clone().into_reader()?;
808/// let install_scriptlet = reader.read_install_scriptlet()?;
809///
810/// // Iterate over the data entries in the package archive.
811/// let mut reader = package.clone().into_reader()?;
812/// for data_entry in reader.data_entries()? {
813/// let mut data_entry = data_entry?;
814/// let content = data_entry.content()?;
815/// // Note: data_entry also implements `Read`, so you can read from it directly.
816/// }
817/// # Ok(())
818/// # }
819/// ```
820///
821/// # Notes
822///
823/// This API is designed with **streaming** and **single-pass iteration** in mind.
824///
825/// Calling [`Package::into_reader`] creates a new [`PackageReader`] each time,
826/// which consumes the underlying archive in a forward-only manner. This allows
827/// efficient access to package contents without needing to load the entire archive
828/// into memory.
829///
830/// If you need to perform multiple operations on a package, you can call
831/// [`Package::into_reader`] multiple times — each reader starts fresh and ensures
832/// predictable, deterministic access to the archive's contents.
833///
834/// Please note that convenience methods on [`Package`] itself, such as
835/// [`Package::read_pkginfo`], are also provided for better ergonomics
836/// and ease of use.
837///
838/// The lifetimes `'c` is for the [`CompressionDecoder`] of the [`Archive`]
839pub struct PackageReader<'c> {
840 archive: Archive<CompressionDecoder<'c>>,
841}
842
843impl Debug for PackageReader<'_> {
844 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
845 f.debug_struct("PackageReader")
846 .field("archive", &"Archive<CompressionDecoder>")
847 .finish()
848 }
849}
850
851impl<'c> PackageReader<'c> {
852 /// Creates a new [`PackageReader`] from an [`Archive<CompressionDecoder>`].
853 pub fn new(archive: Archive<CompressionDecoder<'c>>) -> Self {
854 Self { archive }
855 }
856
857 /// Returns an iterator over the raw entries of the package's tar archive.
858 ///
859 /// The returned [`Entries`] implements an iterator over each [`Entry`],
860 /// which provides direct data access to all entries of the package's tar archive.
861 ///
862 /// # Errors
863 ///
864 /// Returns an error if the [`Entries`] cannot be read from the package's tar archive.
865 pub fn raw_entries(&mut self) -> Result<Entries<'_, CompressionDecoder<'c>>, crate::Error> {
866 self.archive
867 .entries()
868 .map_err(|source| crate::Error::IoRead {
869 context: "reading package archive entries",
870 source,
871 })
872 }
873
874 /// Returns an iterator over the known files in the [alpm-package] file.
875 ///
876 /// This iterator yields a set of [`PackageEntry`] variants, which may only contain data
877 /// from metadata files (i.e. [ALPM-MTREE], [BUILDINFO] or [PKGINFO]) or an install scriptlet
878 /// (i.e. [alpm-install-scriplet]).
879 ///
880 /// # Note
881 ///
882 /// The file names of metadata file formats (i.e. [ALPM-MTREE], [BUILDINFO], [PKGINFO])
883 /// and install scriptlets (i.e. [alpm-install-scriptlet]) are prefixed with a dot (`.`)
884 /// in [alpm-package] files.
885 ///
886 /// As [alpm-package] files are assumed to contain a sorted list of entries, these files are
887 /// considered first. The iterator stops as soon as it encounters an entry that does not
888 /// match any known metadata file or install scriptlet file name.
889 ///
890 /// # Errors
891 ///
892 /// Returns an error if
893 ///
894 /// - reading the package archive entries fails,
895 /// - reading a package archive entry fails,
896 /// - reading the contents of a package archive entry fails,
897 /// - or retrieving the path of a package archive entry fails.
898 ///
899 /// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
900 /// [BUILDINFO]: https://alpm.archlinux.page/specifications/BUILDINFO.5.html
901 /// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
902 /// [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
903 /// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
904 pub fn entries<'a>(&'a mut self) -> Result<PackageEntryIterator<'a, 'c>, crate::Error> {
905 let entries = self.raw_entries()?;
906 Ok(PackageEntryIterator::new(entries))
907 }
908
909 /// Returns an iterator over the metadata entries in the package archive.
910 ///
911 /// This iterator yields [`MetadataEntry`]s, which can be either [PKGINFO], [BUILDINFO],
912 /// or [ALPM-MTREE].
913 ///
914 /// The iterator stops when it encounters an entry that does not match any
915 /// known package files.
916 ///
917 /// It is a wrapper around [`PackageReader::entries`] that filters out
918 /// the install scriptlet.
919 ///
920 /// # Errors
921 ///
922 /// Returns an error if [`PackageReader::entries`] fails to read the entries.
923 ///
924 /// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
925 /// [BUILDINFO]: https://alpm.archlinux.page/specifications/BUILDINFO.5.html
926 /// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
927 pub fn metadata_entries<'a>(
928 &'a mut self,
929 ) -> Result<MetadataEntryIterator<'a, 'c>, crate::Error> {
930 let entries = self.entries()?;
931 Ok(MetadataEntryIterator::new(entries))
932 }
933
934 /// Returns an iterator over the data files of the [alpm-package] archive.
935 ///
936 /// This iterator yields the path and content of each data file of a package archive in the form
937 /// of a [`DataEntry`].
938 ///
939 /// # Notes
940 ///
941 /// This iterator filters out the known metadata files [PKGINFO], [BUILDINFO] and [ALPM-MTREE].
942 /// and the [alpm-install-scriplet] file.
943 ///
944 /// # Errors
945 ///
946 /// Returns an error if
947 ///
948 /// - reading the package archive entries fails,
949 /// - reading a package archive entry fails,
950 /// - reading the contents of a package archive entry fails,
951 /// - or retrieving the path of a package archive entry fails.
952 ///
953 /// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
954 /// [BUILDINFO]: https://alpm.archlinux.page/specifications/BUILDINFO.5.html
955 /// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
956 /// [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
957 /// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
958 pub fn data_entries<'a>(
959 &'a mut self,
960 ) -> Result<impl Iterator<Item = Result<DataEntry<'a, 'c>, crate::Error>>, crate::Error> {
961 let non_data_file_names = [
962 MetadataFileName::PackageInfo.as_ref(),
963 MetadataFileName::BuildInfo.as_ref(),
964 MetadataFileName::Mtree.as_ref(),
965 INSTALL_SCRIPTLET_FILE_NAME,
966 ];
967 let entries = self.raw_entries()?;
968 Ok(entries.filter_map(move |entry| {
969 let filter = (|| {
970 let entry = entry.map_err(|source| crate::Error::IoRead {
971 context: "reading package archive entry",
972 source,
973 })?;
974 let path = entry.path().map_err(|source| crate::Error::IoRead {
975 context: "retrieving path of package archive entry",
976 source,
977 })?;
978 // Filter out known metadata files
979 if non_data_file_names.contains(&path.to_string_lossy().as_ref()) {
980 return Ok(None);
981 }
982 Ok(Some(DataEntry {
983 path: path.to_path_buf(),
984 entry,
985 }))
986 })();
987 filter.transpose()
988 }))
989 }
990
991 /// Reads all metadata from an [alpm-package] file.
992 ///
993 /// This method reads all the metadata entries in the package file and returns a
994 /// [`Metadata`] struct containing the processed data.
995 ///
996 /// # Errors
997 ///
998 /// Returns an error if
999 ///
1000 /// - reading the metadata entries fails,
1001 /// - parsing a metadata entry fails,
1002 /// - or if any of the required metadata files are not found in the package.
1003 ///
1004 /// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
1005 pub fn metadata(&mut self) -> Result<Metadata, crate::Error> {
1006 let mut pkginfo = None;
1007 let mut buildinfo = None;
1008 let mut mtree = None;
1009 for entry in self.metadata_entries()? {
1010 match entry? {
1011 MetadataEntry::PackageInfo(m) => pkginfo = Some(m),
1012 MetadataEntry::BuildInfo(m) => buildinfo = Some(m),
1013 MetadataEntry::Mtree(m) => mtree = Some(m),
1014 }
1015 }
1016 Ok(Metadata {
1017 pkginfo: pkginfo.ok_or(crate::Error::MetadataFileNotFound {
1018 name: MetadataFileName::PackageInfo,
1019 })?,
1020 buildinfo: buildinfo.ok_or(crate::Error::MetadataFileNotFound {
1021 name: MetadataFileName::BuildInfo,
1022 })?,
1023 mtree: mtree.ok_or(crate::Error::MetadataFileNotFound {
1024 name: MetadataFileName::Mtree,
1025 })?,
1026 })
1027 }
1028
1029 /// Reads the data of a specific metadata file from the [alpm-package] file.
1030 ///
1031 /// This method searches for a metadata file that matches the provided
1032 /// [`MetadataFileName`] and returns the corresponding [`MetadataEntry`].
1033 ///
1034 /// # Errors
1035 ///
1036 /// Returns an error if
1037 ///
1038 /// - [`PackageReader::metadata_entries`] fails to retrieve the metadata entries,
1039 /// - or a [`MetadataEntry`] is not valid.
1040 ///
1041 /// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
1042 pub fn read_metadata_file(
1043 &mut self,
1044 file_name: MetadataFileName,
1045 ) -> Result<MetadataEntry, crate::Error> {
1046 for entry in self.metadata_entries()? {
1047 let entry = entry?;
1048 match (&entry, &file_name) {
1049 (MetadataEntry::PackageInfo(_), MetadataFileName::PackageInfo)
1050 | (MetadataEntry::BuildInfo(_), MetadataFileName::BuildInfo)
1051 | (MetadataEntry::Mtree(_), MetadataFileName::Mtree) => return Ok(entry),
1052 _ => continue,
1053 }
1054 }
1055 Err(crate::Error::MetadataFileNotFound { name: file_name })
1056 }
1057
1058 /// Reads the content of the [alpm-install-scriptlet] from the package archive, if it exists.
1059 ///
1060 /// # Errors
1061 ///
1062 /// Returns an error if [`PackageReader::entries`] fails to read the entries.
1063 ///
1064 /// [alpm-install-scriplet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
1065 pub fn read_install_scriptlet(&mut self) -> Result<Option<String>, crate::Error> {
1066 for entry in self.entries()? {
1067 let entry = entry?;
1068 if let PackageEntry::InstallScriptlet(scriptlet) = entry {
1069 return Ok(Some(scriptlet));
1070 }
1071 }
1072 Ok(None)
1073 }
1074
1075 /// Reads a [`DataEntry`] matching a specific path name from the package archive.
1076 ///
1077 /// Returns [`None`] if no [`DataEntry`] is found in the package archive that matches `path`.
1078 ///
1079 /// # Errors
1080 ///
1081 /// Returns an error if
1082 ///
1083 /// - [`PackageReader::data_entries`] fails to retrieve the data entries,
1084 /// - or retrieving the details of a data entry fails.
1085 pub fn read_data_entry<'a, P: AsRef<Path>>(
1086 &'a mut self,
1087 path: P,
1088 ) -> Result<Option<DataEntry<'a, 'c>>, crate::Error> {
1089 for entry in self.data_entries()? {
1090 let entry = entry?;
1091 if entry.path() == path.as_ref() {
1092 return Ok(Some(entry));
1093 }
1094 }
1095 Ok(None)
1096 }
1097}
1098
1099impl TryFrom<Package> for PackageReader<'_> {
1100 type Error = crate::Error;
1101
1102 /// Creates a [`PackageReader`] from a [`Package`].
1103 ///
1104 /// # Errors
1105 ///
1106 /// Returns an error if:
1107 ///
1108 /// - the package file cannot be opened,
1109 /// - the package file extension cannot be determined,
1110 /// - or the compression decoder cannot be created from the file and its extension.
1111 fn try_from(package: Package) -> Result<Self, Self::Error> {
1112 let path = package.to_path_buf();
1113 let file = File::open(&path).map_err(|source| crate::Error::IoPath {
1114 path: path.clone(),
1115 context: "opening package file",
1116 source,
1117 })?;
1118 let extension = CompressionAlgorithmFileExtension::try_from(path.as_path())?;
1119 let algorithm = CompressionAlgorithm::try_from(extension)?;
1120 let decoder = CompressionDecoder::new(file, algorithm)?;
1121 let archive = Archive::new(decoder);
1122 Ok(Self::new(archive))
1123 }
1124}
1125
1126impl TryFrom<&Path> for PackageReader<'_> {
1127 type Error = crate::Error;
1128
1129 /// Creates a [`PackageReader`] from a [`Path`].
1130 ///
1131 /// # Errors
1132 ///
1133 /// Returns an error if:
1134 ///
1135 /// - [`Package::try_from`] fails to create a [`Package`] from `path`,
1136 /// - or [`PackageReader::try_from`] fails to create a [`PackageReader`] from the package.
1137 fn try_from(path: &Path) -> Result<Self, Self::Error> {
1138 let package = Package::try_from(path)?;
1139 PackageReader::try_from(package)
1140 }
1141}
1142
1143/// An [alpm-package] file.
1144///
1145/// Tracks the [`PackageFileName`] of the [alpm-package] as well as its absolute `parent_dir`.
1146///
1147/// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
1148#[derive(Clone, Debug)]
1149pub struct Package {
1150 file_name: PackageFileName,
1151 parent_dir: ExistingAbsoluteDir,
1152}
1153
1154impl Package {
1155 /// Creates a new [`Package`].
1156 ///
1157 /// # Errors
1158 ///
1159 /// Returns an error if no file exists at the path defined by `parent_dir` and `filename`.
1160 pub fn new(
1161 file_name: PackageFileName,
1162 parent_dir: ExistingAbsoluteDir,
1163 ) -> Result<Self, crate::Error> {
1164 let file_path = parent_dir.to_path_buf().join(file_name.to_path_buf());
1165 if !file_path.exists() {
1166 return Err(crate::Error::PathDoesNotExist { path: file_path });
1167 }
1168 if !file_path.is_file() {
1169 return Err(crate::Error::PathIsNotAFile { path: file_path });
1170 }
1171
1172 Ok(Self {
1173 file_name,
1174 parent_dir,
1175 })
1176 }
1177
1178 /// Returns the absolute path of the [`Package`].
1179 pub fn to_path_buf(&self) -> PathBuf {
1180 self.parent_dir.join(self.file_name.to_path_buf())
1181 }
1182
1183 /// Returns the [`PackageInfo`] of the package.
1184 ///
1185 /// This is a convenience wrapper around [`PackageReader::read_metadata_file`].
1186 ///
1187 /// # Errors
1188 ///
1189 /// Returns an error if
1190 ///
1191 /// - a [`PackageReader`] cannot be created for the package,
1192 /// - the package does not contain a [PKGINFO] file,
1193 /// - or the [PKGINFO] file in the package is not valid.
1194 ///
1195 /// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
1196 pub fn read_pkginfo(&self) -> Result<PackageInfo, crate::Error> {
1197 let mut reader = PackageReader::try_from(self.clone())?;
1198 let metadata = reader.read_metadata_file(MetadataFileName::PackageInfo)?;
1199 match metadata {
1200 MetadataEntry::PackageInfo(pkginfo) => Ok(pkginfo),
1201 _ => Err(crate::Error::MetadataFileNotFound {
1202 name: MetadataFileName::PackageInfo,
1203 }),
1204 }
1205 }
1206
1207 /// Returns the [`BuildInfo`] of the package.
1208 ///
1209 /// This is a convenience wrapper around [`PackageReader::read_metadata_file`].
1210 ///
1211 /// # Errors
1212 ///
1213 /// Returns an error if
1214 ///
1215 /// - a [`PackageReader`] cannot be created for the package,
1216 /// - the package does not contain a [BUILDINFO] file,
1217 /// - or the [BUILDINFO] file in the package is not valid.
1218 ///
1219 /// [BUILDINFO]: https://alpm.archlinux.page/specifications/BUILDINFO.5.html
1220 pub fn read_buildinfo(&self) -> Result<BuildInfo, crate::Error> {
1221 let mut reader = PackageReader::try_from(self.clone())?;
1222 let metadata = reader.read_metadata_file(MetadataFileName::BuildInfo)?;
1223 match metadata {
1224 MetadataEntry::BuildInfo(buildinfo) => Ok(buildinfo),
1225 _ => Err(crate::Error::MetadataFileNotFound {
1226 name: MetadataFileName::BuildInfo,
1227 }),
1228 }
1229 }
1230
1231 /// Returns the [`Mtree`] of the package.
1232 ///
1233 /// This is a convenience wrapper around [`PackageReader::read_metadata_file`].
1234 ///
1235 /// # Errors
1236 ///
1237 /// Returns an error if
1238 ///
1239 /// - a [`PackageReader`] cannot be created for the package,
1240 /// - the package does not contain a [ALPM-MTREE] file,
1241 /// - or the [ALPM-MTREE] file in the package is not valid.
1242 ///
1243 /// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
1244 pub fn read_mtree(&self) -> Result<Mtree, crate::Error> {
1245 let mut reader = PackageReader::try_from(self.clone())?;
1246 let metadata = reader.read_metadata_file(MetadataFileName::Mtree)?;
1247 match metadata {
1248 MetadataEntry::Mtree(mtree) => Ok(mtree),
1249 _ => Err(crate::Error::MetadataFileNotFound {
1250 name: MetadataFileName::Mtree,
1251 }),
1252 }
1253 }
1254
1255 /// Returns the contents of the optional [alpm-install-scriptlet] of the package.
1256 ///
1257 /// Returns [`None`] if the package does not contain an [alpm-install-scriptlet] file.
1258 ///
1259 /// # Errors
1260 ///
1261 /// Returns an error if
1262 ///
1263 /// - a [`PackageReader`] cannot be created for the package,
1264 /// - or reading the entries using [`PackageReader::metadata_entries`].
1265 ///
1266 /// [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
1267 pub fn read_install_scriptlet(&self) -> Result<Option<String>, crate::Error> {
1268 let mut reader = PackageReader::try_from(self.clone())?;
1269 reader.read_install_scriptlet()
1270 }
1271
1272 /// Creates a [`PackageReader`] for the package.
1273 ///
1274 /// Convenience wrapper for [`PackageReader::try_from`].
1275 ///
1276 /// # Errors
1277 ///
1278 /// Returns an error if `self` cannot be converted into a [`PackageReader`].
1279 pub fn into_reader<'c>(self) -> Result<PackageReader<'c>, crate::Error> {
1280 PackageReader::try_from(self)
1281 }
1282}
1283
1284impl TryFrom<&Path> for Package {
1285 type Error = crate::Error;
1286
1287 /// Creates a [`Package`] from a [`Path`] reference.
1288 ///
1289 /// # Errors
1290 ///
1291 /// Returns an error if
1292 ///
1293 /// - no file name can be retrieved from `path`,
1294 /// - `value` has no parent directory,
1295 /// - or [`Package::new`] fails.
1296 fn try_from(value: &Path) -> Result<Self, Self::Error> {
1297 debug!("Attempt to create a package representation from path {value:?}");
1298 let Some(parent_dir) = value.parent() else {
1299 return Err(crate::Error::PathHasNoParent {
1300 path: value.to_path_buf(),
1301 });
1302 };
1303 let Some(filename) = value.file_name().and_then(|name| name.to_str()) else {
1304 return Err(PackageError::InvalidPackageFileNamePath {
1305 path: value.to_path_buf(),
1306 }
1307 .into());
1308 };
1309
1310 Self::new(PackageFileName::from_str(filename)?, parent_dir.try_into()?)
1311 }
1312}
1313
1314impl TryFrom<&PackageCreationConfig> for Package {
1315 type Error = crate::Error;
1316
1317 /// Creates a new [`Package`] from a [`PackageCreationConfig`].
1318 ///
1319 /// Before creating a [`Package`], guarantees the on-disk file consistency with the
1320 /// help of available [`Mtree`] data.
1321 ///
1322 /// # Errors
1323 ///
1324 /// Returns an error if
1325 ///
1326 /// - creating a [`PackageFileName`] from `value` fails,
1327 /// - creating a [`CompressionEncoder`] fails,
1328 /// - creating a compressed or uncompressed package file fails,
1329 /// - validating any of the paths using ALPM-MTREE data (available through `value`) fails,
1330 /// - appending files to a compressed or uncompressed package file fails,
1331 /// - finishing a compressed or uncompressed package file fails,
1332 /// - or creating a [`Package`] fails.
1333 fn try_from(value: &PackageCreationConfig) -> Result<Self, Self::Error> {
1334 let filename = PackageFileName::from(value);
1335 let parent_dir: ExistingAbsoluteDir = value.output_dir().into();
1336 let output_path = value.output_dir().join(filename.to_path_buf());
1337
1338 // Create the output file.
1339 let file = File::create(output_path.as_path()).map_err(|source| crate::Error::IoPath {
1340 path: output_path.clone(),
1341 context: "creating a package file",
1342 source,
1343 })?;
1344
1345 // If compression is requested, create a dedicated compression encoder streaming to a file
1346 // and a tar builder that streams to the compression encoder.
1347 // Append all files and directories to it, then finish the tar builder and the compression
1348 // encoder streams.
1349 if let Some(compression) = value.compression() {
1350 let encoder = CompressionEncoder::new(file, compression)?;
1351 let mut builder = Builder::new(encoder);
1352 // We do not want to follow symlinks but instead archive symlinks!
1353 builder.follow_symlinks(false);
1354 let builder = append_relative_files(
1355 builder,
1356 value.package_input().mtree()?,
1357 &value.package_input().input_paths()?,
1358 )?;
1359 let encoder = builder
1360 .into_inner()
1361 .map_err(|source| Error::FinishArchive {
1362 package_path: output_path.clone(),
1363 source,
1364 })?;
1365 encoder.finish()?;
1366 // If no compression is requested, only create a tar builder.
1367 // Append all files and directories to it, then finish the tar builder stream.
1368 } else {
1369 let mut builder = Builder::new(file);
1370 // We do not want to follow symlinks but instead archive symlinks!
1371 builder.follow_symlinks(false);
1372 let mut builder = append_relative_files(
1373 builder,
1374 value.package_input().mtree()?,
1375 &value.package_input().input_paths()?,
1376 )?;
1377 builder.finish().map_err(|source| Error::FinishArchive {
1378 package_path: output_path.clone(),
1379 source,
1380 })?;
1381 }
1382
1383 Self::new(filename, parent_dir)
1384 }
1385}
1386
1387#[cfg(test)]
1388mod tests {
1389
1390 use std::fs::create_dir;
1391
1392 use log::{LevelFilter, debug};
1393 use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
1394 use tempfile::{NamedTempFile, TempDir};
1395 use testresult::TestResult;
1396
1397 use super::*;
1398
1399 /// Initializes a global [`TermLogger`].
1400 fn init_logger() {
1401 if TermLogger::init(
1402 LevelFilter::Debug,
1403 Config::default(),
1404 TerminalMode::Mixed,
1405 ColorChoice::Auto,
1406 )
1407 .is_err()
1408 {
1409 debug!("Not initializing another logger, as one is initialized already.");
1410 }
1411 }
1412
1413 /// Ensures that [`ExistingAbsoluteDir::new`] creates non-existing, absolute paths.
1414 #[test]
1415 fn absolute_dir_new_creates_dir() -> TestResult {
1416 init_logger();
1417
1418 let temp_dir = TempDir::new()?;
1419 let path = temp_dir.path().join("additional");
1420
1421 if let Err(error) = ExistingAbsoluteDir::new(path) {
1422 return Err(format!("Failed although it should have succeeded: {error}").into());
1423 }
1424
1425 Ok(())
1426 }
1427
1428 /// Ensures that [`ExistingAbsoluteDir::new`] fails on non-absolute paths and those representing
1429 /// a file.
1430 #[test]
1431 fn absolute_dir_new_fails() -> TestResult {
1432 init_logger();
1433
1434 if let Err(error) = ExistingAbsoluteDir::new(PathBuf::from("test")) {
1435 assert!(matches!(
1436 error,
1437 crate::Error::AlpmCommon(alpm_common::Error::NonAbsolutePaths { paths: _ })
1438 ));
1439 } else {
1440 return Err("Succeeded although it should have failed".into());
1441 }
1442
1443 let temp_file = NamedTempFile::new()?;
1444 let path = temp_file.path();
1445 if let Err(error) = ExistingAbsoluteDir::new(path.to_path_buf()) {
1446 assert!(matches!(
1447 error,
1448 crate::Error::AlpmCommon(alpm_common::Error::NotADirectory { path: _ })
1449 ));
1450 } else {
1451 return Err("Succeeded although it should have failed".into());
1452 }
1453
1454 Ok(())
1455 }
1456
1457 /// Ensures that utility methods of [`ExistingAbsoluteDir`] are functional.
1458 #[test]
1459 fn absolute_dir_utilities() -> TestResult {
1460 let temp_dir = TempDir::new()?;
1461 let path = temp_dir.path();
1462
1463 // Create from &Path
1464 let absolute_dir: ExistingAbsoluteDir = path.try_into()?;
1465
1466 assert_eq!(absolute_dir.as_path(), path);
1467 assert_eq!(absolute_dir.as_ref(), path);
1468
1469 Ok(())
1470 }
1471
1472 /// Ensure that [`Package::new`] can succeeds.
1473 #[test]
1474 fn package_new() -> TestResult {
1475 let temp_dir = TempDir::new()?;
1476 let path = temp_dir.path();
1477 let absolute_dir = ExistingAbsoluteDir::new(path.to_path_buf())?;
1478 let package_name = "example-1.0.0-1-x86_64.pkg.tar.zst";
1479 File::create(absolute_dir.join(package_name))?;
1480
1481 let Ok(_package) = Package::new(package_name.parse()?, absolute_dir.clone()) else {
1482 return Err("Failed although it should have succeeded".into());
1483 };
1484
1485 Ok(())
1486 }
1487
1488 /// Ensure that [`Package::new`] fails on a non-existent file and on paths that are not a file.
1489 #[test]
1490 fn package_new_fails() -> TestResult {
1491 let temp_dir = TempDir::new()?;
1492 let path = temp_dir.path();
1493 let absolute_dir = ExistingAbsoluteDir::new(path.to_path_buf())?;
1494 let package_name = "example-1.0.0-1-x86_64.pkg.tar.zst";
1495
1496 // The file does not exist.
1497 if let Err(error) = Package::new(package_name.parse()?, absolute_dir.clone()) {
1498 assert!(matches!(error, crate::Error::PathDoesNotExist { path: _ }))
1499 } else {
1500 return Err("Succeeded although it should have failed".into());
1501 }
1502
1503 // The file is a directory.
1504 create_dir(absolute_dir.join(package_name))?;
1505 if let Err(error) = Package::new(package_name.parse()?, absolute_dir.clone()) {
1506 assert!(matches!(error, crate::Error::PathIsNotAFile { path: _ }))
1507 } else {
1508 return Err("Succeeded although it should have failed".into());
1509 }
1510
1511 Ok(())
1512 }
1513
1514 /// Ensure that [`Package::try_from`] fails on paths not providing a file name and paths not
1515 /// providing a parent directory.
1516 #[test]
1517 fn package_try_from_path_fails() -> TestResult {
1518 init_logger();
1519
1520 // Fail on trying to use a directory without a file name as a package.
1521 assert!(Package::try_from(PathBuf::from("/").as_path()).is_err());
1522
1523 // Fail on trying to use a file without a parent
1524 assert!(
1525 Package::try_from(
1526 PathBuf::from("/something_very_unlikely_to_ever_exist_in_a_filesystem").as_path()
1527 )
1528 .is_err()
1529 );
1530
1531 Ok(())
1532 }
1533
1534 /// Ensure that the Debug implementation of [`PackageEntryIterator`] and
1535 /// [`MetadataEntryIterator`] works as expected.
1536 #[test]
1537 fn package_entry_iterators_debug() -> TestResult {
1538 init_logger();
1539
1540 let temp_dir = TempDir::new()?;
1541 let path = temp_dir.path();
1542 let absolute_dir = ExistingAbsoluteDir::new(path.to_path_buf())?;
1543 let package_name = "example-1.0.0-1-x86_64.pkg.tar.zst";
1544 File::create(absolute_dir.join(package_name))?;
1545 let package = Package::new(package_name.parse()?, absolute_dir.clone())?;
1546
1547 // Create iterators
1548 let mut reader = PackageReader::try_from(package.clone())?;
1549 let entry_iter = reader.entries()?;
1550
1551 let mut reader = PackageReader::try_from(package.clone())?;
1552 let metadata_iter = reader.metadata_entries()?;
1553
1554 assert_eq!(
1555 format!("{entry_iter:?}"),
1556 "PackageEntryIterator { entries: \"Entries<CompressionDecoder>\", found_buildinfo: false, \
1557 found_mtree: false, found_pkginfo: false, checked_install_scriptlet: false }"
1558 );
1559 assert_eq!(
1560 format!("{metadata_iter:?}"),
1561 "MetadataEntryIterator { entries: PackageEntryIterator { entries: \"Entries<CompressionDecoder>\", \
1562 found_buildinfo: false, found_mtree: false, found_pkginfo: false, checked_install_scriptlet: false }, \
1563 found_buildinfo: false, found_mtree: false, found_pkginfo: false }"
1564 );
1565
1566 Ok(())
1567 }
1568}