Skip to content

Commit 1797803

Browse files
committed
calculate hash only once, improve performance
1 parent cd616e0 commit 1797803

File tree

6 files changed

+185
-49
lines changed

6 files changed

+185
-49
lines changed

Cargo.lock

Lines changed: 29 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "Encrypt/decrypt files in git repo using one password"
55
homepage = "https://github.com/lxl66566/git-simple-encrypt"
66
repository = "https://github.com/lxl66566/git-simple-encrypt"
77
license = "MIT"
8-
version = "1.1.0"
8+
version = "1.1.1"
99
edition = "2021"
1010
readme = "./README.md"
1111
categories = ["cryptography"]
@@ -44,6 +44,7 @@ openssl = { version = "0.10", features = ["vendored"] }
4444
libz-sys = { version = "1.1.16", features = ["static"] }
4545

4646
[dev-dependencies]
47+
rand = "0.8.5"
4748
temp_testdir = "0.2.3"
4849

4950
[[bin]]

src/crypt.rs

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ use anyhow::{anyhow, Context, Result};
1212
use colored::Colorize;
1313
use die_exit::die;
1414
use log::{debug, info};
15-
use sha3::{Digest, Sha3_224};
1615
use tap::Tap;
1716

1817
#[cfg(any(test, debug_assertions))]
@@ -27,22 +26,7 @@ pub static ENCRYPTED_EXTENSION: &str = "enc";
2726
pub static COMPRESSED_EXTENSION: &str = "zst";
2827

2928
pub fn encrypt(key: &[u8], text: &[u8]) -> std::result::Result<Vec<u8>, aes_gcm_siv::Error> {
30-
#[cfg(any(test, debug_assertions))]
31-
println!("Key: {}", String::from_utf8_lossy(key).green());
32-
let mut hasher = Sha3_224::default();
33-
hasher.update(key);
34-
let hash_result = hasher.finalize();
35-
let hash_result_slice = hash_result.as_slice();
36-
#[cfg(any(test, debug_assertions))]
37-
{
38-
println!("Hash result: {}", format_hex(hash_result_slice).green());
39-
println!(
40-
"Hash Cut result: {}",
41-
format_hex(&hash_result_slice[..16]).green()
42-
);
43-
}
44-
let cipher =
45-
Aes128GcmSiv::new_from_slice(&hash_result_slice[..16]).expect("cipher key length error.");
29+
let cipher = Aes128GcmSiv::new_from_slice(key).expect("cipher key length error.");
4630
let encrypted = cipher.encrypt(NONCE.deref(), text)?;
4731

4832
#[cfg(any(test, debug_assertions))]
@@ -52,10 +36,7 @@ pub fn encrypt(key: &[u8], text: &[u8]) -> std::result::Result<Vec<u8>, aes_gcm_
5236
}
5337

5438
pub fn decrypt(key: &[u8], text: &[u8]) -> std::result::Result<Vec<u8>, aes_gcm_siv::Error> {
55-
let mut hasher = Sha3_224::default();
56-
hasher.update(key);
57-
let cipher = Aes128GcmSiv::new_from_slice(&hasher.finalize().as_slice()[..16])
58-
.expect("cipher key length error.");
39+
let cipher = Aes128GcmSiv::new_from_slice(key).expect("cipher key length error.");
5940
let plaintext = cipher.decrypt(NONCE.deref(), text)?;
6041
Ok(plaintext)
6142
}
@@ -154,7 +135,7 @@ pub async fn encrypt_file(
154135

155136
let (encrypted, new_file) = tokio::task::spawn_blocking(move || {
156137
let (compressed, new_file) = try_compress(&bytes, new_file, repo.conf.zstd_level)?;
157-
encrypt_change_path(repo.get_key().as_bytes(), &compressed, new_file)
138+
encrypt_change_path(repo.get_key_sha(), &compressed, new_file)
158139
})
159140
.await??;
160141

@@ -179,8 +160,7 @@ pub async fn decrypt_file(
179160
.with_context(|| format!("{:?}", file.as_ref()))?;
180161

181162
let (decompressed, new_file) = tokio::task::spawn_blocking(move || {
182-
let (decrypted, new_file) =
183-
try_decrypt_change_path(repo.get_key().as_bytes(), &bytes, new_file)?;
163+
let (decrypted, new_file) = try_decrypt_change_path(repo.get_key_sha(), &bytes, new_file)?;
184164
try_decompress(&decrypted, new_file)
185165
})
186166
.await??;
@@ -253,7 +233,7 @@ mod test {
253233

254234
#[test]
255235
fn test_encrypt_decrypt() -> Result<()> {
256-
let key = b"123456";
236+
let key = b"602bdc204140db0a";
257237
let content = b"456789";
258238
let encrypted_content = encrypt(key, content).unwrap();
259239
assert_ne!(content.to_vec(), encrypted_content);

src/repo.rs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
use std::path::{Path, PathBuf};
1+
use std::{
2+
path::{Path, PathBuf},
3+
sync::OnceLock,
4+
};
25

36
use anyhow::{anyhow, Result};
47
use assert2::assert;
8+
#[cfg(any(test, debug_assertions))]
9+
use colored::Colorize;
510
use die_exit::Die;
611
use log::debug;
12+
use sha3::{Digest, Sha3_224};
713
use tap::Tap;
814

915
use crate::{
@@ -14,10 +20,11 @@ use crate::{
1420
pub const GIT_CONFIG_PREFIX: &str =
1521
const_str::replace!(concat!(env!("CARGO_CRATE_NAME"), "."), "_", "-");
1622

17-
#[derive(Debug, Clone)]
23+
#[derive(Debug, Clone, Default)]
1824
pub struct Repo {
1925
pub path: PathBuf,
2026
pub conf: Config,
27+
pub key_sha: OnceLock<Box<[u8]>>,
2128
}
2229

2330
impl Repo {
@@ -35,7 +42,11 @@ impl Repo {
3542
}
3643
println!("Open repo: {}", path.display());
3744
let conf = Config::load_or_create_from(path.join(CONFIG_FILE));
38-
Ok(Self { path, conf })
45+
Ok(Self {
46+
path,
47+
conf,
48+
key_sha: OnceLock::new(),
49+
})
3950
}
4051
pub fn path(&self) -> &Path {
4152
&self.path
@@ -60,6 +71,32 @@ impl Repo {
6071
self.get_config("key")
6172
.die("Key not found, please exec `git-se set key <KEY>` first.")
6273
}
74+
75+
/// returns the first 16 bytes of sha3-224 of the key.
76+
/// The sha result will only be calculated once in the lifetime of the
77+
/// object.
78+
pub fn get_key_sha(&self) -> &[u8] {
79+
self.key_sha.get_or_init(|| {
80+
let key = self.get_key();
81+
#[cfg(any(test, debug_assertions))]
82+
println!("Key: {}", key.green());
83+
let mut hasher = Sha3_224::default();
84+
hasher.update(key);
85+
let hash_result = hasher.finalize();
86+
let hash_result_slice = hash_result.as_slice();
87+
let hash_result_slice_cut = &hash_result_slice[..16];
88+
#[cfg(any(test, debug_assertions))]
89+
{
90+
use crate::utils::format_hex;
91+
println!("Hash result: {}", format_hex(hash_result_slice).green());
92+
println!(
93+
"Hash Cut result: {}",
94+
format_hex(hash_result_slice_cut).green()
95+
);
96+
}
97+
hash_result_slice_cut.into()
98+
})
99+
}
63100
}
64101

65102
pub trait GitCommand {

src/utils/pathutils.rs

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,16 @@ impl PathAppendExt for PathBuf {
2727
self.tap_mut(|p| p.as_mut_os_string().push(format!(".{ext}")))
2828
}
2929
}
30-
pub trait PathStripPrefix {
31-
fn strip_prefix_better(&mut self, prefix: impl AsRef<Path>) -> &mut Self;
32-
}
33-
impl PathStripPrefix for PathBuf {
34-
fn strip_prefix_better(&mut self, prefix: impl AsRef<Path>) -> &mut Self {
35-
if let Ok(res) = self.strip_prefix(prefix) {
36-
*self = res.to_path_buf();
37-
}
38-
self
39-
}
40-
}
4130
#[allow(unused)]
4231
pub trait PathToUnixStyle {
4332
fn to_unix_style(&self) -> PathBuf;
4433
}
4534
impl<T: AsRef<Path>> PathToUnixStyle for T {
4635
fn to_unix_style(&self) -> PathBuf {
47-
self.as_ref().to_string_lossy().replace('\\', "/").into()
36+
#[cfg(not(unix))]
37+
return self.as_ref().to_string_lossy().replace('\\', "/").into();
38+
#[cfg(unix)]
39+
return self.as_ref().to_path_buf();
4840
}
4941
}
5042

@@ -55,12 +47,22 @@ pub trait Git2Patch {
5547
}
5648
impl<T: AsRef<Path>> Git2Patch for T {
5749
fn patch(&self) -> PathBuf {
58-
self.as_ref().to_path_buf().tap_mut(|x| {
59-
#[cfg(target_family = "unix")]
60-
x.strip_prefix_better("./");
61-
#[cfg(target_family = "windows")]
62-
x.strip_prefix_better(".\\");
63-
})
50+
let path = self.as_ref().to_path_buf();
51+
52+
#[cfg(target_family = "unix")]
53+
let mut prefix = "./".to_string();
54+
#[cfg(target_family = "windows")]
55+
let mut prefix = ".\\".to_string();
56+
57+
let res = path.strip_prefix(&prefix);
58+
if let Ok(mut ok_stripped) = res {
59+
prefix.remove(0);
60+
while let Ok(stripped_again) = ok_stripped.strip_prefix(&prefix) {
61+
ok_stripped = stripped_again;
62+
}
63+
return ok_stripped.to_path_buf();
64+
}
65+
path
6466
}
6567
}
6668

@@ -77,3 +79,30 @@ impl<T: AsRef<Path>> PathToAbsolute for T {
7779
})
7880
}
7981
}
82+
83+
#[cfg(test)]
84+
mod tests {
85+
use super::*;
86+
87+
#[test]
88+
#[cfg(target_family = "unix")]
89+
fn test_patch() {
90+
let patched = Path::new("src/utils/pathutils.rs");
91+
assert_eq!(patched.patch(), patched);
92+
let p = Path::new("./src/utils/pathutils.rs");
93+
assert_eq!(p.patch(), patched);
94+
let p = Path::new(".////src/utils/pathutils.rs");
95+
assert_eq!(p.patch(), patched);
96+
}
97+
98+
#[test]
99+
#[cfg(target_family = "windows")]
100+
fn test_patch() {
101+
let patched = Path::new("src\\utils\\pathutils.rs");
102+
assert_eq!(patched.patch(), patched);
103+
let p = Path::new(".\\src\\utils\\pathutils.rs");
104+
assert_eq!(p.patch(), patched);
105+
let p = Path::new(".\\\\\\\\src\\utils\\pathutils.rs");
106+
assert_eq!(p.patch(), patched);
107+
}
108+
}

tests/intergration_test.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
#![crate_type = "proc-macro"]
22
extern crate proc_macro;
33

4-
use std::process::{Command, Output};
4+
use std::{
5+
path::PathBuf,
6+
process::{Command, Output},
7+
};
58

69
use anyhow::Ok;
710
use colored::Colorize;
811
use git_simple_encrypt::*;
12+
use rand::seq::SliceRandom;
13+
use tap::Tap;
914
use temp_testdir::TempDir;
1015

1116
#[tokio::test]
@@ -170,3 +175,59 @@ async fn test_reencrypt() -> anyhow::Result<()> {
170175

171176
Ok(())
172177
}
178+
179+
#[tokio::test]
180+
#[ignore = "This test takes too long to run, and it's not necessary to run it every time. You can run it manually if you want."]
181+
async fn test_many_files() -> anyhow::Result<()> {
182+
let _lock = TempDir::default();
183+
let temp_dir = &_lock;
184+
let exec = |cmd: &str| -> std::io::Result<Output> {
185+
let mut temp = cmd.split_whitespace();
186+
let mut command = Command::new(temp.next().unwrap());
187+
command.args(temp).current_dir(temp_dir).output()
188+
};
189+
macro_rules! run {
190+
($cmd:expr) => {
191+
run(&Cli {
192+
command: $cmd,
193+
repo: temp_dir.to_path_buf(),
194+
})
195+
.await?;
196+
};
197+
}
198+
199+
exec("git init")?;
200+
let dir = temp_dir.join("dir");
201+
std::fs::create_dir(&dir)?;
202+
let files = (1..2000)
203+
.map(|i| {
204+
dir.join(format!("file{}.txt", i))
205+
.tap(|f| std::fs::write(f, "Hello").unwrap())
206+
})
207+
.collect::<Vec<PathBuf>>();
208+
209+
// Set key
210+
run!(SubCommand::Set {
211+
field: SetField::key,
212+
value: "123".to_owned(),
213+
});
214+
215+
// Add file
216+
run!(SubCommand::Add {
217+
path: vec!["dir".into()]
218+
});
219+
220+
// Encrypt
221+
run!(SubCommand::Encrypt);
222+
// Decrypt
223+
run!(SubCommand::Decrypt);
224+
225+
// Test
226+
for _ in 1..10 {
227+
let file_name = files.choose(&mut rand::thread_rng()).unwrap();
228+
println!("Testing file: {}", file_name.display());
229+
assert_eq!(std::fs::read_to_string(file_name)?, "Hello");
230+
}
231+
232+
Ok(())
233+
}

0 commit comments

Comments
 (0)