1use std::{
12 fs::read_dir,
13 path::{Path, PathBuf},
14};
15
16use alpm_types::{INSTALL_SCRIPTLET_FILE_NAME, MetadataFileName};
17
18#[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 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 pub fn base_dir(&self) -> &Path {
59 self.base_dir
60 }
61
62 pub fn path(&self) -> &Path {
64 self.path
65 }
66
67 pub fn to_path_buf(&self) -> PathBuf {
69 self.base_dir.join(self.path)
70 }
71}
72
73#[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 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 pub fn base_dir(&self) -> &Path {
120 self.base_dir
121 }
122
123 pub fn paths(&self) -> &[PathBuf] {
125 self.paths
126 }
127}
128
129pub 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
150pub 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 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 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 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 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_dir_all(path.join("usr/share/foo/bar/baz"))?;
337 File::create(path.join("usr/share/foo/beh.txt"))?.write_all(b"test")?;
339 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 #[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 #[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 #[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 #[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}