Repository: lsd-rs/lsd (language: rust)

Issue:
Panic: No such local time
- os: Archlinux
- `lsd --version`: lsd 0.20.1
- `echo $TERM`: xterm-kitty
- `echo $LS_COLORS`: quite long and hopefully not relevant :slightly_smiling_face: 

## Expected behavior
I have an Onyx Boox ereader which I'm mounting via [jmtpfs](https://github.com/JasonFerrara/jmtpfs) - looking at how old that project is I probably should switch to something else, but alas...

For some reason, this results in files apparently created in the year 4437052:

```
$ /bin/ls --full-time
total 0
drwxr-xr-x 26 florian florian 0 4437052-10-03 00:35:12.000000000 +0200 'Internal storage'
```

I'd expect `lsd` to show the file in the same way, or perhaps just show an "invalid time" or something.

## Actual behavior

lsd crashes with `thread 'main' panicked at 'No such local time', /build/.cargo/registry/src/github.com-1ecc6299db9ec823/chrono-0.4.15/src/offset/mod.rs:173:34`.

Stack:

```
   0: std::panicking::begin_panic
   1: <chrono::datetime::DateTime<chrono::offset::local::Local> as core::convert::From<std::time::SystemTime>>::from
   2: lsd::meta::Meta::from_path
   3: lsd::meta::Meta::recurse_into
   4: lsd::core::Core::run
   5: lsd::main
```


Discussion / hints:
it should be caused by the invalid time format here:

https://github.com/Peltoche/lsd/blob/48210f30fbadfd910659b7ae40d8db4cf3303946/src/meta/date.rs#L11

we could check the time before converting it into the `DateTime`.


Fix patch:
diff --git a/src/meta/date.rs b/src/meta/date.rs
index 01d0cd060..f6b3a6e54 100644
--- a/src/meta/date.rs
+++ b/src/meta/date.rs
@@ -3,49 +3,72 @@ use crate::flags::{DateFlag, Flags};
 use chrono::{DateTime, Duration, Local};
 use chrono_humanize::HumanTime;
 use std::fs::Metadata;
+use std::panic;
+use std::time::SystemTime;
 
 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
-pub struct Date(DateTime<Local>);
+pub enum Date {
+    Date(DateTime<Local>),
+    Invalid,
+}
 
-impl<'a> From<&'a Metadata> for Date {
-    fn from(meta: &'a Metadata) -> Self {
-        let modified_time = meta.modified().expect("failed to retrieve modified date");
+// Note that this is split from the From for Metadata so we can test this one (as we can't mock Metadata)
+impl<'a> From<SystemTime> for Date {
+    fn from(systime: SystemTime) -> Self {
+        // FIXME: This should really involve a result, but there's upstream issues in chrono. See https://github.com/chronotope/chrono/issues/110
+        let res = panic::catch_unwind(|| systime.into());
 
-        let time = modified_time.into();
+        if let Ok(time) = res {
+            Date::Date(time)
+        } else {
+            Date::Invalid
+        }
+    }
+}
 
-        Date(time)
+impl<'a> From<&'a Metadata> for Date {
+    fn from(meta: &'a Metadata) -> Self {
+        meta.modified()
+            .expect("failed to retrieve modified date")
+            .into()
     }
 }
 
 impl Date {
     pub fn render(&self, colors: &Colors, flags: &Flags) -> ColoredString {
         let now = Local::now();
-
-        let elem = if self.0 > now - Duration::hours(1) {
-            Elem::HourOld
-        } else if self.0 > now - Duration::days(1) {
-            Elem::DayOld
+        let elem = if let Date::Date(val) = self {
+            if *val > now - Duration::hours(1) {
+                Elem::HourOld
+            } else if *val > now - Duration::days(1) {
+                Elem::DayOld
+            } else {
+                Elem::Older
+            }
         } else {
             Elem::Older
         };
-
         colors.colorize(self.date_string(flags), &elem)
     }
 
     pub fn date_string(&self, flags: &Flags) -> String {
-        match &flags.date {
-            DateFlag::Date => self.0.format("%c").to_string(),
-            DateFlag::Relative => format!("{}", HumanTime::from(self.0 - Local::now())),
-            DateFlag::ISO => {
-                // 365.2425 * 24 * 60 * 60 = 31556952 seconds per year
-                // 15778476 seconds are 6 months
-                if self.0 > Local::now() - Duration::seconds(15_778_476) {
-                    self.0.format("%m-%d %R").to_string()
-                } else {
-                    self.0.format("%F").to_string()
+        if let Date::Date(val) = self {
+            match &flags.date {
+                DateFlag::Date => val.format("%c").to_string(),
+                DateFlag::Relative => format!("{}", HumanTime::from(*val - Local::now())),
+                DateFlag::ISO => {
+                    // 365.2425 * 24 * 60 * 60 = 31556952 seconds per year
+                    // 15778476 seconds are 6 months
+                    if *val > Local::now() - Duration::seconds(15_778_476) {
+                        val.format("%m-%d %R").to_string()
+                    } else {
+                        val.format("%F").to_string()
+                    }
                 }
+                DateFlag::Formatted(format) => val.format(format).to_string(),
             }
-            DateFlag::Formatted(format) => self.0.format(format).to_string(),
+        } else {
+            String::from("-")
         }
     }
 }
@@ -60,6 +83,7 @@ mod test {
     use std::io;
     use std::path::Path;
     use std::process::{Command, ExitStatus};
+    use std::time;
     use std::{env, fs};
 
     #[cfg(unix)]
@@ -283,4 +307,24 @@ mod test {
 
         fs::remove_file(file_path).unwrap();
     }
+
+    #[test]
+    #[cfg(all(not(windows), target_arch = "x86_64"))]
+    fn test_bad_date() {
+        // 4437052 is the bad year taken from https://github.com/Peltoche/lsd/issues/529 that we know is both
+        // a) high enough to break chrono
+        // b) not high enough to break SystemTime (as Duration::MAX would)
+        let end_time =
+            time::SystemTime::UNIX_EPOCH + time::Duration::new(4437052 * 365 * 24 * 60 * 60, 0);
+        let colors = Colors::new(ThemeOption::Default);
+        let date = Date::from(end_time);
+
+        let mut flags = Flags::default();
+        flags.date = DateFlag::Date;
+
+        assert_eq!(
+            "-".to_string().with(Color::AnsiValue(36)),
+            date.render(&colors, &flags)
+        );
+    }
 }


Test patch: