|
| 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 | +} |
0 commit comments