pars_core/operation/
edit.rs

1use std::path::{Path, PathBuf};
2use std::process::Command;
3use std::{env, fs};
4
5use anyhow::{anyhow, Result};
6use secrecy::ExposeSecret;
7use tempfile::TempDir;
8use zeroize::Zeroize;
9
10use crate::pgp::PGPClient;
11use crate::util::defer::Defer;
12use crate::util::fs_util::{
13    backup_encrypted_file, get_dir_gpg_id_content, path_attack_check, path_to_str,
14    restore_backup_file,
15};
16use crate::util::rand::rand_alphabet_string;
17use crate::{IOErr, IOErrType};
18
19pub fn edit(
20    root: &Path,
21    target: &str,
22    extension: &str,
23    editor: &str,
24    pgp_executable: &str,
25) -> Result<bool> {
26    let target_path = root.join(format!("{}.{}", target, extension));
27    path_attack_check(root, &target_path)?;
28
29    if !target_path.exists() {
30        return Err(IOErr::new(IOErrType::PathNotExist, &target_path).into());
31    } else if !target_path.is_file() {
32        return Err(IOErr::new(IOErrType::ExpectFile, &target_path).into());
33    }
34
35    // Get the appropriate key fingerprints for this path
36    let keys_fpr = get_dir_gpg_id_content(root, &target_path)?;
37    let client = PGPClient::new(
38        pgp_executable,
39        &keys_fpr.iter().map(|s| s.as_str()).collect::<Vec<&str>>(),
40    )?;
41
42    let tmp_dir: PathBuf = {
43        let temp_base = {
44            #[cfg(unix)]
45            {
46                let shm_dir = PathBuf::from("/dev/shm");
47                if !shm_dir.exists() {
48                    env::temp_dir()
49                } else {
50                    shm_dir
51                }
52            }
53            #[cfg(not(unix))]
54            {
55                env::temp_dir()
56            }
57        };
58        TempDir::new_in(temp_base)?.into_path()
59    };
60
61    let temp_filename = target_path.with_extension("txt");
62    let temp_filename = temp_filename
63        .file_name()
64        .ok_or_else(|| IOErr::new(IOErrType::CannotGetFileName, &target_path))?;
65    let temp_filename =
66        format!(".{}-{}", rand_alphabet_string(10), temp_filename.to_string_lossy());
67    let temp_filepath = tmp_dir.join(temp_filename);
68
69    let mut content = client.decrypt_stdin(root, path_to_str(&target_path)?)?;
70    fs::write(&temp_filepath, content.expose_secret())?;
71    let _cleaner = Defer::new(|| {
72        let _ = fs::remove_file(&temp_filepath);
73    });
74    content.zeroize();
75
76    let mut cmd = Command::new(editor).arg(path_to_str(&temp_filepath)?).spawn()?;
77    let status = cmd.wait()?;
78    if status.success() {
79        let new_content = fs::read_to_string(&temp_filepath)?;
80        let mut old_content = client.decrypt_stdin(root, path_to_str(&target_path)?)?;
81        if old_content.expose_secret() == new_content {
82            println!("Password unchanged");
83            return Ok(false);
84        }
85        old_content.zeroize();
86
87        let backup_file = backup_encrypted_file(&target_path)?;
88        match client.encrypt(&new_content, path_to_str(&target_path)?) {
89            Ok(_) => {
90                fs::remove_file(&backup_file)?;
91            }
92            Err(e) => {
93                restore_backup_file(&backup_file)?;
94                return Err(e);
95            }
96        }
97        println!("Edit password for {} in repo {} using {}.", target, root.display(), editor);
98        Ok(true)
99    } else {
100        Err(anyhow!("Failed to edit file"))
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use pretty_assertions::assert_ne;
107    use serial_test::serial;
108
109    use super::*;
110    use crate::pgp::key_management::key_gen_batch;
111    use crate::util::defer::cleanup;
112    use crate::util::test_util::{
113        clean_up_test_key, create_dir_structure, gen_unique_temp_dir, get_test_email,
114        get_test_executable, gpg_key_edit_example_batch, gpg_key_gen_example_batch, write_gpg_id,
115    };
116
117    #[test]
118    #[serial]
119    #[ignore = "need run interactively"]
120    fn basic() {
121        let executable = &get_test_executable();
122        let email = &get_test_email();
123
124        // structure:
125        // root
126        // ├── file1.gpg
127        // └── dir
128        //     └── file2.gpg
129        let (_tmp_dir, root) = gen_unique_temp_dir();
130        let structure: &[(Option<&str>, &[&str])] = &[(Some("dir"), &[][..])];
131        create_dir_structure(&root, structure);
132
133        let file1_content = "Sending in an eagle\n\n!!! You must edit this to pass the test !!!";
134        let file2_content = "Requesting orbital\n\n!!! Do not edit this to pass the test !!!";
135        cleanup!(
136            {
137                key_gen_batch(&get_test_executable(), &gpg_key_gen_example_batch()).unwrap();
138                let test_client = PGPClient::new(executable, &[email]).unwrap();
139                test_client.key_edit_batch(&gpg_key_edit_example_batch()).unwrap();
140                let new_dir = root.join("file1.gpg");
141                let output = path_to_str(&new_dir).unwrap();
142                println!("{}", output);
143                test_client.encrypt(file1_content, output).unwrap();
144                test_client
145                    .encrypt(
146                        file2_content,
147                        path_to_str(&root.join("dir").join("file2.gpg")).unwrap(),
148                    )
149                    .unwrap();
150                write_gpg_id(&root, &test_client.get_keys_fpr());
151                let res1 = edit(&root, "file1", "gpg", "vim", executable).unwrap();
152                let res2 = edit(&root, "dir/file2", "gpg", "vim", executable).unwrap();
153                assert!(res1);
154                assert!(!res2);
155
156                let file1_new_content = test_client.decrypt_stdin(&root, output).unwrap();
157                let file2_new_content = test_client.decrypt_stdin(&root, "dir/file2.gpg").unwrap();
158
159                assert_ne!(file1_new_content.expose_secret(), file1_content);
160                assert_eq!(file2_new_content.expose_secret(), file2_content);
161            },
162            {
163                clean_up_test_key(executable, &[email]).unwrap();
164            }
165        );
166    }
167}