From 4c79ed5f4483f1de065ba2b409de58ae2b6907d2 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 6 Jan 2016 21:36:31 +0900 Subject: [PATCH] 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 TryBot-Result: Gobot Gobot Reviewed-by: Russ Cox --- src/archive/zip/reader.go | 49 ++++++++++++++++++- src/archive/zip/reader_test.go | 31 ++++++++---- src/archive/zip/struct.go | 3 ++ src/archive/zip/testdata/extra-timestamp.zip | Bin 0 -> 152 bytes src/archive/zip/writer.go | 15 ++++++ src/archive/zip/writer_test.go | 27 ++++++++++ 6 files changed, 113 insertions(+), 12 deletions(-) create mode 100644 src/archive/zip/testdata/extra-timestamp.zip diff --git a/src/archive/zip/reader.go b/src/archive/zip/reader.go index f6c3ead3be..9bbc9a9745 100644 --- a/src/archive/zip/reader.go +++ b/src/archive/zip/reader.go @@ -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:] diff --git a/src/archive/zip/reader_test.go b/src/archive/zip/reader_test.go index dfaae78436..576a1697a4 100644 --- a/src/archive/zip/reader_test.go +++ b/src/archive/zip/reader_test.go @@ -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, }, }, diff --git a/src/archive/zip/struct.go b/src/archive/zip/struct.go index e92d02f8a2..8e6eb840f9 100644 --- a/src/archive/zip/struct.go +++ b/src/archive/zip/struct.go @@ -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. diff --git a/src/archive/zip/testdata/extra-timestamp.zip b/src/archive/zip/testdata/extra-timestamp.zip new file mode 100644 index 0000000000000000000000000000000000000000..819e22cb68df554186061bd3466349c07aa62851 GIT binary patch literal 152 zcmWIWW@h1H00G<8Y93$)l;8x?8L2rr`FbT4B>~=yO!f@86skbAC_rhrHZBH;5F>*G e!>fZKDYYIK8PVwgZ&o&tK1Lvn2GZ&v4g&ywrx`T> literal 0 HcmV?d00001 diff --git a/src/archive/zip/writer.go b/src/archive/zip/writer.go index 3a9292e380..2a747b8f37 100644 --- a/src/archive/zip/writer.go +++ b/src/archive/zip/writer.go @@ -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:] diff --git a/src/archive/zip/writer_test.go b/src/archive/zip/writer_test.go index 86841c755f..f20daa0e3d 100644 --- a/src/archive/zip/writer_test.go +++ b/src/archive/zip/writer_test.go @@ -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) { -- 2.48.1