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}