From 939f3e972cadc5df522ec0f61204cd98986c51d3 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Wed, 24 Apr 2019 16:32:15 +0200 Subject: [PATCH] node: Make sure year of all timestamps is valid Sometimes restic gets bogus timestamps which cannot be converted to JSON, because the stdlib JSON encoder returns an error if the year is not within [0, 9999]. We now make sure that we at least record _some_ timestamp and cap the year either to 0000 or 9999. Before, restic would refuse to save the file at all, so this improves the status quo. This fixes #2174 and #1173 --- changelog/unreleased/issue-2174 | 10 +++++++ internal/restic/node.go | 37 +++++++++++++----------- internal/restic/node_test.go | 51 +++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 changelog/unreleased/issue-2174 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) + } + }) + } +}