cargo_deb/
util.rs

1use std::collections::BTreeSet;
2use std::path::Path;
3
4pub(crate) mod pathbytes;
5pub(crate) mod wordsplit;
6
7pub mod compress;
8
9/// Get the filename from a path.
10/// Note: Due to the way the Path type works the final component is returned
11/// even if it looks like a directory, e.g. "/some/dir/" will return "dir"...
12pub(crate) fn fname_from_path(path: &Path) -> Option<String> {
13    if path.to_bytes().ends_with(b"/") {
14        return None;
15    }
16    let path = path.file_name()?.to_string_lossy();
17    Some(path.into_owned())
18}
19
20use pathbytes::AsUnixPathBytes;
21#[cfg(test)]
22pub(crate) use tests::is_path_file;
23
24#[cfg(not(test))]
25pub(crate) fn is_path_file(path: &Path) -> bool {
26    path.is_file()
27}
28
29#[cfg(test)]
30pub(crate) use tests::read_file_to_string;
31
32#[cfg(not(test))]
33pub(crate) fn read_file_to_string(path: &Path) -> std::io::Result<String> {
34    std::fs::read_to_string(path)
35}
36
37#[cfg(test)]
38pub(crate) use tests::read_file_to_bytes;
39
40#[cfg(not(test))]
41pub(crate) fn read_file_to_bytes(path: &Path) -> std::io::Result<Vec<u8>> {
42    std::fs::read(path)
43}
44
45/// Create a `HashMap` from one or more key => value pairs in a single statement.
46///
47/// # Usage
48///
49/// Any types supported by `HashMap` for keys and values are supported:
50///
51/// ```rust,ignore
52/// let mut one = std::collections::HashMap::new();
53/// one.insert(1, 'a');
54/// assert_eq!(one, map!{ 1 => 'a' });
55///
56/// let mut two = std::collections::HashMap::new();
57/// two.insert("a", 1);
58/// two.insert("b", 2);
59/// assert_eq!(two, map!{ "a" => 1, "b" => 2 });
60/// ```
61///
62/// Empty maps are not supported, attempting to create one will fail to compile:
63/// ```compile_fail
64/// let empty = std::collections::HashMap::new();
65/// assert_eq!(empty, map!{ });
66/// ```
67///
68/// # Provenance
69///
70/// From: <https://stackoverflow.com/a/27582993>
71macro_rules! map(
72    { $($key:expr => $value:expr),+ } => {
73        {
74            let mut m = ::std::collections::HashMap::new();
75            $(
76                m.insert($key, $value);
77            )+
78            m
79        }
80     };
81);
82
83/// A trait for returning a String containing items separated by the given
84/// separator.
85pub(crate) trait MyJoin {
86    fn join(&self, sep: &str) -> String;
87}
88
89/// Returns a String containing the hash set items joined together by the given
90/// separator.
91///
92/// # Usage
93///
94/// ```text
95/// let two: BTreeSet<String> = vec!["a", "b"].into_iter().map(|s| s.to_owned()).collect();
96/// assert_eq!("ab", two.join(""));
97/// assert_eq!("a,b", two.join(","));
98/// ```
99impl MyJoin for BTreeSet<String> {
100    fn join(&self, sep: &str) -> String {
101        self.iter().map(|item| item.as_str()).collect::<Vec<&str>>().join(sep)
102    }
103}
104
105#[cfg(test)]
106pub(crate) mod tests {
107    use lazy_static::lazy_static;
108    use std::collections::HashMap;
109
110    lazy_static! {
111        static ref ERROR_REGEX: regex::Regex = regex::Regex::new(r"^error:(?P<error_name>.+)$").unwrap();
112    }
113
114    // ---------------------------------------------------------------------
115    // Begin: test virtual filesystem
116    // ---------------------------------------------------------------------
117    // The pkgfile() function accesses the filesystem directly via its use
118    // the Path(Buf)::is_file() method which checks for the existence of a
119    // file in the real filesystem.
120    //
121    // To test this without having to create real files and directories we
122    // extend the PathBuf type via a trait with a mock_is_file() method
123    // which, in test builds, is used by pkgfile() instead of the real
124    // PathBuf::is_file() method.
125    //
126    // The mock_is_file() method looks up a path in a vector which
127    // represents a set of paths in a virtual filesystem. However, accessing
128    // global state in a multithreaded test run is unsafe, plus we want each
129    // test to define its own virtual filesystem to test against, not a
130    // single global virtual filesystem shared by all tests.
131    //
132    // This test specific virtual filesystem is implemented as a map,
133    // protected by a thread local such that each test (thread) gets its own
134    // instance. To be able to mutate the map it is wrapped inside a Mutex.
135    // To make this setup easier to work with we define a few  helper
136    // functions:
137    //
138    //   - add_test_fs_paths() - adds paths to the current tests virtual fs
139    //   - set_test_fs_path_content() - set the file content (initially "")
140    //   - with_test_fs() - passes the current tests virtual fs vector to
141    //                      a user defined callback function.
142    use std::sync::Mutex;
143
144    pub(crate) struct TestPath {
145        _filename: &'static str,
146        contents: String,
147        read_count: u16,
148    }
149
150    impl TestPath {
151        fn new(filename: &'static str, contents: String) -> Self {
152            Self {
153                _filename: filename,
154                contents,
155                read_count: 0,
156            }
157        }
158
159        fn read(&mut self) -> String {
160            self.read_count += 1;
161            self.contents.clone()
162        }
163
164        fn count(&self) -> u16 {
165            self.read_count
166        }
167    }
168
169    thread_local!(
170        static MOCK_FS: Mutex<HashMap<&'static str, TestPath>> = Mutex::new(HashMap::new());
171    );
172
173    pub(crate) struct ResetFsGuard;
174
175    impl Drop for ResetFsGuard {
176        fn drop(&mut self) {
177            MOCK_FS.with(|fs| {
178                fs.lock().unwrap().clear();
179            });
180        }
181    }
182
183    #[must_use]
184    pub(crate) fn add_test_fs_paths(paths: &[&'static str]) -> ResetFsGuard {
185        MOCK_FS.with(|fs| {
186            let mut fs_map = fs.lock().unwrap();
187            for path in paths {
188                fs_map.insert(path, TestPath::new(path, String::new()));
189            }
190        });
191        ResetFsGuard
192    }
193
194    pub(crate) fn set_test_fs_path_content(path: &'static str, contents: String) {
195        MOCK_FS.with(|fs| {
196            let mut fs_map = fs.lock().unwrap();
197            fs_map.insert(path, TestPath::new(path, contents));
198        });
199    }
200
201    fn with_test_fs<F, R>(callback: F) -> R
202    where F: Fn(&mut HashMap<&'static str, TestPath>) -> R {
203        MOCK_FS.with(|fs| callback(&mut fs.lock().unwrap()))
204    }
205
206    pub(crate) fn is_path_file(path: &Path) -> bool {
207        with_test_fs(|fs| fs.contains_key(&path.to_str().unwrap()))
208    }
209
210    pub(crate) fn get_read_count(path: &str) -> u16 {
211        with_test_fs(|fs| fs.get(path).unwrap().count())
212    }
213
214    pub(crate) fn read_file_to_string(path: &Path) -> std::io::Result<String> {
215        fn str_to_err(str: &str) -> std::io::Result<String> {
216            Err(std::io::Error::from(match str {
217                "InvalidInput"     => std::io::ErrorKind::InvalidInput,
218                "Interrupted"      => std::io::ErrorKind::Interrupted,
219                "PermissionDenied" => std::io::ErrorKind::PermissionDenied,
220                "NotFound"         => std::io::ErrorKind::NotFound,
221                "Other"            => std::io::ErrorKind::Other,
222                _                  => panic!("Unknown I/O ErrorKind '{str}'")
223            }))
224        }
225
226        with_test_fs(|fs| match fs.get_mut(path.to_str().unwrap()) {
227            None => Err(std::io::Error::new(
228                std::io::ErrorKind::NotFound,
229                format!("Test filesystem path {path:?} does not exist"),
230            )),
231            Some(test_path) => {
232                let contents = test_path.read();
233                match ERROR_REGEX.captures(&contents) {
234                    None => Ok(contents),
235                    Some(caps) => match caps.name("error_name") {
236                        None => Ok(contents),
237                        Some(re_match) => str_to_err(re_match.as_str()),
238                    },
239                }
240            },
241        })
242    }
243
244    pub(crate) fn read_file_to_bytes(path: &Path) -> std::io::Result<Vec<u8>> {
245        match read_file_to_string(path) {
246            Ok(contents) => Ok(Vec::from(contents.as_bytes())),
247            Err(x) => Err(x),
248        }
249    }
250
251    // ---------------------------------------------------------------------
252    // End: test virtual filesystem
253    // ---------------------------------------------------------------------
254
255    use super::*;
256
257    #[test]
258    fn fname_from_path_returns_file_name_even_if_file_does_not_exist() {
259        assert_eq!("some_name", fname_from_path(Path::new("some_name")).unwrap());
260        assert_eq!("some_name", fname_from_path(Path::new("/some_name")).unwrap());
261        assert_eq!("some_name", fname_from_path(Path::new("/a/b/some_name")).unwrap());
262    }
263
264    #[test]
265    fn fname_from_path_fails_when_path_is_empty() {
266        assert_eq!(None, fname_from_path(Path::new("")));
267    }
268
269    #[test]
270    fn fname_from_path_fails_when_path_has_no_filename() {
271        assert_eq!(None, fname_from_path(Path::new("/a/")));
272    }
273
274    #[test]
275    fn map_macro() {
276        let mut one = HashMap::new();
277        one.insert(1, 'a');
278        assert_eq!(one, map! { 1 => 'a' });
279
280        let mut two = HashMap::new();
281        two.insert("a", 1);
282        two.insert("b", 2);
283        assert_eq!(two, map! { "a" => 1, "b" => 2 });
284    }
285
286    #[test]
287    fn btreeset_join() {
288        let empty: BTreeSet<String> = vec![].into_iter().collect();
289        assert_eq!("", empty.join(""));
290        assert_eq!("", empty.join(","));
291
292        let one: BTreeSet<String> = vec!["a"].into_iter().map(|s| s.to_owned()).collect();
293        assert_eq!("a", one.join(""));
294        assert_eq!("a", one.join(","));
295
296        let two: BTreeSet<String> = vec!["a", "b"].into_iter().map(|s| s.to_owned()).collect();
297        assert_eq!("ab", two.join(""));
298        assert_eq!("a,b", two.join(","));
299    }
300}