rvlib/
file_util.rs

1use crate::{
2    cfg::{CfgLegacy, CfgPrj},
3    tools_data::ToolsDataMap,
4};
5use lazy_static::lazy_static;
6use rvimage_domain::{rverr, to_rv, RvResult};
7use serde::{Deserialize, Serialize};
8use std::{
9    ffi::OsStr,
10    fmt::Debug,
11    fs, io,
12    path::{Path, PathBuf},
13};
14use tracing::{error, info};
15
16lazy_static! {
17    pub static ref DEFAULT_TMPDIR: PathBuf = std::env::temp_dir().join("rvimage");
18}
19lazy_static! {
20    pub static ref DEFAULT_HOMEDIR: PathBuf = match dirs::home_dir() {
21        Some(p) => p.join(".rvimage"),
22        _ => std::env::temp_dir().join("rvimage"),
23    };
24}
25lazy_static! {
26    pub static ref DEFAULT_PRJ_PATH: PathBuf =
27        DEFAULT_HOMEDIR.join(DEFAULT_PRJ_NAME).join("default.rvi");
28}
29
30pub fn get_default_homedir() -> &'static str {
31    DEFAULT_HOMEDIR
32        .to_str()
33        .expect("could not get default homedir. cannot work without.")
34}
35
36/// Keys of the annotation maps are the relative paths of the corresponding image files to the project folder.
37pub fn tf_to_annomap_key(path: String, curr_prj_path: Option<&Path>) -> String {
38    let path = path.replace('\\', "/");
39    if let Some(curr_prj_path) = curr_prj_path {
40        let path_ref = Path::new(&path);
41        let prj_parent = curr_prj_path
42            .parent()
43            .ok_or_else(|| rverr!("{curr_prj_path:?} has no parent"));
44        let relative_path =
45            prj_parent.and_then(|prj_parent| path_ref.strip_prefix(prj_parent).map_err(to_rv));
46        if let Ok(relative_path) = relative_path {
47            let without_base = path_to_str(relative_path);
48            if let Ok(without_base) = without_base {
49                without_base.to_string()
50            } else {
51                path
52            }
53        } else {
54            path
55        }
56    } else {
57        path
58    }
59}
60#[derive(Clone, Default, Debug, PartialEq, Eq)]
61pub struct PathPair {
62    path_absolute: String,
63    path_relative: String,
64}
65impl PathPair {
66    pub fn new(path_absolute: String, prj_path: &Path) -> Self {
67        let path_absolute = path_absolute.replace('\\', "/");
68        let prj_path = if prj_path == Path::new("") {
69            None
70        } else {
71            Some(prj_path)
72        };
73        let path_relative = tf_to_annomap_key(path_absolute.clone(), prj_path);
74        PathPair {
75            path_absolute,
76            path_relative,
77        }
78    }
79    pub fn from_relative_path(path_relative: String, prj_path: Option<&Path>) -> Self {
80        if let Some(prj_path) = prj_path {
81            let path_absolute = if let Some(parent) = prj_path.parent() {
82                let path_absolute = parent.join(path_relative.clone());
83                if path_absolute.exists() {
84                    path_to_str(&path_absolute).unwrap().replace('\\', "/")
85                } else {
86                    path_relative.replace('\\', "/")
87                }
88            } else {
89                path_relative.replace('\\', "/")
90            };
91            PathPair {
92                path_absolute,
93                path_relative,
94            }
95        } else {
96            PathPair {
97                path_relative: path_relative.clone(),
98                path_absolute: path_relative,
99            }
100        }
101    }
102    pub fn path_absolute(&self) -> &str {
103        &self.path_absolute
104    }
105    pub fn path_relative(&self) -> &str {
106        &self.path_relative
107    }
108    pub fn filename(&self) -> RvResult<&str> {
109        to_name_str(Path::new(&self.path_relative))
110    }
111    pub fn filestem(&self) -> RvResult<&str> {
112        to_stem_str(Path::new(&self.path_relative))
113    }
114}
115
116pub fn read_to_string<P>(p: P) -> RvResult<String>
117where
118    P: AsRef<Path> + Debug,
119{
120    fs::read_to_string(&p).map_err(|e| rverr!("could not read {:?} due to {:?}", p, e))
121}
122pub trait PixelEffect: FnMut(u32, u32) {}
123impl<T: FnMut(u32, u32)> PixelEffect for T {}
124
125pub fn path_to_str(p: &Path) -> RvResult<&str> {
126    osstr_to_str(Some(p.as_os_str()))
127        .map_err(|e| rverr!("path_to_str could not transform '{:?}' due to '{:?}'", p, e))
128}
129
130pub fn osstr_to_str(p: Option<&OsStr>) -> io::Result<&str> {
131    p.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("{p:?} not found")))?
132        .to_str()
133        .ok_or_else(|| {
134            io::Error::new(
135                io::ErrorKind::InvalidData,
136                format!("{p:?} not convertible to unicode"),
137            )
138        })
139}
140
141pub fn to_stem_str(p: &Path) -> RvResult<&str> {
142    let stem = p.file_stem();
143    if stem.is_none() {
144        Ok("")
145    } else {
146        osstr_to_str(stem)
147            .map_err(|e| rverr!("to_stem_str could not transform '{:?}' due to '{:?}'", p, e))
148    }
149}
150
151pub fn to_name_str(p: &Path) -> RvResult<&str> {
152    osstr_to_str(p.file_name())
153        .map_err(|e| rverr!("to_name_str could not transform '{:?}' due to '{:?}'", p, e))
154}
155
156pub const DEFAULT_PRJ_NAME: &str = "default";
157pub fn is_prjname_set(prj_name: &str) -> bool {
158    prj_name != DEFAULT_PRJ_NAME
159}
160
161#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
162pub struct ExportData {
163    pub version: Option<String>,
164    pub tools_data_map: ToolsDataMap,
165}
166
167#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
168#[serde(untagged)]
169#[allow(clippy::large_enum_variant)]
170pub enum SavedCfg {
171    CfgPrj(CfgPrj),
172    CfgLegacy(CfgLegacy),
173}
174
175pub fn save<T>(file_path: &Path, data: T) -> RvResult<()>
176where
177    T: Serialize,
178{
179    let data_str = serde_json::to_string(&data).map_err(to_rv)?;
180    write(file_path, data_str)
181}
182pub fn checked_remove<'a, P: AsRef<Path> + Debug>(
183    path: &'a P,
184    func: fn(p: &'a P) -> io::Result<()>,
185) {
186    match func(path) {
187        Ok(_) => info!("removed {path:?}"),
188        Err(e) => error!("could not remove {path:?} due to {e:?}"),
189    }
190}
191#[macro_export]
192macro_rules! defer_folder_removal {
193    ($path:expr) => {
194        let func = || $crate::file_util::checked_remove($path, std::fs::remove_dir_all);
195        $crate::defer!(func);
196    };
197}
198#[macro_export]
199macro_rules! defer_file_removal {
200    ($path:expr) => {
201        let func = || $crate::file_util::checked_remove($path, std::fs::remove_file);
202        $crate::defer!(func);
203    };
204}
205
206#[allow(clippy::needless_lifetimes)]
207pub fn files_in_folder<'a>(
208    folder: &'a str,
209    prefix: &'a str,
210    extension: &'a str,
211) -> RvResult<impl Iterator<Item = PathBuf> + 'a> {
212    Ok(fs::read_dir(folder)
213        .map_err(|e| rverr!("could not open folder {} due to {}", folder, e))?
214        .flatten()
215        .map(|de| de.path())
216        .filter(|p| {
217            let prefix: &str = prefix; // Not sure why the borrow checker needs this.
218            p.is_file()
219                && if let Some(fname) = p.file_name() {
220                    fname.to_str().unwrap().starts_with(prefix)
221                } else {
222                    false
223                }
224                && (p.extension() == Some(OsStr::new(extension)))
225        }))
226}
227
228pub fn write<P, C>(path: P, contents: C) -> RvResult<()>
229where
230    P: AsRef<Path> + Debug,
231    C: AsRef<[u8]>,
232{
233    fs::write(&path, contents).map_err(|e| rverr!("could not write to {:?} since {:?}", path, e))
234}
235
236#[macro_export]
237macro_rules! p_to_rv {
238    ($path:expr, $expr:expr) => {
239        $expr.map_err(|e| format_rverr!("{:?}, failed on {e:?}", $path))
240    };
241}
242
243pub struct LastPartOfPath<'a> {
244    pub last_folder: &'a str,
245    // will transform /a/b/c/ to /a/b/c
246    pub path_wo_final_sep: &'a str,
247    // offset is defined by " or ' that might by at the beginning and end of the path
248    pub offset: usize,
249    // ', ", or empty string depending on their existence
250    pub mark: &'a str,
251    // separators can be / on Linux or for http and \ on Windows
252    pub n_removed_separators: usize,
253}
254
255impl LastPartOfPath<'_> {
256    pub fn name(&self) -> String {
257        format!(
258            "{}{}{}",
259            self.mark,
260            self.last_folder.replace(':', "_"),
261            self.mark
262        )
263    }
264}
265
266pub fn url_encode(url: &str) -> String {
267    let mappings = [
268        (" ", "%20"),
269        ("+", "%2B"),
270        (",", "%2C"),
271        (";", "%3B"),
272        ("*", "%2A"),
273        ("(", "%28"),
274        (")", "%29"),
275    ];
276    let mut url = url.replace(mappings[0].0, mappings[1].1);
277    for m in mappings[1..].iter() {
278        url = url
279            .replace(m.0, m.1)
280            .replace(m.1.to_lowercase().as_str(), m.1);
281    }
282    url
283}
284
285fn get_last_part_of_path_by_sep(path: &str, sep: char) -> Option<LastPartOfPath> {
286    if path.contains(sep) {
287        let mark = if path.starts_with('\'') && path.ends_with('\'') {
288            "\'"
289        } else if path.starts_with('"') && path.ends_with('"') {
290            "\""
291        } else {
292            ""
293        };
294        let offset = mark.len();
295        let mut path_wo_final_sep = &path[offset..(path.len() - offset)];
296        let n_fp_slice_initial = path_wo_final_sep.len();
297        let mut last_folder = path_wo_final_sep.split(sep).next_back().unwrap_or("");
298        while last_folder.is_empty() && !path_wo_final_sep.is_empty() {
299            path_wo_final_sep = &path_wo_final_sep[0..path_wo_final_sep.len() - 1];
300            last_folder = path_wo_final_sep.split(sep).next_back().unwrap_or("");
301        }
302        Some(LastPartOfPath {
303            last_folder,
304            path_wo_final_sep,
305            offset,
306            mark,
307            n_removed_separators: n_fp_slice_initial - path_wo_final_sep.len(),
308        })
309    } else {
310        None
311    }
312}
313
314pub fn get_last_part_of_path(path: &str) -> Option<LastPartOfPath> {
315    let lp_fw = get_last_part_of_path_by_sep(path, '/');
316    if let Some(lp) = &lp_fw {
317        if let Some(lp_fwbw) = get_last_part_of_path_by_sep(lp.last_folder, '\\') {
318            Some(lp_fwbw)
319        } else {
320            lp_fw
321        }
322    } else {
323        get_last_part_of_path_by_sep(path, '\\')
324    }
325}
326
327pub fn get_prj_name<'a>(prj_path: &'a Path, opened_folder: Option<&'a str>) -> &'a str {
328    let default_prjname = if let Some(of) = opened_folder {
329        of
330    } else {
331        DEFAULT_PRJ_NAME
332    };
333    osstr_to_str(prj_path.file_stem()).unwrap_or(default_prjname)
334}
335
336pub fn local_file_info<P>(p: P) -> String
337where
338    P: AsRef<Path>,
339{
340    fs::metadata(p)
341        .map(|md| {
342            let n_bytes = md.len();
343            if n_bytes < 1024 {
344                format!("{}b", md.len())
345            } else if n_bytes < 1024u64.pow(2) {
346                format!("{:.3}kb", md.len() as f64 / 1024f64)
347            } else {
348                format!("{:.3}mb", md.len() as f64 / 1024f64.powi(2))
349            }
350        })
351        .unwrap_or_else(|_| "".to_string())
352}
353
354pub fn get_test_folder() -> PathBuf {
355    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test_data")
356}
357
358#[test]
359fn get_last_part() {
360    let path = "http://localhost:8000/a/21%20%20b/Beg.png";
361    let lp = get_last_part_of_path(path).unwrap();
362    assert_eq!(lp.last_folder, "Beg.png");
363}
364
365#[test]
366fn last_folder_part() {
367    assert_eq!(
368        get_last_part_of_path("a/b/c").map(|lp| lp.name()),
369        Some("c".to_string())
370    );
371    assert_eq!(
372        get_last_part_of_path_by_sep("a/b/c", '\\').map(|lp| lp.name()),
373        None
374    );
375    assert_eq!(
376        get_last_part_of_path_by_sep("a\\b\\c", '/').map(|lp| lp.name()),
377        None
378    );
379    assert_eq!(
380        get_last_part_of_path("a\\b\\c").map(|lp| lp.name()),
381        Some("c".to_string())
382    );
383    assert_eq!(get_last_part_of_path("").map(|lp| lp.name()), None);
384    assert_eq!(
385        get_last_part_of_path("a/b/c/").map(|lp| lp.name()),
386        Some("c".to_string())
387    );
388    assert_eq!(
389        get_last_part_of_path("aadfh//bdafl////aksjc/////").map(|lp| lp.name()),
390        Some("aksjc".to_string())
391    );
392    assert_eq!(
393        get_last_part_of_path("\"aa dfh//bdafl////aks jc/////\"").map(|lp| lp.name()),
394        Some("\"aks jc\"".to_string())
395    );
396    assert_eq!(
397        get_last_part_of_path("'aa dfh//bdafl////aks jc/////'").map(|lp| lp.name()),
398        Some("'aks jc'".to_string())
399    );
400}
401
402#[cfg(target_family = "windows")]
403#[test]
404fn test_stem() {
405    assert_eq!(to_stem_str(Path::new("a/b/c.png")).unwrap(), "c");
406    assert_eq!(to_stem_str(Path::new("c:\\c.png")).unwrap(), "c");
407    assert_eq!(to_stem_str(Path::new("c:\\")).unwrap(), "");
408}
409#[cfg(target_family = "unix")]
410#[test]
411fn test_stem() {
412    assert_eq!(to_stem_str(Path::new("a/b/c.png")).unwrap(), "c");
413    assert_eq!(to_stem_str(Path::new("c:\\c.png")).unwrap(), "c:\\c");
414    assert_eq!(to_stem_str(Path::new("/c.png")).unwrap(), "c");
415    assert_eq!(to_stem_str(Path::new("/")).unwrap(), "");
416}
417
418#[test]
419fn test_pathpair() {
420    fn test(
421        path: &str,
422        prj_path: &str,
423        expected_absolute: &str,
424        expected_relative: &str,
425        skip_from_relative: bool,
426    ) {
427        let pp = PathPair::new(path.to_string(), Path::new(prj_path));
428        assert_eq!(pp.path_absolute(), expected_absolute);
429        assert_eq!(pp.path_relative(), expected_relative);
430        if !skip_from_relative {
431            let pp = PathPair::from_relative_path(
432                expected_relative.to_string(),
433                Some(Path::new(prj_path)),
434            );
435            assert_eq!(pp.path_absolute(), expected_absolute);
436            assert_eq!(pp.path_relative(), expected_relative);
437        }
438    }
439
440    let relative_path = "somesubfolder/notanimage.png";
441    let prj_path_p = get_test_folder().join("rvprj_v3-3_test_dummy.rvi");
442    let prj_path_parent_p = prj_path_p.parent().unwrap();
443    let path_p = prj_path_parent_p.join(relative_path);
444    let prj_path = path_to_str(prj_path_p.as_path()).unwrap();
445    let path = path_to_str(path_p.as_path()).unwrap();
446    test(
447        path,
448        prj_path,
449        &path.replace("\\", "/"),
450        relative_path,
451        false,
452    );
453
454    #[cfg(target_family = "windows")]
455    {
456        let prj_path = "a\\b\\c\\prj.rvi";
457        let path = "a\\b\\c\\d\\e.png";
458        test(path, prj_path, &path.replace("\\", "/"), "d/e.png", true);
459    }
460}