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}