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