Skip to content

Commit

Permalink
Add support for pre-unix-epoch file dates on Apple platforms (#108277)
Browse files Browse the repository at this point in the history
Time in UNIX system calls counts from the epoch, 1970-01-01. The timespec
struct used in various system calls represents this as a number of seconds and
a number of nanoseconds. Nanoseconds are required to be between 0 and
999_999_999, because the portion outside that range should be represented in
the seconds field; if nanoseconds were larger than 999_999_999, the seconds
field should go up instead.

Suppose you ask for the time 1969-12-31, what time is that? On UNIX systems
that support times before the epoch, that's seconds=-86400, one day before the
epoch. But now, suppose you ask for the time 1969-12-31 23:59:00.1. In other
words, a tenth of a second after one minute before the epoch.  On most UNIX
systems, that's represented as seconds=-60, nanoseconds=100_000_000. The macOS
bug is that it returns seconds=-59, nanoseconds=-900_000_000.

While that's in some sense an accurate description of the time (59.9 seconds
before the epoch), that violates the invariant of the timespec data structure:
nanoseconds must be between 0 and 999999999. This causes this assertion in the
Rust standard library.

So, on macOS, if we get a Timespec value with seconds less than or equal to
zero, and nanoseconds between -999_999_999 and -1 (inclusive), we can add
1_000_000_000 to the nanoseconds and subtract 1 from the seconds, and then
convert.  The resulting timespec value is still accepted by macOS, and when fed
back into the OS, produces the same results. (If you set a file's mtime with
that timestamp, then read it back, you get back the one with negative
nanoseconds again.)

Co-authored-by: Josh Triplett <josh@joshtriplett.org>
  • Loading branch information
Byron and joshtriplett committed Oct 31, 2023
1 parent 650991d commit a8ece11
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 0 deletions.
42 changes: 42 additions & 0 deletions library/std/src/fs/tests.rs
Expand Up @@ -1708,6 +1708,48 @@ fn test_file_times() {
}
}

#[test]
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos", target_os = "watchos"))]
fn test_file_times_pre_epoch_with_nanos() {
#[cfg(target_os = "ios")]
use crate::os::ios::fs::FileTimesExt;
#[cfg(target_os = "macos")]
use crate::os::macos::fs::FileTimesExt;
#[cfg(target_os = "tvos")]
use crate::os::tvos::fs::FileTimesExt;
#[cfg(target_os = "watchos")]
use crate::os::watchos::fs::FileTimesExt;

let tmp = tmpdir();
let file = File::create(tmp.join("foo")).unwrap();

for (accessed, modified, created) in [
// The first round is to set filetimes to something we know works, but this time
// it's validated with nanoseconds as well which probe the numeric boundary.
(
SystemTime::UNIX_EPOCH + Duration::new(12345, 1),
SystemTime::UNIX_EPOCH + Duration::new(54321, 100_000_000),
SystemTime::UNIX_EPOCH + Duration::new(32123, 999_999_999),
),
// The second rounds uses pre-epoch dates along with nanoseconds that probe
// the numeric boundary.
(
SystemTime::UNIX_EPOCH - Duration::new(1, 1),
SystemTime::UNIX_EPOCH - Duration::new(60, 100_000_000),
SystemTime::UNIX_EPOCH - Duration::new(3600, 999_999_999),
),
] {
let mut times = FileTimes::new();
times = times.set_accessed(accessed).set_modified(modified).set_created(created);
file.set_times(times).unwrap();

let metadata = file.metadata().unwrap();
assert_eq!(metadata.accessed().unwrap(), accessed);
assert_eq!(metadata.modified().unwrap(), modified);
assert_eq!(metadata.created().unwrap(), created);
}
}

#[test]
#[cfg(windows)]
fn windows_unix_socket_exists() {
Expand Down
24 changes: 24 additions & 0 deletions library/std/src/sys/unix/time.rs
Expand Up @@ -76,6 +76,30 @@ impl Timespec {
}

const fn new(tv_sec: i64, tv_nsec: i64) -> Timespec {
// On Apple OS, dates before epoch are represented differently than on other
// Unix platforms: e.g. 1/10th of a second before epoch is represented as `seconds=-1`
// and `nanoseconds=100_000_000` on other platforms, but is `seconds=0` and
// `nanoseconds=-900_000_000` on Apple OS.
//
// To compensate, we first detect this special case by checking if both
// seconds and nanoseconds are in range, and then correct the value for seconds
// and nanoseconds to match the common unix representation.
//
// Please note that Apple OS nonetheless accepts the standard unix format when
// setting file times, which makes this compensation round-trippable and generally
// transparent.
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "tvos",
target_os = "watchos"
))]
let (tv_sec, tv_nsec) =
if (tv_sec <= 0 && tv_sec > i64::MIN) && (tv_nsec < 0 && tv_nsec > -1_000_000_000) {
(tv_sec - 1, tv_nsec + 1_000_000_000)
} else {
(tv_sec, tv_nsec)
};
assert!(tv_nsec >= 0 && tv_nsec < NSEC_PER_SEC as i64);
// SAFETY: The assert above checks tv_nsec is within the valid range
Timespec { tv_sec, tv_nsec: unsafe { Nanoseconds(tv_nsec as u32) } }
Expand Down

0 comments on commit a8ece11

Please sign in to comment.