1use std::{
12 fs::read_dir,
13 path::{Path, PathBuf},
14};
15
16use alpm_types::{INSTALL_SCRIPTLET_FILE_NAME, MetadataFileName};
17
18pub struct InputPath<'a, 'b> {
22 base_dir: &'a Path,
23 path: &'b Path,
24}
25
26impl<'a, 'b> InputPath<'a, 'b> {
27 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 pub fn base_dir(&self) -> &Path {
58 self.base_dir
59 }
60
61 pub fn path(&self) -> &Path {
63 self.path
64 }
65
66 pub fn to_path_buf(&self) -> PathBuf {
68 self.base_dir.join(self.path)
69 }
70}
71
72pub struct InputPaths<'a, 'b> {
76 base_dir: &'a Path,
77 paths: &'b [PathBuf],
78}
79
80impl<'a, 'b> InputPaths<'a, 'b> {
81 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 pub fn base_dir(&self) -> &Path {
118 self.base_dir
119 }
120
121 pub fn paths(&self) -> &[PathBuf] {
123 self.paths
124 }
125}
126
127pub 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
148pub 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 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 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 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 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_dir_all(path.join("usr/share/foo/bar/baz"))?;
335 File::create(path.join("usr/share/foo/beh.txt"))?.write_all(b"test")?;
337 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 #[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 #[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 #[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 #[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}