Skip to main content

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/// ```
67macro_rules! map(
68    { $($key:expr => $value:expr),+ } => {
69        {
70            ::std::collections::HashMap::from([
71                $(
72                    ($key, $value),
73                )+
74            ])
75        }
76     };
77);
78
79/// A trait for returning a String containing items separated by the given
80/// separator.
81pub(crate) trait MyJoin {
82    fn join(&self, sep: &str) -> String;
83}
84
85/// Returns a String containing the hash set items joined together by the given
86/// separator.
87///
88/// # Usage
89///
90/// ```text
91/// let two: BTreeSet<String> = vec!["a", "b"].into_iter().map(|s| s.to_owned()).collect();
92/// assert_eq!("ab", two.join(""));
93/// assert_eq!("a,b", two.join(","));
94/// ```
95impl MyJoin for BTreeSet<String> {
96    fn join(&self, sep: &str) -> String {
97        self.iter().map(|item| item.as_str()).collect::<Vec<&str>>().join(sep)
98    }
99}
100
101#[cfg(test)]
102pub(crate) mod tests {
103    use std::collections::HashMap;
104
105    static ERROR_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| regex::Regex::new(r"^error:(?P<error_name>.+)$").unwrap());
106
107    // ---------------------------------------------------------------------
108    // Begin: test virtual filesystem
109    // ---------------------------------------------------------------------
110    // The pkgfile() function accesses the filesystem directly via its use
111    // the Path(Buf)::is_file() method which checks for the existence of a
112    // file in the real filesystem.
113    //
114    // To test this without having to create real files and directories we
115    // extend the PathBuf type via a trait with a mock_is_file() method
116    // which, in test builds, is used by pkgfile() instead of the real
117    // PathBuf::is_file() method.
118    //
119    // The mock_is_file() method looks up a path in a vector which
120    // represents a set of paths in a virtual filesystem. However, accessing
121    // global state in a multithreaded test run is unsafe, plus we want each
122    // test to define its own virtual filesystem to test against, not a
123    // single global virtual filesystem shared by all tests.
124    //
125    // This test specific virtual filesystem is implemented as a map,
126    // protected by a thread local such that each test (thread) gets its own
127    // instance. To be able to mutate the map it is wrapped inside a Mutex.
128    // To make this setup easier to work with we define a few  helper
129    // functions:
130    //
131    //   - add_test_fs_paths() - adds paths to the current tests virtual fs
132    //   - set_test_fs_path_content() - set the file content (initially "")
133    //   - with_test_fs() - passes the current tests virtual fs vector to
134    //                      a user defined callback function.
135    use std::sync::{LazyLock, Mutex};
136
137    pub(crate) struct TestPath {
138        _filename: &'static str,
139        contents: String,
140        read_count: u16,
141    }
142
143    impl TestPath {
144        fn new(filename: &'static str, contents: String) -> Self {
145            Self {
146                _filename: filename,
147                contents,
148                read_count: 0,
149            }
150        }
151
152        fn read(&mut self) -> String {
153            self.read_count += 1;
154            self.contents.clone()
155        }
156
157        fn count(&self) -> u16 {
158            self.read_count
159        }
160    }
161
162    thread_local!(
163        static MOCK_FS: Mutex<HashMap<&'static str, TestPath>> = Mutex::new(HashMap::new());
164    );
165
166    pub(crate) struct ResetFsGuard;
167
168    impl Drop for ResetFsGuard {
169        fn drop(&mut self) {
170            MOCK_FS.with(|fs| {
171                fs.lock().unwrap().clear();
172            });
173        }
174    }
175
176    #[must_use]
177    pub(crate) fn add_test_fs_paths(paths: &[&'static str]) -> ResetFsGuard {
178        MOCK_FS.with(|fs| {
179            let mut fs_map = fs.lock().unwrap();
180            for path in paths {
181                fs_map.insert(path, TestPath::new(path, String::new()));
182            }
183        });
184        ResetFsGuard
185    }
186
187    pub(crate) fn set_test_fs_path_content(path: &'static str, contents: String) {
188        MOCK_FS.with(|fs| {
189            let mut fs_map = fs.lock().unwrap();
190            fs_map.insert(path, TestPath::new(path, contents));
191        });
192    }
193
194    fn with_test_fs<F, R>(callback: F) -> R
195    where F: Fn(&mut HashMap<&'static str, TestPath>) -> R {
196        MOCK_FS.with(|fs| callback(&mut fs.lock().unwrap()))
197    }
198
199    pub(crate) fn is_path_file(path: &Path) -> bool {
200        with_test_fs(|fs| fs.contains_key(&path.to_str().unwrap()))
201    }
202
203    pub(crate) fn get_read_count(path: &str) -> u16 {
204        with_test_fs(|fs| fs.get(path).unwrap().count())
205    }
206
207    pub(crate) fn read_file_to_string(path: &Path) -> std::io::Result<String> {
208        fn str_to_err(str: &str) -> std::io::Result<String> {
209            Err(std::io::Error::from(match str {
210                "InvalidInput"     => std::io::ErrorKind::InvalidInput,
211                "Interrupted"      => std::io::ErrorKind::Interrupted,
212                "PermissionDenied" => std::io::ErrorKind::PermissionDenied,
213                "NotFound"         => std::io::ErrorKind::NotFound,
214                "Other"            => std::io::ErrorKind::Other,
215                _                  => panic!("Unknown I/O ErrorKind '{str}'")
216            }))
217        }
218
219        with_test_fs(|fs| match fs.get_mut(path.to_str().unwrap()) {
220            None => Err(std::io::Error::new(
221                std::io::ErrorKind::NotFound,
222                format!("Test filesystem path {path:?} does not exist"),
223            )),
224            Some(test_path) => {
225                let contents = test_path.read();
226                match ERROR_REGEX.captures(&contents) {
227                    None => Ok(contents),
228                    Some(caps) => match caps.name("error_name") {
229                        None => Ok(contents),
230                        Some(re_match) => str_to_err(re_match.as_str()),
231                    },
232                }
233            },
234        })
235    }
236
237    pub(crate) fn read_file_to_bytes(path: &Path) -> std::io::Result<Vec<u8>> {
238        match read_file_to_string(path) {
239            Ok(contents) => Ok(Vec::from(contents.as_bytes())),
240            Err(x) => Err(x),
241        }
242    }
243
244    // ---------------------------------------------------------------------
245    // End: test virtual filesystem
246    // ---------------------------------------------------------------------
247
248    use super::*;
249
250    #[test]
251    fn fname_from_path_returns_file_name_even_if_file_does_not_exist() {
252        assert_eq!("some_name", fname_from_path(Path::new("some_name")).unwrap());
253        assert_eq!("some_name", fname_from_path(Path::new("/some_name")).unwrap());
254        assert_eq!("some_name", fname_from_path(Path::new("/a/b/some_name")).unwrap());
255    }
256
257    #[test]
258    fn fname_from_path_fails_when_path_is_empty() {
259        assert_eq!(None, fname_from_path(Path::new("")));
260    }
261
262    #[test]
263    fn fname_from_path_fails_when_path_has_no_filename() {
264        assert_eq!(None, fname_from_path(Path::new("/a/")));
265    }
266
267    #[test]
268    fn map_macro() {
269        let mut one = HashMap::new();
270        one.insert(1, 'a');
271        assert_eq!(one, map! { 1 => 'a' });
272
273        let mut two = HashMap::new();
274        two.insert("a", 1);
275        two.insert("b", 2);
276        assert_eq!(two, map! { "a" => 1, "b" => 2 });
277    }
278
279    #[test]
280    fn btreeset_join() {
281        let empty: BTreeSet<String> = vec![].into_iter().collect();
282        assert_eq!("", empty.join(""));
283        assert_eq!("", empty.join(","));
284
285        let one: BTreeSet<String> = vec!["a"].into_iter().map(|s| s.to_owned()).collect();
286        assert_eq!("a", one.join(""));
287        assert_eq!("a", one.join(","));
288
289        let two: BTreeSet<String> = vec!["a", "b"].into_iter().map(|s| s.to_owned()).collect();
290        assert_eq!("ab", two.join(""));
291        assert_eq!("a,b", two.join(","));
292    }
293}