pars_core/operation/
edit.rs1use 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 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 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}