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)]
42pub enum FileLocking {
43 #[default]
46 Enabled,
47 Disabled,
49 BestEffort,
53}
54
55impl FileLocking {
56 pub fn from_env() -> Self {
59 Self::parse_env_value(std::env::var("HDF5_USE_FILE_LOCKING").ok().as_deref())
60 }
61
62 pub fn from_env_or(default: FileLocking) -> Self {
64 match std::env::var("HDF5_USE_FILE_LOCKING").ok().as_deref() {
65 None => default,
66 Some(v) => Self::parse_env_value(Some(v)),
67 }
68 }
69
70 pub(crate) fn parse_env_value(value: Option<&str>) -> Self {
71 match value {
72 None => FileLocking::Enabled,
73 Some(v) => {
74 let trimmed = v.trim();
75 if trimmed.eq_ignore_ascii_case("FALSE")
76 || trimmed == "0"
77 || trimmed.eq_ignore_ascii_case("OFF")
78 || trimmed.eq_ignore_ascii_case("NO")
79 {
80 FileLocking::Disabled
81 } else if trimmed.eq_ignore_ascii_case("BEST_EFFORT")
82 || trimmed.eq_ignore_ascii_case("BEST-EFFORT")
83 || trimmed.eq_ignore_ascii_case("BESTEFFORT")
84 {
85 FileLocking::BestEffort
86 } else {
87 FileLocking::Enabled
89 }
90 }
91 }
92 }
93}
94
95pub fn try_acquire(file: &File, mode: LockMode, policy: FileLocking) -> io::Result<bool> {
102 if matches!(policy, FileLocking::Disabled) {
103 return Ok(false);
104 }
105
106 let attempt = match mode {
107 LockMode::Shared => file.try_lock_shared(),
108 LockMode::Exclusive => file.try_lock(),
109 };
110
111 match attempt {
112 Ok(()) => Ok(true),
113 Err(std::fs::TryLockError::WouldBlock) => match policy {
114 FileLocking::Enabled => Err(io::Error::new(
115 io::ErrorKind::WouldBlock,
116 "unable to lock file: another process holds a conflicting lock",
117 )),
118 FileLocking::BestEffort => Ok(false),
119 FileLocking::Disabled => unreachable!(),
120 },
121 Err(std::fs::TryLockError::Error(e)) => match policy {
122 FileLocking::Enabled => Err(e),
123 FileLocking::BestEffort => Ok(false),
124 FileLocking::Disabled => unreachable!(),
125 },
126 }
127}
128
129pub fn release(file: &File) -> io::Result<()> {
133 file.unlock()
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn parse_env_value_defaults_to_enabled() {
142 assert_eq!(FileLocking::parse_env_value(None), FileLocking::Enabled);
143 }
144
145 #[test]
146 fn parse_env_value_recognizes_disabled() {
147 for v in ["FALSE", "false", "0", "off", "no"] {
148 assert_eq!(
149 FileLocking::parse_env_value(Some(v)),
150 FileLocking::Disabled,
151 "value: {v}",
152 );
153 }
154 }
155
156 #[test]
157 fn parse_env_value_recognizes_best_effort() {
158 for v in ["BEST_EFFORT", "best_effort", "best-effort", "BestEffort"] {
159 assert_eq!(
160 FileLocking::parse_env_value(Some(v)),
161 FileLocking::BestEffort,
162 "value: {v}",
163 );
164 }
165 }
166
167 #[test]
168 fn parse_env_value_recognizes_enabled() {
169 for v in ["TRUE", "true", "1", "on", "yes", "garbage"] {
170 assert_eq!(
171 FileLocking::parse_env_value(Some(v)),
172 FileLocking::Enabled,
173 "value: {v}",
174 );
175 }
176 }
177
178 #[test]
179 fn try_acquire_disabled_is_noop() {
180 let dir =
181 std::env::temp_dir().join(format!("rust_hdf5_lock_disabled_{}", std::process::id()));
182 std::fs::create_dir_all(&dir).unwrap();
183 let path = dir.join("noop.bin");
184 let f = std::fs::File::create(&path).unwrap();
185 let acquired = try_acquire(&f, LockMode::Exclusive, FileLocking::Disabled).unwrap();
186 assert!(!acquired, "Disabled policy must not acquire a lock");
187 let _ = std::fs::remove_dir_all(&dir);
188 }
189
190 #[test]
191 fn try_acquire_exclusive_then_shared_fails() {
192 let dir = std::env::temp_dir().join(format!("rust_hdf5_lock_excl_{}", std::process::id()));
193 std::fs::create_dir_all(&dir).unwrap();
194 let path = dir.join("conflict.bin");
195 let f1 = std::fs::File::create(&path).unwrap();
196 assert!(try_acquire(&f1, LockMode::Exclusive, FileLocking::Enabled).unwrap());
197 let f2 = std::fs::OpenOptions::new().read(true).open(&path).unwrap();
198 let res = try_acquire(&f2, LockMode::Shared, FileLocking::Enabled);
199 assert!(res.is_err(), "expected lock conflict");
200 let res2 = try_acquire(&f2, LockMode::Shared, FileLocking::BestEffort).unwrap();
202 assert!(!res2, "best-effort must report unsuccessful lock as false");
203 release(&f1).unwrap();
204 let _ = std::fs::remove_dir_all(&dir);
205 }
206
207 #[test]
208 fn shared_locks_coexist() {
209 let dir =
210 std::env::temp_dir().join(format!("rust_hdf5_lock_shared_{}", std::process::id()));
211 std::fs::create_dir_all(&dir).unwrap();
212 let path = dir.join("shared.bin");
213 std::fs::File::create(&path).unwrap();
214 let f1 = std::fs::OpenOptions::new().read(true).open(&path).unwrap();
215 let f2 = std::fs::OpenOptions::new().read(true).open(&path).unwrap();
216 assert!(try_acquire(&f1, LockMode::Shared, FileLocking::Enabled).unwrap());
217 assert!(try_acquire(&f2, LockMode::Shared, FileLocking::Enabled).unwrap());
218 release(&f1).unwrap();
219 release(&f2).unwrap();
220 let _ = std::fs::remove_dir_all(&dir);
221 }
222
223 #[test]
224 fn release_then_relock_works() {
225 let dir =
226 std::env::temp_dir().join(format!("rust_hdf5_lock_release_{}", std::process::id()));
227 std::fs::create_dir_all(&dir).unwrap();
228 let path = dir.join("release.bin");
229 let f1 = std::fs::File::create(&path).unwrap();
230 assert!(try_acquire(&f1, LockMode::Exclusive, FileLocking::Enabled).unwrap());
231 release(&f1).unwrap();
233
234 let f2 = std::fs::OpenOptions::new()
235 .read(true)
236 .write(true)
237 .open(&path)
238 .unwrap();
239 assert!(try_acquire(&f2, LockMode::Exclusive, FileLocking::Enabled).unwrap());
240
241 release(&f2).unwrap();
242 let _ = std::fs::remove_dir_all(&dir);
243 }
244}