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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
//! Provides a method `unix_time` which returns elapsed time since *UNIX EPOCH*. The returned time
//! can be faked in each thread separately.
//!
//! ## Compilation Time Switch
//!
//! Faketime can be disabled at compilation time to avoid runtime overhead and potential
//! vulnerability of manipulating the process time.
//!
//! It is disabled via rust cfg `disable_faketime`, e.g.:
//!
//! ```shell
//! RUSTFLAGS="--cfg disable_faketime" cargo build
//! ```
//!
//! By default, without setting the cfg, faketime is available for all the threads.
//!
//! ## Usage
//!
//! The faketime setting is per thread. If it is enabled in a thread, a file path is also
//! configured. The file should store the milliseconds since UNIX EPOCH. This function will first
//! try to read the time from this file, and fallback to the system time when an error occurs.
//!
//! The most straightforward way to enable faketime is calling `faketime::enable(path)` in the
//! thread, and `path` is the configured timestamp file. It also overrides the auto-detected
//! settings, as mentioned below.
//!
//! ```
//! assert_ne!(faketime::unix_time().as_secs(), 100);
//! let faketime_file = faketime::millis_tempfile(100_000).expect("create faketime file");
//! faketime::enable(&faketime_file);
//! assert_eq!(faketime::unix_time().as_secs(), 100);
//! ```
//!
//! In each thread, when this function is first invoked, and neither `faketime::enable(path)` nor
//! `faketime::disable()` has been invoked in the thread already, it will detect whether faketime
//! should be enabled and which timestamp file should be used.
//!
//! First, if the environment variable `FAKETIME` exists, faketime is enabled, and the timestamp
//! file path is the environment variable value.
//!
//! ```
//! use std::env;
//! use std::thread;
//!
//! let faketime_file = faketime::millis_tempfile(123_456).expect("create faketime file");
//! env::set_var("FAKETIME", faketime_file.as_os_str());
//!
//! thread::spawn(|| assert_eq!(123, faketime::unix_time().as_secs()))
//!     .join()
//!     .expect("join thread");
//! ```
//!
//! If the environment variable `FAKETIME` is missing, but this thread has a name and the name
//! starts with `FAKETIME=` literally, faketime is also enabled, and the timestamp file is the
//! portion of the thread name after `FAKETIME=`.
//!
//! ```
//! use std::thread;
//!
//! let faketime_file = faketime::millis_tempfile(123_456).expect("create faketime file");
//!
//! thread::Builder::new()
//!     .name(format!("FAKETIME={}", faketime_file.display()))
//!     .spawn(|| assert_eq!(123, faketime::unix_time().as_secs()))
//!     .expect("spawn thread")
//!     .join()
//!     .expect("join thread");
//! ```
//!
//! Otherwise, faketime is disabled in the thread, until it is manually enabled via
//! `faketime::enable(path)`.
//!
//! ```
//! use std::thread;
//!
//! let start = faketime::system::unix_time();
//! thread::spawn(move || assert!((faketime::unix_time() - start).as_secs() < 60))
//!     .join()
//!     .expect("join thread");
//! ```
//!
//! ## Atomic Write
//!
//! This function reads timestamp from the file when faketime is enabled. To ensure the written
//! content is read in a whole, it is recommended to write the file atomically. One solution is
//! writing to a temporary file and then moving to the target path, as implemented in
//! `faketime::write_millis(path, millis)`.
//!
//! Following snippet demonstrates how to write time to file /tmp/faketime.
//!
//! ```shell
//! cat 100 > /tmp/faketime_
//! mv /tmp/faketime_ /tmp/faketime
//! ```

use crate::system::unix_time as system_unix_time;
use std::cell::{Cell, RefCell};
use std::env;
use std::fs;
use std::io::{Error, ErrorKind, Write};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::thread;
use std::time::Duration;
use tempfile::{NamedTempFile, TempPath};

pub use std::io::Result;

thread_local! {
    /// Some(true): Enabled
    /// Some(false): Disabled
    /// None: Undecided
    static FAKETIME_ENABLED: Cell<Option<bool>> = Cell::new(None);
    static FAKETIME_PATH: RefCell<PathBuf> = Default::default();
}

const KEY_FAKETIME: &str = "FAKETIME";
const PREFIX_FAKETIME_EQ: &str = "FAKETIME=";

/// Gets elapsed time since *UNIX EPOCH*.
///
/// ## Panics
///
/// Panics if the time is before *UNIX EPOCH*.
pub fn unix_time() -> Duration {
    FAKETIME_ENABLED.with(|enabled_cell| match enabled_cell.get() {
        Some(true) => FAKETIME_PATH.with(|path_cell| read_or_system(path_cell.borrow().deref())),
        Some(false) => system_unix_time(),
        None => auto_detect(&enabled_cell),
    })
}

fn auto_detect(enabled_cell: &Cell<Option<bool>>) -> Duration {
    if let Some(path) = match env::var(KEY_FAKETIME) {
        Ok(val) => Some(PathBuf::from(val)),
        _ => match thread::current().name() {
            Some(name) if name.starts_with(PREFIX_FAKETIME_EQ) => {
                Some(PathBuf::from(&name[PREFIX_FAKETIME_EQ.len()..]))
            }
            _ => None,
        },
    } {
        let duration = read_or_system(&path);
        FAKETIME_PATH.with(|file_cell| file_cell.replace(path));
        enabled_cell.set(Some(true));
        duration
    } else {
        enabled_cell.set(Some(false));
        system_unix_time()
    }
}

/// Enables faketime in current thread and use the specified timestamp file.
pub fn enable<T: AsRef<Path>>(path: T) {
    let path_buf = path.as_ref().to_path_buf();
    FAKETIME_PATH.with(|cell| cell.replace(path_buf));
    FAKETIME_ENABLED.with(|cell| cell.set(Some(true)));
}

/// Disables faketime in current thread.
pub fn disable() {
    FAKETIME_ENABLED.with(|cell| cell.set(Some(false)));
}

fn read_millis<T: AsRef<Path>>(path: T) -> Result<u64> {
    fs::read_to_string(path).and_then(|text| {
        text.trim()
            .parse()
            .map_err(|err| Error::new(ErrorKind::Other, err))
    })
}

fn read_or_system<T: AsRef<Path>>(path: T) -> Duration {
    read_millis(path)
        .ok()
        .map_or_else(system_unix_time, Duration::from_millis)
}

/// Writes time as milliseconds since *UNIX EPOCH* into the specified timestamp file.
pub fn write_millis<T: AsRef<Path>>(path: T, millis: u64) -> Result<()> {
    let mut file = NamedTempFile::new()?;
    file.write_all(millis.to_string().as_bytes())?;
    file.into_temp_path().persist(path)?;
    Ok(())
}

/// Writes time into a temporary file and return the file.
///
///
/// It returns the handle to the timestamp file on success.
///
/// ```
/// use std::fs;
///
/// let faketime_file = faketime::millis_tempfile(123).expect("create faketime file");
/// assert_eq!(fs::read_to_string(&faketime_file).ok(), Some("123".to_string()));
/// ```
///
/// The file is deleted when the handle goes out of scope.
///
/// ```
/// let path = {
///     let faketime_file = faketime::millis_tempfile(123).expect("create faketime file");
///     faketime_file.to_path_buf()
/// };
/// assert!(!path.exists());
/// ```
pub fn millis_tempfile(millis: u64) -> Result<TempPath> {
    let path = NamedTempFile::new()?.into_temp_path();
    write_millis(&path, millis)?;
    Ok(path)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_mock_file_io() {
        let tempdir = tempfile::tempdir().expect("create tempdir");
        let faketime_file = tempdir.path().join("faketime");

        assert_eq!(None, read_millis(&faketime_file).ok());
        fs::write(&faketime_file, "x").expect("write millis");
        assert_eq!(None, read_millis(&faketime_file).ok());
        fs::write(&faketime_file, "12345\n").expect("write millis");
        assert_eq!(12345, read_millis(&faketime_file).expect("read millis"));
        write_millis(&faketime_file, 54321).expect("write millis");
        assert_eq!(54321, read_millis(&faketime_file).expect("read millis"));
    }
}