1use std::env::current_dir;
2use std::fmt;
3use std::fs;
4use std::io::{self, Seek as _, SeekFrom, Write};
5use std::path::{Path, PathBuf};
6use std::str::FromStr;
7
8use anyhow::{anyhow, Context as _};
9use serde::{de, Deserialize, Deserializer, Serialize};
10
11use crate::{Error, Result};
12
13fn expand<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
15 Ok(shellexpand::full(&path.as_ref().to_string_lossy())?.parse()?)
16}
17
18#[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash)]
20pub struct AbsPathBuf(PathBuf);
21
22impl AbsPathBuf {
23 pub fn try_new<P: AsRef<Path>>(path: P) -> Result<Self> {
29 let path = path.as_ref();
30 if !path.is_absolute() {
31 return Err(anyhow!("Path is not absolute : {}", path.display()));
32 }
33 let mut ret = Self(PathBuf::new());
34 ret.push(path);
35 Ok(ret)
36 }
37
38 pub fn from_shell_path<P: AsRef<Path>>(path: P) -> Result<Self> {
42 Self::try_new(expand(path)?)
43 }
44
45 pub fn cwd() -> Result<Self> {
47 Ok(Self(current_dir()?))
48 }
49
50 pub fn join<P: AsRef<Path>>(&self, path: P) -> Self {
52 Self(self.0.join(path))
53 }
54
55 pub fn join_expand<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
57 Ok(self.join(expand(path)?))
58 }
59
60 fn push<P: AsRef<Path>>(&mut self, path: P) {
61 self.0.push(path)
62 }
63
64 pub fn parent(&self) -> Option<Self> {
66 self.0.parent().map(|parent| Self(parent.to_owned()))
67 }
68
69 pub fn search_dir_contains(&self, file_name: &str) -> Option<Self> {
70 for dir in self.0.ancestors() {
71 let mut file_path = dir.join(file_name);
72 if file_path.is_file() {
73 file_path.pop();
74 return Some(Self(file_path));
75 }
76 }
77 None
78 }
79
80 pub fn save_pretty(
81 &self,
82 save: impl FnOnce(fs::File) -> Result<()>,
83 overwrite: bool,
84 base_dir: Option<&AbsPathBuf>,
85 cnsl: &mut dyn Write,
86 ) -> Result<Option<bool>> {
87 write!(
88 cnsl,
89 "Saving {} ... ",
90 self.strip_prefix_if(base_dir).display()
91 )?;
92 let result = self.save(save, overwrite);
93 let msg = match result {
94 Ok(Some(true)) => "overwritten",
95 Ok(Some(false)) => "saved",
96 Ok(None) => "already exists",
97 Err(_) => "failed",
98 };
99 writeln!(cnsl, "{}", msg)?;
100 result
101 }
102
103 pub fn save(
105 &self,
106 save: impl FnOnce(fs::File) -> Result<()>,
107 overwrite: bool,
108 ) -> Result<Option<bool>> {
109 let is_existed = self.as_ref().is_file();
110 if !overwrite && is_existed {
111 return Ok(None);
112 }
113 self.create_dir_all_and_open(false, true)
114 .with_context(|| format!("Could not open file : {}", self))
115 .and_then(|mut file| {
116 file.seek(SeekFrom::Start(0))?;
118 file.set_len(0)?;
119 Ok(file)
120 })
121 .and_then(save)?;
122 Ok(Some(is_existed))
123 }
124
125 pub fn load_pretty<T>(
126 &self,
127 load: impl FnOnce(fs::File) -> Result<T>,
128 base_dir: Option<&AbsPathBuf>,
129 cnsl: &mut dyn Write,
130 ) -> Result<T> {
131 write!(
132 cnsl,
133 "Loading {} ... ",
134 self.strip_prefix_if(base_dir).display()
135 )?;
136 let result = self.load(load);
137 let msg = match result {
138 Ok(_) => "loaded",
139 Err(_) => "failed",
140 };
141 writeln!(cnsl, "{}", msg)?;
142 result
143 }
144
145 pub fn load<T>(&self, load: impl FnOnce(fs::File) -> Result<T>) -> Result<T> {
146 fs::OpenOptions::new()
147 .read(true)
148 .open(&self.0)
149 .with_context(|| format!("Could not open file : {}", self))
150 .and_then(load)
151 }
152
153 pub fn remove_dir_all_pretty(
154 &self,
155 base_dir: Option<&AbsPathBuf>,
156 cnsl: &mut dyn Write,
157 ) -> Result<bool> {
158 write!(
159 cnsl,
160 "Removing {} ... ",
161 self.strip_prefix_if(base_dir).display()
162 )?;
163 let result = self.remove_dir_all();
164 let msg = match result {
165 Ok(true) => "removed",
166 Ok(false) => "not existed",
167 Err(_) => "failed",
168 };
169 writeln!(cnsl, "{}", msg)?;
170 result
171 }
172
173 fn remove_dir_all(&self) -> Result<bool> {
174 if !self.as_ref().exists() {
175 return Ok(false);
176 }
177 fs::remove_dir_all(self.as_ref())?;
178 Ok(true)
179 }
180
181 pub fn remove_file_pretty(
182 &self,
183 base_dir: Option<&AbsPathBuf>,
184 cnsl: &mut dyn Write,
185 ) -> Result<bool> {
186 write!(
187 cnsl,
188 "Removing {} ... ",
189 self.strip_prefix_if(base_dir).display()
190 )?;
191 let result = if self.as_ref().exists() {
192 self.remove_file().map(|_| true)
193 } else {
194 Ok(false)
195 };
196 let msg = match result {
197 Ok(true) => "removed",
198 Ok(false) => "not existed",
199 Err(_) => "failed",
200 };
201 writeln!(cnsl, "{}", msg)?;
202 result
203 }
204
205 fn remove_file(&self) -> Result<()> {
206 fs::remove_file(self.as_ref())?;
207 Ok(())
208 }
209
210 pub fn move_from_pretty(
211 &self,
212 from: &AbsPathBuf,
213 base_dir: Option<&AbsPathBuf>,
214 cnsl: &mut dyn Write,
215 ) -> Result<()> {
216 write!(
217 cnsl,
218 "Moving {} to {} ... ",
219 from.strip_prefix_if(base_dir).display(),
220 self.strip_prefix_if(base_dir).display()
221 )?;
222 let result = self.move_from(from);
223 let msg = match result {
224 Ok(_) => "moved",
225 Err(_) => "failed",
226 };
227 writeln!(cnsl, "{}", msg)?;
228 result
229 }
230
231 fn move_from(&self, from: &AbsPathBuf) -> Result<()> {
232 fs::rename(from.as_ref(), self.as_ref())?;
233 Ok(())
234 }
235
236 pub fn create_dir_all_and_open(&self, is_read: bool, is_write: bool) -> io::Result<fs::File> {
237 if let Some(dir) = self.parent() {
238 dir.create_dir_all()?
239 }
240 self.open(is_read, is_write)
241 }
242
243 pub fn create_dir_all(&self) -> io::Result<()> {
244 fs::create_dir_all(self.as_ref())
245 }
246
247 fn open(&self, is_read: bool, is_write: bool) -> io::Result<fs::File> {
248 fs::OpenOptions::new()
249 .read(is_read)
250 .write(is_write)
251 .create(true)
252 .open(&self.0)
253 }
254
255 pub fn strip_prefix(&self, base: &AbsPathBuf) -> &Path {
256 self.0
257 .strip_prefix(&base.0)
258 .unwrap_or_else(|_| self.0.as_path())
259 }
260
261 fn strip_prefix_if(&self, base: Option<&AbsPathBuf>) -> &Path {
262 if let Some(base) = base {
263 self.strip_prefix(base)
264 } else {
265 self.0.as_path()
266 }
267 }
268}
269
270impl AsRef<PathBuf> for AbsPathBuf {
271 fn as_ref(&self) -> &PathBuf {
272 &self.0
273 }
274}
275
276impl FromStr for AbsPathBuf {
277 type Err = Error;
278
279 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
283 Self::from_shell_path(s)
284 }
285}
286
287impl<'de> Deserialize<'de> for AbsPathBuf {
288 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
289 where
290 D: Deserializer<'de>,
291 {
292 String::deserialize(deserializer)?
293 .parse()
294 .map_err(de::Error::custom)
295 }
296}
297
298impl fmt::Display for AbsPathBuf {
299 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
300 self.0.display().fmt(f)
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use lazy_static::lazy_static;
307
308 use super::*;
309
310 use crate::assert_matches;
311
312 lazy_static! {
313 static ref DRIVE: String = std::env::var("ACICK_TEST_DRIVE").unwrap_or_else(|_| String::from("C"));
314 static ref SHELL_PATH_SUCCESS_TESTS: Vec<(String, PathBuf)> = {
315 let mut tests = vec![
316 (prefix("/a/b"), PathBuf::from(prefix("/a/b"))),
317 ("~/a/b".into(), dirs::home_dir().unwrap().join("a/b")),
318 if cfg!(windows) {
319 ("$APPDATA/a/b".into(), PathBuf::from(std::env::var("APPDATA").unwrap()).join("a/b"))
320 } else {
321 ("$HOME/a/b".into(), dirs::home_dir().unwrap().join("a/b"))
322 },
323 (prefix("/a//b"), PathBuf::from(prefix("/a/b"))),
324 (prefix("/a/./b"), PathBuf::from(prefix("/a/b"))),
325 (prefix("/a/b/"), PathBuf::from(prefix("/a/b"))),
326 (prefix("/a/../b"), PathBuf::from(prefix("/a/../b"))),
327 ];
328 if cfg!(windows) {
329 tests.extend_from_slice(&[(
330 format!("{}:\\a\\b", &*DRIVE),
331 PathBuf::from(format!("{}:\\a\\b", &*DRIVE)),
332 )]);
333 }
334 tests
335 };
336 static ref SHELL_PATH_FAILURE_TESTS: Vec<&'static str> = {
337 let mut tests = vec!["./a/b/", "a/b", "$ACICK_UNKNOWN_VAR"];
338 if cfg!(windows) {
339 tests.extend_from_slice(&[
340 "%APPDATA%", "/a/b", ]);
343 }
344 tests
345 };
346 }
347
348 #[derive(Serialize, Deserialize, Debug)]
349 struct TestData {
350 abs_path: AbsPathBuf,
351 }
352
353 fn prefix(path: &str) -> String {
354 if cfg!(windows) {
355 format!("{}:{}", &*DRIVE, path)
356 } else {
357 path.to_string()
358 }
359 }
360
361 #[test]
362 fn test_try_new_success() -> anyhow::Result<()> {
363 let tests = &[
364 (prefix("/a/b"), prefix("/a/b")),
365 (prefix("/a//b"), prefix("/a/b")),
366 (prefix("/a/./b"), prefix("/a/b")),
367 (prefix("/a/b/"), prefix("/a/b")),
368 (prefix("/a/../b"), prefix("/a/../b")),
369 ];
370 for (actual, expected) in tests {
371 let actual = AbsPathBuf::try_new(actual)?;
372 let expected = PathBuf::from(expected);
373 assert_eq!(actual.as_ref(), &expected);
374 }
375 Ok(())
376 }
377
378 #[test]
379 fn test_try_new_failure() -> anyhow::Result<()> {
380 let tests = &[
381 "~/a/b",
382 if cfg!(windows) {
383 "$APPDATA/a/b"
384 } else {
385 "$HOME/a/b"
386 },
387 "./a/b/",
388 "a/b",
389 "$ACICK_UNKNOWN_VAR",
390 ];
391 for test in tests {
392 assert_matches!(AbsPathBuf::try_new(test) => Err(_));
393 }
394 Ok(())
395 }
396
397 #[test]
398 fn test_parent() -> anyhow::Result<()> {
399 let tests = &[(prefix("/a/b"), Some(prefix("/a"))), (prefix("/"), None)];
400 for (left, right) in tests {
401 let actual = AbsPathBuf::try_new(left)?.parent();
402 let expected = right
403 .as_ref()
404 .map(|path| AbsPathBuf::try_new(path).unwrap());
405 assert_eq!(actual, expected);
406 }
407 Ok(())
408 }
409
410 #[test]
411 fn test_from_str_success() -> anyhow::Result<()> {
412 for (actual, expected) in SHELL_PATH_SUCCESS_TESTS.iter() {
413 let actual: AbsPathBuf = actual.parse()?;
414 assert_eq!(actual.as_ref(), expected);
415 }
416 Ok(())
417 }
418
419 #[test]
420 fn test_from_str_failure() -> anyhow::Result<()> {
421 for test in SHELL_PATH_FAILURE_TESTS.iter() {
422 assert_matches!(AbsPathBuf::from_str(test) => Err(_));
423 }
424 Ok(())
425 }
426
427 #[cfg(not(windows))]
428 #[test]
429 fn test_serialize_success_unix() -> anyhow::Result<()> {
430 let test_data = TestData {
431 abs_path: AbsPathBuf::try_new("/a/b")?,
432 };
433 let actual = serde_yaml::to_string(&test_data)?;
434 let expected = format!("---\nabs_path: {}\n", "/a/b");
435 assert_eq!(actual, expected);
436 Ok(())
437 }
438
439 #[cfg(windows)]
440 #[test]
441 fn test_serialize_success_windows() -> anyhow::Result<()> {
442 let tests = &[
443 (
444 format!(r#"{}:\a\b"#, &*DRIVE),
445 format!(r#""{}:\\a\\b""#, &*DRIVE),
446 ),
447 (
448 format!(r#"{}:/a/b"#, &*DRIVE),
449 format!(r#""{}:/a/b""#, &*DRIVE),
450 ),
451 ];
452 for (left, right) in tests {
453 let test_data = TestData {
454 abs_path: AbsPathBuf::try_new(left)?,
455 };
456 let actual = serde_yaml::to_string(&test_data)?;
457 let expected = format!(
458 r#"---
459abs_path: {}
460"#,
461 right
462 );
463 assert_eq!(actual, expected);
464 }
465 Ok(())
466 }
467
468 #[test]
469 fn test_deserialize_success() -> anyhow::Result<()> {
470 for (actual, expected) in SHELL_PATH_SUCCESS_TESTS.iter() {
471 let yaml_str = format!("---\nabs_path: {}", actual);
472 let test_data: TestData = serde_yaml::from_str(&yaml_str)?;
473 assert_eq!(test_data.abs_path.as_ref(), expected);
474 }
475 Ok(())
476 }
477
478 #[test]
479 fn test_deserialize_failure() -> anyhow::Result<()> {
480 for test in SHELL_PATH_FAILURE_TESTS.iter() {
481 let yaml_str = format!("abs_path: {}", test);
482 let result = serde_yaml::from_str::<TestData>(&yaml_str);
483 assert_matches!(result => Err(_));
484 }
485 Ok(())
486 }
487
488 #[test]
489 fn test_display() -> anyhow::Result<()> {
490 let actual: AbsPathBuf = "~/a".parse()?;
491 let expected = PathBuf::from(format!("{}/a", dirs::home_dir().unwrap().display()));
492 assert_eq!(actual.as_ref(), &expected);
493 assert_eq!(format!("{}", actual), format!("{}", expected.display()));
494 Ok(())
495 }
496}