2011-07-10 11:30:16 +10:00
|
|
|
// Copyright 2011 The Go Authors. All rights reserved.
|
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
|
|
package zip
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
archive/zip: add FileHeader.Modified field
The ModifiedTime and ModifiedDate fields are not expressive enough
for many of the time extensions that have since been added to ZIP,
nor are they easy to access since they in a legacy MS-DOS format,
and must be set and retrieved via the SetModTime and ModTime methods.
Instead, we add new field Modified of time.Time type that contains
all of the previous information and more.
Support for extended timestamps have been attempted before, but the
change was reverted because it provided no ability for the user to
specify the timezone of the legacy MS-DOS fields.
Technically the old API did not either, but users were manually offsetting
the timestamp to achieve the same effect.
The Writer now writes the legacy timestamps according to the timezone
of the FileHeader.Modified field. When the Modified field is set via
the SetModTime method, it is in UTC, which preserves the old behavior.
The Reader attempts to determine the timezone if both the legacy
and extended timestamps are present since it can compute the delta
between the two values.
Since Modified is a superset of the information in ModifiedTime and ModifiedDate,
we mark ModifiedTime, ModifiedDate, ModTime, and SetModTime as deprecated.
Fixes #18359
Change-Id: I29c6bc0a62908095d02740df3e6902f50d3152f1
Reviewed-on: https://go-review.googlesource.com/74970
Run-TryBot: Joe Tsai <thebrokentoaster@gmail.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
2017-08-28 12:07:58 -07:00
|
|
|
"fmt"
|
2014-08-31 21:32:13 -07:00
|
|
|
"io"
|
2011-07-10 11:30:16 +10:00
|
|
|
"io/ioutil"
|
2011-11-08 15:40:58 -08:00
|
|
|
"math/rand"
|
2011-12-12 15:22:55 -05:00
|
|
|
"os"
|
2017-08-26 18:44:27 +09:00
|
|
|
"strings"
|
2011-07-10 11:30:16 +10:00
|
|
|
"testing"
|
archive/zip: add FileHeader.Modified field
The ModifiedTime and ModifiedDate fields are not expressive enough
for many of the time extensions that have since been added to ZIP,
nor are they easy to access since they in a legacy MS-DOS format,
and must be set and retrieved via the SetModTime and ModTime methods.
Instead, we add new field Modified of time.Time type that contains
all of the previous information and more.
Support for extended timestamps have been attempted before, but the
change was reverted because it provided no ability for the user to
specify the timezone of the legacy MS-DOS fields.
Technically the old API did not either, but users were manually offsetting
the timestamp to achieve the same effect.
The Writer now writes the legacy timestamps according to the timezone
of the FileHeader.Modified field. When the Modified field is set via
the SetModTime method, it is in UTC, which preserves the old behavior.
The Reader attempts to determine the timezone if both the legacy
and extended timestamps are present since it can compute the delta
between the two values.
Since Modified is a superset of the information in ModifiedTime and ModifiedDate,
we mark ModifiedTime, ModifiedDate, ModTime, and SetModTime as deprecated.
Fixes #18359
Change-Id: I29c6bc0a62908095d02740df3e6902f50d3152f1
Reviewed-on: https://go-review.googlesource.com/74970
Run-TryBot: Joe Tsai <thebrokentoaster@gmail.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
2017-08-28 12:07:58 -07:00
|
|
|
"time"
|
2011-07-10 11:30:16 +10:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// TODO(adg): a more sophisticated test suite
|
|
|
|
|
|
2011-09-25 20:48:03 -03:00
|
|
|
type WriteTest struct {
|
|
|
|
|
Name string
|
|
|
|
|
Data []byte
|
|
|
|
|
Method uint16
|
2011-12-12 15:22:55 -05:00
|
|
|
Mode os.FileMode
|
2011-09-25 20:48:03 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var writeTests = []WriteTest{
|
2011-12-01 14:33:24 -08:00
|
|
|
{
|
2011-09-25 20:48:03 -03:00
|
|
|
Name: "foo",
|
|
|
|
|
Data: []byte("Rabbits, guinea pigs, gophers, marsupial rats, and quolls."),
|
|
|
|
|
Method: Store,
|
2011-12-12 15:22:55 -05:00
|
|
|
Mode: 0666,
|
2011-09-25 20:48:03 -03:00
|
|
|
},
|
2011-12-01 14:33:24 -08:00
|
|
|
{
|
2011-09-25 20:48:03 -03:00
|
|
|
Name: "bar",
|
|
|
|
|
Data: nil, // large data set in the test
|
|
|
|
|
Method: Deflate,
|
2011-12-12 15:22:55 -05:00
|
|
|
Mode: 0644,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Name: "setuid",
|
|
|
|
|
Data: []byte("setuid file"),
|
|
|
|
|
Method: Deflate,
|
|
|
|
|
Mode: 0755 | os.ModeSetuid,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Name: "setgid",
|
|
|
|
|
Data: []byte("setgid file"),
|
|
|
|
|
Method: Deflate,
|
|
|
|
|
Mode: 0755 | os.ModeSetgid,
|
|
|
|
|
},
|
|
|
|
|
{
|
2012-02-06 11:58:32 -02:00
|
|
|
Name: "symlink",
|
|
|
|
|
Data: []byte("../link/target"),
|
2011-12-12 15:22:55 -05:00
|
|
|
Method: Deflate,
|
2012-02-06 11:58:32 -02:00
|
|
|
Mode: 0755 | os.ModeSymlink,
|
2011-09-25 20:48:03 -03:00
|
|
|
},
|
|
|
|
|
}
|
2011-07-10 11:30:16 +10:00
|
|
|
|
|
|
|
|
func TestWriter(t *testing.T) {
|
|
|
|
|
largeData := make([]byte, 1<<17)
|
2017-07-03 15:38:55 -07:00
|
|
|
if _, err := rand.Read(largeData); err != nil {
|
|
|
|
|
t.Fatal("rand.Read failed:", err)
|
2011-07-10 11:30:16 +10:00
|
|
|
}
|
2011-09-25 20:48:03 -03:00
|
|
|
writeTests[1].Data = largeData
|
|
|
|
|
defer func() {
|
|
|
|
|
writeTests[1].Data = nil
|
|
|
|
|
}()
|
2011-07-10 11:30:16 +10:00
|
|
|
|
|
|
|
|
// write a zip file
|
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
|
w := NewWriter(buf)
|
2011-09-25 20:48:03 -03:00
|
|
|
|
|
|
|
|
for _, wt := range writeTests {
|
|
|
|
|
testCreate(t, w, &wt)
|
|
|
|
|
}
|
|
|
|
|
|
2011-07-10 11:30:16 +10:00
|
|
|
if err := w.Close(); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// read it back
|
2012-02-15 12:58:00 +11:00
|
|
|
r, err := NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
2011-07-10 11:30:16 +10:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2011-09-25 20:48:03 -03:00
|
|
|
for i, wt := range writeTests {
|
|
|
|
|
testReadFile(t, r.File[i], &wt)
|
|
|
|
|
}
|
2011-07-10 11:30:16 +10:00
|
|
|
}
|
|
|
|
|
|
2017-08-26 18:44:27 +09:00
|
|
|
// TestWriterComment is test for EOCD comment read/write.
|
|
|
|
|
func TestWriterComment(t *testing.T) {
|
|
|
|
|
var tests = []struct {
|
|
|
|
|
comment string
|
|
|
|
|
ok bool
|
|
|
|
|
}{
|
|
|
|
|
{"hi, hello", true},
|
|
|
|
|
{"hi, こんにちわ", true},
|
|
|
|
|
{strings.Repeat("a", uint16max), true},
|
|
|
|
|
{strings.Repeat("a", uint16max+1), false},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, test := range tests {
|
|
|
|
|
// write a zip file
|
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
|
w := NewWriter(buf)
|
|
|
|
|
w.Comment = test.comment
|
|
|
|
|
|
|
|
|
|
if err := w.Close(); test.ok == (err != nil) {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if w.closed != test.ok {
|
|
|
|
|
t.Fatalf("Writer.closed: got %v, want %v", w.closed, test.ok)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// skip read test in failure cases
|
|
|
|
|
if !test.ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// read it back
|
|
|
|
|
r, err := NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if r.Comment != test.comment {
|
|
|
|
|
t.Fatalf("Reader.Comment: got %v, want %v", r.Comment, test.comment)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-05 17:32:09 +09:00
|
|
|
func TestWriterUTF8(t *testing.T) {
|
|
|
|
|
var utf8Tests = []struct {
|
|
|
|
|
name string
|
|
|
|
|
comment string
|
|
|
|
|
expect uint16
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "hi, hello",
|
|
|
|
|
comment: "in the world",
|
|
|
|
|
expect: 0x8,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "hi, こんにちわ",
|
|
|
|
|
comment: "in the world",
|
|
|
|
|
expect: 0x808,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "hi, hello",
|
|
|
|
|
comment: "in the 世界",
|
|
|
|
|
expect: 0x808,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "hi, こんにちわ",
|
|
|
|
|
comment: "in the 世界",
|
|
|
|
|
expect: 0x808,
|
|
|
|
|
},
|
2017-10-23 13:47:15 -07:00
|
|
|
{
|
|
|
|
|
// Name is Japanese encoded in Shift JIS.
|
|
|
|
|
name: "\x93\xfa\x96{\x8c\xea.txt",
|
|
|
|
|
comment: "in the 世界",
|
|
|
|
|
expect: 0x008, // UTF-8 must not be set
|
|
|
|
|
},
|
2017-04-05 17:32:09 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// write a zip file
|
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
|
w := NewWriter(buf)
|
|
|
|
|
|
|
|
|
|
for _, test := range utf8Tests {
|
|
|
|
|
h := &FileHeader{
|
|
|
|
|
Name: test.name,
|
|
|
|
|
Comment: test.comment,
|
|
|
|
|
Method: Deflate,
|
|
|
|
|
}
|
|
|
|
|
w, err := w.CreateHeader(h)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
w.Write([]byte{})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := w.Close(); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// read it back
|
|
|
|
|
r, err := NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for i, test := range utf8Tests {
|
|
|
|
|
got := r.File[i].Flags
|
|
|
|
|
t.Logf("name %v, comment %v", test.name, test.comment)
|
|
|
|
|
if got != test.expect {
|
|
|
|
|
t.Fatalf("Flags: got %v, want %v", got, test.expect)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
archive/zip: add FileHeader.Modified field
The ModifiedTime and ModifiedDate fields are not expressive enough
for many of the time extensions that have since been added to ZIP,
nor are they easy to access since they in a legacy MS-DOS format,
and must be set and retrieved via the SetModTime and ModTime methods.
Instead, we add new field Modified of time.Time type that contains
all of the previous information and more.
Support for extended timestamps have been attempted before, but the
change was reverted because it provided no ability for the user to
specify the timezone of the legacy MS-DOS fields.
Technically the old API did not either, but users were manually offsetting
the timestamp to achieve the same effect.
The Writer now writes the legacy timestamps according to the timezone
of the FileHeader.Modified field. When the Modified field is set via
the SetModTime method, it is in UTC, which preserves the old behavior.
The Reader attempts to determine the timezone if both the legacy
and extended timestamps are present since it can compute the delta
between the two values.
Since Modified is a superset of the information in ModifiedTime and ModifiedDate,
we mark ModifiedTime, ModifiedDate, ModTime, and SetModTime as deprecated.
Fixes #18359
Change-Id: I29c6bc0a62908095d02740df3e6902f50d3152f1
Reviewed-on: https://go-review.googlesource.com/74970
Run-TryBot: Joe Tsai <thebrokentoaster@gmail.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
2017-08-28 12:07:58 -07:00
|
|
|
func TestWriterTime(t *testing.T) {
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
h := &FileHeader{
|
|
|
|
|
Name: "test.txt",
|
|
|
|
|
Modified: time.Date(2017, 10, 31, 21, 11, 57, 0, timeZone(-7*time.Hour)),
|
|
|
|
|
}
|
|
|
|
|
w := NewWriter(&buf)
|
|
|
|
|
if _, err := w.CreateHeader(h); err != nil {
|
|
|
|
|
t.Fatalf("unexpected CreateHeader error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := w.Close(); err != nil {
|
|
|
|
|
t.Fatalf("unexpected Close error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
want, err := ioutil.ReadFile("testdata/time-go.zip")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("unexpected ReadFile error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got := buf.Bytes(); !bytes.Equal(got, want) {
|
|
|
|
|
fmt.Printf("%x\n%x\n", got, want)
|
|
|
|
|
t.Error("contents of time-go.zip differ")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-03-12 11:54:11 +11:00
|
|
|
func TestWriterOffset(t *testing.T) {
|
2015-01-19 14:39:33 +01:00
|
|
|
largeData := make([]byte, 1<<17)
|
2017-07-03 15:38:55 -07:00
|
|
|
if _, err := rand.Read(largeData); err != nil {
|
|
|
|
|
t.Fatal("rand.Read failed:", err)
|
2015-01-19 14:39:33 +01:00
|
|
|
}
|
|
|
|
|
writeTests[1].Data = largeData
|
|
|
|
|
defer func() {
|
|
|
|
|
writeTests[1].Data = nil
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// write a zip file
|
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
|
existingData := []byte{1, 2, 3, 1, 2, 3, 1, 2, 3}
|
|
|
|
|
n, _ := buf.Write(existingData)
|
2015-03-12 11:54:11 +11:00
|
|
|
w := NewWriter(buf)
|
|
|
|
|
w.SetOffset(int64(n))
|
2015-01-19 14:39:33 +01:00
|
|
|
|
|
|
|
|
for _, wt := range writeTests {
|
|
|
|
|
testCreate(t, w, &wt)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := w.Close(); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// read it back
|
|
|
|
|
r, err := NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for i, wt := range writeTests {
|
|
|
|
|
testReadFile(t, r.File[i], &wt)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2014-08-31 21:32:13 -07:00
|
|
|
func TestWriterFlush(t *testing.T) {
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
w := NewWriter(struct{ io.Writer }{&buf})
|
|
|
|
|
_, err := w.Create("foo")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if buf.Len() > 0 {
|
|
|
|
|
t.Fatalf("Unexpected %d bytes already in buffer", buf.Len())
|
|
|
|
|
}
|
|
|
|
|
if err := w.Flush(); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if buf.Len() == 0 {
|
|
|
|
|
t.Fatal("No bytes written after Flush")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2011-09-25 20:48:03 -03:00
|
|
|
func testCreate(t *testing.T, w *Writer, wt *WriteTest) {
|
2011-07-10 11:30:16 +10:00
|
|
|
header := &FileHeader{
|
2011-09-25 20:48:03 -03:00
|
|
|
Name: wt.Name,
|
|
|
|
|
Method: wt.Method,
|
|
|
|
|
}
|
|
|
|
|
if wt.Mode != 0 {
|
|
|
|
|
header.SetMode(wt.Mode)
|
2011-07-10 11:30:16 +10:00
|
|
|
}
|
|
|
|
|
f, err := w.CreateHeader(header)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2011-09-25 20:48:03 -03:00
|
|
|
_, err = f.Write(wt.Data)
|
2011-07-10 11:30:16 +10:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2011-09-25 20:48:03 -03:00
|
|
|
func testReadFile(t *testing.T, f *File, wt *WriteTest) {
|
|
|
|
|
if f.Name != wt.Name {
|
2011-09-27 09:33:26 -07:00
|
|
|
t.Fatalf("File name: got %q, want %q", f.Name, wt.Name)
|
2011-09-25 20:48:03 -03:00
|
|
|
}
|
2012-03-09 14:12:02 -08:00
|
|
|
testFileMode(t, wt.Name, f, wt.Mode)
|
2011-07-10 11:30:16 +10:00
|
|
|
rc, err := f.Open()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal("opening:", err)
|
|
|
|
|
}
|
|
|
|
|
b, err := ioutil.ReadAll(rc)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal("reading:", err)
|
|
|
|
|
}
|
|
|
|
|
err = rc.Close()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal("closing:", err)
|
|
|
|
|
}
|
2011-09-25 20:48:03 -03:00
|
|
|
if !bytes.Equal(b, wt.Data) {
|
|
|
|
|
t.Errorf("File contents %q, want %q", b, wt.Data)
|
2011-07-10 11:30:16 +10:00
|
|
|
}
|
|
|
|
|
}
|
2014-02-09 13:56:47 -08:00
|
|
|
|
|
|
|
|
func BenchmarkCompressedZipGarbage(b *testing.B) {
|
|
|
|
|
bigBuf := bytes.Repeat([]byte("a"), 1<<20)
|
2017-02-10 12:46:14 -05:00
|
|
|
|
|
|
|
|
runOnce := func(buf *bytes.Buffer) {
|
2014-02-09 13:56:47 -08:00
|
|
|
buf.Reset()
|
2017-02-10 12:46:14 -05:00
|
|
|
zw := NewWriter(buf)
|
2014-02-09 13:56:47 -08:00
|
|
|
for j := 0; j < 3; j++ {
|
|
|
|
|
w, _ := zw.CreateHeader(&FileHeader{
|
|
|
|
|
Name: "foo",
|
|
|
|
|
Method: Deflate,
|
|
|
|
|
})
|
|
|
|
|
w.Write(bigBuf)
|
|
|
|
|
}
|
|
|
|
|
zw.Close()
|
|
|
|
|
}
|
2017-02-10 12:46:14 -05:00
|
|
|
|
|
|
|
|
b.ReportAllocs()
|
|
|
|
|
// Run once and then reset the timer.
|
|
|
|
|
// This effectively discards the very large initial flate setup cost,
|
|
|
|
|
// as well as the initialization of bigBuf.
|
|
|
|
|
runOnce(&bytes.Buffer{})
|
|
|
|
|
b.ResetTimer()
|
|
|
|
|
|
|
|
|
|
b.RunParallel(func(pb *testing.PB) {
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
for pb.Next() {
|
|
|
|
|
runOnce(&buf)
|
|
|
|
|
}
|
|
|
|
|
})
|
2014-02-09 13:56:47 -08:00
|
|
|
}
|