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}