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}