diff --git a/changelog/unreleased/issue-2174 b/changelog/unreleased/issue-2174 new file mode 100644 index 000000000..8a7942e9e --- /dev/null +++ b/changelog/unreleased/issue-2174 @@ -0,0 +1,10 @@ +Bugfix: Save files with invalid timestamps + +When restic reads invalid timestamps (year is before 0000 or after 9999) it +refused to read and archive the file. We've changed the behavior and will now +save modified timestamps with the year set to either 0000 or 9999, the rest of +the timestamp stays the same, so the file will be saved (albeit with a bogus +timestamp). + +https://github.com/restic/restic/issues/2174 +https://github.com/restic/restic/issues/1173 diff --git a/internal/restic/node.go b/internal/restic/node.go index fd63de58f..95f209113 100644 --- a/internal/restic/node.go +++ b/internal/restic/node.go @@ -332,24 +332,27 @@ func (node *Node) createFifoAt(path string) error { return mkfifo(path, 0600) } +// FixTime returns a time.Time which can safely be used to marshal as JSON. If +// the timestamp is ealier that year zero, the year is set to zero. In the same +// way, if the year is larger than 9999, the year is set to 9999. Other than +// the year nothing is changed. +func FixTime(t time.Time) time.Time { + switch { + case t.Year() < 0000: + return t.AddDate(-t.Year(), 0, 0) + case t.Year() > 9999: + return t.AddDate(-(t.Year() - 9999), 0, 0) + default: + return t + } +} + func (node Node) MarshalJSON() ([]byte, error) { - if node.ModTime.Year() < 0 || node.ModTime.Year() > 9999 { - err := errors.Errorf("node %v has invalid ModTime year %d: %v", - node.Path, node.ModTime.Year(), node.ModTime) - return nil, err - } - - if node.ChangeTime.Year() < 0 || node.ChangeTime.Year() > 9999 { - err := errors.Errorf("node %v has invalid ChangeTime year %d: %v", - node.Path, node.ChangeTime.Year(), node.ChangeTime) - return nil, err - } - - if node.AccessTime.Year() < 0 || node.AccessTime.Year() > 9999 { - err := errors.Errorf("node %v has invalid AccessTime year %d: %v", - node.Path, node.AccessTime.Year(), node.AccessTime) - return nil, err - } + // make sure invalid timestamps for mtime and atime are converted to + // something we can actually save. + node.ModTime = FixTime(node.ModTime) + node.AccessTime = FixTime(node.AccessTime) + node.ChangeTime = FixTime(node.ChangeTime) type nodeJSON Node nj := nodeJSON(node) diff --git a/internal/restic/node_test.go b/internal/restic/node_test.go index f12353a0a..dc2449bca 100644 --- a/internal/restic/node_test.go +++ b/internal/restic/node_test.go @@ -244,3 +244,54 @@ func AssertFsTimeEqual(t *testing.T, label string, nodeType string, t1 time.Time rtest.Assert(t, equal, "%s: %s doesn't match (%v != %v)", label, nodeType, t1, t2) } + +func parseTimeNano(t testing.TB, s string) time.Time { + // 2006-01-02T15:04:05.999999999Z07:00 + ts, err := time.Parse(time.RFC3339Nano, s) + if err != nil { + t.Fatalf("error parsing %q: %v", s, err) + } + return ts +} + +func TestFixTime(t *testing.T) { + // load UTC location + utc, err := time.LoadLocation("") + if err != nil { + t.Fatal(err) + } + + var tests = []struct { + src, want time.Time + }{ + { + src: parseTimeNano(t, "2006-01-02T15:04:05.999999999+07:00"), + want: parseTimeNano(t, "2006-01-02T15:04:05.999999999+07:00"), + }, + { + src: time.Date(0, 1, 2, 3, 4, 5, 6, utc), + want: parseTimeNano(t, "0000-01-02T03:04:05.000000006+00:00"), + }, + { + src: time.Date(-2, 1, 2, 3, 4, 5, 6, utc), + want: parseTimeNano(t, "0000-01-02T03:04:05.000000006+00:00"), + }, + { + src: time.Date(12345, 1, 2, 3, 4, 5, 6, utc), + want: parseTimeNano(t, "9999-01-02T03:04:05.000000006+00:00"), + }, + { + src: time.Date(9999, 1, 2, 3, 4, 5, 6, utc), + want: parseTimeNano(t, "9999-01-02T03:04:05.000000006+00:00"), + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + res := restic.FixTime(test.src) + if !res.Equal(test.want) { + t.Fatalf("wrong result for %v, want:\n %v\ngot:\n %v", test.src, test.want, res) + } + }) + } +}