file_guard/lib.rs
1//! A cross-platform library for simple advisory file locking.
2//!
3//! The lock supports both exclusive and shared locking modes for a byte range
4//! of an opened `File` object. Exclusively locking a portion of a file denies
5//! all other processes both shared and exclusive access to the specified
6//! region of the file. Shared locking a portion of a file denies all processes
7//! exclusive access to the specified region of the file. The locked range does
8//! not need to exist within the file, and the ranges may be used for any
9//! arbitrary advisory locking protocol between processes.
10//!
11//! The result of a [`lock()`], [`try_lock()`], or [`lock_any()`] is a
12//! [`FileGuard`]. When dropped, this [`FileGuard`] will unlock the region of
13//! the file currently held. Exclusive locks may be [`.downgrade()`]'ed to
14//! either a shared lock cross platform.
15//!
16//! On Unix systems `fcntl` is used to perform the locking, and on Windows, `LockFileEx`.
17//! All generally available behavior is consistent across platforms. For platform-
18//! specific behavior, traits may be used for the respective platform. For example,
19//! on Windows, locks cannot be safely upgraded, whereas on Unix systems, this can
20//! be done safely and atomically. To use this feature, the
21//! [`file_guard::os::unix::FileGuardExt`] may `use`ed, enabling the [`.upgrade()`]
22//! and [`.try_upgrade()`] methods.
23//!
24//! Note that on Windows, the file must be open with write permissions to lock it.
25//!
26//! # Examples
27//!
28//! ```
29//! use file_guard::Lock;
30//! use std::fs::OpenOptions;
31//!
32//! # fn main() -> std::io::Result<()> {
33//! let mut file = OpenOptions::new()
34//! .read(true)
35//! .write(true)
36//! .create(true)
37//! .open("example-lock")?;
38//!
39//! let mut lock = file_guard::lock(&mut file, Lock::Exclusive, 0, 1)?;
40//! write_to_file(&mut lock)?;
41//! # fn write_to_file(f: &mut std::fs::File) -> std::io::Result<()> { Ok(()) }
42//! // the lock will be unlocked when it goes out of scope
43//! # Ok(())
44//! # }
45//! ```
46//!
47//! You can store one or more locks in a struct:
48//!
49//! ```
50//! use file_guard::{FileGuard, Lock};
51//! use std::fs::{File, OpenOptions};
52//!
53//! # fn main() -> std::io::Result<()> {
54//! let file = OpenOptions::new()
55//! .read(true)
56//! .write(true)
57//! .create(true)
58//! .open("example-lock")?;
59//!
60//! struct Thing<'file> {
61//! a: FileGuard<&'file File>,
62//! b: FileGuard<&'file File>,
63//! }
64//!
65//! let t = Thing {
66//! a: file_guard::lock(&file, Lock::Exclusive, 0, 1)?,
67//! b: file_guard::lock(&file, Lock::Shared, 1, 2)?,
68//! };
69//! // both locks will be unlocked when t goes out of scope
70//! # Ok(())
71//! # }
72//! ```
73//!
74//! Anything that can `Deref` or `DerefMut` to a `File` can be used with the [`FileGuard`]
75//! (i.e. `Rc<File>`):
76//!
77//! ```
78//! use file_guard::{FileGuard, Lock};
79//! use std::fs::{File, OpenOptions};
80//! use std::rc::Rc;
81//!
82//! # fn main() -> std::io::Result<()> {
83//! let file = Rc::new(
84//! OpenOptions::new()
85//! .read(true)
86//! .write(true)
87//! .create(true)
88//! .open("example-lock")?
89//! );
90//!
91//! struct Thing {
92//! a: FileGuard<Rc<File>>,
93//! b: FileGuard<Rc<File>>,
94//! }
95//!
96//! let t = Thing {
97//! a: file_guard::lock(file.clone(), Lock::Exclusive, 0, 1)?,
98//! b: file_guard::lock(file, Lock::Shared, 1, 2)?,
99//! };
100//! // both locks will be unlocked and the file will be closed when t goes out of scope
101//! # Ok(())
102//! # }
103//! ```
104//!
105//! [`FileGuard`]: struct.FileGuard.html
106//! [`lock()`]: fn.lock.html
107//! [`try_lock()`]: fn.try_lock.html
108//! [`lock_any()`]: fn.lock_any.html
109//! [`.downgrade()`]: struct.FileGuard.html#method.downgrade
110//! [`file_guard::os::unix::FileGuardExt`]: os/unix/trait.FileGuardExt.html
111//! [`.upgrade()`]: os/unix/trait.FileGuardExt.html#tymethod.upgrade
112//! [`.try_upgrade()`]: os/unix/trait.FileGuardExt.html#tymethod.try_upgrade
113
114#![deny(missing_docs)]
115
116use std::fs::File;
117use std::io::ErrorKind;
118use std::ops::{Deref, DerefMut, Range};
119use std::{fmt, io};
120
121pub mod os;
122use self::os::{raw_file_downgrade, raw_file_lock};
123
124/// The type of a lock operation.
125///
126/// This is used to specify the desired lock type when used with [`lock()`]
127/// and [`try_lock()`], and it is the successful result type returned by
128/// [`lock_any()`].
129///
130/// [`lock()`]: fn.lock.html
131/// [`try_lock()`]: fn.try_lock.html
132/// [`lock_any()`]: fn.lock_any.html
133#[derive(Copy, Clone, PartialEq, Debug)]
134pub enum Lock {
135 /// A shared lock may be concurrently held by multiple processes while
136 /// preventing future exclusive locks its lifetime.
137 ///
138 /// The shared lock type cannot be obtained while an exclusive lock is held
139 /// by another process. When successful, a shared lock guarantees that only
140 /// one or more shared locks are concurrently held, and that no exclusive
141 /// locks are held.
142 ///
143 /// This lock type--often referred to as a read lock--may be used as a
144 /// means of signaling read integrity. When used cooperatively, they ensure
145 /// no exclusive lock is held, and thus, no other process may be writing to
146 /// a shared resource.
147 Shared,
148 /// An exclusive lock may only be held by a single process.
149 ///
150 /// The exclusive lock type can neither be obtained while any shared locks
151 /// are held or while any other exclusive locks are held. This linearizes
152 /// the sequence of processes attempting to acquire an exclusive lock.
153 ///
154 /// This lock type--also known as a write lock--may be used as a means of
155 /// ensuring write exclusivity. In a cooperative locking environment, all
156 /// access to a shared resource is halted until the exlusive lock is
157 /// released.
158 Exclusive,
159}
160
161/// Wait and claim the desired [`Lock`] type using a byte range of a file.
162///
163/// The byte range does not need to exist in the underlying file.
164///
165/// [`Lock`]: enum.Lock.html
166pub fn lock<T: Deref<Target = File>>(
167 file: T,
168 lock: Lock,
169 offset: usize,
170 len: usize,
171) -> io::Result<FileGuard<T>> {
172 unsafe {
173 raw_file_lock(&file, Some(lock), offset, len, true)?;
174 }
175 Ok(FileGuard {
176 offset,
177 len,
178 file,
179 lock,
180 })
181}
182
183/// Attempt to claim the desired [`Lock`] type using a byte range of a file.
184///
185/// If the desired [`Lock`] type cannot be obtained without blocking, an
186/// `Error` of kind `ErrorKind::WouldBlock` is returned. Otherwise if
187/// successful, the lock is held.
188///
189/// The byte range does not need to exist in the underlying file.
190///
191/// [`Lock`]: enum.Lock.html
192pub fn try_lock<T: Deref<Target = File>>(
193 file: T,
194 lock: Lock,
195 offset: usize,
196 len: usize,
197) -> io::Result<FileGuard<T>> {
198 unsafe {
199 raw_file_lock(&file, Some(lock), offset, len, false)?;
200 }
201 Ok(FileGuard {
202 offset,
203 len,
204 file,
205 lock,
206 })
207}
208
209/// First attempt to claim an [`Exclusive`] lock and then fallback to a
210/// [`Shared`] lock for a byte range of a file. This is not currently an
211/// atomic operation.
212///
213/// When successful, the [`FileGuard`] may be inspected for the lock type
214/// obtained using [`.lock_type()`], [`.is_shared()`], or [`.is_exclusive()`].
215///
216/// The byte range does not need to exist in the underlying file.
217///
218/// [`Exclusive`]: enum.Lock.html#variant.Exclusive
219/// [`Shared`]: enum.Lock.html#variant.Shared
220/// [`FileGuard`]: struct.FileGuard.html
221/// [`.lock_type()`]: struct.FileGuard.html#method.lock_type
222/// [`.is_shared()`]: struct.FileGuard.html#method.is_shared
223/// [`.is_exclusive()`]: struct.FileGuard.html#method.is_exclusive
224pub fn lock_any<T: Deref<Target = File>>(
225 file: T,
226 offset: usize,
227 len: usize,
228) -> io::Result<FileGuard<T>> {
229 let lock = match unsafe { raw_file_lock(&file, Some(Lock::Exclusive), offset, len, false) } {
230 Ok(_) => Lock::Exclusive,
231 Err(e) => {
232 if e.kind() == ErrorKind::WouldBlock {
233 unsafe {
234 raw_file_lock(&file, Some(Lock::Shared), offset, len, true)?;
235 }
236 Lock::Shared
237 } else {
238 return Err(e);
239 }
240 }
241 };
242 Ok(FileGuard {
243 offset,
244 len,
245 file,
246 lock,
247 })
248}
249
250/// An RAII implementation of a "scoped lock" of a file. When this structure
251/// is dropped (falls out of scope), the lock will be unlocked.
252///
253/// This structure is created by the [`lock()`], [`try_lock()`], and
254/// [`lock_any()`] functions.
255///
256/// [`lock()`]: fn.lock.html
257/// [`try_lock()`]: fn.try_lock.html
258/// [`lock_any()`]: fn.lock_any.html
259#[must_use = "if unused the file lock will immediately unlock"]
260pub struct FileGuard<T: Deref<Target = File>> {
261 offset: usize,
262 len: usize,
263 file: T,
264 lock: Lock,
265}
266
267impl<T> fmt::Debug for FileGuard<T>
268where
269 T: Deref<Target = File>,
270{
271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272 write!(
273 f,
274 "FileGuard::{:?}({}, {})",
275 self.lock, self.offset, self.len
276 )
277 }
278}
279
280impl<T> FileGuard<T>
281where
282 T: Deref<Target = File>,
283{
284 /// Gets the [`Lock`] type currently held.
285 ///
286 /// [`Lock`]: enum.Lock.html
287 #[inline]
288 pub fn lock_type(&self) -> Lock {
289 self.lock
290 }
291
292 /// Test if the currently held [`Lock`] type is [`Shared`].
293 ///
294 /// [`Lock`]: enum.Lock.html
295 /// [`Shared`]: enum.Lock.html#variant.Shared
296 #[inline]
297 pub fn is_shared(&self) -> bool {
298 self.lock == Lock::Shared
299 }
300
301 /// Test if the currently held [`Lock`] type is [`Exclusive`].
302 ///
303 /// [`Lock`]: enum.Lock.html
304 /// [`Exclusive`]: enum.Lock.html#variant.Exclusive
305 #[inline]
306 pub fn is_exclusive(&self) -> bool {
307 self.lock == Lock::Exclusive
308 }
309
310 /// Gets the byte range of the held lock.
311 #[inline]
312 pub fn range(&self) -> Range<usize> {
313 self.offset..(self.offset + self.len)
314 }
315
316 /// Gets the byte offset of the held lock.
317 #[inline]
318 pub fn offset(&self) -> usize {
319 self.offset
320 }
321
322 /// Gets the byte length of the held lock.
323 #[inline]
324 pub fn len(&self) -> usize {
325 self.len
326 }
327
328 /// Tests if the byte range of the lock has a length of zero.
329 #[inline]
330 pub fn is_empty(&self) -> bool {
331 self.len == 0
332 }
333
334 /// Safely exchanges an [`Exclusive`] [`Lock`] for a [`Shared`] one.
335 ///
336 /// If the currently held lock is already [`Shared`], no change is made and
337 /// the method succeeds. This exchange safely ensures no lock is released
338 /// during operation. That is, no waiting [`Exclusive`] lock attempts may
339 /// obtain the lock during the downgrade. Other [`Shared`] locks waiting
340 /// will be granted a lock as a result, however.
341 ///
342 /// [`Lock`]: enum.Lock.html
343 /// [`Exclusive`]: enum.Lock.html#variant.Exclusive
344 /// [`Shared`]: enum.Lock.html#variant.Shared
345 pub fn downgrade(&mut self) -> io::Result<()> {
346 if self.is_exclusive() {
347 unsafe {
348 raw_file_downgrade(&self.file, self.offset, self.len)?;
349 }
350 self.lock = Lock::Shared;
351 }
352 Ok(())
353 }
354}
355
356impl<T> Deref for FileGuard<T>
357where
358 T: Deref<Target = File>,
359{
360 type Target = T;
361
362 fn deref(&self) -> &T {
363 &self.file
364 }
365}
366
367impl<T> DerefMut for FileGuard<T>
368where
369 T: DerefMut<Target = File>,
370{
371 fn deref_mut(&mut self) -> &mut T {
372 &mut self.file
373 }
374}
375
376impl<T> Drop for FileGuard<T>
377where
378 T: Deref<Target = File>,
379{
380 #[inline]
381 fn drop(&mut self) {
382 let _ = unsafe { raw_file_lock(&self.file, None, self.offset, self.len, false) };
383 }
384}