Skip to content

Commit 08e6ca8

Browse files
committed
Merge branch 'fix-stat-with-negative-times'
2 parents dfb7b0f + bf49cd4 commit 08e6ca8

File tree

5 files changed

+63
-10
lines changed

5 files changed

+63
-10
lines changed

gix-index/src/fs.rs

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,11 @@ impl Metadata {
6666
#[cfg(target_os = "aix")]
6767
let nanoseconds = self.0.st_mtim.tv_nsec;
6868

69-
Some(system_time_from_secs_nanos(
70-
seconds.try_into().ok()?,
71-
nanoseconds.try_into().ok()?,
72-
))
69+
// All operating systems treat the seconds as offset from unix epoch, hence it must
70+
// be signed in order to deal with dates before epoch.
71+
// Rustix seems to think this value is u64, but we fix it here for now.
72+
let seconds = seconds as i64;
73+
system_time_from_secs_nanos(seconds, nanoseconds.try_into().ok()?)
7374
}
7475
#[cfg(windows)]
7576
self.0.modified().ok()
@@ -94,10 +95,11 @@ impl Metadata {
9495
#[cfg(target_os = "aix")]
9596
let nanoseconds = self.0.st_ctim.tv_nsec;
9697

97-
Some(system_time_from_secs_nanos(
98-
seconds.try_into().ok()?,
99-
nanoseconds.try_into().ok()?,
100-
))
98+
// All operating systems treat the seconds as offset from unix epoch, hence it must
99+
// be signed in order to deal with dates before epoch.
100+
// Rustix seems to think this value is u64, but we fix it here for now.
101+
let seconds = seconds as i64;
102+
system_time_from_secs_nanos(seconds, nanoseconds.try_into().ok()?)
101103
}
102104
#[cfg(windows)]
103105
self.0.created().ok()
@@ -186,6 +188,30 @@ impl Metadata {
186188
}
187189

188190
#[cfg(not(windows))]
189-
fn system_time_from_secs_nanos(secs: u64, nanos: u32) -> SystemTime {
190-
std::time::UNIX_EPOCH + std::time::Duration::new(secs, nanos)
191+
fn system_time_from_secs_nanos(secs: i64, nanos: i32) -> Option<SystemTime> {
192+
// Copied from https://github.com/rust-lang/rust at a8ece1190bf6b340175bc5b688e52bd29924f483, MIT licensed, and adapted.
193+
// On Apple OS, dates before epoch are represented differently than on other
194+
// Unix platforms: e.g. 1/10th of a second before epoch is represented as `seconds=-1`
195+
// and `nanoseconds=100_000_000` on other platforms, but is `seconds=0` and
196+
// `nanoseconds=-900_000_000` on Apple OS.
197+
//
198+
// To compensate, we first detect this special case by checking if both
199+
// seconds and nanoseconds are in range, and then correct the value for seconds
200+
// and nanoseconds to match the common unix representation.
201+
//
202+
// Please note that Apple OS nonetheless accepts the standard unix format when
203+
// setting file times, which makes this compensation round-trippable and generally
204+
// transparent.
205+
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos", target_os = "watchos"))]
206+
let (secs, nanos) = if (secs <= 0 && secs > i64::MIN) && (nanos < 0 && nanos > -1_000_000_000) {
207+
(secs - 1, nanos + 1_000_000_000)
208+
} else {
209+
(secs, nanos)
210+
};
211+
let d = std::time::Duration::new(secs.abs_diff(0), nanos.try_into().ok()?);
212+
Some(if secs < 0 {
213+
std::time::UNIX_EPOCH - d
214+
} else {
215+
std::time::UNIX_EPOCH + d
216+
})
191217
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/bash
2+
set -eu -o pipefail
3+
4+
# The largest-possible date for Ext4, nanos are special there, but ont usually on other filesystems
5+
touch -d "2446-05-10 22:38:55.111111111" future
6+
# The smallest-possible date for Ext4, nanos are special there, but ont usually on other filesystems
7+
touch -d "1901-12-13 20:45:52.222222222" past
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
file_metadata.tar.xz

gix-index/tests/index/fs.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
mod metadata {
2+
use gix_index::fs::Metadata;
3+
4+
#[test]
5+
fn from_path_no_follow() -> crate::Result {
6+
let root = gix_testtools::scripted_fixture_read_only_standalone("file_metadata.sh")?;
7+
8+
// For now, don't assert on the values of the metadata as these depends on the filesystem,
9+
// which might truncate it, or fail entirely.
10+
for filename in ["future", "past"] {
11+
let meta = Metadata::from_path_no_follow(&root.join(filename))?;
12+
assert!(meta.created().is_some());
13+
assert!(meta.modified().is_some());
14+
assert_eq!(meta.len(), 0);
15+
}
16+
Ok(())
17+
}
18+
}

gix-index/tests/index/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use gix_hash::ObjectId;
55
mod access;
66
mod entry;
77
mod file;
8+
mod fs;
89
mod init;
910

1011
pub fn hex_to_id(hex: &str) -> ObjectId {

0 commit comments

Comments
 (0)