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