Add support for pre-unix-epoch file dates on Apple platforms (#108277)
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>
This commit is contained in:
parent
650991d62c
commit
a8ece1190b
2 changed files with 66 additions and 0 deletions
|
@ -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() {
|
||||
|
|
|
@ -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) } }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue