il2_utils/fs/shared/
mod.rs

1/*
2 * BSD 3-Clause License
3 *
4 * Copyright (c) 2019-2020, InterlockLedger Network
5 * All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions are met:
9 *
10 * * Redistributions of source code must retain the above copyright notice, this
11 *   list of conditions and the following disclaimer.
12 *
13 * * Redistributions in binary form must reproduce the above copyright notice,
14 *   this list of conditions and the following disclaimer in the documentation
15 *   and/or other materials provided with the distribution.
16 *
17 * * Neither the name of the copyright holder nor the names of its
18 *   contributors may be used to endorse or promote products derived from
19 *   this software without specific prior written permission.
20 *
21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 */
32//! This module implements a shared file with access controlled by an
33//! "advisory lock". It allows multiple read access while restricts write
34//! access to a single actor at any given time.
35//!
36//! This lock can be used to coordinate access to the file but should not be
37//! used as a mean to guarantee security restrictions to t
38
39#[cfg(test)]
40mod tests;
41
42use std::ffi::{OsStr, OsString};
43use std::fs::{DirBuilder, File, OpenOptions};
44use std::io::{Error, ErrorKind, Read, Result, Seek, SeekFrom, Write};
45use std::path::Path;
46
47//=============================================================================
48// SharedFileLockNameBuilder
49//-----------------------------------------------------------------------------
50pub trait SharedFileLockNameBuilder {
51    /// Creates the lock file name from the target file.
52    ///
53    /// Arguments:
54    /// - `file`: The path to the target file;
55    ///
56    /// Returns:
57    /// - `Ok(x)`: The lock file path;
58    /// - `Err(e)`: If the lock file name cannot be created from the specified path;
59    fn create_lock_file_path(&self, file: &Path) -> Result<OsString> {
60        let file_name = match file.file_name() {
61            Some(name) => name,
62            None => {
63                return Err(Error::new(
64                    ErrorKind::InvalidInput,
65                    "Unable to extract the file name.",
66                ))
67            }
68        };
69        let lock_file_name = self.create_lock_file_name(&file_name);
70        match self.get_lock_directory(file) {
71            Some(v) => Ok(v.join(lock_file_name).into_os_string()),
72            None => Ok(lock_file_name),
73        }
74    }
75
76    /// Returns the lock directory. It is used by [`Self::create_lock_file_path()`] to
77    /// compose the lock file name.
78    ///
79    /// By default it is the same directory of the protected file.
80    ///
81    /// Arguments:
82    /// - `file`: The path to the protected file.
83    ///
84    /// Returns:
85    /// - `Some(x)`: The parent path;
86    /// - `None`: If the parent path is not present or is not available;
87    fn get_lock_directory<'a>(&self, file: &'a Path) -> Option<&'a Path> {
88        file.parent()
89    }
90
91    /// Creates the lock file name based on the original file name. It is used by
92    /// [`Self::create_lock_file_path()`] to compose the lock file name.
93    ///
94    /// Arguments:
95    /// - `file_name`: The name of the file that will be protected;
96    ///
97    /// Returns the name of the lock file.
98    fn create_lock_file_name(&self, file_name: &OsStr) -> OsString;
99}
100
101//=============================================================================
102// DefaultSharedFileLockNameBuilder
103//-----------------------------------------------------------------------------
104/// This is the default implementation of the [`SharedFileLockNameBuilder`].
105///
106/// The lock file will be in the same directory of the target file
107/// and will have the same name of the target file with the prefix
108/// "." and the suffix ".lock~".
109pub struct DefaultSharedFileLockNameBuilder;
110
111impl DefaultSharedFileLockNameBuilder {
112    /// Prefix of the lock file.
113    pub const LOCK_FILE_PREFIX: &'static str = ".";
114
115    /// Suffic of the lock file.
116    pub const LOCK_FILE_SUFFIX: &'static str = ".lock~";
117}
118
119impl SharedFileLockNameBuilder for DefaultSharedFileLockNameBuilder {
120    fn create_lock_file_name(&self, file_name: &OsStr) -> OsString {
121        let mut lock_file_name = OsString::from(Self::LOCK_FILE_PREFIX);
122        lock_file_name.push(file_name);
123        lock_file_name.push(Self::LOCK_FILE_SUFFIX);
124        lock_file_name
125    }
126}
127
128//=============================================================================
129// SharedFileReadLockGuard
130//-----------------------------------------------------------------------------
131/// An RAII implementation of an “advisory lock” of a shared read to the
132/// protected file. When this structure is dropped (falls out of scope), the
133/// shared read lock is released.
134///
135/// It exposes the the traits [`Read`] and [`Seek`] over the shared file in
136/// order to restrict the access to read operations.
137///
138/// See [`SharedFile`] for further details about how it works.
139pub struct SharedFileReadLockGuard<'a> {
140    file: &'a mut File,
141    _lock: fd_lock::RwLockReadGuard<'a, File>,
142}
143
144impl<'a> SharedFileReadLockGuard<'a> {
145    /// Returns a reference to the protected file.
146    pub fn file(&self) -> &File {
147        self.file
148    }
149}
150
151impl<'a> Read for SharedFileReadLockGuard<'a> {
152    fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
153        self.file.read(buf)
154    }
155}
156
157impl<'a> Seek for SharedFileReadLockGuard<'a> {
158    fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
159        self.file.seek(pos)
160    }
161}
162
163//=============================================================================
164// SharedFileWriteLockGuard
165//-----------------------------------------------------------------------------
166/// An RAII implementation of an “advisory lock” of a exclusive read and write
167/// to the protected file. When this structure is dropped (falls out of scope),
168/// the shared read lock is released.
169///
170/// It exposes the the traits [`Read`], [`Write`] and [`Seek`] over the shared
171/// file. Since it grants exclusive access to the file, this struct also
172/// grants a mutable access to the protecte file instance.
173///
174/// See [`SharedFile`] for further details about how it works.
175pub struct SharedFileWriteLockGuard<'a> {
176    file: &'a mut File,
177    _lock: fd_lock::RwLockWriteGuard<'a, File>,
178}
179
180impl<'a> SharedFileWriteLockGuard<'a> {
181    /// Returns a reference to the inner file.
182    pub fn file(&self) -> &File {
183        self.file
184    }
185
186    /// Returns a mutable reference to the inner file.
187    pub fn mut_file(&mut self) -> &mut File {
188        self.file
189    }
190}
191
192impl<'a> Read for SharedFileWriteLockGuard<'a> {
193    fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
194        self.file.read(buf)
195    }
196}
197
198impl<'a> Write for SharedFileWriteLockGuard<'a> {
199    fn write(&mut self, buf: &[u8]) -> Result<usize> {
200        self.file.write(buf)
201    }
202    fn flush(&mut self) -> Result<()> {
203        self.file.flush()
204    }
205}
206
207impl<'a> Seek for SharedFileWriteLockGuard<'a> {
208    fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
209        self.file.seek(pos)
210    }
211}
212
213//=============================================================================
214// SharedFile
215//-----------------------------------------------------------------------------
216/// This struct implements an “advisory lock” of a file using an auxiliary
217/// lock file to control the shared read access to the file as well as an
218/// exclusive read and write access to it.
219///
220/// Internally, it uses the crate `fd-lock` to control the access to the lock
221/// file while protecting the access tot the actual file.
222///
223/// The protected file is always opened with shared read and write and create
224/// options.
225///
226/// ## Locking the same file in multiple threads
227///
228/// It is very important to notice that this struct is not thread safe and must
229/// be protected by a Mutex whenever necessary. It happens because the file
230/// offset pointer cannot be safely shared among multiple threads even for
231/// read operations. Unfortunately, the use of the mutex or other sync
232/// mechanisms will lead to the serialization of both read and write locks
233/// inside the same application.
234///
235/// Because of that, it is recommended to create multiple instances of this
236/// struct pointing to the same file. The access control will be guaranteed
237/// by the use of the lock file instead of the traditional thread sync
238/// mechanisms.
239pub struct SharedFile {
240    lock: fd_lock::RwLock<File>,
241    file: File,
242}
243
244impl SharedFile {
245    /// Creates a new `SharedFile`. The name of the lock file will be determine
246    /// automatically based on the name of the original file.
247    ///
248    /// The shared file is opened with the [`Self::default_options()`].
249    ///
250    /// Arguments:
251    /// - `file`: The file to be protected;
252    ///
253    /// Returns the new instance of an IO error to indicate what went wrong.
254    pub fn new(file: &Path) -> Result<Self> {
255        let options = Self::default_options();
256        Self::with_options(file, &options)
257    }
258
259    /// Creates a new `SharedFile`. The name of the lock file will be determine
260    /// automatically based on the name of the original file.
261    ///
262    /// Arguments:
263    /// - `file`: The file to be protected;    
264    /// - `options`: [`OpenOptions`] used to open the file;
265    ///
266    /// Returns the new instance of an IO error to indicate what went wrong.
267    pub fn with_options(file: &Path, options: &OpenOptions) -> Result<Self> {
268        let lock_file_builder = DefaultSharedFileLockNameBuilder;
269        Self::with_option_builder(file, options, &lock_file_builder)
270    }
271
272    /// Creates a new `SharedFile`. The name of the lock file will be determine
273    /// by the specified [`SharedFileLockNameBuilder`].
274    ///
275    /// Arguments:
276    /// - `file`: The file to be protected;
277    /// - `options`: [`OpenOptions`] used to open the file;
278    /// - `lock_file_builder`: The lock file builder to use;
279    ///
280    /// Returns the new instance of an IO error to indicate what went wrong.
281    pub fn with_option_builder(
282        file: &Path,
283        options: &OpenOptions,
284        lock_file_builder: &dyn SharedFileLockNameBuilder,
285    ) -> Result<Self> {
286        let lock_file = lock_file_builder.create_lock_file_path(file)?;
287        Self::with_option_lock_file(file, options, Path::new(lock_file.as_os_str()))
288    }
289
290    /// Creates a new `SharedFile`.
291    ///
292    /// Arguments:
293    /// - `file`: The file to be protected;
294    /// - `options`: [`OpenOptions`] used to open the file;
295    /// - `lock_file`: The lock file to use;
296    ///
297    /// Returns the new instance of an IO error to indicate what went wrong.
298    pub fn with_option_lock_file(
299        file: &Path,
300        options: &OpenOptions,
301        lock_file: &Path,
302    ) -> Result<Self> {
303        Ok(Self {
304            lock: fd_lock::RwLock::new(File::create(lock_file)?),
305            file: options.open(file)?,
306        })
307    }
308
309    /// Returns the default open options used to open the target file. It sets
310    /// read write and create to true.
311    pub fn default_options() -> OpenOptions {
312        let mut options = OpenOptions::new();
313        options.read(true).write(true).create(true);
314        options
315    }
316
317    /// Locks the file for shared read.
318    ///
319    /// Returns read lock that grants access to the file.
320    pub fn read(&mut self) -> Result<SharedFileReadLockGuard<'_>> {
321        Ok(SharedFileReadLockGuard {
322            _lock: self.lock.read()?,
323            file: &mut self.file,
324        })
325    }
326
327    /// Locks the file for exclusive write and read
328    ///
329    /// Returns read/write lock that grants access to the file.
330    pub fn write(&mut self) -> Result<SharedFileWriteLockGuard<'_>> {
331        Ok(SharedFileWriteLockGuard {
332            _lock: self.lock.write()?,
333            file: &mut self.file,
334        })
335    }
336
337    /// Attempts to locks the file for shared read. It fails without waiting if
338    /// the lock cannot be acquired.
339    ///
340    /// Returns read lock that grants access to the file.
341    pub fn try_read(&mut self) -> Result<SharedFileReadLockGuard<'_>> {
342        Ok(SharedFileReadLockGuard {
343            _lock: self.lock.try_read()?,
344            file: &mut self.file,
345        })
346    }
347
348    /// Attempts to acquire the file lock for exclusive write and read. It fails
349    /// without waiting if the lock cannot be acquired.
350    ///
351    /// Returns read/write lock that grants access to the file.
352    pub fn try_write(&mut self) -> Result<SharedFileWriteLockGuard<'_>> {
353        Ok(SharedFileWriteLockGuard {
354            _lock: self.lock.try_write()?,
355            file: &mut self.file,
356        })
357    }
358}
359
360//=============================================================================
361// SharedDirectoryReadLockGuard
362//-----------------------------------------------------------------------------
363/// An RAII implementation of an “advisory lock” of a shared read to the
364/// protected directory. When this structure is dropped (falls out of scope), the
365/// shared read lock is released.
366pub struct SharedDirectoryReadLockGuard<'a> {
367    _lock: fd_lock::RwLockReadGuard<'a, File>,
368}
369
370//=============================================================================
371// SharedDirectoryWriteLockGuard
372//-----------------------------------------------------------------------------
373/// An RAII implementation of an “advisory lock” of a exclusive read and write
374/// to the protected directory. When this structure is dropped (falls out of scope),
375/// the shared read lock is released.
376pub struct SharedDirectoryWriteLockGuard<'a> {
377    _lock: fd_lock::RwLockWriteGuard<'a, File>,
378}
379
380//=============================================================================
381// SharedDirectory
382//-----------------------------------------------------------------------------
383/// This struct implements an “advisory lock” of a directory by using an
384/// auxiliary lock file to control the shared read access to it.
385///
386/// Internally, it uses the crate `fd-lock` to control the access to the lock
387/// file while protecting the access tot the actual file.
388///
389/// ## Locking the same file in multiple threads
390///
391/// It is very important to notice that this struct is not thread safe and must
392/// be protected by a Mutex whenever necessary. It happens because the file
393/// offset pointer cannot be safely shared among multiple threads even for
394/// read operations. Unfortunately, the use of the mutex or other sync
395/// mechanisms will lead to the serialization of both read and write locks
396/// inside the same application.
397///
398/// Because of that, it is recommended to create multiple instances of this
399/// struct pointing to the same file. The access control will be guaranteed
400/// by the use of the lock file instead of the traditional thread sync
401/// mechanisms.
402pub struct SharedDirectory {
403    lock: fd_lock::RwLock<File>,
404    dir_name: OsString,
405}
406
407impl SharedDirectory {
408    /// This constant defines the default name of the file used to control the
409    /// access to the directory.
410    ///
411    /// This name was choosen to be as unlikely as possible in order to avoid
412    /// accidental collisions with existing files. Following the
413    /// nothing-up-my-sleeve number principle, the name of this file is actually
414    /// the **MD5** of the string "SharedDirectory lock file!" encoded in
415    /// **Base64** as defined by **RFC 4648**.
416    ///
417    /// This claim can be checked with the command:
418    ///
419    /// - `printf "SharedDirectory lock file!" | openssl md5 -binary | base64`
420    ///
421    /// While **MD5** is no longer safe to be used as a cryptographic hash
422    /// function, it can be used as a good way to hash values for other purposes.
423    ///
424    /// Note from the author: I tried other variants of the above string but
425    /// they where discarded because the end result were not suitable to be
426    /// proper file names.
427    pub const DEFAULT_LOCK_FILE_NAME: &'static str = ".~yonAOEyQtQXj90nWCzHYJA.lock";
428
429    /// Creates a new `SharedDirectory`. It will create a file inside the protected
430    /// directory with the name defined by [`Self::DEFAULT_LOCK_FILE_NAME`] to control
431    /// the access to it.
432    ///
433    /// The protected directory will be automatically created it it does not
434    /// exist.
435    ///
436    /// Arguments:
437    /// - `directory`: The directory to be protected;
438    ///
439    /// Returns the new instance of an IO error to indicate what went wrong.
440    pub fn new(directory: &Path) -> Result<Self> {
441        Self::with_lock_file_name(directory, Self::DEFAULT_LOCK_FILE_NAME)
442    }
443
444    /// Creates a new `SharedDirectory`.
445    ///
446    /// The protected directory will be automatically created it it does not
447    /// exist.
448    ///
449    /// Arguments:
450    /// - `directory`: The directory to be protected;
451    /// - `lock_file_name`: The lock file name. This file will be created inside the
452    ///   shared directory;
453    ///
454    /// Returns the new instance of an IO error to indicate what went wrong.
455    pub fn with_lock_file_name(directory: &Path, lock_file_name: &str) -> Result<Self> {
456        let lock_file_path = directory.join(Path::new(lock_file_name));
457        Self::with_lock_file_path(directory, Path::new(lock_file_path.as_os_str()), true)
458    }
459
460    /// Creates a new `SharedDirectory`.
461    ///
462    /// The protected directory will be automatically created it it does not
463    /// exist.
464    ///
465    /// Arguments:
466    /// - `directory`: The directory to be protected;
467    /// - `lock_file`: The file to be used as the lock. This file will be created
468    ///   if it does not exist;
469    /// - `recursive`: Create the full directory path recursively;
470    ///
471    /// Returns the new instance of an IO error to indicate what went wrong.
472    pub fn with_lock_file_path(
473        directory: &Path,
474        lock_file: &Path,
475        recursive: bool,
476    ) -> Result<Self> {
477        // Check the directory.
478        if !directory.exists() {
479            let mut builder = DirBuilder::new();
480            builder.recursive(recursive);
481            builder.create(directory)?;
482        } else if !directory.is_dir() {
483            return Err(Error::new(
484                ErrorKind::InvalidInput,
485                format!("{:?} is not a directory.", directory),
486            ));
487        }
488        // Check the lock file.
489        if lock_file.exists() && !lock_file.is_file() {
490            return Err(Error::new(
491                ErrorKind::InvalidInput,
492                format!("{:?} is not a file.", directory),
493            ));
494        }
495        Ok(Self {
496            lock: fd_lock::RwLock::new(File::create(lock_file)?),
497            dir_name: directory.as_os_str().to_os_string(),
498        })
499    }
500
501    /// Returns the path to the protected directory.
502    pub fn directory(&self) -> &Path {
503        Path::new(&self.dir_name)
504    }
505
506    /// Locks the file for shared read.
507    ///
508    /// Returns read lock that grants access to the file.
509    pub fn read(&mut self) -> Result<SharedDirectoryReadLockGuard<'_>> {
510        Ok(SharedDirectoryReadLockGuard {
511            _lock: self.lock.read()?,
512        })
513    }
514
515    /// Locks the file for exclusive write and read
516    ///
517    /// Returns read/write lock that grants access to the file.
518    pub fn write(&mut self) -> Result<SharedDirectoryWriteLockGuard<'_>> {
519        Ok(SharedDirectoryWriteLockGuard {
520            _lock: self.lock.write()?,
521        })
522    }
523
524    /// Attempts to locks the file for shared read. It fails without waiting if
525    /// the lock cannot be acquired.
526    ///
527    /// Returns read lock that grants access to the file.
528    pub fn try_read(&mut self) -> Result<SharedDirectoryReadLockGuard<'_>> {
529        Ok(SharedDirectoryReadLockGuard {
530            _lock: self.lock.try_read()?,
531        })
532    }
533
534    /// Attempts to acquire the file lock for exclusive write and read. It fails
535    /// without waiting if the lock cannot be acquired.
536    ///
537    /// Returns read/write lock that grants access to the file.
538    pub fn try_write(&mut self) -> Result<SharedDirectoryWriteLockGuard<'_>> {
539        Ok(SharedDirectoryWriteLockGuard {
540            _lock: self.lock.try_write()?,
541        })
542    }
543}