Skip to content

Commit d06200b

Browse files
Rollup merge of rust-lang#117451 - Byron:issue-108277-apple-fix, r=joshtriplett
Add support for pre-unix-epoch file dates on Apple platforms (rust-lang#108277) Please note that even though the assertion being hit is the same on MacOS and thus similar to what's described in rust-lang#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 <[email protected]>
2 parents 86d69f9 + a8ece11 commit d06200b

File tree

2 files changed

+66
-0
lines changed

2 files changed

+66
-0
lines changed

library/std/src/fs/tests.rs

+42
Original file line numberDiff line numberDiff line change
@@ -1708,6 +1708,48 @@ fn test_file_times() {
17081708
}
17091709
}
17101710

1711+
#[test]
1712+
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos", target_os = "watchos"))]
1713+
fn test_file_times_pre_epoch_with_nanos() {
1714+
#[cfg(target_os = "ios")]
1715+
use crate::os::ios::fs::FileTimesExt;
1716+
#[cfg(target_os = "macos")]
1717+
use crate::os::macos::fs::FileTimesExt;
1718+
#[cfg(target_os = "tvos")]
1719+
use crate::os::tvos::fs::FileTimesExt;
1720+
#[cfg(target_os = "watchos")]
1721+
use crate::os::watchos::fs::FileTimesExt;
1722+
1723+
let tmp = tmpdir();
1724+
let file = File::create(tmp.join("foo")).unwrap();
1725+
1726+
for (accessed, modified, created) in [
1727+
// The first round is to set filetimes to something we know works, but this time
1728+
// it's validated with nanoseconds as well which probe the numeric boundary.
1729+
(
1730+
SystemTime::UNIX_EPOCH + Duration::new(12345, 1),
1731+
SystemTime::UNIX_EPOCH + Duration::new(54321, 100_000_000),
1732+
SystemTime::UNIX_EPOCH + Duration::new(32123, 999_999_999),
1733+
),
1734+
// The second rounds uses pre-epoch dates along with nanoseconds that probe
1735+
// the numeric boundary.
1736+
(
1737+
SystemTime::UNIX_EPOCH - Duration::new(1, 1),
1738+
SystemTime::UNIX_EPOCH - Duration::new(60, 100_000_000),
1739+
SystemTime::UNIX_EPOCH - Duration::new(3600, 999_999_999),
1740+
),
1741+
] {
1742+
let mut times = FileTimes::new();
1743+
times = times.set_accessed(accessed).set_modified(modified).set_created(created);
1744+
file.set_times(times).unwrap();
1745+
1746+
let metadata = file.metadata().unwrap();
1747+
assert_eq!(metadata.accessed().unwrap(), accessed);
1748+
assert_eq!(metadata.modified().unwrap(), modified);
1749+
assert_eq!(metadata.created().unwrap(), created);
1750+
}
1751+
}
1752+
17111753
#[test]
17121754
#[cfg(windows)]
17131755
fn windows_unix_socket_exists() {

library/std/src/sys/unix/time.rs

+24
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,30 @@ impl Timespec {
7676
}
7777

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

0 commit comments

Comments
 (0)