archive/zip: handle mtime in NTFS/UNIX/ExtendedTS extra fields

Handle NTFS timestamp, UNIX timestamp, Extended extra timestamp.
Writer supports only Extended extra timestamp field, matching most
zip creators.

Fixes #10242.

Change-Id: Id665db274e63def98659231391fb77392267ac1e
Reviewed-on: https://go-review.googlesource.com/18274
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Russ Cox <rsc@golang.org>
This commit is contained in:
Yasuhiro Matsumoto 2016-01-06 21:36:31 +09:00 committed by Russ Cox
parent 122abe6b12
commit 4c79ed5f44
6 changed files with 113 additions and 12 deletions

View file

@ -13,6 +13,7 @@ import (
"hash/crc32"
"io"
"os"
"time"
)
var (
@ -289,13 +290,16 @@ func readDirectoryHeader(f *File, r io.Reader) error {
// Other zip authors might not even follow the basic format,
// and we'll just ignore the Extra content in that case.
b := readBuf(f.Extra)
Extras:
for len(b) >= 4 { // need at least tag and size
tag := b.uint16()
size := b.uint16()
if int(size) > len(b) {
break
}
if tag == zip64ExtraId {
switch tag {
case zip64ExtraId:
// update directory values from the zip64 extra block.
// They should only be consulted if the sizes read earlier
// are maxed out.
@ -323,7 +327,42 @@ func readDirectoryHeader(f *File, r io.Reader) error {
}
f.headerOffset = int64(eb.uint64())
}
break
break Extras
case ntfsExtraId:
if size == 32 {
eb := readBuf(b[:size])
eb.uint32() // reserved
eb.uint16() // tag1
size1 := eb.uint16()
if size1 == 24 {
sub := readBuf(eb[:size1])
lo := sub.uint32()
hi := sub.uint32()
tick := (uint64(uint64(lo)|uint64(hi)<<32) - 116444736000000000) / 10000000
f.SetModTime(time.Unix(int64(tick), 0))
}
}
break Extras
case unixExtraId:
if size >= 12 {
eb := readBuf(b[:size])
eb.uint32() // AcTime
epoch := eb.uint32() // ModTime
f.SetModTime(time.Unix(int64(epoch), 0))
break Extras
}
case exttsExtraId:
if size >= 3 {
eb := readBuf(b[:size])
flags := eb.uint8() // Flags
epoch := eb.uint32() // AcTime/ModTime/CrTime
if flags&1 != 0 {
f.SetModTime(time.Unix(int64(epoch), 0))
}
break Extras
}
}
b = b[size:]
}
@ -508,6 +547,12 @@ func findSignatureInBlock(b []byte) int {
type readBuf []byte
func (b *readBuf) uint8() uint8 {
v := uint8((*b)[0])
*b = (*b)[1:]
return v
}
func (b *readBuf) uint16() uint16 {
v := binary.LittleEndian.Uint16(*b)
*b = (*b)[2:]

View file

@ -65,13 +65,13 @@ var tests = []ZipTest{
{
Name: "test.txt",
Content: []byte("This is a test text file.\n"),
Mtime: "09-05-10 12:12:02",
Mtime: "09-05-10 02:12:00",
Mode: 0644,
},
{
Name: "gophercolor16x16.png",
File: "gophercolor16x16.png",
Mtime: "09-05-10 15:52:58",
Mtime: "09-05-10 05:52:58",
Mode: 0644,
},
},
@ -83,13 +83,13 @@ var tests = []ZipTest{
{
Name: "test.txt",
Content: []byte("This is a test text file.\n"),
Mtime: "09-05-10 12:12:02",
Mtime: "09-05-10 02:12:00",
Mode: 0644,
},
{
Name: "gophercolor16x16.png",
File: "gophercolor16x16.png",
Mtime: "09-05-10 15:52:58",
Mtime: "09-05-10 05:52:58",
Mode: 0644,
},
},
@ -144,6 +144,17 @@ var tests = []ZipTest{
Name: "unix.zip",
File: crossPlatform,
},
{
Name: "extra-timestamp.zip",
File: []ZipTestFile{
{
Name: "hello.txt",
Content: []byte(""),
Mtime: "01-06-16 12:25:56",
Mode: 0666,
},
},
},
{
// created by Go, before we wrote the "optional" data
// descriptor signatures (which are required by OS X)
@ -152,13 +163,13 @@ var tests = []ZipTest{
{
Name: "foo.txt",
Content: []byte("foo\n"),
Mtime: "03-08-12 16:59:10",
Mtime: "03-09-12 00:59:10",
Mode: 0644,
},
{
Name: "bar.txt",
Content: []byte("bar\n"),
Mtime: "03-08-12 16:59:12",
Mtime: "03-09-12 00:59:12",
Mode: 0644,
},
},
@ -205,13 +216,13 @@ var tests = []ZipTest{
{
Name: "foo.txt",
Content: []byte("foo\n"),
Mtime: "03-08-12 16:59:10",
Mtime: "03-09-12 00:59:10",
Mode: 0644,
},
{
Name: "bar.txt",
Content: []byte("bar\n"),
Mtime: "03-08-12 16:59:12",
Mtime: "03-09-12 00:59:12",
Mode: 0644,
},
},
@ -225,14 +236,14 @@ var tests = []ZipTest{
{
Name: "foo.txt",
Content: []byte("foo\n"),
Mtime: "03-08-12 16:59:10",
Mtime: "03-09-12 00:59:10",
Mode: 0644,
ContentErr: ErrChecksum,
},
{
Name: "bar.txt",
Content: []byte("bar\n"),
Mtime: "03-08-12 16:59:12",
Mtime: "03-09-12 00:59:12",
Mode: 0644,
},
},

View file

@ -63,6 +63,9 @@ const (
// extra header id's
zip64ExtraId = 0x0001 // zip64 Extended Information Extra Field
ntfsExtraId = 0x000a // NTFS Extra Field
unixExtraId = 0x000d // UNIX Extra Field
exttsExtraId = 0x5455 // Extra Timestamp Extra Field
)
// FileHeader describes a file within a zip file.

Binary file not shown.

View file

@ -98,6 +98,16 @@ func (w *Writer) Close() error {
b.uint32(h.CompressedSize)
b.uint32(h.UncompressedSize)
}
mt := uint32(h.FileHeader.ModTime().Unix())
var mbuf [9]byte // 2x uint16 + uint8 + uint32
eb := writeBuf(mbuf[:])
eb.uint16(exttsExtraId)
eb.uint16(5) // size = uint8 + uint32
eb.uint8(1) // flags = modtime
eb.uint32(mt) // ModTime
h.Extra = append(h.Extra, mbuf[:]...)
b.uint16(uint16(len(h.Name)))
b.uint16(uint16(len(h.Extra)))
b.uint16(uint16(len(h.Comment)))
@ -376,6 +386,11 @@ func (w nopCloser) Close() error {
type writeBuf []byte
func (b *writeBuf) uint8(v uint8) {
(*b)[0] = v
*b = (*b)[1:]
}
func (b *writeBuf) uint16(v uint16) {
binary.LittleEndian.PutUint16(*b, v)
*b = (*b)[2:]

View file

@ -11,6 +11,7 @@ import (
"math/rand"
"os"
"testing"
"time"
)
// TODO(adg): a more sophisticated test suite
@ -20,6 +21,7 @@ type WriteTest struct {
Data []byte
Method uint16
Mode os.FileMode
Mtime string
}
var writeTests = []WriteTest{
@ -28,30 +30,35 @@ var writeTests = []WriteTest{
Data: []byte("Rabbits, guinea pigs, gophers, marsupial rats, and quolls."),
Method: Store,
Mode: 0666,
Mtime: "02-01-08 00:01:02",
},
{
Name: "bar",
Data: nil, // large data set in the test
Method: Deflate,
Mode: 0644,
Mtime: "03-02-08 01:02:03",
},
{
Name: "setuid",
Data: []byte("setuid file"),
Method: Deflate,
Mode: 0755 | os.ModeSetuid,
Mtime: "04-03-08 02:03:04",
},
{
Name: "setgid",
Data: []byte("setgid file"),
Method: Deflate,
Mode: 0755 | os.ModeSetgid,
Mtime: "05-04-08 03:04:04",
},
{
Name: "symlink",
Data: []byte("../link/target"),
Method: Deflate,
Mode: 0755 | os.ModeSymlink,
Mtime: "03-02-08 11:22:33",
},
}
@ -148,6 +155,11 @@ func testCreate(t *testing.T, w *Writer, wt *WriteTest) {
if wt.Mode != 0 {
header.SetMode(wt.Mode)
}
mtime, err := time.Parse("01-02-06 15:04:05", wt.Mtime)
if err != nil {
t.Fatal("time.Parse:", err)
}
header.SetModTime(mtime)
f, err := w.CreateHeader(header)
if err != nil {
t.Fatal(err)
@ -178,6 +190,21 @@ func testReadFile(t *testing.T, f *File, wt *WriteTest) {
if !bytes.Equal(b, wt.Data) {
t.Errorf("File contents %q, want %q", b, wt.Data)
}
mtime, err := time.Parse("01-02-06 15:04:05", wt.Mtime)
if err != nil {
t.Fatal("time.Parse:", err)
}
diff := mtime.Sub(f.ModTime())
if diff < 0 {
diff = -diff
}
// allow several time span
if diff > 5*time.Second {
t.Errorf("File modtime %v, want %v", mtime, f.ModTime())
}
}
func BenchmarkCompressedZipGarbage(b *testing.B) {