1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#![deny(rustdoc, missing_docs, warnings, clippy::pedantic)]
#![forbid(unsafe_code)]
#![allow(missing_doc_code_examples, clippy::inline_always)]
#![doc(test(attr(deny(warnings), forbid(unsafe_code))))]
//! This crate provide an extension trait for [`OpenOptions`] to avoid dereferencing if the given
//! path is a symbolic link when opening a file.
//!
//! ```
//! use nofollow::OpenOptionsExt;
//! use std::{fs::OpenOptions, io::prelude::*};
//!
//! let mut content = String::new();
//! OpenOptions::new()
//!     .read(true)
//!     .no_follow()
//!     .open(file!())
//!     .unwrap()
//!     .read_to_string(&mut content)
//!     .unwrap();
//! assert_eq!(content, include_str!(concat!("../", file!())));
//! ```

use sealed::Sealed;
use std::fs::OpenOptions;
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt as FsOpenOptionsExt;
#[cfg(windows)]
use std::os::windows::fs::OpenOptionsExt as FsOpenOptionsExt;

mod sealed {
    pub trait Sealed {}
}

/// An extension trait for [`OpenOptions`].
pub trait OpenOptionsExt: Sealed {
    /// Don't dereference if the given path is a symbolic link when [opening](OpenOptions::open)
    /// the file, and set [custom open flags](FsOpenOptionsExt::custom_flags) at the same time.
    ///
    /// Note that due to the behaviour of [`OpenOptionsExt::custom_flags`](FsOpenOptionsExt::custom_flags),
    /// this function will **overwrite** any previously set custom open flags.
    ///
    /// ## Platform-specific behaviour
    /// The meaning of "do not dereference" is platform-specific. On Unix-like operating systems,
    /// [`OpenOptions::open`] [fails with `ELOOP`](https://docs.rs/libc/0.2/libc/constant.O_NOFOLLOW.html).
    /// On Windows, it [opens the symbolic link itself](https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew#FILE_FLAG_OPEN_REPARSE_POINT)
    /// instead.
    fn no_follow_with(&mut self, #[cfg(unix)] flags: i32, #[cfg(windows)] flags: u32) -> &mut Self;

    /// Don't dereference if the given path is a symbolic link when [opening](`OpenOptions::open`)
    /// the file.
    ///
    /// Equivalent to [`self.no_follow_with(0)`](Self::no_follow_with) (note that `0` means it will
    /// **clear** any previously set custom open flags at the same time), see that method for
    /// platform-specific behaviour.
    #[inline(always)]
    fn no_follow(&mut self) -> &mut Self {
        self.no_follow_with(0)
    }
}

impl Sealed for OpenOptions {}

impl OpenOptionsExt for OpenOptions {
    #[cfg(unix)]
    #[inline(always)]
    fn no_follow_with(&mut self, flags: i32) -> &mut Self {
        self.custom_flags(flags | libc::O_NOFOLLOW)
    }

    #[cfg(windows)]
    #[inline(always)]
    fn no_follow_with(&mut self, flags: u32) -> &mut Self {
        const OPEN_REPARSE_POINT: u32 = 0x20_0000;
        self.custom_flags(flags | OPEN_REPARSE_POINT)
    }
}

#[cfg(test)]
#[allow(clippy::blacklisted_name)]
mod tests {
    use super::OpenOptionsExt;
    #[cfg(unix)]
    use std::os::unix::fs::symlink;
    #[cfg(windows)]
    use std::os::windows::fs::symlink_file as symlink;
    use std::{
        fs::{File, OpenOptions},
        io::prelude::*,
        path::Path,
    };
    use tempfile::tempdir;

    fn dont_open_symlink(path: impl AsRef<Path>) -> File {
        let file = OpenOptions::new()
            .read(true)
            .no_follow()
            .open(path)
            .unwrap();
        assert!(
            !file.metadata().unwrap().file_type().is_symlink(),
            "file is a symbolic link"
        );
        file
    }

    #[test]
    fn fine_with_normal_file() {
        let mut content = String::new();
        dont_open_symlink(file!())
            .read_to_string(&mut content)
            .unwrap();
        assert_eq!(content, include_str!(concat!("../", file!())));
    }

    #[test]
    #[should_panic = "ymbolic link"]
    fn fail_on_symlink() {
        let dir = tempdir().unwrap();
        let foo = dir.path().join("foo");
        File::create(&foo).unwrap();
        let bar = dir.path().join("bar");
        symlink(foo, &bar).unwrap();
        dont_open_symlink(bar);
    }
}