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
36pub 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; 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 pub path_wo_final_sep: &'a str,
247 pub offset: usize,
249 pub mark: &'a str,
251 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}