pars_core/util/
fs_util.rs

1use 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}