tiverse_mmap/
file_lock.rs

1//! File locking integration for safe concurrent access to memory-mapped files.
2
3use crate::{MmapError, Result};
4use std::fs::File;
5use std::os::unix::io::AsRawFd;
6
7/// Type of file lock
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum LockType {
10    /// Shared (read) lock - multiple readers allowed
11    Shared,
12    /// Exclusive (write) lock - only one writer allowed
13    Exclusive,
14}
15
16/// File lock guard that automatically releases the lock when dropped
17pub struct FileLock {
18    file: File,
19    lock_type: LockType,
20}
21
22impl FileLock {
23    /// Acquire a file lock (blocking)
24    ///
25    /// This will block until the lock can be acquired.
26    ///
27    /// # Platform Support
28    ///
29    /// - **Unix**: Uses `flock(2)`
30    /// - **Windows**: Uses `LockFile`/`LockFileEx`
31    ///
32    /// # Examples
33    ///
34    /// ```ignore
35    /// use mmap_rs::{FileLock, LockType};
36    /// use std::fs::File;
37    ///
38    /// let file = File::open("data.bin")?;
39    /// let lock = FileLock::lock(file, LockType::Shared)?;
40    /// // File is locked for reading
41    /// // Lock automatically released when `lock` is dropped
42    /// # Ok::<(), mmap_rs::MmapError>(())
43    /// ```
44    #[cfg(unix)]
45    pub fn lock(file: File, lock_type: LockType) -> Result<Self> {
46        let fd = file.as_raw_fd();
47
48        let operation = match lock_type {
49            LockType::Shared => libc::LOCK_SH,
50            LockType::Exclusive => libc::LOCK_EX,
51        };
52
53        // SAFETY: flock is safe to call with a valid file descriptor
54        let result = unsafe { libc::flock(fd, operation) };
55
56        if result != 0 {
57            let err = std::io::Error::last_os_error();
58            return Err(MmapError::SystemError(format!(
59                "Failed to acquire file lock: {}",
60                err
61            )));
62        }
63
64        Ok(Self { file, lock_type })
65    }
66
67    /// Try to acquire a file lock (non-blocking)
68    ///
69    /// Returns immediately if the lock cannot be acquired.
70    ///
71    /// # Examples
72    ///
73    /// ```ignore
74    /// use mmap_rs::{FileLock, LockType};
75    /// use std::fs::File;
76    ///
77    /// let file = File::open("data.bin")?;
78    /// match FileLock::try_lock(file, LockType::Exclusive) {
79    ///     Ok(lock) => {
80    ///         // Got the lock
81    ///     }
82    ///     Err(_) => {
83    ///         // Lock not available
84    ///     }
85    /// }
86    /// # Ok::<(), mmap_rs::MmapError>(())
87    /// ```
88    #[cfg(unix)]
89    pub fn try_lock(file: File, lock_type: LockType) -> Result<Self> {
90        let fd = file.as_raw_fd();
91
92        let operation = match lock_type {
93            LockType::Shared => libc::LOCK_SH | libc::LOCK_NB,
94            LockType::Exclusive => libc::LOCK_EX | libc::LOCK_NB,
95        };
96
97        // SAFETY: flock is safe to call with a valid file descriptor
98        let result = unsafe { libc::flock(fd, operation) };
99
100        if result != 0 {
101            let err = std::io::Error::last_os_error();
102            return Err(MmapError::SystemError(format!(
103                "Failed to acquire file lock (would block): {}",
104                err
105            )));
106        }
107
108        Ok(Self { file, lock_type })
109    }
110
111    /// Windows implementation of lock
112    #[cfg(windows)]
113    pub fn lock(file: File, lock_type: LockType) -> Result<Self> {
114        use std::os::windows::io::AsRawHandle;
115        use windows::Win32::Foundation::{HANDLE, INVALID_HANDLE_VALUE};
116        use windows::Win32::Storage::FileSystem::{LockFileEx, LOCKFILE_EXCLUSIVE_LOCK};
117
118        let handle = HANDLE(file.as_raw_handle() as isize);
119        if handle == INVALID_HANDLE_VALUE {
120            return Err(MmapError::SystemError("Invalid file handle".to_string()));
121        }
122
123        let flags = match lock_type {
124            LockType::Shared => 0,
125            LockType::Exclusive => LOCKFILE_EXCLUSIVE_LOCK.0,
126        };
127
128        let mut overlapped = unsafe { std::mem::zeroed() };
129
130        let result = unsafe { LockFileEx(handle, flags, 0, u32::MAX, u32::MAX, &mut overlapped) };
131
132        if result.is_err() {
133            return Err(MmapError::SystemError(
134                "Failed to acquire file lock".to_string(),
135            ));
136        }
137
138        Ok(Self { file, lock_type })
139    }
140
141    /// Windows implementation of try_lock
142    #[cfg(windows)]
143    pub fn try_lock(file: File, lock_type: LockType) -> Result<Self> {
144        use std::os::windows::io::AsRawHandle;
145        use windows::Win32::Foundation::{HANDLE, INVALID_HANDLE_VALUE};
146        use windows::Win32::Storage::FileSystem::{
147            LockFileEx, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
148        };
149
150        let handle = HANDLE(file.as_raw_handle() as isize);
151        if handle == INVALID_HANDLE_VALUE {
152            return Err(MmapError::SystemError("Invalid file handle".to_string()));
153        }
154
155        let flags = match lock_type {
156            LockType::Shared => LOCKFILE_FAIL_IMMEDIATELY.0,
157            LockType::Exclusive => LOCKFILE_EXCLUSIVE_LOCK.0 | LOCKFILE_FAIL_IMMEDIATELY.0,
158        };
159
160        let mut overlapped = unsafe { std::mem::zeroed() };
161
162        let result = unsafe { LockFileEx(handle, flags, 0, u32::MAX, u32::MAX, &mut overlapped) };
163
164        if result.is_err() {
165            return Err(MmapError::SystemError(
166                "Failed to acquire file lock (would block)".to_string(),
167            ));
168        }
169
170        Ok(Self { file, lock_type })
171    }
172
173    /// Get a reference to the underlying file
174    pub fn file(&self) -> &File {
175        &self.file
176    }
177
178    /// Get the lock type
179    pub fn lock_type(&self) -> LockType {
180        self.lock_type
181    }
182
183    /// Unlock and consume the lock guard, returning the file
184    pub fn unlock(self) -> File {
185        // Manually unlock before consuming
186        #[cfg(unix)]
187        {
188            let fd = self.file.as_raw_fd();
189            unsafe {
190                let _ = libc::flock(fd, libc::LOCK_UN);
191            }
192        }
193
194        #[cfg(windows)]
195        {
196            use std::os::windows::io::AsRawHandle;
197            use windows::Win32::Foundation::HANDLE;
198            use windows::Win32::Storage::FileSystem::UnlockFileEx;
199
200            let handle = HANDLE(self.file.as_raw_handle() as isize);
201            let mut overlapped = unsafe { std::mem::zeroed() };
202
203            unsafe {
204                let _ = UnlockFileEx(handle, 0, u32::MAX, u32::MAX, &mut overlapped);
205            }
206        }
207
208        // Prevent Drop from running
209        let file = unsafe { std::ptr::read(&self.file) };
210        std::mem::forget(self);
211        file
212    }
213}
214
215impl Drop for FileLock {
216    fn drop(&mut self) {
217        #[cfg(unix)]
218        {
219            let fd = self.file.as_raw_fd();
220            // SAFETY: flock with LOCK_UN is safe to call
221            unsafe {
222                let _ = libc::flock(fd, libc::LOCK_UN);
223            }
224        }
225
226        #[cfg(windows)]
227        {
228            use std::os::windows::io::AsRawHandle;
229            use windows::Win32::Foundation::HANDLE;
230            use windows::Win32::Storage::FileSystem::UnlockFileEx;
231
232            let handle = HANDLE(self.file.as_raw_handle() as isize);
233            let mut overlapped = unsafe { std::mem::zeroed() };
234
235            unsafe {
236                let _ = UnlockFileEx(handle, 0, u32::MAX, u32::MAX, &mut overlapped);
237            }
238        }
239    }
240}
241
242// File locks are Send but not Sync (exclusive file access)
243unsafe impl Send for FileLock {}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use std::io::Write;
249    use tempfile::NamedTempFile;
250
251    fn create_test_file() -> NamedTempFile {
252        let mut file = NamedTempFile::new().unwrap();
253        file.write_all(b"test data").unwrap();
254        file.flush().unwrap();
255        file
256    }
257
258    #[test]
259    fn test_shared_lock() {
260        let file = create_test_file();
261        let file_handle = std::fs::File::open(file.path()).unwrap();
262
263        let lock = FileLock::lock(file_handle, LockType::Shared).unwrap();
264        assert_eq!(lock.lock_type(), LockType::Shared);
265    }
266
267    #[test]
268    fn test_exclusive_lock() {
269        let file = create_test_file();
270        let file_handle = std::fs::File::open(file.path()).unwrap();
271
272        let lock = FileLock::lock(file_handle, LockType::Exclusive).unwrap();
273        assert_eq!(lock.lock_type(), LockType::Exclusive);
274    }
275
276    #[test]
277    fn test_try_lock_success() {
278        let file = create_test_file();
279        let file_handle = std::fs::File::open(file.path()).unwrap();
280
281        let lock = FileLock::try_lock(file_handle, LockType::Shared).unwrap();
282        assert_eq!(lock.lock_type(), LockType::Shared);
283    }
284
285    #[test]
286    fn test_lock_unlock() {
287        let file = create_test_file();
288        let file_handle = std::fs::File::open(file.path()).unwrap();
289
290        let lock = FileLock::lock(file_handle, LockType::Exclusive).unwrap();
291        let _file = lock.unlock();
292        // Lock should be released now
293    }
294
295    #[test]
296    fn test_multiple_shared_locks() {
297        let file = create_test_file();
298        let path = file.path().to_path_buf();
299
300        let file1 = std::fs::File::open(&path).unwrap();
301        let file2 = std::fs::File::open(&path).unwrap();
302
303        let _lock1 = FileLock::lock(file1, LockType::Shared).unwrap();
304        let _lock2 = FileLock::lock(file2, LockType::Shared).unwrap();
305        // Both shared locks should succeed
306    }
307}