1use std::fs::File;
26use std::io;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum LockMode {
32 Shared,
35 Exclusive,
38}
39
40#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
58pub enum FileLocking {
59 #[default]
62 Enabled,
63 Disabled,
66 BestEffort,
72}
73
74impl FileLocking {
75 pub fn from_env() -> Self {
78 Self::parse_env_value(std::env::var("HDF5_USE_FILE_LOCKING").ok().as_deref())
79 }
80
81 pub fn from_env_or(default: FileLocking) -> Self {
83 match std::env::var("HDF5_USE_FILE_LOCKING").ok().as_deref() {
84 None => default,
85 Some(v) => Self::parse_env_value(Some(v)),
86 }
87 }
88
89 pub(crate) fn parse_env_value(value: Option<&str>) -> Self {
90 match value {
91 None => FileLocking::Enabled,
92 Some(v) => {
93 let trimmed = v.trim();
94 if trimmed.eq_ignore_ascii_case("FALSE")
95 || trimmed == "0"
96 || trimmed.eq_ignore_ascii_case("OFF")
97 || trimmed.eq_ignore_ascii_case("NO")
98 {
99 FileLocking::Disabled
100 } else if trimmed.eq_ignore_ascii_case("BEST_EFFORT")
101 || trimmed.eq_ignore_ascii_case("BEST-EFFORT")
102 || trimmed.eq_ignore_ascii_case("BESTEFFORT")
103 {
104 FileLocking::BestEffort
105 } else {
106 FileLocking::Enabled
108 }
109 }
110 }
111 }
112}
113
114pub fn try_acquire(file: &File, mode: LockMode, policy: FileLocking) -> io::Result<bool> {
121 if matches!(policy, FileLocking::Disabled) {
122 return Ok(false);
123 }
124
125 let attempt = match mode {
126 LockMode::Shared => file.try_lock_shared(),
127 LockMode::Exclusive => file.try_lock(),
128 };
129
130 match attempt {
131 Ok(()) => Ok(true),
132 Err(std::fs::TryLockError::WouldBlock) => match policy {
133 FileLocking::Enabled => Err(io::Error::new(
134 io::ErrorKind::WouldBlock,
135 "unable to lock file: another process holds a conflicting lock",
136 )),
137 FileLocking::BestEffort => Ok(false),
138 FileLocking::Disabled => unreachable!(),
139 },
140 Err(std::fs::TryLockError::Error(e)) => match policy {
141 FileLocking::Enabled => Err(e),
142 FileLocking::BestEffort => Ok(false),
143 FileLocking::Disabled => unreachable!(),
144 },
145 }
146}
147
148pub fn release(file: &File) -> io::Result<()> {
152 file.unlock()
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn parse_env_value_defaults_to_enabled() {
161 assert_eq!(FileLocking::parse_env_value(None), FileLocking::Enabled);
162 }
163
164 #[test]
165 fn parse_env_value_recognizes_disabled() {
166 for v in ["FALSE", "false", "0", "off", "no"] {
167 assert_eq!(
168 FileLocking::parse_env_value(Some(v)),
169 FileLocking::Disabled,
170 "value: {v}",
171 );
172 }
173 }
174
175 #[test]
176 fn parse_env_value_recognizes_best_effort() {
177 for v in ["BEST_EFFORT", "best_effort", "best-effort", "BestEffort"] {
178 assert_eq!(
179 FileLocking::parse_env_value(Some(v)),
180 FileLocking::BestEffort,
181 "value: {v}",
182 );
183 }
184 }
185
186 #[test]
187 fn parse_env_value_recognizes_enabled() {
188 for v in ["TRUE", "true", "1", "on", "yes", "garbage"] {
189 assert_eq!(
190 FileLocking::parse_env_value(Some(v)),
191 FileLocking::Enabled,
192 "value: {v}",
193 );
194 }
195 }
196
197 #[test]
198 fn try_acquire_disabled_is_noop() {
199 let dir =
200 std::env::temp_dir().join(format!("rust_hdf5_lock_disabled_{}", std::process::id()));
201 std::fs::create_dir_all(&dir).unwrap();
202 let path = dir.join("noop.bin");
203 let f = std::fs::File::create(&path).unwrap();
204 let acquired = try_acquire(&f, LockMode::Exclusive, FileLocking::Disabled).unwrap();
205 assert!(!acquired, "Disabled policy must not acquire a lock");
206 let _ = std::fs::remove_dir_all(&dir);
207 }
208
209 #[test]
210 fn try_acquire_exclusive_then_shared_fails() {
211 let dir = std::env::temp_dir().join(format!("rust_hdf5_lock_excl_{}", std::process::id()));
212 std::fs::create_dir_all(&dir).unwrap();
213 let path = dir.join("conflict.bin");
214 let f1 = std::fs::File::create(&path).unwrap();
215 assert!(try_acquire(&f1, LockMode::Exclusive, FileLocking::Enabled).unwrap());
216 let f2 = std::fs::OpenOptions::new().read(true).open(&path).unwrap();
217 let res = try_acquire(&f2, LockMode::Shared, FileLocking::Enabled);
218 assert!(res.is_err(), "expected lock conflict");
219 let res2 = try_acquire(&f2, LockMode::Shared, FileLocking::BestEffort).unwrap();
221 assert!(!res2, "best-effort must report unsuccessful lock as false");
222 release(&f1).unwrap();
223 let _ = std::fs::remove_dir_all(&dir);
224 }
225
226 #[test]
227 fn shared_locks_coexist() {
228 let dir =
229 std::env::temp_dir().join(format!("rust_hdf5_lock_shared_{}", std::process::id()));
230 std::fs::create_dir_all(&dir).unwrap();
231 let path = dir.join("shared.bin");
232 std::fs::File::create(&path).unwrap();
233 let f1 = std::fs::OpenOptions::new().read(true).open(&path).unwrap();
234 let f2 = std::fs::OpenOptions::new().read(true).open(&path).unwrap();
235 assert!(try_acquire(&f1, LockMode::Shared, FileLocking::Enabled).unwrap());
236 assert!(try_acquire(&f2, LockMode::Shared, FileLocking::Enabled).unwrap());
237 release(&f1).unwrap();
238 release(&f2).unwrap();
239 let _ = std::fs::remove_dir_all(&dir);
240 }
241
242 #[test]
243 fn release_then_relock_works() {
244 let dir =
245 std::env::temp_dir().join(format!("rust_hdf5_lock_release_{}", std::process::id()));
246 std::fs::create_dir_all(&dir).unwrap();
247 let path = dir.join("release.bin");
248 let f1 = std::fs::File::create(&path).unwrap();
249 assert!(try_acquire(&f1, LockMode::Exclusive, FileLocking::Enabled).unwrap());
250 release(&f1).unwrap();
252
253 let f2 = std::fs::OpenOptions::new()
254 .read(true)
255 .write(true)
256 .open(&path)
257 .unwrap();
258 assert!(try_acquire(&f2, LockMode::Exclusive, FileLocking::Enabled).unwrap());
259
260 release(&f2).unwrap();
261 let _ = std::fs::remove_dir_all(&dir);
262 }
263}