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::{MAIN_SEPARATOR_STR, 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                let mut path = 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                // Add a trailing "/" to directory paths, if there isn't one already.
232                if meta.is_dir()
233                    && path
234                        .as_os_str()
235                        .to_str()
236                        .is_some_and(|path| !path.ends_with(MAIN_SEPARATOR_STR))
237                {
238                    path.as_mut_os_string().push(MAIN_SEPARATOR_STR);
239                }
240
241                path
242            });
243
244            // Call `collect_files` on each directory, retaining the initial `init_path` and
245            // `filter`.
246            if meta.is_dir() {
247                let mut subdir_paths = collect_files(entry.path().as_path(), init_path, filter)?;
248                paths.append(&mut subdir_paths);
249            }
250        }
251
252        // Sort paths.
253        paths.sort();
254
255        Ok(paths)
256    }
257
258    collect_files(path, init_path, filter)
259}
260
261#[cfg(test)]
262mod test {
263    use std::{
264        fs::{File, create_dir_all},
265        io::Write,
266        os::unix::fs::symlink,
267    };
268
269    use rstest::rstest;
270    use tempfile::{NamedTempFile, TempDir, tempdir};
271    use testresult::TestResult;
272
273    use super::*;
274
275    pub const VALID_BUILDINFO_V2_DATA: &str = r#"
276format = 2
277builddate = 1
278builddir = /build
279startdir = /startdir/
280buildtool = devtools
281buildtoolver = 1:1.2.1-1-any
282buildenv = ccache
283buildenv = color
284installed = bar-1.2.3-1-any
285installed = beh-2.2.3-4-any
286options = lto
287options = !strip
288packager = Foobar McFooface <foobar@mcfooface.org>
289pkgarch = any
290pkgbase = example
291pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
292pkgname = example
293pkgver = 1:1.0.0-1
294"#;
295
296    pub const VALID_PKGINFO_V2_DATA: &str = r#"
297pkgname = example
298pkgbase = example
299xdata = pkgtype=pkg
300pkgver = 1:1.0.0-1
301pkgdesc = A project that does something
302url = https://example.org/
303builddate = 1729181726
304packager = John Doe <john@example.org>
305size = 181849963
306arch = any
307license = GPL-3.0-or-later
308replaces = other-package>0.9.0-3
309group = package-group
310conflict = conflicting-package<1.0.0
311provides = some-component
312backup = etc/example/config.toml
313depend = glibc
314optdepend = python: for special-python-script.py
315makedepend = cmake
316checkdepend = extra-test-tool
317"#;
318
319    const VALID_INSTALL_SCRIPTLET: &str = r#"
320pre_install() {
321    echo "Preparing to install package version $1"
322}
323
324post_install() {
325    echo "Package version $1 installed"
326}
327
328pre_upgrade() {
329    echo "Preparing to upgrade from version $2 to $1"
330}
331
332post_upgrade() {
333    echo "Upgraded from version $2 to $1"
334}
335
336pre_remove() {
337    echo "Preparing to remove package version $1"
338}
339
340post_remove() {
341    echo "Package version $1 removed"
342}
343"#;
344
345    fn create_data_files(path: impl AsRef<Path>) -> TestResult {
346        let path = path.as_ref();
347        // Create dummy directory structure
348        create_dir_all(path.join("usr/share/foo/bar/baz"))?;
349        // Create dummy text file
350        File::create(path.join("usr/share/foo/beh.txt"))?.write_all(b"test")?;
351        // Create relative symlink to actual text file
352        symlink("../../beh.txt", path.join("usr/share/foo/bar/baz/beh.txt"))?;
353        Ok(())
354    }
355
356    fn create_metadata_files(path: impl AsRef<Path>) -> TestResult {
357        let path = path.as_ref();
358        for (input_type, input) in [
359            (MetadataFileName::BuildInfo, VALID_BUILDINFO_V2_DATA),
360            (MetadataFileName::PackageInfo, VALID_PKGINFO_V2_DATA),
361        ] {
362            File::create(path.join(input_type.as_ref()))?.write_all(input.as_bytes())?;
363        }
364        Ok(())
365    }
366
367    fn create_scriptlet_file(path: impl AsRef<Path>) -> TestResult {
368        let path = path.as_ref();
369        let mut output = File::create(path.join(INSTALL_SCRIPTLET_FILE_NAME))?;
370        write!(output, "{VALID_INSTALL_SCRIPTLET}")?;
371        Ok(())
372    }
373
374    /// Tests the successful collection of data files relative to a directory.
375    #[rstest]
376    fn relative_data_files_collect_successfully() -> TestResult {
377        let tempdir = tempdir()?;
378
379        create_data_files(tempdir.path())?;
380        create_metadata_files(tempdir.path())?;
381        create_scriptlet_file(tempdir.path())?;
382
383        let expected_paths = vec![
384            PathBuf::from("usr"),
385            PathBuf::from("usr/share"),
386            PathBuf::from("usr/share/foo"),
387            PathBuf::from("usr/share/foo/bar"),
388            PathBuf::from("usr/share/foo/bar/baz"),
389            PathBuf::from("usr/share/foo/bar/baz/beh.txt"),
390            PathBuf::from("usr/share/foo/beh.txt"),
391        ];
392
393        let collected_files = relative_data_files(tempdir)?;
394        assert_eq!(expected_paths.as_slice(), collected_files.as_slice());
395
396        Ok(())
397    }
398
399    /// Tests the successful collection of all files relative to a directory.
400    #[rstest]
401    fn relative_files_are_collected_successfully_without_filter() -> TestResult {
402        let tempdir = tempdir()?;
403
404        create_data_files(tempdir.path())?;
405        create_metadata_files(tempdir.path())?;
406        create_scriptlet_file(tempdir.path())?;
407
408        let expected_paths = vec![
409            PathBuf::from(MetadataFileName::BuildInfo.as_ref()),
410            PathBuf::from(INSTALL_SCRIPTLET_FILE_NAME),
411            PathBuf::from(MetadataFileName::PackageInfo.as_ref()),
412            PathBuf::from("usr"),
413            PathBuf::from("usr/share"),
414            PathBuf::from("usr/share/foo"),
415            PathBuf::from("usr/share/foo/bar"),
416            PathBuf::from("usr/share/foo/bar/baz"),
417            PathBuf::from("usr/share/foo/bar/baz/beh.txt"),
418            PathBuf::from("usr/share/foo/beh.txt"),
419        ];
420
421        let collected_files = relative_files(tempdir, &[])?;
422        assert_eq!(expected_paths.as_slice(), collected_files.as_slice());
423
424        Ok(())
425    }
426
427    /// Tests all success and failure scenarios when creating [`InputPath`].
428    #[test]
429    fn input_path_new() -> TestResult {
430        let temp_dir = TempDir::new()?;
431        let temp_dir_path = temp_dir.path();
432        let temp_file = NamedTempFile::new()?;
433        let temp_file_path = temp_file.path();
434
435        let relative_path = PathBuf::from("some_file.txt");
436        let absolute_path = PathBuf::from("/some_file.txt");
437
438        assert!(InputPath::new(temp_dir_path, &relative_path).is_ok());
439        assert!(matches!(
440            InputPath::new(temp_dir_path, &absolute_path),
441            Err(crate::Error::NonRelativePaths { .. })
442        ));
443        assert!(matches!(
444            InputPath::new(temp_file_path, &relative_path),
445            Err(crate::Error::NotADirectory { .. })
446        ));
447        assert!(matches!(
448            InputPath::new(&relative_path, &relative_path),
449            Err(crate::Error::NonAbsolutePaths { .. })
450        ));
451
452        Ok(())
453    }
454
455    /// Tests all success and failure scenarios when creating [`InputPaths`].
456    #[test]
457    fn input_paths_new() -> TestResult {
458        let temp_dir = TempDir::new()?;
459        let temp_dir_path = temp_dir.path();
460        let temp_file = NamedTempFile::new()?;
461        let temp_file_path = temp_file.path();
462
463        let relative_path_a = PathBuf::from("some_file.txt");
464        let relative_path_b = PathBuf::from("some_other_file.txt");
465        let absolute_path_a = PathBuf::from("/some_file.txt");
466        let absolute_path_b = PathBuf::from("/some_other_file.txt");
467
468        assert!(
469            InputPaths::new(
470                temp_dir_path,
471                &[relative_path_a.clone(), relative_path_b.clone()]
472            )
473            .is_ok()
474        );
475        assert!(matches!(
476            InputPaths::new(temp_dir_path, &[absolute_path_a, absolute_path_b]),
477            Err(crate::Error::NonRelativePaths { .. })
478        ));
479        assert!(matches!(
480            InputPaths::new(temp_file_path, std::slice::from_ref(&relative_path_a)),
481            Err(crate::Error::NotADirectory { .. })
482        ));
483        assert!(matches!(
484            InputPaths::new(&relative_path_a, std::slice::from_ref(&relative_path_a)),
485            Err(crate::Error::NonAbsolutePaths { .. })
486        ));
487
488        Ok(())
489    }
490}