diff --git a/internal/unpackinfo/lchtimes_darwin.go b/internal/unpackinfo/lchtimes_darwin.go index 201a83e..c080148 100644 --- a/internal/unpackinfo/lchtimes_darwin.go +++ b/internal/unpackinfo/lchtimes_darwin.go @@ -8,13 +8,11 @@ import ( ) // Lchtimes modifies the access and modified timestamps on a target path -// This capability is only available on Linux and Darwin as of now. The -// timestamps within UnpackInfo would have already been rounded by -// archive/tar so there is no need for subsecond precision. +// This capability is only available on Linux and Darwin as of now. func (i UnpackInfo) Lchtimes() error { return unix.Lutimes(i.Path, []unix.Timeval{ - {Sec: i.OriginalAccessTime.Unix(), Usec: int32(i.OriginalAccessTime.UnixMicro() % 1000)}, - {Sec: i.OriginalModTime.Unix(), Usec: int32(i.OriginalModTime.UnixMicro() % 1000)}}, + {Sec: i.OriginalAccessTime.Unix(), Usec: int32(i.OriginalAccessTime.Nanosecond() / 1e6 % 1e6)}, + {Sec: i.OriginalModTime.Unix(), Usec: int32(i.OriginalModTime.Nanosecond() / 1e6 % 1e6)}}, ) } diff --git a/internal/unpackinfo/lchtimes_linux.go b/internal/unpackinfo/lchtimes_linux.go index 1a929e8..c9ed6b3 100644 --- a/internal/unpackinfo/lchtimes_linux.go +++ b/internal/unpackinfo/lchtimes_linux.go @@ -8,13 +8,11 @@ import ( ) // Lchtimes modifies the access and modified timestamps on a target path -// This capability is only available on Linux and Darwin as of now. The -// timestamps within UnpackInfo would have already been rounded by -// archive/tar so there is no need for subsecond precision. +// This capability is only available on Linux and Darwin as of now. func (i UnpackInfo) Lchtimes() error { return unix.Lutimes(i.Path, []unix.Timeval{ - {Sec: i.OriginalAccessTime.Unix(), Usec: i.OriginalAccessTime.UnixMicro() % 1000}, - {Sec: i.OriginalModTime.Unix(), Usec: i.OriginalModTime.UnixMicro() % 1000}}, + {Sec: i.OriginalAccessTime.Unix(), Usec: int64(i.OriginalAccessTime.Nanosecond() / 1e6 % 1e6)}, + {Sec: i.OriginalModTime.Unix(), Usec: int64(i.OriginalModTime.Nanosecond() / 1e6 % 1e6)}}, ) } diff --git a/slug.go b/slug.go index d0d0ea6..619c5ae 100644 --- a/slug.go +++ b/slug.go @@ -7,7 +7,6 @@ import ( "io" "os" "path/filepath" - "sort" "strings" "github.com/hashicorp/go-slug/internal/ignorefiles" @@ -476,14 +475,6 @@ func (p *Packer) Unpack(r io.Reader, dst string) error { } } - // Now that extraction is complete, restore mode and timestamps previously saved - // about directories. But first, sort the list of directories by lexical order, which - // should impose a top-down ordering so that, in case subdirectories appear first in - // the archive, they won't change the modified timestamps of their parent directories. - sort.Slice(directoriesExtracted, func(i, j int) bool { - return directoriesExtracted[i].Path < directoriesExtracted[j].Path - }) - for _, dir := range directoriesExtracted { if err := dir.RestoreInfo(); err != nil { return err diff --git a/slug_test.go b/slug_test.go index 1b7fd08..71de69f 100644 --- a/slug_test.go +++ b/slug_test.go @@ -10,6 +10,7 @@ import ( "io/fs" "io/ioutil" "os" + "path" "path/filepath" "reflect" "strings" @@ -568,6 +569,50 @@ func TestUnpack(t *testing.T) { verifyPerms(t, filepath.Join(dst, "exe"), 0755) } +func TestUnpack_HeaderOrdering(t *testing.T) { + // Tests that when a tar file has subdirectories ordered before parent directories, the + // timestamps get restored correctly in the plaform where the tests are run. + + // This file is created by the go program found in `testdata/subdir-ordering` + f, err := os.Open("testdata/subdir-appears-first.tar.gz") + if err != nil { + t.Fatal(err) + } + + dir := t.TempDir() + + packer, err := NewPacker() + if err != nil { + t.Fatalf("expected no error, got %s", err) + } + + err = packer.Unpack(f, dir) + if err != nil { + t.Fatalf("expected no error, got %s", err) + } + + // These times were recorded when the archive was created + testCases := []struct { + Path string + TS time.Time + }{ + {TS: time.Unix(0, 1698787142347461403).Round(time.Second), Path: path.Join(dir, "super/duper")}, + {TS: time.Unix(0, 1698780461367973574).Round(time.Second), Path: path.Join(dir, "super")}, + {TS: time.Unix(0, 1698787142347461286).Round(time.Second), Path: path.Join(dir, "super/duper/trooper")}, + {TS: time.Unix(0, 1698780470254368545).Round(time.Second), Path: path.Join(dir, "super/duper/trooper/foo.txt")}, + } + + for _, tc := range testCases { + info, err := os.Stat(tc.Path) + if err != nil { + t.Fatalf("error when stat %q: %s", tc.Path, err) + } + if info.ModTime() != tc.TS { + t.Errorf("timestamp of file %q (%d) did not match expected value %d", tc.Path, info.ModTime().UnixNano(), tc.TS.UnixNano()) + } + } +} + func TestUnpackDuplicateNoWritePerm(t *testing.T) { dir, err := ioutil.TempDir("", "slug") if err != nil { diff --git a/testdata/subdir-appears-first.tar.gz b/testdata/subdir-appears-first.tar.gz new file mode 100644 index 0000000..b65efa4 Binary files /dev/null and b/testdata/subdir-appears-first.tar.gz differ diff --git a/testdata/subdir-ordering/README.md b/testdata/subdir-ordering/README.md new file mode 100644 index 0000000..3724564 --- /dev/null +++ b/testdata/subdir-ordering/README.md @@ -0,0 +1 @@ +Builds the `subdir-appears-first.tar.gz` test dependency for TestUnpack_HeaderOrdering diff --git a/testdata/subdir-ordering/main.go b/testdata/subdir-ordering/main.go new file mode 100644 index 0000000..d331c05 --- /dev/null +++ b/testdata/subdir-ordering/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "log" + "os" +) + +func main() { + w, err := os.Create("../subdir-appears-first.tar.gz") + if err != nil { + log.Fatal(err) + } + defer w.Close() + + gzipW, err := gzip.NewWriterLevel(w, gzip.BestSpeed) + if err != nil { + log.Fatal(err) + } + defer gzipW.Close() + + tarW := tar.NewWriter(gzipW) + defer tarW.Close() + + // The order of headers to write to the output file + targets := []string{ + "super/duper", + "super/duper/trooper", + "super", + "super/duper/trooper/foo.txt", + } + + for _, t := range targets { + info, err := os.Stat(t) + if err != nil { + log.Fatal(err) + } + + header := &tar.Header{ + Format: tar.FormatUnknown, + Name: t, + ModTime: info.ModTime(), + Mode: int64(info.Mode()), + } + + switch { + case info.IsDir(): + header.Typeflag = tar.TypeDir + header.Name += "/" + default: + header.Typeflag = tar.TypeReg + header.Size = info.Size() + } + + // Write the header first to the archive. + if err := tarW.WriteHeader(header); err != nil { + log.Fatal(err) + } + + fmt.Printf("Added %q, unix nano mtime %d / %d\n", header.Name, info.ModTime().Unix(), info.ModTime().UnixNano()) + + if info.IsDir() { + continue + } + + f, err := os.Open(t) + if err != nil { + log.Fatal(err) + } + defer f.Close() + + _, err = io.Copy(tarW, f) + if err != nil { + log.Fatal(err) + } + } + + fmt.Printf("Copy these values into the +} diff --git a/testdata/subdir-ordering/super/duper/trooper/foo.txt b/testdata/subdir-ordering/super/duper/trooper/foo.txt new file mode 100644 index 0000000..48cdce8 --- /dev/null +++ b/testdata/subdir-ordering/super/duper/trooper/foo.txt @@ -0,0 +1 @@ +placeholder