use crate::crypto::{ENCRYPTED_MAGIC, decrypt_notes, encrypt_notes, is_encrypted_data};
use crate::model::Note;
use once_cell::sync::Lazy;
use std::fs;
use std::io::{BufRead, BufReader, Read, Write};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use zeroize::Zeroizing;
static NOTES_PATH_OVERRIDE: Lazy<Mutex<Option<PathBuf>>> = Lazy::new(|| Mutex::new(None));
static ACTIVE_PASSWORD: Lazy<Mutex<Zeroizing<String>>> =
Lazy::new(|| Mutex::new(Zeroizing::new(String::new())));
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
m.lock().expect("lock poisoned")
}
pub fn set_notes_path_override(path: Option<PathBuf>) {
*lock(&NOTES_PATH_OVERRIDE) = path;
}
pub fn set_active_password(password: String) {
*lock(&ACTIVE_PASSWORD) = Zeroizing::new(password);
}
pub(crate) fn active_password_zeroized() -> Zeroizing<String> {
lock(&ACTIVE_PASSWORD).clone()
}
#[deprecated(
since = "1.3.0",
note = "leaks password to non-zeroized memory; use has_active_password() instead"
)]
pub fn active_password() -> String {
String::clone(&lock(&ACTIVE_PASSWORD))
}
pub fn has_active_password() -> bool {
!lock(&ACTIVE_PASSWORD).is_empty()
}
pub fn notes_path() -> PathBuf {
if let Some(p) = lock(&NOTES_PATH_OVERRIDE).clone() {
return p;
}
let data_dir = if cfg!(target_os = "windows") {
std::env::var("APPDATA").unwrap_or_default()
} else if cfg!(target_os = "macos") {
let home = std::env::var("HOME").unwrap_or_default();
Path::new(&home)
.join("Library")
.join("Application Support")
.to_string_lossy()
.into_owned()
} else {
let xdg = std::env::var("XDG_DATA_HOME").unwrap_or_default();
if !xdg.is_empty() {
xdg
} else {
let home = std::env::var("HOME").unwrap_or_default();
Path::new(&home)
.join(".local")
.join("share")
.to_string_lossy()
.into_owned()
}
};
let base = if data_dir.is_empty() {
eprintln!("warning: HOME not set, using current directory for notes");
PathBuf::from(".")
} else {
PathBuf::from(data_dir)
};
base.join("scriv").join("notes.json")
}
pub fn notes_file_is_encrypted() -> bool {
let path = notes_path();
let file = fs::File::open(path);
let mut file = match file {
Ok(f) => f,
Err(_) => return false,
};
let mut header = [0_u8; ENCRYPTED_MAGIC.len()];
match file.read_exact(&mut header) {
Ok(()) => header == *ENCRYPTED_MAGIC,
Err(_) => false,
}
}
pub fn load_notes() -> Result<Vec<Note>, String> {
let path = notes_path();
let raw = match fs::read(&path) {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(format!("cannot read from {}: {}", path.display(), e)),
};
let decrypted: Zeroizing<Vec<u8>>;
let buf: &[u8] = if is_encrypted_data(&raw) {
decrypted = Zeroizing::new(decrypt_notes(&raw, &active_password_zeroized())?);
&decrypted
} else {
&raw
};
let reader = BufReader::new(buf);
let mut notes = Vec::new();
for line in reader.lines() {
let line = line.map_err(|e| format!("cannot read from {}: {}", path.display(), e))?;
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let note: Note = serde_json::from_str(trimmed).map_err(|_| {
"notes file is corrupted. Run 'scriv clear --force' to reset.".to_string()
})?;
notes.push(note);
}
Ok(notes)
}
pub fn save_notes(notes: &[Note]) -> Result<(), String> {
let path = notes_path();
let dir = path
.parent()
.ok_or_else(|| format!("cannot write to {}", path.display()))?
.to_path_buf();
fs::create_dir_all(&dir).map_err(|e| format!("cannot write to {}: {}", dir.display(), e))?;
let mut ndjson = Zeroizing::new(Vec::new());
for note in notes {
let line = serde_json::to_string(note).map_err(|e| e.to_string())?;
ndjson.extend_from_slice(line.as_bytes());
ndjson.push(b'\n');
}
let pw = active_password_zeroized();
let payload = if pw.is_empty() {
(*ndjson).clone()
} else {
encrypt_notes(&ndjson, &pw).map_err(|e| format!("cannot encrypt notes: {}", e))?
};
let mut builder = tempfile::Builder::new();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
builder.permissions(std::fs::Permissions::from_mode(0o600));
}
let mut tmp = builder
.tempfile_in(&dir)
.map_err(|e| format!("cannot write to {}: {}", dir.display(), e))?;
tmp.write_all(&payload)
.map_err(|e| format!("cannot write to {}: {}", path.display(), e))?;
tmp.persist(&path)
.map_err(|e| format!("cannot write to {}: {}", path.display(), e.error))?;
Ok(())
}