1use crate::crypto::{ENCRYPTED_MAGIC, decrypt_notes, encrypt_notes, is_encrypted_data};
4use crate::model::Note;
5use once_cell::sync::Lazy;
6use std::fs;
7use std::io::{BufRead, BufReader, Read, Write};
8use std::path::{Path, PathBuf};
9use std::sync::Mutex;
10use zeroize::Zeroizing;
11
12static NOTES_PATH_OVERRIDE: Lazy<Mutex<Option<PathBuf>>> = Lazy::new(|| Mutex::new(None));
13static ACTIVE_PASSWORD: Lazy<Mutex<Zeroizing<String>>> =
14 Lazy::new(|| Mutex::new(Zeroizing::new(String::new())));
15
16pub fn set_notes_path_override(path: Option<PathBuf>) {
18 let mut guard = NOTES_PATH_OVERRIDE
19 .lock()
20 .expect("notes path override lock poisoned");
21 *guard = path;
22}
23
24pub fn set_active_password(password: String) {
26 let mut guard = ACTIVE_PASSWORD
27 .lock()
28 .expect("active password lock poisoned");
29 *guard = Zeroizing::new(password);
30}
31
32pub(crate) fn active_password_zeroized() -> Zeroizing<String> {
34 let guard = ACTIVE_PASSWORD
35 .lock()
36 .expect("active password lock poisoned");
37 guard.clone()
38}
39
40pub fn active_password() -> String {
42 let guard = ACTIVE_PASSWORD
43 .lock()
44 .expect("active password lock poisoned");
45 String::clone(&guard)
46}
47
48pub fn notes_path() -> PathBuf {
50 if let Some(p) = NOTES_PATH_OVERRIDE
51 .lock()
52 .expect("notes path override lock poisoned")
53 .clone()
54 {
55 return p;
56 }
57
58 let data_dir = if cfg!(target_os = "windows") {
59 std::env::var("APPDATA").unwrap_or_default()
60 } else if cfg!(target_os = "macos") {
61 let home = std::env::var("HOME").unwrap_or_default();
62 Path::new(&home)
63 .join("Library")
64 .join("Application Support")
65 .to_string_lossy()
66 .into_owned()
67 } else {
68 let xdg = std::env::var("XDG_DATA_HOME").unwrap_or_default();
69 if !xdg.is_empty() {
70 xdg
71 } else {
72 let home = std::env::var("HOME").unwrap_or_default();
73 Path::new(&home)
74 .join(".local")
75 .join("share")
76 .to_string_lossy()
77 .into_owned()
78 }
79 };
80
81 let base = if data_dir.is_empty() {
82 PathBuf::from(".")
83 } else {
84 PathBuf::from(data_dir)
85 };
86
87 base.join("scriv").join("notes.json")
88}
89
90pub fn notes_file_is_encrypted() -> bool {
92 let path = notes_path();
93 let file = fs::File::open(path);
94 let mut file = match file {
95 Ok(f) => f,
96 Err(_) => return false,
97 };
98
99 let mut header = [0_u8; ENCRYPTED_MAGIC.len()];
100 match file.read_exact(&mut header) {
101 Ok(()) => header == *ENCRYPTED_MAGIC,
102 Err(_) => false,
103 }
104}
105
106pub fn load_notes() -> Result<Vec<Note>, String> {
108 let path = notes_path();
109 let mut data = match fs::read(&path) {
110 Ok(b) => b,
111 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
112 Err(e) => return Err(format!("cannot read from {}: {}", path.display(), e)),
113 };
114
115 if is_encrypted_data(&data) {
116 data = decrypt_notes(&data, &active_password_zeroized())?;
117 }
118
119 let reader = BufReader::new(data.as_slice());
120 let mut notes = Vec::new();
121
122 for line in reader.lines() {
123 let line = line.map_err(|e| format!("cannot read from {}: {}", path.display(), e))?;
124 let trimmed = line.trim();
125 if trimmed.is_empty() {
126 continue;
127 }
128 let note: Note = serde_json::from_str(trimmed).map_err(|_| {
129 "notes file is corrupted. Run 'scriv clear --force' to reset.".to_string()
130 })?;
131 notes.push(note);
132 }
133
134 Ok(notes)
135}
136
137pub fn save_notes(notes: &[Note]) -> Result<(), String> {
139 let path = notes_path();
140 let dir = path
141 .parent()
142 .ok_or_else(|| format!("cannot write to {}", path.display()))?
143 .to_path_buf();
144
145 fs::create_dir_all(&dir).map_err(|e| format!("cannot write to {}: {}", dir.display(), e))?;
146
147 let mut ndjson = Vec::new();
148 for note in notes {
149 let line = serde_json::to_string(note).map_err(|e| e.to_string())?;
150 ndjson.extend_from_slice(line.as_bytes());
151 ndjson.push(b'\n');
152 }
153
154 let pw = active_password_zeroized();
155 let payload = if pw.is_empty() {
156 ndjson
157 } else {
158 encrypt_notes(&ndjson, &pw).map_err(|e| format!("cannot encrypt notes: {}", e))?
159 };
160
161 let mut tmp = tempfile::NamedTempFile::new_in(&dir)
162 .map_err(|e| format!("cannot write to {}: {}", dir.display(), e))?;
163
164 #[cfg(unix)]
165 {
166 use std::os::unix::fs::PermissionsExt;
167 let perms = std::fs::Permissions::from_mode(0o600);
168 tmp.as_file()
169 .set_permissions(perms)
170 .map_err(|e| format!("cannot set permissions on {}: {}", path.display(), e))?;
171 }
172
173 tmp.write_all(&payload)
174 .map_err(|e| format!("cannot write to {}: {}", path.display(), e))?;
175 tmp.persist(&path)
176 .map_err(|e| format!("cannot write to {}: {}", path.display(), e.error))?;
177
178 Ok(())
179}