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