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 editor_args = [path_to_str(&temp_filepath)?];
77    let mut cmd = Command::new(editor).args(&editor_args).spawn()?;
78    let status = cmd.wait()?;
79    if status.success() {
80        let new_content = fs::read_to_string(&temp_filepath)?;
81        let mut old_content = client.decrypt_stdin(root, path_to_str(&target_path)?)?;
82        if old_content.expose_secret() == new_content {
83            println!("Password unchanged");
84            return Ok(false);
85        }
86        old_content.zeroize();
87
88        let backup_file = backup_encrypted_file(&target_path)?;
89        match client.encrypt(&new_content, path_to_str(&target_path)?) {
90            Ok(_) => {
91                fs::remove_file(&backup_file)?;
92            }
93            Err(e) => {
94                restore_backup_file(&backup_file)?;
95                return Err(e);
96            }
97        }
98        println!("Edit password for {} in repo {} using {}.", target, root.display(), editor);
99        Ok(true)
100    } else {
101        Err(anyhow!("Failed to edit file"))
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use pretty_assertions::assert_ne;
108    use serial_test::serial;
109
110    use super::*;
111    use crate::pgp::key_management::key_gen_batch;
112    use crate::util::defer::cleanup;
113    use crate::util::test_util::{
114        clean_up_test_key, create_dir_structure, gen_unique_temp_dir, get_test_email,
115        get_test_executable, gpg_key_edit_example_batch, gpg_key_gen_example_batch, write_gpg_id,
116    };
117
118    fn create_fake_editor(root: &Path) -> PathBuf {
119        #[cfg(unix)]
120        let fake_editor_content = r#"#!/bin/bash
121file="$1"
122sed -i '1d' "$file"
123"#;
124
125        #[cfg(windows)]
126        let fake_editor_content = r#"@echo off
127set file=%1
128powershell -Command "(Get-Content %file% | Select-Object -Skip 1) | Set-Content %file%"
129"#;
130
131        let fake_editor_path =
132            root.join(if cfg!(unix) { "fake_editor.sh" } else { "fake_editor.bat" });
133        fs::write(&fake_editor_path, fake_editor_content).expect("Failed to write fake editor");
134
135        #[cfg(unix)]
136        {
137            use std::os::unix::fs::PermissionsExt;
138            let mut perms = fs::metadata(&fake_editor_path).unwrap().permissions();
139            perms.set_mode(0o755);
140            fs::set_permissions(&fake_editor_path, perms).unwrap();
141        }
142
143        fake_editor_path
144    }
145
146    #[test]
147    #[serial]
148    #[ignore = "need run interactively"]
149    fn basic() {
150        let executable = &get_test_executable();
151        let email = &get_test_email();
152
153        // structure:
154        // root
155        // ├── file1.gpg
156        // └── dir
157        //     └── file2.gpg
158        let (_tmp_dir, root) = gen_unique_temp_dir();
159        let structure: &[(Option<&str>, &[&str])] = &[(Some("dir"), &[][..])];
160        create_dir_structure(&root, structure);
161
162        let file1_content = "Sending in an eagle\n\n!!! You must edit this to pass the test !!!";
163        let file2_content = "Requesting orbital\n\n!!! Do not edit this to pass the test !!!";
164        cleanup!(
165            {
166                key_gen_batch(&get_test_executable(), &gpg_key_gen_example_batch()).unwrap();
167                let test_client = PGPClient::new(executable, &[email]).unwrap();
168                test_client.key_edit_batch(&gpg_key_edit_example_batch()).unwrap();
169                let new_dir = root.join("file1.gpg");
170                let output = path_to_str(&new_dir).unwrap();
171                println!("{}", output);
172                test_client.encrypt(file1_content, output).unwrap();
173                test_client
174                    .encrypt(
175                        file2_content,
176                        path_to_str(&root.join("dir").join("file2.gpg")).unwrap(),
177                    )
178                    .unwrap();
179                write_gpg_id(&root, &test_client.get_keys_fpr());
180
181                let fake_editor = create_fake_editor(&root);
182                let res1 =
183                    edit(&root, "file1", "gpg", path_to_str(&fake_editor).unwrap(), executable)
184                        .unwrap();
185                let res2 = edit(&root, "dir/file2", "gpg", "cat", executable).unwrap();
186                assert!(res1);
187                assert!(!res2);
188
189                let file1_new_content = test_client.decrypt_stdin(&root, output).unwrap();
190                let file2_new_content = test_client.decrypt_stdin(&root, "dir/file2.gpg").unwrap();
191
192                assert_ne!(file1_new_content.expose_secret(), file1_content);
193                assert_eq!(file2_new_content.expose_secret(), file2_content);
194            },
195            {
196                clean_up_test_key(executable, &[email]).unwrap();
197            }
198        );
199    }
200}