Skip to content

Commit

Permalink
Refine timestamp handling
Browse files Browse the repository at this point in the history
It turns out Chtimes does not affect the parent directory mtime, because no matter how I order the tar headers for root and subdirectories, it doesn't seem to matter which order I restore the timestamps in.

I included a test to ensure this is true in the CI platform (linux)
  • Loading branch information
brandonc committed Oct 31, 2023
1 parent c31d54f commit b9ee75a
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 19 deletions.
8 changes: 3 additions & 5 deletions internal/unpackinfo/lchtimes_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)}},
)
}

Expand Down
8 changes: 3 additions & 5 deletions internal/unpackinfo/lchtimes_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)}},
)
}

Expand Down
9 changes: 0 additions & 9 deletions slug.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"io"
"os"
"path/filepath"
"sort"
"strings"

"github.com/hashicorp/go-slug/internal/ignorefiles"
Expand Down Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions slug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"io/fs"
"io/ioutil"
"os"
"path"
"path/filepath"
"reflect"
"strings"
Expand Down Expand Up @@ -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 {
Expand Down
Binary file added testdata/subdir-appears-first.tar.gz
Binary file not shown.
1 change: 1 addition & 0 deletions testdata/subdir-ordering/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Builds the `subdir-appears-first.tar.gz` test dependency for TestUnpack_HeaderOrdering
82 changes: 82 additions & 0 deletions testdata/subdir-ordering/main.go
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions testdata/subdir-ordering/super/duper/trooper/foo.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
placeholder

0 comments on commit b9ee75a

Please sign in to comment.