Skip to content

Commit

Permalink
Handle timestamps before UNIX_EPOCH (#658)
Browse files Browse the repository at this point in the history
Instead of returning a Duration since the epoch from file metadata,
which cannot represent times before it, return the SystemTime directly.

Move conversion closer to where it's needed, and perform it infallibly.
  • Loading branch information
Freaky committed May 18, 2020
1 parent 78ba0b8 commit bc830b9
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 45 deletions.
33 changes: 12 additions & 21 deletions src/fs/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::io::Error as IOError;
use std::io::Result as IOResult;
use std::os::unix::fs::{MetadataExt, PermissionsExt, FileTypeExt};
use std::path::{Path, PathBuf};
use std::time::{UNIX_EPOCH, Duration};
use std::time::{SystemTime, UNIX_EPOCH};

use log::{debug, error};

Expand Down Expand Up @@ -326,35 +326,26 @@ impl<'dir> File<'dir> {
}

/// This file’s last modified timestamp.
/// If the file's time is invalid, assume it was modified today
pub fn modified_time(&self) -> Duration {
match self.metadata.modified() {
Ok(system_time) => system_time.duration_since(UNIX_EPOCH).unwrap(),
Err(_) => Duration::new(0, 0),
}
/// If the file's time is invalid, assume it was modified at the epoch
pub fn modified_time(&self) -> SystemTime {
self.metadata.modified().unwrap_or(UNIX_EPOCH)
}

/// This file’s last changed timestamp.
pub fn changed_time(&self) -> Duration {
Duration::new(self.metadata.ctime() as u64, self.metadata.ctime_nsec() as u32)
pub fn changed_time(&self) -> SystemTime {
self.metadata.modified().unwrap_or(UNIX_EPOCH)
}

/// This file’s last accessed timestamp.
/// If the file's time is invalid, assume it was accessed today
pub fn accessed_time(&self) -> Duration {
match self.metadata.accessed() {
Ok(system_time) => system_time.duration_since(UNIX_EPOCH).unwrap(),
Err(_) => Duration::new(0, 0),
}
/// If the file's time is invalid, assume it was accessed at the epoch
pub fn accessed_time(&self) -> SystemTime {
self.metadata.accessed().unwrap_or(UNIX_EPOCH)
}

/// This file’s created timestamp.
/// If the file's time is invalid, assume it was created today
pub fn created_time(&self) -> Duration {
match self.metadata.created() {
Ok(system_time) => system_time.duration_since(UNIX_EPOCH).unwrap(),
Err(_) => Duration::new(0, 0),
}
/// If the file's time is invalid, assume it was created at the epoch
pub fn created_time(&self) -> SystemTime {
self.metadata.created().unwrap_or(UNIX_EPOCH)
}

/// This file’s ‘type’.
Expand Down
2 changes: 1 addition & 1 deletion src/output/render/times.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub trait Render {
format: &TimeFormat) -> TextCell;
}

impl Render for std::time::Duration {
impl Render for std::time::SystemTime {
fn render(self, style: Style,
tz: &Option<TimeZone>,
format: &TimeFormat) -> TextCell {
Expand Down
58 changes: 35 additions & 23 deletions src/output/time.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Timestamp formatting.
use std::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH};

use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece};
use datetime::fmt::DateFormat;
Expand Down Expand Up @@ -51,7 +51,7 @@ pub enum TimeFormat {
// timestamps are separate types.

impl TimeFormat {
pub fn format_local(&self, time: Duration) -> String {
pub fn format_local(&self, time: SystemTime) -> String {
match *self {
TimeFormat::DefaultFormat(ref fmt) => fmt.format_local(time),
TimeFormat::ISOFormat(ref iso) => iso.format_local(time),
Expand All @@ -60,7 +60,7 @@ impl TimeFormat {
}
}

pub fn format_zoned(&self, time: Duration, zone: &TimeZone) -> String {
pub fn format_zoned(&self, time: SystemTime, zone: &TimeZone) -> String {
match *self {
TimeFormat::DefaultFormat(ref fmt) => fmt.format_zoned(time, zone),
TimeFormat::ISOFormat(ref iso) => iso.format_zoned(time, zone),
Expand Down Expand Up @@ -146,11 +146,11 @@ impl DefaultFormat {
}

#[allow(trivial_numeric_casts)]
fn format_local(&self, time: Duration) -> String {
if time.as_nanos() == 0 {
fn format_local(&self, time: SystemTime) -> String {
if time == UNIX_EPOCH {
return "-".to_string();
}
let date = LocalDateTime::at(time.as_secs() as i64);
let date = LocalDateTime::at(systemtime_epoch(time));

if self.is_recent(date) {
format!("{:2} {} {:02}:{:02}",
Expand All @@ -163,12 +163,12 @@ impl DefaultFormat {
}

#[allow(trivial_numeric_casts)]
fn format_zoned(&self, time: Duration, zone: &TimeZone) -> String {
if time.as_nanos() == 0 {
fn format_zoned(&self, time: SystemTime, zone: &TimeZone) -> String {
if time == UNIX_EPOCH {
return "-".to_string();
}

let date = zone.to_zoned(LocalDateTime::at(time.as_secs() as i64));
let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time)));

if self.is_recent(date) {
format!("{:2} {} {:02}:{:02}",
Expand All @@ -181,43 +181,55 @@ impl DefaultFormat {
}
}

fn systemtime_epoch(time: SystemTime) -> i64 {
time
.duration_since(UNIX_EPOCH)
.map(|t| t.as_secs() as i64)
.unwrap_or_else(|e| -(e.duration().as_secs() as i64))
}

fn systemtime_nanos(time: SystemTime) -> u32 {
time
.duration_since(UNIX_EPOCH)
.map(|t| t.subsec_nanos())
.unwrap_or_else(|e| e.duration().subsec_nanos())
}

#[allow(trivial_numeric_casts)]
fn long_local(time: Duration) -> String {
let date = LocalDateTime::at(time.as_secs() as i64);
fn long_local(time: SystemTime) -> String {
let date = LocalDateTime::at(systemtime_epoch(time));
format!("{:04}-{:02}-{:02} {:02}:{:02}",
date.year(), date.month() as usize, date.day(),
date.hour(), date.minute())
}

#[allow(trivial_numeric_casts)]
fn long_zoned(time: Duration, zone: &TimeZone) -> String {
let date = zone.to_zoned(LocalDateTime::at(time.as_secs() as i64));
fn long_zoned(time: SystemTime, zone: &TimeZone) -> String {
let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time)));
format!("{:04}-{:02}-{:02} {:02}:{:02}",
date.year(), date.month() as usize, date.day(),
date.hour(), date.minute())
}


#[allow(trivial_numeric_casts)]
fn full_local(time: Duration) -> String {
let date = LocalDateTime::at(time.as_secs() as i64);
fn full_local(time: SystemTime) -> String {
let date = LocalDateTime::at(systemtime_epoch(time));
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09}",
date.year(), date.month() as usize, date.day(),
date.hour(), date.minute(), date.second(), time.subsec_nanos())
date.hour(), date.minute(), date.second(), systemtime_nanos(time))
}

#[allow(trivial_numeric_casts)]
fn full_zoned(time: Duration, zone: &TimeZone) -> String {
fn full_zoned(time: SystemTime, zone: &TimeZone) -> String {
use datetime::Offset;

let local = LocalDateTime::at(time.as_secs() as i64);
let local = LocalDateTime::at(systemtime_epoch(time));
let date = zone.to_zoned(local);
let offset = Offset::of_seconds(zone.offset(local) as i32).expect("Offset out of range");
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09} {:+03}{:02}",
date.year(), date.month() as usize, date.day(),
date.hour(), date.minute(), date.second(), time.subsec_nanos(),
date.hour(), date.minute(), date.second(), systemtime_nanos(time),
offset.hours(), offset.minutes().abs())
}

Expand All @@ -244,8 +256,8 @@ impl ISOFormat {
}

#[allow(trivial_numeric_casts)]
fn format_local(&self, time: Duration) -> String {
let date = LocalDateTime::at(time.as_secs() as i64);
fn format_local(&self, time: SystemTime) -> String {
let date = LocalDateTime::at(systemtime_epoch(time));

if self.is_recent(date) {
format!("{:02}-{:02} {:02}:{:02}",
Expand All @@ -259,8 +271,8 @@ impl ISOFormat {
}

#[allow(trivial_numeric_casts)]
fn format_zoned(&self, time: Duration, zone: &TimeZone) -> String {
let date = zone.to_zoned(LocalDateTime::at(time.as_secs() as i64));
fn format_zoned(&self, time: SystemTime, zone: &TimeZone) -> String {
let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time)));

if self.is_recent(date) {
format!("{:02}-{:02} {:02}:{:02}",
Expand Down

0 comments on commit bc830b9

Please sign in to comment.