alpm_common/package/
input.rs

1//! Helpers for package input handling.
2//!
3//! Contains functions for generically deriving the files and directories contained in a package
4//! input directory.
5//! This functionality is used by libraries and tools that deal with files in input directories
6//! (e.g. [ALPM-MTREE] and [alpm-package]).
7//!
8//! [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
9//! [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
10
11use std::{
12    fs::read_dir,
13    path::{Path, PathBuf},
14};
15
16use alpm_types::{INSTALL_SCRIPTLET_FILE_NAME, MetadataFileName};
17
18/// An input path.
19///
20/// Tracks an absolute base directory and a relative path.
21#[derive(Clone, Copy, Debug)]
22pub struct InputPath<'a, 'b> {
23    base_dir: &'a Path,
24    path: &'b Path,
25}
26
27impl<'a, 'b> InputPath<'a, 'b> {
28    /// Creates a new [`InputPath`].
29    ///
30    /// # Errors
31    ///
32    /// Returns an error if
33    /// - `base_dir` is not absolute,
34    /// - `base_dir` is not a directory,
35    /// - or `path` is not relative.
36    pub fn new(base_dir: &'a Path, path: &'b Path) -> Result<Self, crate::Error> {
37        if !base_dir.is_absolute() {
38            return Err(crate::Error::NonAbsolutePaths {
39                paths: vec![base_dir.to_path_buf()],
40            });
41        }
42        if !base_dir.is_dir() {
43            return Err(crate::Error::NotADirectory {
44                path: base_dir.to_path_buf(),
45            });
46        }
47
48        if !path.is_relative() {
49            return Err(crate::Error::NonRelativePaths {
50                paths: vec![path.to_path_buf()],
51            });
52        }
53
54        Ok(Self { base_dir, path })
55    }
56
57    /// Returns a reference to the base dir.
58    pub fn base_dir(&self) -> &Path {
59        self.base_dir
60    }
61
62    /// Returns a reference to the relative path in base dir.
63    pub fn path(&self) -> &Path {
64        self.path
65    }
66
67    /// Returns an absolute path as combination of base directory and relative path.
68    pub fn to_path_buf(&self) -> PathBuf {
69        self.base_dir.join(self.path)
70    }
71}
72
73/// A set of input paths.
74///
75/// Tracks a base directory and a set of relative paths.
76#[derive(Clone, Copy, Debug)]
77pub struct InputPaths<'a, 'b> {
78    base_dir: &'a Path,
79    paths: &'b [PathBuf],
80}
81
82impl<'a, 'b> InputPaths<'a, 'b> {
83    /// Creates a new [`InputPaths`].
84    ///
85    /// # Errors
86    ///
87    /// Returns an error if
88    /// - `base_dir` is not absolute,
89    /// - `base_dir` is not a directory,
90    /// - or one or more paths in `paths` are not relative.
91    pub fn new(base_dir: &'a Path, paths: &'b [PathBuf]) -> Result<Self, crate::Error> {
92        if !base_dir.is_absolute() {
93            return Err(crate::Error::NonAbsolutePaths {
94                paths: vec![base_dir.to_path_buf()],
95            });
96        }
97        if !base_dir.is_dir() {
98            return Err(crate::Error::NotADirectory {
99                path: base_dir.to_path_buf(),
100            });
101        }
102
103        let mut non_relative = Vec::new();
104        for path in paths {
105            if !path.is_relative() {
106                non_relative.push(path.clone())
107            }
108        }
109        if !non_relative.is_empty() {
110            return Err(crate::Error::NonRelativePaths {
111                paths: non_relative,
112            });
113        }
114
115        Ok(Self { base_dir, paths })
116    }
117
118    /// Returns a reference to the base dir.
119    pub fn base_dir(&self) -> &Path {
120        self.base_dir
121    }
122
123    /// Returns a reference to the list of relative paths in base dir.
124    pub fn paths(&self) -> &[PathBuf] {
125        self.paths
126    }
127}
128
129/// Collects all data files in a directory, relative to it.
130///
131/// Convenience wrapper around [`relative_files`] that passes in all variants of
132/// [`MetadataFileName`] as well as [`INSTALL_SCRIPTLET_FILE_NAME`] to its `filter` option.
133/// This ensures, that only the paths of data files are returned.
134///
135/// # Errors
136///
137/// Returns an error if [`relative_files`] fails.
138pub fn relative_data_files(path: impl AsRef<Path>) -> Result<Vec<PathBuf>, crate::Error> {
139    relative_files(
140        path,
141        &[
142            MetadataFileName::BuildInfo.as_ref(),
143            MetadataFileName::Mtree.as_ref(),
144            MetadataFileName::PackageInfo.as_ref(),
145            INSTALL_SCRIPTLET_FILE_NAME,
146        ],
147    )
148}
149
150/// Collects all files contained in a directory `path` as a list of sorted relative paths.
151///
152/// Recursively iterates over all entries of `path` (see [`read_dir`]).
153/// All returned entries are stripped using `path` (see [`Path::strip_prefix`]), effectively
154/// providing a list of relative paths below `path`.
155/// The list of paths is sorted (see [`slice::sort`]).
156///
157/// When providing file names using `filter`, any path found ending with one of the filter names
158/// will be skipped and not returned in the list of paths.
159///
160/// # Note
161///
162/// This function does not follow symlinks but instead returns the path of a symlink.
163///
164/// # Errors
165///
166/// Returns an error if
167///
168/// - calling [`read_dir`] on `path` or any of its subdirectories fails,
169/// - an entry in one of the (sub)directories can not be retrieved,
170/// - or stripping the prefix of a file in a (sub)directory fails.
171pub fn relative_files(
172    path: impl AsRef<Path>,
173    filter: &[&str],
174) -> Result<Vec<PathBuf>, crate::Error> {
175    let path = path.as_ref();
176    let init_path = path;
177
178    /// Collects all files in a `path` as a sorted list of paths and strips `init_path` from them.
179    ///
180    /// Recursively calls itself on all directories contained in `path`, retaining `init_path` and
181    /// `filter` in these calls.
182    /// When providing filenames using `filter`, paths that end in those filenames will be skipped
183    /// and not returned in the list of paths.
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if
188    ///
189    /// - calling [`read_dir`] on `path` or any of its subdirectories fails,
190    /// - an entry in one of the (sub)directories can not be retrieved,
191    /// - or stripping the prefix of a file in a (sub)directory fails.
192    fn collect_files(
193        path: &Path,
194        init_path: &Path,
195        filter: &[&str],
196    ) -> Result<Vec<PathBuf>, crate::Error> {
197        let mut paths = Vec::new();
198        let entries = read_dir(path).map_err(|source| crate::Error::IoPath {
199            path: path.to_path_buf(),
200            context: "reading entries of directory",
201            source,
202        })?;
203        for entry in entries {
204            let entry = entry.map_err(|source| crate::Error::IoPath {
205                path: path.to_path_buf(),
206                context: "reading entry in directory",
207                source,
208            })?;
209            let meta = entry.metadata().map_err(|source| crate::Error::IoPath {
210                path: entry.path(),
211                context: "getting metadata of file",
212                source,
213            })?;
214
215            // Ignore filtered files or directories.
216            if filter.iter().any(|filter| entry.path().ends_with(filter)) {
217                continue;
218            }
219
220            paths.push(
221                entry
222                    .path()
223                    .strip_prefix(init_path)
224                    .map_err(|source| crate::Error::PathStripPrefix {
225                        prefix: path.to_path_buf(),
226                        path: entry.path(),
227                        source,
228                    })?
229                    .to_path_buf(),
230            );
231
232            // Call `collect_files` on each directory, retaining the initial `init_path` and
233            // `filter`.
234            if meta.is_dir() {
235                let mut subdir_paths = collect_files(entry.path().as_path(), init_path, filter)?;
236                paths.append(&mut subdir_paths);
237            }
238        }
239
240        // Sort paths.
241        paths.sort();
242
243        Ok(paths)
244    }
245
246    collect_files(path, init_path, filter)
247}
248
249#[cfg(test)]
250mod test {
251    use std::{
252        fs::{File, create_dir_all},
253        io::Write,
254        os::unix::fs::symlink,
255    };
256
257    use rstest::rstest;
258    use tempfile::{NamedTempFile, TempDir, tempdir};
259    use testresult::TestResult;
260
261    use super::*;
262
263    pub const VALID_BUILDINFO_V2_DATA: &str = r#"
264builddate = 1
265builddir = /build
266startdir = /startdir/
267buildtool = devtools
268buildtoolver = 1:1.2.1-1-any
269buildenv = ccache
270buildenv = color
271format = 2
272installed = bar-1.2.3-1-any
273installed = beh-2.2.3-4-any
274options = lto
275options = !strip
276packager = Foobar McFooface <foobar@mcfooface.org>
277pkgarch = any
278pkgbase = example
279pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
280pkgname = example
281pkgver = 1:1.0.0-1
282"#;
283
284    pub const VALID_PKGINFO_V2_DATA: &str = r#"
285pkgname = example
286pkgbase = example
287xdata = pkgtype=pkg
288pkgver = 1:1.0.0-1
289pkgdesc = A project that does something
290url = https://example.org/
291builddate = 1729181726
292packager = John Doe <john@example.org>
293size = 181849963
294arch = any
295license = GPL-3.0-or-later
296replaces = other-package>0.9.0-3
297group = package-group
298conflict = conflicting-package<1.0.0
299provides = some-component
300backup = etc/example/config.toml
301depend = glibc
302optdepend = python: for special-python-script.py
303makedepend = cmake
304checkdepend = extra-test-tool
305"#;
306
307    const VALID_INSTALL_SCRIPTLET: &str = r#"
308pre_install() {
309    echo "Preparing to install package version $1"
310}
311
312post_install() {
313    echo "Package version $1 installed"
314}
315
316pre_upgrade() {
317    echo "Preparing to upgrade from version $2 to $1"
318}
319
320post_upgrade() {
321    echo "Upgraded from version $2 to $1"
322}
323
324pre_remove() {
325    echo "Preparing to remove package version $1"
326}
327
328post_remove() {
329    echo "Package version $1 removed"
330}
331"#;
332
333    fn create_data_files(path: impl AsRef<Path>) -> TestResult {
334        let path = path.as_ref();
335        // Create dummy directory structure
336        create_dir_all(path.join("usr/share/foo/bar/baz"))?;
337        // Create dummy text file
338        File::create(path.join("usr/share/foo/beh.txt"))?.write_all(b"test")?;
339        // Create relative symlink to actual text file
340        symlink("../../beh.txt", path.join("usr/share/foo/bar/baz/beh.txt"))?;
341        Ok(())
342    }
343
344    fn create_metadata_files(path: impl AsRef<Path>) -> TestResult {
345        let path = path.as_ref();
346        for (input_type, input) in [
347            (MetadataFileName::BuildInfo, VALID_BUILDINFO_V2_DATA),
348            (MetadataFileName::PackageInfo, VALID_PKGINFO_V2_DATA),
349        ] {
350            File::create(path.join(input_type.as_ref()))?.write_all(input.as_bytes())?;
351        }
352        Ok(())
353    }
354
355    fn create_scriptlet_file(path: impl AsRef<Path>) -> TestResult {
356        let path = path.as_ref();
357        let mut output = File::create(path.join(INSTALL_SCRIPTLET_FILE_NAME))?;
358        write!(output, "{VALID_INSTALL_SCRIPTLET}")?;
359        Ok(())
360    }
361
362    /// Tests the successful collection of data files relative to a directory.
363    #[rstest]
364    fn relative_data_files_collect_successfully() -> TestResult {
365        let tempdir = tempdir()?;
366
367        create_data_files(tempdir.path())?;
368        create_metadata_files(tempdir.path())?;
369        create_scriptlet_file(tempdir.path())?;
370
371        let expected_paths = vec![
372            PathBuf::from("usr"),
373            PathBuf::from("usr/share"),
374            PathBuf::from("usr/share/foo"),
375            PathBuf::from("usr/share/foo/bar"),
376            PathBuf::from("usr/share/foo/bar/baz"),
377            PathBuf::from("usr/share/foo/bar/baz/beh.txt"),
378            PathBuf::from("usr/share/foo/beh.txt"),
379        ];
380
381        let collected_files = relative_data_files(tempdir)?;
382        assert_eq!(expected_paths.as_slice(), collected_files.as_slice());
383
384        Ok(())
385    }
386
387    /// Tests the successful collection of all files relative to a directory.
388    #[rstest]
389    fn relative_files_are_collected_successfully_without_filter() -> TestResult {
390        let tempdir = tempdir()?;
391
392        create_data_files(tempdir.path())?;
393        create_metadata_files(tempdir.path())?;
394        create_scriptlet_file(tempdir.path())?;
395
396        let expected_paths = vec![
397            PathBuf::from(MetadataFileName::BuildInfo.as_ref()),
398            PathBuf::from(INSTALL_SCRIPTLET_FILE_NAME),
399            PathBuf::from(MetadataFileName::PackageInfo.as_ref()),
400            PathBuf::from("usr"),
401            PathBuf::from("usr/share"),
402            PathBuf::from("usr/share/foo"),
403            PathBuf::from("usr/share/foo/bar"),
404            PathBuf::from("usr/share/foo/bar/baz"),
405            PathBuf::from("usr/share/foo/bar/baz/beh.txt"),
406            PathBuf::from("usr/share/foo/beh.txt"),
407        ];
408
409        let collected_files = relative_files(tempdir, &[])?;
410        assert_eq!(expected_paths.as_slice(), collected_files.as_slice());
411
412        Ok(())
413    }
414
415    /// Tests all success and failure scenarios when creating [`InputPath`].
416    #[test]
417    fn input_path_new() -> TestResult {
418        let temp_dir = TempDir::new()?;
419        let temp_dir_path = temp_dir.path();
420        let temp_file = NamedTempFile::new()?;
421        let temp_file_path = temp_file.path();
422
423        let relative_path = PathBuf::from("some_file.txt");
424        let absolute_path = PathBuf::from("/some_file.txt");
425
426        assert!(InputPath::new(temp_dir_path, &relative_path).is_ok());
427        assert!(matches!(
428            InputPath::new(temp_dir_path, &absolute_path),
429            Err(crate::Error::NonRelativePaths { .. })
430        ));
431        assert!(matches!(
432            InputPath::new(temp_file_path, &relative_path),
433            Err(crate::Error::NotADirectory { .. })
434        ));
435        assert!(matches!(
436            InputPath::new(&relative_path, &relative_path),
437            Err(crate::Error::NonAbsolutePaths { .. })
438        ));
439
440        Ok(())
441    }
442
443    /// Tests all success and failure scenarios when creating [`InputPaths`].
444    #[test]
445    fn input_paths_new() -> TestResult {
446        let temp_dir = TempDir::new()?;
447        let temp_dir_path = temp_dir.path();
448        let temp_file = NamedTempFile::new()?;
449        let temp_file_path = temp_file.path();
450
451        let relative_path_a = PathBuf::from("some_file.txt");
452        let relative_path_b = PathBuf::from("some_other_file.txt");
453        let absolute_path_a = PathBuf::from("/some_file.txt");
454        let absolute_path_b = PathBuf::from("/some_other_file.txt");
455
456        assert!(
457            InputPaths::new(
458                temp_dir_path,
459                &[relative_path_a.clone(), relative_path_b.clone()]
460            )
461            .is_ok()
462        );
463        assert!(matches!(
464            InputPaths::new(temp_dir_path, &[absolute_path_a, absolute_path_b]),
465            Err(crate::Error::NonRelativePaths { .. })
466        ));
467        assert!(matches!(
468            InputPaths::new(temp_file_path, std::slice::from_ref(&relative_path_a)),
469            Err(crate::Error::NotADirectory { .. })
470        ));
471        assert!(matches!(
472            InputPaths::new(&relative_path_a, std::slice::from_ref(&relative_path_a)),
473            Err(crate::Error::NonAbsolutePaths { .. })
474        ));
475
476        Ok(())
477    }
478}