1use std::fs::File;
21use std::io;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum LockMode {
27 Shared,
30 Exclusive,
33}
34
35#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
37pub enum FileLocking {
38 #[default]
41 Enabled,
42 Disabled,
44 BestEffort,
48}
49
50impl FileLocking {
51 pub fn from_env() -> Self {
54 Self::parse_env_value(std::env::var("HDF5_USE_FILE_LOCKING").ok().as_deref())
55 }
56
57 pub fn from_env_or(default: FileLocking) -> Self {
59 match std::env::var("HDF5_USE_FILE_LOCKING").ok().as_deref() {
60 None => default,
61 Some(v) => Self::parse_env_value(Some(v)),
62 }
63 }
64
65 pub(crate) fn parse_env_value(value: Option<&str>) -> Self {
66 match value {
67 None => FileLocking::Enabled,
68 Some(v) => {
69 let trimmed = v.trim();
70 if trimmed.eq_ignore_ascii_case("FALSE")
71 || trimmed == "0"
72 || trimmed.eq_ignore_ascii_case("OFF")
73 || trimmed.eq_ignore_ascii_case("NO")
74 {
75 FileLocking::Disabled
76 } else if trimmed.eq_ignore_ascii_case("BEST_EFFORT")
77 || trimmed.eq_ignore_ascii_case("BEST-EFFORT")
78 || trimmed.eq_ignore_ascii_case("BESTEFFORT")
79 {
80 FileLocking::BestEffort
81 } else {
82 FileLocking::Enabled
84 }
85 }
86 }
87 }
88}
89
90pub fn try_acquire(file: &File, mode: LockMode, policy: FileLocking) -> io::Result<bool> {
97 if matches!(policy, FileLocking::Disabled) {
98 return Ok(false);
99 }
100
101 let attempt = match mode {
102 LockMode::Shared => file.try_lock_shared(),
103 LockMode::Exclusive => file.try_lock(),
104 };
105
106 match attempt {
107 Ok(()) => Ok(true),
108 Err(std::fs::TryLockError::WouldBlock) => match policy {
109 FileLocking::Enabled => Err(io::Error::new(
110 io::ErrorKind::WouldBlock,
111 "unable to lock file: another process holds a conflicting lock",
112 )),
113 FileLocking::BestEffort => Ok(false),
114 FileLocking::Disabled => unreachable!(),
115 },
116 Err(std::fs::TryLockError::Error(e)) => match policy {
117 FileLocking::Enabled => Err(e),
118 FileLocking::BestEffort => Ok(false),
119 FileLocking::Disabled => unreachable!(),
120 },
121 }
122}
123
124pub fn release(file: &File) -> io::Result<()> {
128 file.unlock()
129}
130
131pub fn downgrade_to_shared(file: &File, policy: FileLocking) -> io::Result<bool> {
137 if matches!(policy, FileLocking::Disabled) {
138 return Ok(false);
139 }
140 file.unlock()?;
141 try_acquire(file, LockMode::Shared, policy)
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn parse_env_value_defaults_to_enabled() {
150 assert_eq!(FileLocking::parse_env_value(None), FileLocking::Enabled);
151 }
152
153 #[test]
154 fn parse_env_value_recognizes_disabled() {
155 for v in ["FALSE", "false", "0", "off", "no"] {
156 assert_eq!(
157 FileLocking::parse_env_value(Some(v)),
158 FileLocking::Disabled,
159 "value: {v}",
160 );
161 }
162 }
163
164 #[test]
165 fn parse_env_value_recognizes_best_effort() {
166 for v in ["BEST_EFFORT", "best_effort", "best-effort", "BestEffort"] {
167 assert_eq!(
168 FileLocking::parse_env_value(Some(v)),
169 FileLocking::BestEffort,
170 "value: {v}",
171 );
172 }
173 }
174
175 #[test]
176 fn parse_env_value_recognizes_enabled() {
177 for v in ["TRUE", "true", "1", "on", "yes", "garbage"] {
178 assert_eq!(
179 FileLocking::parse_env_value(Some(v)),
180 FileLocking::Enabled,
181 "value: {v}",
182 );
183 }
184 }
185
186 #[test]
187 fn try_acquire_disabled_is_noop() {
188 let dir = std::env::temp_dir().join(format!(
189 "rust_hdf5_lock_disabled_{}",
190 std::process::id()
191 ));
192 std::fs::create_dir_all(&dir).unwrap();
193 let path = dir.join("noop.bin");
194 let f = std::fs::File::create(&path).unwrap();
195 let acquired = try_acquire(&f, LockMode::Exclusive, FileLocking::Disabled).unwrap();
196 assert!(!acquired, "Disabled policy must not acquire a lock");
197 let _ = std::fs::remove_dir_all(&dir);
198 }
199
200 #[test]
201 fn try_acquire_exclusive_then_shared_fails() {
202 let dir = std::env::temp_dir().join(format!(
203 "rust_hdf5_lock_excl_{}",
204 std::process::id()
205 ));
206 std::fs::create_dir_all(&dir).unwrap();
207 let path = dir.join("conflict.bin");
208 let f1 = std::fs::File::create(&path).unwrap();
209 assert!(try_acquire(&f1, LockMode::Exclusive, FileLocking::Enabled).unwrap());
210 let f2 = std::fs::OpenOptions::new()
211 .read(true)
212 .open(&path)
213 .unwrap();
214 let res = try_acquire(&f2, LockMode::Shared, FileLocking::Enabled);
215 assert!(res.is_err(), "expected lock conflict");
216 let res2 = try_acquire(&f2, LockMode::Shared, FileLocking::BestEffort).unwrap();
218 assert!(!res2, "best-effort must report unsuccessful lock as false");
219 release(&f1).unwrap();
220 let _ = std::fs::remove_dir_all(&dir);
221 }
222
223 #[test]
224 fn shared_locks_coexist() {
225 let dir = std::env::temp_dir().join(format!(
226 "rust_hdf5_lock_shared_{}",
227 std::process::id()
228 ));
229 std::fs::create_dir_all(&dir).unwrap();
230 let path = dir.join("shared.bin");
231 std::fs::File::create(&path).unwrap();
232 let f1 = std::fs::OpenOptions::new()
233 .read(true)
234 .open(&path)
235 .unwrap();
236 let f2 = std::fs::OpenOptions::new()
237 .read(true)
238 .open(&path)
239 .unwrap();
240 assert!(try_acquire(&f1, LockMode::Shared, FileLocking::Enabled).unwrap());
241 assert!(try_acquire(&f2, LockMode::Shared, FileLocking::Enabled).unwrap());
242 release(&f1).unwrap();
243 release(&f2).unwrap();
244 let _ = std::fs::remove_dir_all(&dir);
245 }
246
247 #[test]
248 fn downgrade_releases_exclusive() {
249 let dir = std::env::temp_dir().join(format!(
250 "rust_hdf5_lock_downgrade_{}",
251 std::process::id()
252 ));
253 std::fs::create_dir_all(&dir).unwrap();
254 let path = dir.join("downgrade.bin");
255 let f1 = std::fs::File::create(&path).unwrap();
256 assert!(try_acquire(&f1, LockMode::Exclusive, FileLocking::Enabled).unwrap());
257 downgrade_to_shared(&f1, FileLocking::Enabled).unwrap();
258
259 let f2 = std::fs::OpenOptions::new()
261 .read(true)
262 .open(&path)
263 .unwrap();
264 assert!(try_acquire(&f2, LockMode::Shared, FileLocking::Enabled).unwrap());
265 let f3 = std::fs::OpenOptions::new()
267 .read(true)
268 .write(true)
269 .open(&path)
270 .unwrap();
271 assert!(try_acquire(&f3, LockMode::Exclusive, FileLocking::Enabled).is_err());
272
273 release(&f1).unwrap();
274 release(&f2).unwrap();
275 let _ = std::fs::remove_dir_all(&dir);
276 }
277}