Skip to content

Commit 44ded2b

Browse files
committed
feat: Add functions to get relative file paths from input dirs
Signed-off-by: David Runge <[email protected]>
1 parent 8a06069 commit 44ded2b

File tree

6 files changed

+349
-0
lines changed

6 files changed

+349
-0
lines changed

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

alpm-common/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,9 @@ version = "0.1.0"
1313

1414
[dependencies]
1515
alpm-types.workspace = true
16+
thiserror.workspace = true
17+
18+
[dev-dependencies]
19+
rstest.workspace = true
20+
tempfile.workspace = true
21+
testresult.workspace = true

alpm-common/src/error.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use std::path::{PathBuf, StripPrefixError};
2+
3+
/// An error that can occur when dealing with package inputs.
4+
#[derive(Debug, thiserror::Error)]
5+
pub enum Error {
6+
/// An I/O error occurred at a path.
7+
#[error("I/O error at path {path} while {context}:\n{source}")]
8+
IoPath {
9+
/// The path at which the error occurred.
10+
path: PathBuf,
11+
/// The context in which the error occurred.
12+
///
13+
/// This is meant to complete the sentence "I/O error at path while ".
14+
context: &'static str,
15+
/// The source error.
16+
source: std::io::Error,
17+
},
18+
19+
/// A path's prefix cannot be stripped.
20+
#[error("Cannot strip prefix {prefix} from path {path}:\n{source}")]
21+
PathStripPrefix {
22+
/// The prefix that is supposed to be stripped from `path`.
23+
prefix: PathBuf,
24+
/// The path that is supposed to stripped.
25+
path: PathBuf,
26+
/// The source error.
27+
source: StripPrefixError,
28+
},
29+
}

alpm-common/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
#![doc = include_str!("../README.md")]
22

3+
mod error;
4+
mod package;
35
mod traits;
6+
pub use error::Error;
7+
pub use package::input::{relative_data_files, relative_files};
48
pub use traits::{metadata_file::MetadataFile, schema::FileFormatSchema};

alpm-common/src/package/input.rs

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
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+
11+
use std::{
12+
fs::read_dir,
13+
path::{Path, PathBuf},
14+
};
15+
16+
use alpm_types::{INSTALL_SCRIPTLET_FILE_NAME, MetadataFileName};
17+
18+
/// Collects all data files in a directory, relative to it.
19+
///
20+
/// Convenience wrapper around [`relative_files`] that passes in all variants of
21+
/// [`MetadataFileName`] as well as [`INSTALL_SCRIPTLET_FILE_NAME`] to its `filter` option.
22+
/// This ensures, that only the paths of data files are returned.
23+
///
24+
/// # Errors
25+
///
26+
/// Returns an error if [`relative_files`] fails.
27+
pub fn relative_data_files(path: impl AsRef<Path>) -> Result<Vec<PathBuf>, crate::Error> {
28+
relative_files(
29+
path,
30+
&[
31+
MetadataFileName::BuildInfo.as_ref(),
32+
MetadataFileName::Mtree.as_ref(),
33+
MetadataFileName::PackageInfo.as_ref(),
34+
INSTALL_SCRIPTLET_FILE_NAME,
35+
],
36+
)
37+
}
38+
39+
/// Collects all files contained in a directory `path` as a list of sorted relative paths.
40+
///
41+
/// Recursively iterates over all entries of `path` (see [`read_dir`]).
42+
/// All returned entries are stripped using `path` (see [`Path::strip_prefix`]), effectively
43+
/// providing a list of relative paths below `path`.
44+
/// The list of paths is sorted (see [`slice::sort`]).
45+
///
46+
/// When providing file names using `filter`, any path found ending with one of the filter names
47+
/// will be skipped and not returned in the list of paths.
48+
///
49+
/// # Note
50+
///
51+
/// This function does not follow symlinks but instead returns the path of a symlink.
52+
///
53+
/// # Errors
54+
///
55+
/// Returns an error if
56+
///
57+
/// - calling [`read_dir`] on `path` or any of its subdirectories fails,
58+
/// - an entry in one of the (sub)directories can not be retrieved,
59+
/// - or stripping the prefix of a file in a (sub)directory fails.
60+
pub fn relative_files(
61+
path: impl AsRef<Path>,
62+
filter: &[&str],
63+
) -> Result<Vec<PathBuf>, crate::Error> {
64+
let path = path.as_ref();
65+
let init_path = path;
66+
67+
/// Collects all files in a `path` as a sorted list of paths and strips `init_path` from them.
68+
///
69+
/// Recursively calls itself on all directories contained in `path`, retaining `init_path` and
70+
/// `filter` in these calls.
71+
/// When providing filenames using `filter`, paths that end in those filenames will be skipped
72+
/// and not returned in the list of paths.
73+
///
74+
/// # Errors
75+
///
76+
/// Returns an error if
77+
///
78+
/// - calling [`read_dir`] on `path` or any of its subdirectories fails,
79+
/// - an entry in one of the (sub)directories can not be retrieved,
80+
/// - or stripping the prefix of a file in a (sub)directory fails.
81+
fn collect_files(
82+
path: &Path,
83+
init_path: &Path,
84+
filter: &[&str],
85+
) -> Result<Vec<PathBuf>, crate::Error> {
86+
let mut paths = Vec::new();
87+
let entries = read_dir(path).map_err(|source| crate::Error::IoPath {
88+
path: path.to_path_buf(),
89+
context: "reading entries of directory",
90+
source,
91+
})?;
92+
for entry in entries {
93+
let entry = entry.map_err(|source| crate::Error::IoPath {
94+
path: path.to_path_buf(),
95+
context: "reading entry in directory",
96+
source,
97+
})?;
98+
let meta = entry.metadata().map_err(|source| crate::Error::IoPath {
99+
path: entry.path(),
100+
context: "getting metadata of file",
101+
source,
102+
})?;
103+
104+
// Ignore filtered files or directories.
105+
if filter.iter().any(|filter| entry.path().ends_with(filter)) {
106+
continue;
107+
}
108+
109+
paths.push(
110+
entry
111+
.path()
112+
.strip_prefix(init_path)
113+
.map_err(|source| crate::Error::PathStripPrefix {
114+
prefix: path.to_path_buf(),
115+
path: entry.path(),
116+
source,
117+
})?
118+
.to_path_buf(),
119+
);
120+
121+
// Call `collect_files` on each directory, retaining the initial `init_path` and
122+
// `filter`.
123+
if meta.is_dir() {
124+
let mut subdir_paths = collect_files(entry.path().as_path(), init_path, filter)?;
125+
paths.append(&mut subdir_paths);
126+
}
127+
}
128+
129+
// Sort paths.
130+
paths.sort();
131+
132+
Ok(paths)
133+
}
134+
135+
collect_files(path, init_path, filter)
136+
}
137+
138+
#[cfg(test)]
139+
mod test {
140+
use std::{
141+
fs::{File, create_dir_all},
142+
io::Write,
143+
os::unix::fs::symlink,
144+
};
145+
146+
use rstest::rstest;
147+
use tempfile::tempdir;
148+
use testresult::TestResult;
149+
150+
use super::*;
151+
152+
pub const VALID_BUILDINFO_V2_DATA: &str = r#"
153+
builddate = 1
154+
builddir = /build
155+
startdir = /startdir/
156+
buildtool = devtools
157+
buildtoolver = 1:1.2.1-1-any
158+
buildenv = ccache
159+
buildenv = color
160+
format = 2
161+
installed = bar-1.2.3-1-any
162+
installed = beh-2.2.3-4-any
163+
options = lto
164+
options = !strip
165+
packager = Foobar McFooface <[email protected]>
166+
pkgarch = any
167+
pkgbase = example
168+
pkgbuild_sha256sum = b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
169+
pkgname = example
170+
pkgver = 1:1.0.0-1
171+
"#;
172+
173+
pub const VALID_PKGINFO_V2_DATA: &str = r#"
174+
pkgname = example
175+
pkgbase = example
176+
xdata = pkgtype=pkg
177+
pkgver = 1:1.0.0-1
178+
pkgdesc = A project that does something
179+
url = https://example.org/
180+
builddate = 1729181726
181+
packager = John Doe <[email protected]>
182+
size = 181849963
183+
arch = any
184+
license = GPL-3.0-or-later
185+
replaces = other-package>0.9.0-3
186+
group = package-group
187+
conflict = conflicting-package<1.0.0
188+
provides = some-component
189+
backup = etc/example/config.toml
190+
depend = glibc
191+
optdepend = python: for special-python-script.py
192+
makedepend = cmake
193+
checkdepend = extra-test-tool
194+
"#;
195+
196+
const VALID_INSTALL_SCRIPTLET: &str = r#"
197+
pre_install() {
198+
echo "Preparing to install package version $1"
199+
}
200+
201+
post_install() {
202+
echo "Package version $1 installed"
203+
}
204+
205+
pre_upgrade() {
206+
echo "Preparing to upgrade from version $2 to $1"
207+
}
208+
209+
post_upgrade() {
210+
echo "Upgraded from version $2 to $1"
211+
}
212+
213+
pre_remove() {
214+
echo "Preparing to remove package version $1"
215+
}
216+
217+
post_remove() {
218+
echo "Package version $1 removed"
219+
}
220+
"#;
221+
222+
fn create_data_files(path: impl AsRef<Path>) -> TestResult {
223+
let path = path.as_ref();
224+
// Create dummy directory structure
225+
create_dir_all(path.join("usr/share/foo/bar/baz"))?;
226+
// Create dummy text file
227+
File::create(path.join("usr/share/foo/beh.txt"))?.write_all(b"test")?;
228+
// Create relative symlink to actual text file
229+
symlink("../../beh.txt", path.join("usr/share/foo/bar/baz/beh.txt"))?;
230+
Ok(())
231+
}
232+
233+
fn create_metadata_files(path: impl AsRef<Path>) -> TestResult {
234+
let path = path.as_ref();
235+
for (input_type, input) in [
236+
(MetadataFileName::BuildInfo, VALID_BUILDINFO_V2_DATA),
237+
(MetadataFileName::PackageInfo, VALID_PKGINFO_V2_DATA),
238+
] {
239+
File::create(path.join(input_type.as_ref()))?.write_all(input.as_bytes())?;
240+
}
241+
Ok(())
242+
}
243+
244+
fn create_scriptlet_file(path: impl AsRef<Path>) -> TestResult {
245+
let path = path.as_ref();
246+
let mut output = File::create(path.join(INSTALL_SCRIPTLET_FILE_NAME))?;
247+
write!(output, "{VALID_INSTALL_SCRIPTLET}")?;
248+
Ok(())
249+
}
250+
251+
/// Tests the successful collection of data files relative to a directory.
252+
#[rstest]
253+
fn relative_data_files_collect_successfully() -> TestResult {
254+
let tempdir = tempdir()?;
255+
256+
create_data_files(tempdir.path())?;
257+
create_metadata_files(tempdir.path())?;
258+
create_scriptlet_file(tempdir.path())?;
259+
260+
let expected_paths = vec![
261+
PathBuf::from("usr"),
262+
PathBuf::from("usr/share"),
263+
PathBuf::from("usr/share/foo"),
264+
PathBuf::from("usr/share/foo/bar"),
265+
PathBuf::from("usr/share/foo/bar/baz"),
266+
PathBuf::from("usr/share/foo/bar/baz/beh.txt"),
267+
PathBuf::from("usr/share/foo/beh.txt"),
268+
];
269+
270+
let collected_files = relative_data_files(tempdir)?;
271+
assert_eq!(expected_paths.as_slice(), collected_files.as_slice());
272+
273+
Ok(())
274+
}
275+
276+
/// Tests the successful collection of all files relative to a directory.
277+
#[rstest]
278+
fn relative_files_are_collected_successfully_without_filter() -> TestResult {
279+
let tempdir = tempdir()?;
280+
281+
create_data_files(tempdir.path())?;
282+
create_metadata_files(tempdir.path())?;
283+
create_scriptlet_file(tempdir.path())?;
284+
285+
let expected_paths = vec![
286+
PathBuf::from(MetadataFileName::BuildInfo.as_ref()),
287+
PathBuf::from(INSTALL_SCRIPTLET_FILE_NAME),
288+
PathBuf::from(MetadataFileName::PackageInfo.as_ref()),
289+
PathBuf::from("usr"),
290+
PathBuf::from("usr/share"),
291+
PathBuf::from("usr/share/foo"),
292+
PathBuf::from("usr/share/foo/bar"),
293+
PathBuf::from("usr/share/foo/bar/baz"),
294+
PathBuf::from("usr/share/foo/bar/baz/beh.txt"),
295+
PathBuf::from("usr/share/foo/beh.txt"),
296+
];
297+
298+
let collected_files = relative_files(tempdir, &[])?;
299+
assert_eq!(expected_paths.as_slice(), collected_files.as_slice());
300+
301+
Ok(())
302+
}
303+
}

alpm-common/src/package/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
//! Helpers related to package handling.
2+
3+
pub mod input;

0 commit comments

Comments
 (0)