use std::fs::File;
use std::io;
use std::path::Path;
use anyhow::{Context, Result};
use crate::cli::i18n;
use crate::hooks::executor::HookExecutor;
use crate::models::configuration::{Configuration, StoreRegistration, encrypt_store};
use crate::models::password_store::PasswordStore;
fn acquire_store_lock(path: &Path) -> Result<Option<File>> {
let path_display = path.display().to_string();
match File::open(path) {
Ok(file) => {
file.lock()
.with_context(|| i18n::error_could_not_acquire_store_lock(&path_display))?;
Ok(Some(file))
}
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => {
Err(error).with_context(|| i18n::error_could_not_open_store_for_lock(&path_display))
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum StoreMutation {
Unchanged,
Modified,
}
pub(super) fn with_store<F, T>(
configuration: &Configuration,
store_name: Option<&String>,
offline: bool,
f: F,
) -> Result<T>
where
F: FnOnce(&StoreRegistration, &mut PasswordStore) -> Result<(T, StoreMutation)>,
{
with_store_then(configuration, store_name, offline, f, |_| Ok(()))
}
pub(super) fn with_store_then<F, P, T>(
configuration: &Configuration,
store_name: Option<&String>,
offline: bool,
f: F,
then: P,
) -> Result<T>
where
F: FnOnce(&StoreRegistration, &mut PasswordStore) -> Result<(T, StoreMutation)>,
P: FnOnce(&T) -> Result<()>,
{
let registration = configuration
.select_store(store_name)
.context(i18n::error_no_store_in_configuration())?;
let hooks = HookExecutor {
configuration,
registration,
offline,
force: false,
};
hooks.execute_pull_commands()?;
let lock = acquire_store_lock(registration.path())?;
let mut store = configuration.decrypt_store(registration)?;
let (value, mutation) = f(registration, &mut store)?;
if matches!(mutation, StoreMutation::Modified) {
encrypt_store(registration, &store).context(i18n::error_cannot_encrypt_store())?;
}
drop(lock);
then(&value)?;
if matches!(mutation, StoreMutation::Modified) {
hooks.execute_push_commands()?;
}
Ok(value)
}
#[cfg(test)]
mod tests {
use std::sync::mpsc;
use std::time::Duration;
use assert_fs::TempDir;
use assert_fs::prelude::*;
use super::*;
#[test]
fn acquire_store_lock_returns_some_for_existing_file() {
let temp = TempDir::new().unwrap();
let file = temp.child("store");
file.write_str("payload").unwrap();
let lock = acquire_store_lock(file.path()).unwrap();
assert!(lock.is_some(), "existing file should yield a held lock");
drop(lock);
}
#[test]
fn acquire_store_lock_returns_none_for_missing_file() {
let temp = TempDir::new().unwrap();
let missing = temp.child("never-existed");
let lock = acquire_store_lock(missing.path()).unwrap();
assert!(lock.is_none());
}
#[test]
fn acquire_store_lock_is_reacquirable_after_drop() {
let temp = TempDir::new().unwrap();
let file = temp.child("store");
file.write_str("payload").unwrap();
let first = acquire_store_lock(file.path()).unwrap();
assert!(first.is_some());
drop(first);
let second = acquire_store_lock(file.path()).unwrap();
assert!(second.is_some());
}
#[test]
fn acquire_store_lock_blocks_concurrent_acquisition_until_first_drops() {
let temp = TempDir::new().unwrap();
let file = temp.child("store");
file.write_str("payload").unwrap();
let path = file.path().to_path_buf();
let guard = acquire_store_lock(&path).unwrap();
assert!(guard.is_some());
let (tx, rx) = mpsc::channel();
let worker_path = path.clone();
let worker = std::thread::spawn(move || {
let second = acquire_store_lock(&worker_path).unwrap();
assert!(second.is_some());
tx.send(()).unwrap();
drop(second);
});
assert!(
rx.recv_timeout(Duration::from_millis(150)).is_err(),
"second acquisition completed while the first guard was still held"
);
drop(guard);
rx.recv_timeout(Duration::from_secs(2))
.expect("second acquisition should succeed within 2s of first being released");
worker.join().unwrap();
}
}