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}
157
158pub const DEFAULT_PRJ_NAME: &str = "default";
159pub fn is_prjname_set(prj_name: &str) -> bool {
160 prj_name != DEFAULT_PRJ_NAME
161}
162
163#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
164pub struct ExportData {
165 pub version: Option<String>,
166 pub tools_data_map: ToolsDataMap,
167}
168
169#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
170#[serde(untagged)]
171#[allow(clippy::large_enum_variant)]
172pub enum SavedCfg {
173 CfgPrj(CfgPrj),
174 CfgLegacy(CfgLegacy),
175}
176
177pub fn save<T>(file_path: &Path, data: T) -> RvResult<()>
178where
179 T: Serialize,
180{
181 let data_str = serde_json::to_string(&data).map_err(to_rv)?;
182 write(file_path, data_str)
183}
184pub fn checked_remove<'a, P: AsRef<Path> + Debug>(
185 path: &'a P,
186 func: fn(p: &'a P) -> io::Result<()>,
187) {
188 match func(path) {
189 Ok(_) => info!("removed {path:?}"),
190 Err(e) => error!("could not remove {path:?} due to {e:?}"),
191 }
192}
193#[macro_export]
194macro_rules! defer_folder_removal {
195 ($path:expr) => {
196 let func = || $crate::file_util::checked_remove($path, std::fs::remove_dir_all);
197 $crate::defer!(func);
198 };
199}
200#[macro_export]
201macro_rules! defer_file_removal {
202 ($path:expr) => {
203 let func = || $crate::file_util::checked_remove($path, std::fs::remove_file);
204 $crate::defer!(func);
205 };
206}
207
208#[allow(clippy::needless_lifetimes)]
209pub fn files_in_folder<'a>(
210 folder: &'a str,
211 prefix: &'a str,
212 extension: &'a str,
213) -> RvResult<impl Iterator<Item = PathBuf> + 'a> {
214 Ok(fs::read_dir(folder)
215 .map_err(|e| rverr!("could not open folder {} due to {}", folder, e))?
216 .flatten()
217 .map(|de| de.path())
218 .filter(|p| {
219 let prefix: &str = prefix; p.is_file()
221 && if let Some(fname) = p.file_name() {
222 fname.to_str().unwrap().starts_with(prefix)
223 } else {
224 false
225 }
226 && (p.extension() == Some(OsStr::new(extension)))
227 }))
228}
229
230pub fn write<P, C>(path: P, contents: C) -> RvResult<()>
231where
232 P: AsRef<Path> + Debug,
233 C: AsRef<[u8]>,
234{
235 fs::write(&path, contents).map_err(|e| rverr!("could not write to {:?} since {:?}", path, e))
236}
237
238#[macro_export]
239macro_rules! p_to_rv {
240 ($path:expr, $expr:expr) => {
241 $expr.map_err(|e| format_rverr!("{:?}, failed on {e:?}", $path))
242 };
243}
244
245pub struct LastPartOfPath<'a> {
246 pub last_folder: &'a str,
247 pub path_wo_final_sep: &'a str,
249 pub offset: usize,
251 pub mark: &'a str,
253 pub n_removed_separators: usize,
255}
256
257impl LastPartOfPath<'_> {
258 pub fn name(&self) -> String {
259 format!(
260 "{}{}{}",
261 self.mark,
262 self.last_folder.replace(':', "_"),
263 self.mark
264 )
265 }
266}
267
268pub fn url_encode(url: &str) -> String {
269 let mappings = [
270 (" ", "%20"),
271 ("+", "%2B"),
272 (",", "%2C"),
273 (";", "%3B"),
274 ("*", "%2A"),
275 ("(", "%28"),
276 (")", "%29"),
277 ];
278 let mut url = url.replace(mappings[0].0, mappings[0].1);
279 for m in mappings[1..].iter() {
280 url = url
281 .replace(m.0, m.1)
282 .replace(m.1.to_lowercase().as_str(), m.1);
283 }
284 url
285}
286
287fn get_last_part_of_path_by_sep(path: &str, sep: char) -> Option<LastPartOfPath<'_>> {
288 if path.contains(sep) {
289 let mark = if path.starts_with('\'') && path.ends_with('\'') {
290 "\'"
291 } else if path.starts_with('"') && path.ends_with('"') {
292 "\""
293 } else {
294 ""
295 };
296 let offset = mark.len();
297 let mut path_wo_final_sep = &path[offset..(path.len() - offset)];
298 let n_fp_slice_initial = path_wo_final_sep.len();
299 let mut last_folder = path_wo_final_sep.split(sep).next_back().unwrap_or("");
300 while last_folder.is_empty() && !path_wo_final_sep.is_empty() {
301 path_wo_final_sep = &path_wo_final_sep[0..path_wo_final_sep.len() - 1];
302 last_folder = path_wo_final_sep.split(sep).next_back().unwrap_or("");
303 }
304 Some(LastPartOfPath {
305 last_folder,
306 path_wo_final_sep,
307 offset,
308 mark,
309 n_removed_separators: n_fp_slice_initial - path_wo_final_sep.len(),
310 })
311 } else {
312 None
313 }
314}
315
316pub fn get_last_part_of_path(path: &str) -> Option<LastPartOfPath<'_>> {
317 let lp_fw = get_last_part_of_path_by_sep(path, '/');
318 if let Some(lp) = &lp_fw {
319 if let Some(lp_fwbw) = get_last_part_of_path_by_sep(lp.last_folder, '\\') {
320 Some(lp_fwbw)
321 } else {
322 lp_fw
323 }
324 } else {
325 get_last_part_of_path_by_sep(path, '\\')
326 }
327}
328
329pub fn get_prj_name<'a>(prj_path: &'a Path, opened_folder: Option<&'a str>) -> &'a str {
330 let default_prjname = if let Some(of) = opened_folder {
331 of
332 } else {
333 DEFAULT_PRJ_NAME
334 };
335 osstr_to_str(prj_path.file_stem()).unwrap_or(default_prjname)
336}
337
338pub fn local_file_info<P>(p: P) -> String
339where
340 P: AsRef<Path>,
341{
342 fs::metadata(p)
343 .map(|md| {
344 let n_bytes = md.len();
345 if n_bytes < 1024 {
346 format!("{}b", md.len())
347 } else if n_bytes < 1024u64.pow(2) {
348 format!("{:.3}kb", md.len() as f64 / 1024f64)
349 } else {
350 format!("{:.3}mb", md.len() as f64 / 1024f64.powi(2))
351 }
352 })
353 .unwrap_or_else(|_| "".to_string())
354}
355
356pub fn copy_and_unzip<P>(src_zip: P, dst_folder: P) -> RvResult<()>
357where
358 P: AsRef<Path>,
359{
360 let file = std::fs::File::open(src_zip).map_err(to_rv)?;
361 let mut archive = zip::ZipArchive::new(file).map_err(to_rv)?;
362 archive.extract(dst_folder).map_err(to_rv)
363}
364pub fn dl_and_unzip<T, P>(src_url: T, dst_folder: P) -> RvResult<()>
365where
366 T: IntoUrl,
367 P: AsRef<Path>,
368{
369 let response = reqwest::blocking::get(src_url).map_err(to_rv)?;
370 let content = response.bytes().map_err(to_rv)?;
371 let des = Cursor::new(content);
372 let mut archive = zip::ZipArchive::new(des).map_err(to_rv)?;
373 archive.extract(dst_folder).map_err(to_rv)
374}
375pub fn get_test_folder() -> PathBuf {
376 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test_data")
377}
378
379pub fn copy_folder_recursively<P>(src: P, dst: P) -> RvResult<()>
380where
381 P: AsRef<Path>,
382{
383 std::fs::create_dir_all(&dst).map_err(to_rv)?;
384 for entry in std::fs::read_dir(src).map_err(to_rv)? {
385 let entry = entry.map_err(to_rv)?;
386 let file_type = entry.file_type().map_err(to_rv)?;
387 let src_path = entry.path();
388 let dst_path = dst.as_ref().join(entry.file_name());
389 if file_type.is_dir() {
390 copy_folder_recursively(&src_path, &dst_path)?;
391 } else {
392 std::fs::copy(&src_path, &dst_path).map_err(to_rv)?;
393 }
394 }
395 Ok(())
396}
397
398pub fn is_url<P>(s: P) -> bool
399where
400 P: AsRef<str>,
401{
402 s.as_ref().starts_with("http://")
403 || s.as_ref().starts_with("https://")
404 || s.as_ref().starts_with("ftp://")
405}
406
407pub fn relative_to_prj_path<P>(prj_path: &Path, filepath: P) -> RvResult<PathBuf>
408where
409 P: AsRef<Path>,
410{
411 let prj_parent = prj_path.parent();
412 if let Some(prj_parent) = prj_parent
413 && !filepath.as_ref().is_absolute()
414 {
415 let full_path = prj_parent.join(filepath);
416 Ok(full_path)
417 } else {
418 Ok(filepath.as_ref().to_path_buf())
419 }
420}
421
422#[test]
423fn get_last_part() {
424 let path = "http://localhost:8000/a/21%20%20b/Beg.png";
425 let lp = get_last_part_of_path(path).unwrap();
426 assert_eq!(lp.last_folder, "Beg.png");
427}
428
429#[test]
430fn encode() {
431 let url = "http://localhost:8000/a/ +,;*()/Beg.png";
432 let encoded = url_encode(url);
433 let url_enc = "http://localhost:8000/a/%20%2B%2C%3B%2A%28%29/Beg.png";
434 assert_eq!(encoded, url_enc);
435}
436
437#[test]
438fn last_folder_part() {
439 assert_eq!(
440 get_last_part_of_path("a/b/c").map(|lp| lp.name()),
441 Some("c".to_string())
442 );
443 assert_eq!(
444 get_last_part_of_path_by_sep("a/b/c", '\\').map(|lp| lp.name()),
445 None
446 );
447 assert_eq!(
448 get_last_part_of_path_by_sep("a\\b\\c", '/').map(|lp| lp.name()),
449 None
450 );
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!(get_last_part_of_path("").map(|lp| lp.name()), None);
456 assert_eq!(
457 get_last_part_of_path("a/b/c/").map(|lp| lp.name()),
458 Some("c".to_string())
459 );
460 assert_eq!(
461 get_last_part_of_path("aadfh//bdafl////aksjc/////").map(|lp| lp.name()),
462 Some("aksjc".to_string())
463 );
464 assert_eq!(
465 get_last_part_of_path("\"aa dfh//bdafl////aks jc/////\"").map(|lp| lp.name()),
466 Some("\"aks jc\"".to_string())
467 );
468 assert_eq!(
469 get_last_part_of_path("'aa dfh//bdafl////aks jc/////'").map(|lp| lp.name()),
470 Some("'aks jc'".to_string())
471 );
472}
473
474#[cfg(target_family = "windows")]
475#[test]
476fn test_stem() {
477 assert_eq!(to_stem_str(Path::new("a/b/c.png")).unwrap(), "c");
478 assert_eq!(to_stem_str(Path::new("c:\\c.png")).unwrap(), "c");
479 assert_eq!(to_stem_str(Path::new("c:\\")).unwrap(), "");
480}
481#[cfg(target_family = "unix")]
482#[test]
483fn test_stem() {
484 assert_eq!(to_stem_str(Path::new("a/b/c.png")).unwrap(), "c");
485 assert_eq!(to_stem_str(Path::new("c:\\c.png")).unwrap(), "c:\\c");
486 assert_eq!(to_stem_str(Path::new("/c.png")).unwrap(), "c");
487 assert_eq!(to_stem_str(Path::new("/")).unwrap(), "");
488}
489
490#[test]
491fn test_pathpair() {
492 fn test(
493 path: &str,
494 prj_path: &str,
495 expected_absolute: &str,
496 expected_relative: &str,
497 skip_from_relative: bool,
498 ) {
499 let pp = PathPair::new(path.to_string(), Path::new(prj_path));
500 assert_eq!(pp.path_absolute(), expected_absolute);
501 assert_eq!(pp.path_relative(), expected_relative);
502 if !skip_from_relative {
503 let pp = PathPair::from_relative_path(
504 expected_relative.to_string(),
505 Some(Path::new(prj_path)),
506 );
507 assert_eq!(pp.path_absolute(), expected_absolute);
508 assert_eq!(pp.path_relative(), expected_relative);
509 }
510 }
511
512 let relative_path = "somesubfolder/notanimage.png";
513 let prj_path_p = get_test_folder().join("rvprj_v3-3_test_dummy.rvi");
514 let prj_path_parent_p = prj_path_p.parent().unwrap();
515 let path_p = prj_path_parent_p.join(relative_path);
516 let prj_path = path_to_str(prj_path_p.as_path()).unwrap();
517 let path = path_to_str(path_p.as_path()).unwrap();
518 test(
519 path,
520 prj_path,
521 &path.replace("\\", "/"),
522 relative_path,
523 false,
524 );
525
526 #[cfg(target_family = "windows")]
527 {
528 let prj_path = "a\\b\\c\\prj.rvi";
529 let path = "a\\b\\c\\d\\e.png";
530 test(path, prj_path, &path.replace("\\", "/"), "d/e.png", true);
531 }
532}