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