commondir/
lib.rs

1//! The goal of the functionality within this module is to find a common subdirectory `D` for a set
2//! of input files, split the input files at `D`, and concat the second element (directories and
3//! files which aren't a common path for all input files) to an output directory.
4//!
5//! In summary, we aim to mirror an unrooted file structure to a new root.
6
7use std::path::{Path, PathBuf};
8
9/// A common dir (1st element), and a set of paths (2nd element) which when concatenated with the
10/// common dir, result in a full path again.
11#[derive(Debug)]
12pub struct CommonDir(
13    PathBuf, // The common directory
14    Vec<(
15        PathBuf, // The original input path
16        PathBuf, // The `k` in `concat(common dir, k)`
17    )>,
18);
19
20impl CommonDir {
21    /// Expects canonicalized paths
22    pub fn try_new<P: AsRef<Path>, I: IntoIterator<Item = P> + Clone>(
23        paths: I,
24    ) -> Result<Self, Error> {
25        naive_find_common_dir(paths)
26    }
27
28    /// The found common root path
29    pub fn common_root(&self) -> &Path {
30        self.0.as_path()
31    }
32
33    /// The original input paths
34    pub fn input_paths(&self) -> Vec<&Path> {
35        self.1.iter().map(|(lp, _)| lp.as_path()).collect()
36    }
37
38    ///  The k's in `concat(common dir, k)`
39    pub fn path_branches(&self) -> Vec<&Path> {
40        self.1.iter().map(|(_, rp)| rp.as_path()).collect()
41    }
42
43    /// A tuple of the original input path, and its path branch
44    pub fn path_combinations(&self) -> Vec<(&Path, &Path)> {
45        self.1
46            .iter()
47            .map(|(lp, rp)| (lp.as_path(), rp.as_path()))
48            .collect()
49    }
50}
51
52fn naive_find_common_dir<P: AsRef<Path>, I: IntoIterator<Item = P> + Clone>(
53    paths: I,
54) -> Result<CommonDir, Error> {
55    let mut iter = paths.clone().into_iter();
56    let first_path = iter.next().ok_or(Error::Empty)?;
57    let first_path = first_path.as_ref();
58
59    let path_trunk = first_path
60        .parent()
61        .ok_or_else(|| Error::NoRootDirectory(first_path.to_path_buf()))?;
62    let ancestors = path_trunk.ancestors();
63
64    let set_of_paths = paths
65        .into_iter()
66        .map(|p| p.as_ref().to_path_buf())
67        .collect::<Vec<PathBuf>>();
68
69    for ancestor in ancestors {
70        // checks whether the current path has a common ancestors for all inputs
71        if set_of_paths.iter().all(|path| path.starts_with(ancestor)) {
72            let vec: Vec<(PathBuf, PathBuf)> = set_of_paths
73                .iter()
74                .map(|path| (path.to_path_buf(), unroot(ancestor, path)))
75                .collect();
76
77            return Ok(CommonDir(ancestor.to_path_buf(), vec));
78        }
79    }
80
81    let set = set_of_paths
82        .iter()
83        .map(|p| {
84            p.file_name()
85                .map(|str| (p.to_path_buf(), PathBuf::from(str.to_os_string())))
86                .ok_or_else(|| Error::InvalidPath(p.to_path_buf()))
87        })
88        .collect::<Result<Vec<_>, _>>()?;
89
90    Ok(CommonDir(path_trunk.to_path_buf(), set))
91}
92
93#[derive(Debug, Eq, PartialEq, thiserror::Error)]
94pub enum Error {
95    #[error("Empty input: got no path(s)")]
96    Empty,
97
98    #[error("Unable to mirror input directory to output: found an invalid file path: '{0}'")]
99    InvalidPath(PathBuf),
100
101    #[error("No root directory for path: '{0}'")]
102    NoRootDirectory(PathBuf),
103}
104
105// The intention of this function is to remove a common root path from a given path.
106fn unroot(root: &Path, path: &Path) -> PathBuf {
107    let root_len = root.components().count();
108
109    path.components()
110        .skip(root_len)
111        .fold(PathBuf::new(), |mut parent, child| {
112            parent.push(child.as_os_str());
113
114            parent
115        })
116}
117
118#[cfg(test)]
119mod test {
120    use super::*;
121
122    #[test]
123    fn unroot_test_file_only() {
124        let root = Path::new("/my/common");
125        let full = Path::new("/my/common/a.png");
126
127        let expected = PathBuf::from("a.png".to_string());
128        assert_eq!(unroot(root, full), expected);
129    }
130
131    #[test]
132    fn unroot_with_similar_dir_test() {
133        let root = Path::new("/my");
134        let full = Path::new("/my/common/a.png");
135
136        let expected = PathBuf::from("common/a.png".to_string());
137        assert_eq!(unroot(root, full), expected);
138    }
139
140    #[test]
141    fn uncommon_dir_test() {
142        let common = CommonDir::try_new(vec![
143            "/my/common/path/a.png",
144            "/my/common/path/b.png",
145            "/my/uncommon/path/c.png",
146        ])
147        .unwrap();
148
149        assert_eq!(common.common_root(), Path::new("/my"));
150
151        let stem = common.path_branches();
152
153        assert_eq!(stem[0], Path::new("common/path/a.png"));
154        assert_eq!(stem[1], Path::new("common/path/b.png"));
155        assert_eq!(stem[2], Path::new("uncommon/path/c.png"));
156    }
157
158    #[test]
159    fn common_dir_test() {
160        let common = CommonDir::try_new(vec![
161            "/my/common/path/a.png",
162            "/my/common/path/b.png",
163            "/my/common/path/c.png",
164        ])
165        .unwrap();
166
167        assert_eq!(common.common_root(), Path::new("/my/common/path/"));
168
169        let stem = common.path_branches();
170
171        assert_eq!(stem[0], Path::new("a.png"));
172        assert_eq!(stem[1], Path::new("b.png"));
173        assert_eq!(stem[2], Path::new("c.png"));
174    }
175
176    #[test]
177    fn no_path_before_file() {
178        let common = CommonDir::try_new(vec!["a.png", "b.png", "c.png"]).unwrap();
179
180        assert_eq!(common.common_root(), Path::new(""));
181
182        let stem = common.path_branches();
183
184        assert_eq!(stem[0], Path::new("a.png"));
185        assert_eq!(stem[1], Path::new("b.png"));
186        assert_eq!(stem[2], Path::new("c.png"));
187    }
188
189    #[test]
190    fn empty_common_dir() {
191        let common = CommonDir::try_new(Vec::<PathBuf>::new());
192        assert!(common.is_err());
193    }
194}