Skip to content

Commit

Permalink
Rollup merge of #117451 - Byron:issue-108277-apple-fix, r=joshtriplett
Browse files Browse the repository at this point in the history
Add support for pre-unix-epoch file dates on Apple platforms (#108277)

Please note that even though the assertion being hit is the same on MacOS and thus similar to what's described in #108277, on MacOS it's possible to convert the numbers such that they are valid, don't hit the assertion and are round-trippable.
Doing so effectively fixes the issue on Apple platforms.

This PR does not attempt to harden other platforms against negative nanoseconds, which can happen for many reasons including mild filesystem corruption.

----

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
matthiaskrgr and joshtriplett committed Oct 31, 2023
2 parents 86d69f9 + a8ece11 commit d06200b
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 d06200b

Please sign in to comment.