From 2bcc24e9770955eb15b98dba866898ceaae08d2a Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 14 Aug 2017 15:24:31 -0700 Subject: [PATCH] archive/tar: support PAX subsecond resolution times Add support for PAX subsecond resolution times. Since the parser supports negative timestamps, the formatter also handles negative timestamps. The relevant PAX specification is: <<< Portable file timestamps cannot be negative. If pax encounters a file with a negative timestamp in copy or write mode, it can reject the file, substitute a non-negative timestamp, or generate a non-portable timestamp with a leading '-'. >>> <<< All of these time records shall be formatted as a decimal representation of the time in seconds since the Epoch. If a ( '.' ) decimal point character is present, the digits to the right of the point shall represent the units of a subsecond timing granularity, where the first digit is tenths of a second and each subsequent digit is a tenth of the previous digit. >>> Fixes #11171 Change-Id: Ied108f3d2654390bc1b0ddd66a4081c2b83e490b Reviewed-on: https://go-review.googlesource.com/55552 Run-TryBot: Joe Tsai TryBot-Result: Gobot Gobot Reviewed-by: Ian Lance Taylor --- src/archive/tar/common.go | 3 +-- src/archive/tar/strconv.go | 19 +++++++++++++- src/archive/tar/strconv_test.go | 45 +++++++++++++++++++++++++++++++++ src/archive/tar/writer.go | 12 +++------ 4 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/archive/tar/common.go b/src/archive/tar/common.go index 5b7bbe5a1e..5b921486f1 100644 --- a/src/archive/tar/common.go +++ b/src/archive/tar/common.go @@ -124,8 +124,7 @@ func (h *Header) allowedFormats() (format int, paxHdrs map[string]string) { if paxKey == paxNone { format &^= formatPAX // No PAX } else { - // TODO(dsnet): Support PAX time here. - // paxHdrs[paxKey] = formatPAXTime(ts) + paxHdrs[paxKey] = formatPAXTime(ts) } } } diff --git a/src/archive/tar/strconv.go b/src/archive/tar/strconv.go index 6aad6805aa..a93fc4ac7a 100644 --- a/src/archive/tar/strconv.go +++ b/src/archive/tar/strconv.go @@ -6,6 +6,7 @@ package tar import ( "bytes" + "fmt" "strconv" "strings" "time" @@ -218,7 +219,23 @@ func parsePAXTime(s string) (time.Time, error) { return time.Unix(secs, int64(nsecs)), nil } -// TODO(dsnet): Implement formatPAXTime. +// formatPAXTime converts ts into a time of the form %d.%d as described in the +// PAX specification. This function is capable of negative timestamps. +func formatPAXTime(ts time.Time) (s string) { + secs, nsecs := ts.Unix(), ts.Nanosecond() + if nsecs == 0 { + return strconv.FormatInt(secs, 10) + } + + // If seconds is negative, then perform correction. + sign := "" + if secs < 0 { + sign = "-" // Remember sign + secs = -(secs + 1) // Add a second to secs + nsecs = -(nsecs - 1E9) // Take that second away from nsecs + } + return strings.TrimRight(fmt.Sprintf("%s%d.%09d", sign, secs, nsecs), "0") +} // parsePAXRecord parses the input PAX record string into a key-value pair. // If parsing is successful, it will slice off the currently read record and diff --git a/src/archive/tar/strconv_test.go b/src/archive/tar/strconv_test.go index e2527dc61f..7156368ede 100644 --- a/src/archive/tar/strconv_test.go +++ b/src/archive/tar/strconv_test.go @@ -294,6 +294,51 @@ func TestParsePAXTime(t *testing.T) { } } +func TestFormatPAXTime(t *testing.T) { + vectors := []struct { + sec, nsec int64 + want string + }{ + {1350244992, 0, "1350244992"}, + {1350244992, 300000000, "1350244992.3"}, + {1350244992, 23960100, "1350244992.0239601"}, + {1350244992, 23960108, "1350244992.023960108"}, + {+1, +1E9 - 1E0, "1.999999999"}, + {+1, +1E9 - 1E3, "1.999999"}, + {+1, +1E9 - 1E6, "1.999"}, + {+1, +0E0 - 0E0, "1"}, + {+1, +1E6 - 0E0, "1.001"}, + {+1, +1E3 - 0E0, "1.000001"}, + {+1, +1E0 - 0E0, "1.000000001"}, + {0, 1E9 - 1E0, "0.999999999"}, + {0, 1E9 - 1E3, "0.999999"}, + {0, 1E9 - 1E6, "0.999"}, + {0, 0E0, "0"}, + {0, 1E6 + 0E0, "0.001"}, + {0, 1E3 + 0E0, "0.000001"}, + {0, 1E0 + 0E0, "0.000000001"}, + {-1, -1E9 + 1E0, "-1.999999999"}, + {-1, -1E9 + 1E3, "-1.999999"}, + {-1, -1E9 + 1E6, "-1.999"}, + {-1, -0E0 + 0E0, "-1"}, + {-1, -1E6 + 0E0, "-1.001"}, + {-1, -1E3 + 0E0, "-1.000001"}, + {-1, -1E0 + 0E0, "-1.000000001"}, + {-1350244992, 0, "-1350244992"}, + {-1350244992, -300000000, "-1350244992.3"}, + {-1350244992, -23960100, "-1350244992.0239601"}, + {-1350244992, -23960108, "-1350244992.023960108"}, + } + + for _, v := range vectors { + got := formatPAXTime(time.Unix(v.sec, v.nsec)) + if got != v.want { + t.Errorf("formatPAXTime(%ds, %dns): got %q, want %q", + v.sec, v.nsec, got, v.want) + } + } +} + func TestParsePAXRecord(t *testing.T) { medName := strings.Repeat("CD", 50) longName := strings.Repeat("AB", 100) diff --git a/src/archive/tar/writer.go b/src/archive/tar/writer.go index d88e5ef340..7d393b46df 100644 --- a/src/archive/tar/writer.go +++ b/src/archive/tar/writer.go @@ -67,17 +67,13 @@ func (tw *Writer) WriteHeader(hdr *Header) error { return err } - // TODO(dsnet): Add PAX timestamps with nanosecond support. - hdrCpy := *hdr - hdrCpy.ModTime = hdrCpy.ModTime.Truncate(time.Second) - - switch allowedFormats, paxHdrs := hdrCpy.allowedFormats(); { + switch allowedFormats, paxHdrs := hdr.allowedFormats(); { case allowedFormats&formatUSTAR != 0: - return tw.writeUSTARHeader(&hdrCpy) + return tw.writeUSTARHeader(hdr) case allowedFormats&formatPAX != 0: - return tw.writePAXHeader(&hdrCpy, paxHdrs) + return tw.writePAXHeader(hdr, paxHdrs) case allowedFormats&formatGNU != 0: - return tw.writeGNUHeader(&hdrCpy) + return tw.writeGNUHeader(hdr) default: return ErrHeader }