pars_core/util/
fs_util.rs1use std::io::{BufRead, Read, Write};
2#[cfg(unix)]
3use std::os::unix::fs::symlink;
4#[cfg(windows)]
5use std::os::windows::fs::{symlink_dir, symlink_file};
6use std::path::{Path, PathBuf};
7use std::{env, fs, io};
8
9use anyhow::{anyhow, Result};
10use clean_path::Clean;
11use directories::ProjectDirs;
12use fs_extra::dir::{self, CopyOptions};
13use log::debug;
14use secrecy::{ExposeSecret, SecretBox};
15
16use crate::constants::default_constants::BACKUP_EXTENSION;
17use crate::pgp::PGPClient;
18use crate::{IOErr, IOErrType};
19
20pub fn find_executable_in_path(executable: &str) -> Option<PathBuf> {
21 if let Some(paths) = env::var_os("PATH") {
22 for path in env::split_paths(&paths) {
23 let full_path = path.join(executable);
24
25 if is_executable(&full_path).is_ok() {
26 return Some(full_path);
27 }
28 }
29 }
30 None
31}
32
33pub fn better_rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
34 let from = from.as_ref();
35 let to = to.as_ref();
36 if let Err(err) = fs::rename(from, to) {
37 if err.kind() == io::ErrorKind::CrossesDevices {
38 if from.is_dir() {
39 dir::copy(from, to, &CopyOptions::new())?;
40 fs::remove_dir_all(from)?;
41 } else {
42 fs::copy(from, to)?;
43 fs::remove_file(from)?;
44 }
45 }
46 }
47
48 Ok(())
49}
50
51pub fn copy_dir_recursive<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
52 let mut options = CopyOptions::new();
53 options.overwrite = false;
54 options.copy_inside = true;
55 dir::copy(from, to, &options)?;
56 Ok(())
57}
58
59pub fn get_home_dir() -> PathBuf {
60 dirs::home_dir().unwrap_or(PathBuf::from("~"))
61}
62
63pub fn get_dir_gpg_id_content(root: &Path, cur_dir: &Path) -> Result<Vec<String>> {
64 path_attack_check(root, cur_dir)?;
65 let mut to_check = cur_dir.to_path_buf();
66
67 while to_check != root {
68 if to_check.is_dir() {
69 let key_file = to_check.join(".gpg-id");
70 debug!("Check {:?} for .gpg-id file", key_file);
71
72 if key_file.exists() && key_file.is_file() {
73 if let Ok(key) = fs::read_to_string(key_file) {
74 debug!("Found key(s): {:?}", key);
75
76 return Ok(key
77 .lines()
78 .map(|line| line.trim())
79 .filter(|line| !line.is_empty())
80 .map(|line| line.to_string())
81 .collect());
82 }
83 }
84 }
85 match to_check.parent() {
86 Some(parent) => {
87 to_check = parent.to_path_buf();
88 }
89 None => break,
90 }
91 }
92
93 if root.is_dir() {
94 let key_file = root.join(".gpg-id");
95 debug!("Checking root {:?} for .gpg-id file", root);
96 if key_file.exists() && key_file.is_file() {
97 if let Ok(key) = fs::read_to_string(key_file) {
98 debug!("Found key: {:?}", key);
99 return Ok(key
100 .split('\n')
101 .map(|line| line.trim())
102 .filter(|line| !line.is_empty())
103 .map(|line| line.to_string())
104 .collect());
105 }
106 }
107 }
108 Err(anyhow!(format!("Cannot find '.gpg-id' for {:?}", cur_dir)))
109}
110
111pub(crate) fn backup_encrypted_file(file_path: &Path) -> Result<PathBuf> {
112 let extension = format!(
113 "{}.{}",
114 file_path.extension().unwrap_or_default().to_string_lossy(),
115 BACKUP_EXTENSION
116 );
117 let backup_path = file_path.with_extension(&extension);
118 fs::rename(file_path, &backup_path)?;
119 Ok(backup_path)
120}
121
122pub(crate) fn restore_backup_file(file_path: &Path) -> Result<()> {
123 if let Some(extension) = file_path.extension() {
124 return if extension == BACKUP_EXTENSION {
125 let original_path = file_path.with_extension("");
126 fs::rename(file_path, original_path)?;
127 Ok(())
128 } else {
129 Err(anyhow!(format!("File extension is not {}", BACKUP_EXTENSION)))
130 };
131 }
132 Err(anyhow!("File does not has extension"))
133}
134
135fn is_executable(path: &Path) -> Result<bool> {
136 if path.is_file() {
137 #[cfg(unix)]
138 {
139 use std::os::unix::fs::PermissionsExt;
140 Ok(fs::metadata(path).map(|metadata| metadata.permissions().mode() & 0o111 != 0)?)
141 }
142
143 #[cfg(windows)]
144 {
145 Ok(path.extension().is_some_and(|ext| ext == "exe" || ext == "bat" || ext == "cmd"))
146 }
147
148 #[cfg(not(any(unix, windows)))]
149 {
150 Ok(false)
151 }
152 } else {
153 Ok(false)
154 }
155}
156
157pub fn create_symlink<P: AsRef<Path>>(original: P, link: P) -> Result<()> {
158 #[cfg(unix)]
159 {
160 Ok(symlink(original, link)?)
161 }
162
163 #[cfg(windows)]
164 {
165 if original.as_ref().is_dir() {
166 Ok(symlink_dir(original, link)?)
167 } else {
168 Ok(symlink_file(original, link)?)
169 }
170 }
171
172 #[cfg(not(any(unix, windows)))]
173 {
174 Err("Symlinks are not supported on this platform".into())
175 }
176}
177
178pub fn path_to_str(path: &Path) -> Result<&str> {
179 Ok(path.to_str().ok_or_else(|| IOErr::new(IOErrType::InvalidPath, path))?)
180}
181
182pub fn filename_to_str(path: &Path) -> Result<&str> {
183 Ok(path
184 .file_name()
185 .ok_or_else(|| IOErr::new(IOErrType::InvalidPath, path))?
186 .to_str()
187 .ok_or_else(|| IOErr::new(IOErrType::InvalidName, path))?)
188}
189
190pub fn is_sub_path_of<P: AsRef<Path>>(parent: P, child: P) -> Result<bool> {
191 let child_clean = child.as_ref().clean();
192 let parent_clean = parent.as_ref().clean();
193 Ok(child_clean.starts_with(&parent_clean))
194}
195
196pub fn set_readonly<P: AsRef<Path>>(path: P, readonly: bool) -> Result<()> {
197 let metadata = fs::metadata(path.as_ref())?;
198 let mut permissions = metadata.permissions();
199 permissions.set_readonly(readonly);
200 fs::set_permissions(path, permissions)?;
201 Ok(())
202}
203
204pub fn path_attack_check(root: &Path, child: &Path) -> Result<()> {
205 if !is_sub_path_of(root, child)? {
206 Err(IOErr::new(IOErrType::PathNotInRepo, child).into())
207 } else {
208 Ok(())
209 }
210}
211
212pub fn prompt_overwrite<R: Read + BufRead, W: Write>(
213 in_s: &mut R,
214 err_s: &mut W,
215 pass_name: &str,
216) -> Result<bool> {
217 write!(err_s, "An entry already exists for {}. Overwrite? [y/N]: ", pass_name)?;
218 let mut input = String::new();
219 in_s.read_line(&mut input)?;
220 Ok(input.trim().eq_ignore_ascii_case("y"))
221}
222
223pub fn create_or_overwrite(
224 client: &PGPClient,
225 pass_path: &Path,
226 password: &SecretBox<str>,
227) -> Result<()> {
228 if pass_path.exists() {
229 let backup = backup_encrypted_file(pass_path)?;
230 match client.encrypt(password.expose_secret(), path_to_str(pass_path)?) {
231 Ok(_) => {
232 fs::remove_file(&backup)?;
233 Ok(())
234 }
235 Err(e) => {
236 restore_backup_file(&backup)?;
237 Err(e)
238 }
239 }
240 } else {
241 client.encrypt(password.expose_secret(), path_to_str(pass_path)?)?;
242 Ok(())
243 }
244}
245
246pub fn default_config_path() -> String {
247 if let Some(proj_dirs) = ProjectDirs::from("", "", "pars") {
248 let config_path = proj_dirs.config_dir().join("config.toml");
249 config_path.to_string_lossy().into_owned()
250 } else {
251 eprintln!(
252 "Error determining config directory, falling back to '~/.config/pars/config.toml'"
253 );
254 "~/.config/pars/config.toml".into()
255 }
256}