keyhog_core/
state_file.rs1use fs2::FileExt;
5use std::ffi::OsString;
6use std::fs::{File, OpenOptions};
7use std::io::Read;
8use std::path::{Path, PathBuf};
9
10pub struct StateFileWriteLock {
17 file: File,
18}
19
20impl StateFileWriteLock {
21 pub fn acquire(state_path: &Path) -> std::io::Result<Self> {
23 let lock_path = state_file_lock_path(state_path)?;
24 let parent = lock_path.parent().ok_or_else(|| {
25 std::io::Error::new(
26 std::io::ErrorKind::InvalidInput,
27 "state-file lock path has no parent directory",
28 )
29 })?;
30 std::fs::create_dir_all(parent)?;
31 let mut options = OpenOptions::new();
32 options.create(true).read(true).write(true);
33 #[cfg(unix)]
34 {
35 use std::os::unix::fs::OpenOptionsExt;
36 options.mode(0o600);
37 }
38 let file = options.open(lock_path)?;
39 file.lock_exclusive()?;
40 Ok(Self { file })
41 }
42}
43
44impl Drop for StateFileWriteLock {
45 fn drop(&mut self) {
46 if let Err(error) = FileExt::unlock(&self.file) {
47 tracing::warn!(%error, "failed to unlock KeyHog state file; closing the lock file will release the OS lock");
48 }
49 }
50}
51
52pub fn state_file_lock_path(state_path: &Path) -> std::io::Result<PathBuf> {
54 let Some(base_name) = state_path.file_name() else {
55 return Err(std::io::Error::new(
56 std::io::ErrorKind::InvalidInput,
57 format!("state path '{}' has no file name", state_path.display()),
58 ));
59 };
60 let mut file_name = OsString::from(base_name);
61 file_name.push(".lock");
62 Ok(state_path.with_file_name(file_name))
63}
64
65pub(crate) const CALIBRATION_CACHE_FILE_BYTES: u64 = 16 * 1024 * 1024;
70
71pub(crate) const RULE_CONFIG_FILE_BYTES: u64 = 16 * 1024 * 1024;
76
77pub(crate) const MERKLE_INDEX_CACHE_FILE_BYTES: u64 = 512 * 1024 * 1024;
83
84pub(crate) fn read_capped(path: &Path, cap: u64, kind: &str) -> std::io::Result<Vec<u8>> {
86 let file = std::fs::File::open(path)?;
87 let len = file.metadata()?.len();
88 if len > cap {
89 return Err(std::io::Error::new(
90 std::io::ErrorKind::InvalidData,
91 format!(
92 "{kind} {} exceeds {cap} byte cap; delete the cache file and rerun",
93 path.display()
94 ),
95 ));
96 }
97
98 let mut data = Vec::with_capacity(len as usize);
99 file.take(cap.saturating_add(1)).read_to_end(&mut data)?;
100 if data.len() as u64 > cap {
101 return Err(std::io::Error::new(
102 std::io::ErrorKind::InvalidData,
103 format!(
104 "{kind} {} grew past {cap} byte cap while reading; retry after the file is stable",
105 path.display()
106 ),
107 ));
108 }
109 Ok(data)
110}
111
112pub(crate) fn write_atomically(path: &Path, prefix: &str, bytes: &[u8]) -> std::io::Result<()> {
121 let parent = match path.parent().filter(|p| !p.as_os_str().is_empty()) {
122 Some(parent) => parent,
123 None => Path::new("."),
124 };
125 std::fs::create_dir_all(parent)?;
126 let mut tmp = tempfile::Builder::new()
127 .prefix(prefix)
128 .tempfile_in(parent)?;
129 std::io::Write::write_all(&mut tmp, bytes)?;
130 tmp.as_file().sync_all()?;
131 tmp.persist(path).map_err(|e| e.error)?;
132 Ok(())
133}
134
135pub(crate) fn sweep_stale_tmp_siblings(
144 cache_path: &Path,
145 prefixes: &[&str],
146 cutoff_secs: u64,
147) -> usize {
148 let Some(parent) = cache_path.parent() else {
149 return 0;
150 };
151 let Ok(entries) = std::fs::read_dir(parent) else {
152 return 0;
153 };
154 let now = std::time::SystemTime::now();
155 let mut swept = 0usize;
156 for entry in entries {
157 let entry = match entry {
158 Ok(entry) => entry,
159 Err(error) => {
162 tracing::warn!(dir = %parent.display(), %error, "skip unreadable tmp dir entry during stale-state sweep");
163 continue;
164 }
165 };
166 let name = entry.file_name();
167 let Some(name_str) = name.to_str() else {
168 continue;
169 };
170 if !prefixes.iter().any(|p| name_str.starts_with(p)) {
171 continue;
172 }
173 let path = entry.path();
174 if path == cache_path {
175 continue;
176 }
177 let Ok(meta) = path.metadata() else {
178 continue;
179 };
180 let Ok(modified) = meta.modified() else {
181 continue;
182 };
183 let Ok(age) = now.duration_since(modified) else {
185 continue;
186 };
187 if age.as_secs() < cutoff_secs {
188 continue;
189 }
190 if std::fs::remove_file(&path).is_ok() {
191 swept += 1;
192 }
193 }
194 swept
195}