summaryrefslogtreecommitdiff
path: root/src/pkg/archive/zip
diff options
context:
space:
mode:
Diffstat (limited to 'src/pkg/archive/zip')
-rw-r--r--src/pkg/archive/zip/Makefile13
-rw-r--r--src/pkg/archive/zip/example_test.go75
-rw-r--r--src/pkg/archive/zip/reader.go252
-rw-r--r--src/pkg/archive/zip/reader_test.go339
-rw-r--r--src/pkg/archive/zip/struct.go230
-rw-r--r--src/pkg/archive/zip/testdata/crc32-not-streamed.zipbin0 -> 314 bytes
-rw-r--r--src/pkg/archive/zip/testdata/go-no-datadesc-sig.zipbin0 -> 330 bytes
-rw-r--r--src/pkg/archive/zip/testdata/go-with-datadesc-sig.zipbin0 -> 242 bytes
-rw-r--r--src/pkg/archive/zip/testdata/r.zipbin440 -> 0 bytes
-rw-r--r--src/pkg/archive/zip/testdata/symlink.zipbin0 -> 173 bytes
-rw-r--r--src/pkg/archive/zip/testdata/unix.zipbin0 -> 620 bytes
-rw-r--r--src/pkg/archive/zip/testdata/winxp.zipbin0 -> 412 bytes
-rw-r--r--src/pkg/archive/zip/writer.go202
-rw-r--r--src/pkg/archive/zip/writer_test.go82
-rw-r--r--src/pkg/archive/zip/zip_test.go50
15 files changed, 918 insertions, 325 deletions
diff --git a/src/pkg/archive/zip/Makefile b/src/pkg/archive/zip/Makefile
deleted file mode 100644
index 9071690f0..000000000
--- a/src/pkg/archive/zip/Makefile
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright 2010 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.
-
-include ../../../Make.inc
-
-TARG=archive/zip
-GOFILES=\
- reader.go\
- struct.go\
- writer.go\
-
-include ../../../Make.pkg
diff --git a/src/pkg/archive/zip/example_test.go b/src/pkg/archive/zip/example_test.go
new file mode 100644
index 000000000..c2ed9e79c
--- /dev/null
+++ b/src/pkg/archive/zip/example_test.go
@@ -0,0 +1,75 @@
+// Copyright 2012 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_test
+
+import (
+ "archive/zip"
+ "bytes"
+ "fmt"
+ "io"
+ "log"
+ "os"
+)
+
+func ExampleWriter() {
+ // Create a buffer to write our archive to.
+ buf := new(bytes.Buffer)
+
+ // Create a new zip archive.
+ w := zip.NewWriter(buf)
+
+ // Add some files to the archive.
+ var files = []struct {
+ Name, Body string
+ }{
+ {"readme.txt", "This archive contains some text files."},
+ {"gopher.txt", "Gopher names:\nGeorge\nGeoffrey\nGonzo"},
+ {"todo.txt", "Get animal handling licence.\nWrite more examples."},
+ }
+ for _, file := range files {
+ f, err := w.Create(file.Name)
+ if err != nil {
+ log.Fatal(err)
+ }
+ _, err = f.Write([]byte(file.Body))
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+
+ // Make sure to check the error on Close.
+ err := w.Close()
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func ExampleReader() {
+ // Open a zip archive for reading.
+ r, err := zip.OpenReader("testdata/readme.zip")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer r.Close()
+
+ // Iterate through the files in the archive,
+ // printing some of their contents.
+ for _, f := range r.File {
+ fmt.Printf("Contents of %s:\n", f.Name)
+ rc, err := f.Open()
+ if err != nil {
+ log.Fatal(err)
+ }
+ _, err = io.CopyN(os.Stdout, rc, 68)
+ if err != nil {
+ log.Fatal(err)
+ }
+ rc.Close()
+ fmt.Println()
+ }
+ // Output:
+ // Contents of README:
+ // This is the source code repository for the Go programming language.
+}
diff --git a/src/pkg/archive/zip/reader.go b/src/pkg/archive/zip/reader.go
index f92f9297a..ddd507538 100644
--- a/src/pkg/archive/zip/reader.go
+++ b/src/pkg/archive/zip/reader.go
@@ -7,18 +7,19 @@ package zip
import (
"bufio"
"compress/flate"
+ "encoding/binary"
+ "errors"
"hash"
"hash/crc32"
- "encoding/binary"
"io"
"io/ioutil"
"os"
)
var (
- FormatError = os.NewError("zip: not a valid zip file")
- UnsupportedMethod = os.NewError("zip: unsupported compression algorithm")
- ChecksumError = os.NewError("zip: checksum error")
+ ErrFormat = errors.New("zip: not a valid zip file")
+ ErrAlgorithm = errors.New("zip: unsupported compression algorithm")
+ ErrChecksum = errors.New("zip: checksum error")
)
type Reader struct {
@@ -44,7 +45,7 @@ func (f *File) hasDataDescriptor() bool {
}
// OpenReader will open the Zip file specified by name and return a ReadCloser.
-func OpenReader(name string) (*ReadCloser, os.Error) {
+func OpenReader(name string) (*ReadCloser, error) {
f, err := os.Open(name)
if err != nil {
return nil, err
@@ -55,16 +56,17 @@ func OpenReader(name string) (*ReadCloser, os.Error) {
return nil, err
}
r := new(ReadCloser)
- if err := r.init(f, fi.Size); err != nil {
+ if err := r.init(f, fi.Size()); err != nil {
f.Close()
return nil, err
}
+ r.f = f
return r, nil
}
// NewReader returns a new Reader reading from r, which is assumed to
// have the given size in bytes.
-func NewReader(r io.ReaderAt, size int64) (*Reader, os.Error) {
+func NewReader(r io.ReaderAt, size int64) (*Reader, error) {
zr := new(Reader)
if err := zr.init(r, size); err != nil {
return nil, err
@@ -72,7 +74,7 @@ func NewReader(r io.ReaderAt, size int64) (*Reader, os.Error) {
return zr, nil
}
-func (z *Reader) init(r io.ReaderAt, size int64) os.Error {
+func (z *Reader) init(r io.ReaderAt, size int64) error {
end, err := readDirectoryEnd(r, size)
if err != nil {
return err
@@ -88,12 +90,12 @@ func (z *Reader) init(r io.ReaderAt, size int64) os.Error {
// The count of files inside a zip is truncated to fit in a uint16.
// Gloss over this by reading headers until we encounter
- // a bad one, and then only report a FormatError or UnexpectedEOF if
+ // a bad one, and then only report a ErrFormat or UnexpectedEOF if
// the file count modulo 65536 is incorrect.
for {
f := &File{zipr: r, zipsize: size}
err = readDirectoryHeader(f, buf)
- if err == FormatError || err == io.ErrUnexpectedEOF {
+ if err == ErrFormat || err == io.ErrUnexpectedEOF {
break
}
if err != nil {
@@ -110,22 +112,18 @@ func (z *Reader) init(r io.ReaderAt, size int64) os.Error {
}
// Close closes the Zip file, rendering it unusable for I/O.
-func (rc *ReadCloser) Close() os.Error {
+func (rc *ReadCloser) Close() error {
return rc.f.Close()
}
// Open returns a ReadCloser that provides access to the File's contents.
-// It is safe to Open and Read from files concurrently.
-func (f *File) Open() (rc io.ReadCloser, err os.Error) {
+// Multiple files may be read concurrently.
+func (f *File) Open() (rc io.ReadCloser, err error) {
bodyOffset, err := f.findBodyOffset()
if err != nil {
return
}
size := int64(f.CompressedSize)
- if size == 0 && f.hasDataDescriptor() {
- // permit SectionReader to see the rest of the file
- size = f.zipsize - (f.headerOffset + bodyOffset)
- }
r := io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset, size)
switch f.Method {
case Store: // (no compression)
@@ -133,11 +131,14 @@ func (f *File) Open() (rc io.ReadCloser, err os.Error) {
case Deflate:
rc = flate.NewReader(r)
default:
- err = UnsupportedMethod
+ err = ErrAlgorithm
+ return
}
- if rc != nil {
- rc = &checksumReader{rc, crc32.NewIEEE(), f, r}
+ var desr io.Reader
+ if f.hasDataDescriptor() {
+ desr = io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset+size, dataDescriptorLen)
}
+ rc = &checksumReader{rc, crc32.NewIEEE(), f, desr, nil}
return
}
@@ -145,101 +146,86 @@ type checksumReader struct {
rc io.ReadCloser
hash hash.Hash32
f *File
- zipr io.Reader // for reading the data descriptor
+ desr io.Reader // if non-nil, where to read the data descriptor
+ err error // sticky error
}
-func (r *checksumReader) Read(b []byte) (n int, err os.Error) {
+func (r *checksumReader) Read(b []byte) (n int, err error) {
+ if r.err != nil {
+ return 0, r.err
+ }
n, err = r.rc.Read(b)
r.hash.Write(b[:n])
- if err != os.EOF {
+ if err == nil {
return
}
- if r.f.hasDataDescriptor() {
- if err = readDataDescriptor(r.zipr, r.f); err != nil {
- return
+ if err == io.EOF {
+ if r.desr != nil {
+ if err1 := readDataDescriptor(r.desr, r.f); err1 != nil {
+ err = err1
+ } else if r.hash.Sum32() != r.f.CRC32 {
+ err = ErrChecksum
+ }
+ } else {
+ // If there's not a data descriptor, we still compare
+ // the CRC32 of what we've read against the file header
+ // or TOC's CRC32, if it seems like it was set.
+ if r.f.CRC32 != 0 && r.hash.Sum32() != r.f.CRC32 {
+ err = ErrChecksum
+ }
}
}
- if r.hash.Sum32() != r.f.CRC32 {
- err = ChecksumError
- }
+ r.err = err
return
}
-func (r *checksumReader) Close() os.Error { return r.rc.Close() }
-
-func readFileHeader(f *File, r io.Reader) os.Error {
- var b [fileHeaderLen]byte
- if _, err := io.ReadFull(r, b[:]); err != nil {
- return err
- }
- c := binary.LittleEndian
- if sig := c.Uint32(b[:4]); sig != fileHeaderSignature {
- return FormatError
- }
- f.ReaderVersion = c.Uint16(b[4:6])
- f.Flags = c.Uint16(b[6:8])
- f.Method = c.Uint16(b[8:10])
- f.ModifiedTime = c.Uint16(b[10:12])
- f.ModifiedDate = c.Uint16(b[12:14])
- f.CRC32 = c.Uint32(b[14:18])
- f.CompressedSize = c.Uint32(b[18:22])
- f.UncompressedSize = c.Uint32(b[22:26])
- filenameLen := int(c.Uint16(b[26:28]))
- extraLen := int(c.Uint16(b[28:30]))
- d := make([]byte, filenameLen+extraLen)
- if _, err := io.ReadFull(r, d); err != nil {
- return err
- }
- f.Name = string(d[:filenameLen])
- f.Extra = d[filenameLen:]
- return nil
-}
+func (r *checksumReader) Close() error { return r.rc.Close() }
// findBodyOffset does the minimum work to verify the file has a header
// and returns the file body offset.
-func (f *File) findBodyOffset() (int64, os.Error) {
+func (f *File) findBodyOffset() (int64, error) {
r := io.NewSectionReader(f.zipr, f.headerOffset, f.zipsize-f.headerOffset)
- var b [fileHeaderLen]byte
- if _, err := io.ReadFull(r, b[:]); err != nil {
+ var buf [fileHeaderLen]byte
+ if _, err := io.ReadFull(r, buf[:]); err != nil {
return 0, err
}
- c := binary.LittleEndian
- if sig := c.Uint32(b[:4]); sig != fileHeaderSignature {
- return 0, FormatError
+ b := readBuf(buf[:])
+ if sig := b.uint32(); sig != fileHeaderSignature {
+ return 0, ErrFormat
}
- filenameLen := int(c.Uint16(b[26:28]))
- extraLen := int(c.Uint16(b[28:30]))
+ b = b[22:] // skip over most of the header
+ filenameLen := int(b.uint16())
+ extraLen := int(b.uint16())
return int64(fileHeaderLen + filenameLen + extraLen), nil
}
// readDirectoryHeader attempts to read a directory header from r.
// It returns io.ErrUnexpectedEOF if it cannot read a complete header,
-// and FormatError if it doesn't find a valid header signature.
-func readDirectoryHeader(f *File, r io.Reader) os.Error {
- var b [directoryHeaderLen]byte
- if _, err := io.ReadFull(r, b[:]); err != nil {
+// and ErrFormat if it doesn't find a valid header signature.
+func readDirectoryHeader(f *File, r io.Reader) error {
+ var buf [directoryHeaderLen]byte
+ if _, err := io.ReadFull(r, buf[:]); err != nil {
return err
}
- c := binary.LittleEndian
- if sig := c.Uint32(b[:4]); sig != directoryHeaderSignature {
- return FormatError
+ b := readBuf(buf[:])
+ if sig := b.uint32(); sig != directoryHeaderSignature {
+ return ErrFormat
}
- f.CreatorVersion = c.Uint16(b[4:6])
- f.ReaderVersion = c.Uint16(b[6:8])
- f.Flags = c.Uint16(b[8:10])
- f.Method = c.Uint16(b[10:12])
- f.ModifiedTime = c.Uint16(b[12:14])
- f.ModifiedDate = c.Uint16(b[14:16])
- f.CRC32 = c.Uint32(b[16:20])
- f.CompressedSize = c.Uint32(b[20:24])
- f.UncompressedSize = c.Uint32(b[24:28])
- filenameLen := int(c.Uint16(b[28:30]))
- extraLen := int(c.Uint16(b[30:32]))
- commentLen := int(c.Uint16(b[32:34]))
- // startDiskNumber := c.Uint16(b[34:36]) // Unused
- // internalAttributes := c.Uint16(b[36:38]) // Unused
- // externalAttributes := c.Uint32(b[38:42]) // Unused
- f.headerOffset = int64(c.Uint32(b[42:46]))
+ f.CreatorVersion = b.uint16()
+ f.ReaderVersion = b.uint16()
+ f.Flags = b.uint16()
+ f.Method = b.uint16()
+ f.ModifiedTime = b.uint16()
+ f.ModifiedDate = b.uint16()
+ f.CRC32 = b.uint32()
+ f.CompressedSize = b.uint32()
+ f.UncompressedSize = b.uint32()
+ filenameLen := int(b.uint16())
+ extraLen := int(b.uint16())
+ commentLen := int(b.uint16())
+ b = b[4:] // skipped start disk number and internal attributes (2x uint16)
+ f.ExternalAttrs = b.uint32()
+ f.headerOffset = int64(b.uint32())
d := make([]byte, filenameLen+extraLen+commentLen)
if _, err := io.ReadFull(r, d); err != nil {
return err
@@ -250,49 +236,75 @@ func readDirectoryHeader(f *File, r io.Reader) os.Error {
return nil
}
-func readDataDescriptor(r io.Reader, f *File) os.Error {
- var b [dataDescriptorLen]byte
- if _, err := io.ReadFull(r, b[:]); err != nil {
+func readDataDescriptor(r io.Reader, f *File) error {
+ var buf [dataDescriptorLen]byte
+
+ // The spec says: "Although not originally assigned a
+ // signature, the value 0x08074b50 has commonly been adopted
+ // as a signature value for the data descriptor record.
+ // Implementers should be aware that ZIP files may be
+ // encountered with or without this signature marking data
+ // descriptors and should account for either case when reading
+ // ZIP files to ensure compatibility."
+ //
+ // dataDescriptorLen includes the size of the signature but
+ // first read just those 4 bytes to see if it exists.
+ if _, err := io.ReadFull(r, buf[:4]); err != nil {
+ return err
+ }
+ off := 0
+ maybeSig := readBuf(buf[:4])
+ if maybeSig.uint32() != dataDescriptorSignature {
+ // No data descriptor signature. Keep these four
+ // bytes.
+ off += 4
+ }
+ if _, err := io.ReadFull(r, buf[off:12]); err != nil {
return err
}
- c := binary.LittleEndian
- f.CRC32 = c.Uint32(b[:4])
- f.CompressedSize = c.Uint32(b[4:8])
- f.UncompressedSize = c.Uint32(b[8:12])
+ b := readBuf(buf[:12])
+ f.CRC32 = b.uint32()
+ f.CompressedSize = b.uint32()
+ f.UncompressedSize = b.uint32()
return nil
}
-func readDirectoryEnd(r io.ReaderAt, size int64) (dir *directoryEnd, err os.Error) {
+func readDirectoryEnd(r io.ReaderAt, size int64) (dir *directoryEnd, err error) {
// look for directoryEndSignature in the last 1k, then in the last 65k
- var b []byte
+ var buf []byte
for i, bLen := range []int64{1024, 65 * 1024} {
if bLen > size {
bLen = size
}
- b = make([]byte, int(bLen))
- if _, err := r.ReadAt(b, size-bLen); err != nil && err != os.EOF {
+ buf = make([]byte, int(bLen))
+ if _, err := r.ReadAt(buf, size-bLen); err != nil && err != io.EOF {
return nil, err
}
- if p := findSignatureInBlock(b); p >= 0 {
- b = b[p:]
+ if p := findSignatureInBlock(buf); p >= 0 {
+ buf = buf[p:]
break
}
if i == 1 || bLen == size {
- return nil, FormatError
+ return nil, ErrFormat
}
}
// read header into struct
- c := binary.LittleEndian
- d := new(directoryEnd)
- d.diskNbr = c.Uint16(b[4:6])
- d.dirDiskNbr = c.Uint16(b[6:8])
- d.dirRecordsThisDisk = c.Uint16(b[8:10])
- d.directoryRecords = c.Uint16(b[10:12])
- d.directorySize = c.Uint32(b[12:16])
- d.directoryOffset = c.Uint32(b[16:20])
- d.commentLen = c.Uint16(b[20:22])
- d.comment = string(b[22 : 22+int(d.commentLen)])
+ b := readBuf(buf[4:]) // skip signature
+ d := &directoryEnd{
+ diskNbr: b.uint16(),
+ dirDiskNbr: b.uint16(),
+ dirRecordsThisDisk: b.uint16(),
+ directoryRecords: b.uint16(),
+ directorySize: b.uint32(),
+ directoryOffset: b.uint32(),
+ commentLen: b.uint16(),
+ }
+ l := int(d.commentLen)
+ if l > len(b) {
+ return nil, errors.New("zip: invalid comment length")
+ }
+ d.comment = string(b[:l])
return d, nil
}
@@ -309,3 +321,17 @@ func findSignatureInBlock(b []byte) int {
}
return -1
}
+
+type readBuf []byte
+
+func (b *readBuf) uint16() uint16 {
+ v := binary.LittleEndian.Uint16(*b)
+ *b = (*b)[2:]
+ return v
+}
+
+func (b *readBuf) uint32() uint32 {
+ v := binary.LittleEndian.Uint32(*b)
+ *b = (*b)[4:]
+ return v
+}
diff --git a/src/pkg/archive/zip/reader_test.go b/src/pkg/archive/zip/reader_test.go
index fd5fed2af..5f1d1b28a 100644
--- a/src/pkg/archive/zip/reader_test.go
+++ b/src/pkg/archive/zip/reader_test.go
@@ -7,25 +7,31 @@ package zip
import (
"bytes"
"encoding/binary"
+ "encoding/hex"
"io"
"io/ioutil"
"os"
+ "path/filepath"
+ "regexp"
"testing"
"time"
)
type ZipTest struct {
Name string
+ Source func() (r io.ReaderAt, size int64) // if non-nil, used instead of testdata/<Name> file
Comment string
File []ZipTestFile
- Error os.Error // the error that Opening this file should return
+ Error error // the error that Opening this file should return
}
type ZipTestFile struct {
- Name string
- Content []byte // if blank, will attempt to compare against File
- File string // name of file to compare to (relative to testdata/)
- Mtime string // modified time in format "mm-dd-yy hh:mm:ss"
+ Name string
+ Content []byte // if blank, will attempt to compare against File
+ ContentErr error
+ File string // name of file to compare to (relative to testdata/)
+ Mtime string // modified time in format "mm-dd-yy hh:mm:ss"
+ Mode os.FileMode
}
// Caution: The Mtime values found for the test files should correspond to
@@ -47,26 +53,45 @@ var tests = []ZipTest{
Name: "test.txt",
Content: []byte("This is a test text file.\n"),
Mtime: "09-05-10 12:12:02",
+ Mode: 0644,
},
{
Name: "gophercolor16x16.png",
File: "gophercolor16x16.png",
Mtime: "09-05-10 15:52:58",
+ Mode: 0644,
+ },
+ },
+ },
+ {
+ Name: "r.zip",
+ Source: returnRecursiveZip,
+ File: []ZipTestFile{
+ {
+ Name: "r/r.zip",
+ Content: rZipBytes(),
+ Mtime: "03-04-10 00:24:16",
+ Mode: 0666,
},
},
},
{
- Name: "r.zip",
+ Name: "symlink.zip",
File: []ZipTestFile{
{
- Name: "r/r.zip",
- File: "r.zip",
- Mtime: "03-04-10 00:24:16",
+ Name: "symlink",
+ Content: []byte("../target"),
+ Mode: 0777 | os.ModeSymlink,
},
},
},
- {Name: "readme.zip"},
- {Name: "readme.notzip", Error: FormatError},
+ {
+ Name: "readme.zip",
+ },
+ {
+ Name: "readme.notzip",
+ Error: ErrFormat,
+ },
{
Name: "dd.zip",
File: []ZipTestFile{
@@ -74,9 +99,136 @@ var tests = []ZipTest{
Name: "filename",
Content: []byte("This is a test textfile.\n"),
Mtime: "02-02-11 13:06:20",
+ Mode: 0666,
},
},
},
+ {
+ // created in windows XP file manager.
+ Name: "winxp.zip",
+ File: crossPlatform,
+ },
+ {
+ // created by Zip 3.0 under Linux
+ Name: "unix.zip",
+ File: crossPlatform,
+ },
+ {
+ // created by Go, before we wrote the "optional" data
+ // descriptor signatures (which are required by OS X)
+ Name: "go-no-datadesc-sig.zip",
+ File: []ZipTestFile{
+ {
+ Name: "foo.txt",
+ Content: []byte("foo\n"),
+ Mtime: "03-08-12 16:59:10",
+ Mode: 0644,
+ },
+ {
+ Name: "bar.txt",
+ Content: []byte("bar\n"),
+ Mtime: "03-08-12 16:59:12",
+ Mode: 0644,
+ },
+ },
+ },
+ {
+ // created by Go, after we wrote the "optional" data
+ // descriptor signatures (which are required by OS X)
+ Name: "go-with-datadesc-sig.zip",
+ File: []ZipTestFile{
+ {
+ Name: "foo.txt",
+ Content: []byte("foo\n"),
+ Mode: 0666,
+ },
+ {
+ Name: "bar.txt",
+ Content: []byte("bar\n"),
+ Mode: 0666,
+ },
+ },
+ },
+ {
+ Name: "Bad-CRC32-in-data-descriptor",
+ Source: returnCorruptCRC32Zip,
+ File: []ZipTestFile{
+ {
+ Name: "foo.txt",
+ Content: []byte("foo\n"),
+ Mode: 0666,
+ ContentErr: ErrChecksum,
+ },
+ {
+ Name: "bar.txt",
+ Content: []byte("bar\n"),
+ Mode: 0666,
+ },
+ },
+ },
+ // Tests that we verify (and accept valid) crc32s on files
+ // with crc32s in their file header (not in data descriptors)
+ {
+ Name: "crc32-not-streamed.zip",
+ File: []ZipTestFile{
+ {
+ Name: "foo.txt",
+ Content: []byte("foo\n"),
+ Mtime: "03-08-12 16:59:10",
+ Mode: 0644,
+ },
+ {
+ Name: "bar.txt",
+ Content: []byte("bar\n"),
+ Mtime: "03-08-12 16:59:12",
+ Mode: 0644,
+ },
+ },
+ },
+ // Tests that we verify (and reject invalid) crc32s on files
+ // with crc32s in their file header (not in data descriptors)
+ {
+ Name: "crc32-not-streamed.zip",
+ Source: returnCorruptNotStreamedZip,
+ File: []ZipTestFile{
+ {
+ Name: "foo.txt",
+ Content: []byte("foo\n"),
+ Mtime: "03-08-12 16:59:10",
+ Mode: 0644,
+ ContentErr: ErrChecksum,
+ },
+ {
+ Name: "bar.txt",
+ Content: []byte("bar\n"),
+ Mtime: "03-08-12 16:59:12",
+ Mode: 0644,
+ },
+ },
+ },
+}
+
+var crossPlatform = []ZipTestFile{
+ {
+ Name: "hello",
+ Content: []byte("world \r\n"),
+ Mode: 0666,
+ },
+ {
+ Name: "dir/bar",
+ Content: []byte("foo \r\n"),
+ Mode: 0666,
+ },
+ {
+ Name: "dir/empty/",
+ Content: []byte{},
+ Mode: os.ModeDir | 0777,
+ },
+ {
+ Name: "readonly",
+ Content: []byte("important \r\n"),
+ Mode: 0444,
+ },
}
func TestReader(t *testing.T) {
@@ -86,17 +238,27 @@ func TestReader(t *testing.T) {
}
func readTestZip(t *testing.T, zt ZipTest) {
- z, err := OpenReader("testdata/" + zt.Name)
+ var z *Reader
+ var err error
+ if zt.Source != nil {
+ rat, size := zt.Source()
+ z, err = NewReader(rat, size)
+ } else {
+ var rc *ReadCloser
+ rc, err = OpenReader(filepath.Join("testdata", zt.Name))
+ if err == nil {
+ z = &rc.Reader
+ }
+ }
if err != zt.Error {
t.Errorf("error=%v, want %v", err, zt.Error)
return
}
// bail if file is not zip
- if err == FormatError {
+ if err == ErrFormat {
return
}
- defer z.Close()
// bail here if no Files expected to be tested
// (there may actually be files in the zip, but we don't care)
@@ -108,12 +270,12 @@ func readTestZip(t *testing.T, zt ZipTest) {
t.Errorf("%s: comment=%q, want %q", zt.Name, z.Comment, zt.Comment)
}
if len(z.File) != len(zt.File) {
- t.Errorf("%s: file count=%d, want %d", zt.Name, len(z.File), len(zt.File))
+ t.Fatalf("%s: file count=%d, want %d", zt.Name, len(z.File), len(zt.File))
}
// test read of each file
for i, ft := range zt.File {
- readTestFile(t, ft, z.File[i])
+ readTestFile(t, zt, ft, z.File[i])
}
// test simultaneous reads
@@ -121,46 +283,35 @@ func readTestZip(t *testing.T, zt ZipTest) {
done := make(chan bool)
for i := 0; i < 5; i++ {
for j, ft := range zt.File {
- go func() {
- readTestFile(t, ft, z.File[j])
+ go func(j int, ft ZipTestFile) {
+ readTestFile(t, zt, ft, z.File[j])
done <- true
- }()
+ }(j, ft)
n++
}
}
for ; n > 0; n-- {
<-done
}
+}
+
+func readTestFile(t *testing.T, zt ZipTest, ft ZipTestFile, f *File) {
+ if f.Name != ft.Name {
+ t.Errorf("%s: name=%q, want %q", zt.Name, f.Name, ft.Name)
+ }
- // test invalid checksum
- if !z.File[0].hasDataDescriptor() { // skip test when crc32 in dd
- z.File[0].CRC32++ // invalidate
- r, err := z.File[0].Open()
+ if ft.Mtime != "" {
+ mtime, err := time.Parse("01-02-06 15:04:05", ft.Mtime)
if err != nil {
t.Error(err)
return
}
- var b bytes.Buffer
- _, err = io.Copy(&b, r)
- if err != ChecksumError {
- t.Errorf("%s: copy error=%v, want %v", z.File[0].Name, err, ChecksumError)
+ if ft := f.ModTime(); !ft.Equal(mtime) {
+ t.Errorf("%s: %s: mtime=%s, want %s", zt.Name, f.Name, ft, mtime)
}
}
-}
-
-func readTestFile(t *testing.T, ft ZipTestFile, f *File) {
- if f.Name != ft.Name {
- t.Errorf("name=%q, want %q", f.Name, ft.Name)
- }
- mtime, err := time.Parse("01-02-06 15:04:05", ft.Mtime)
- if err != nil {
- t.Error(err)
- return
- }
- if got, want := f.Mtime_ns()/1e9, mtime.Seconds(); got != want {
- t.Errorf("%s: mtime=%s (%d); want %s (%d)", f.Name, time.SecondsToUTC(got), got, mtime, want)
- }
+ testFileMode(t, zt.Name, f, ft.Mode)
size0 := f.UncompressedSize
@@ -176,14 +327,16 @@ func readTestFile(t *testing.T, ft ZipTestFile, f *File) {
}
_, err = io.Copy(&b, r)
+ if err != ft.ContentErr {
+ t.Errorf("%s: copying contents: %v (want %v)", zt.Name, err, ft.ContentErr)
+ }
if err != nil {
- t.Error(err)
return
}
r.Close()
var c []byte
- if len(ft.Content) != 0 {
+ if ft.Content != nil {
c = ft.Content
} else if c, err = ioutil.ReadFile("testdata/" + ft.File); err != nil {
t.Error(err)
@@ -203,14 +356,23 @@ func readTestFile(t *testing.T, ft ZipTestFile, f *File) {
}
}
+func testFileMode(t *testing.T, zipName string, f *File, want os.FileMode) {
+ mode := f.Mode()
+ if want == 0 {
+ t.Errorf("%s: %s mode: got %v, want none", zipName, f.Name, mode)
+ } else if mode != want {
+ t.Errorf("%s: %s mode: want %v, got %v", zipName, f.Name, want, mode)
+ }
+}
+
func TestInvalidFiles(t *testing.T) {
const size = 1024 * 70 // 70kb
b := make([]byte, size)
// zeroes
- _, err := NewReader(sliceReaderAt(b), size)
- if err != FormatError {
- t.Errorf("zeroes: error=%v, want %v", err, FormatError)
+ _, err := NewReader(bytes.NewReader(b), size)
+ if err != ErrFormat {
+ t.Errorf("zeroes: error=%v, want %v", err, ErrFormat)
}
// repeated directoryEndSignatures
@@ -219,15 +381,86 @@ func TestInvalidFiles(t *testing.T) {
for i := 0; i < size-4; i += 4 {
copy(b[i:i+4], sig)
}
- _, err = NewReader(sliceReaderAt(b), size)
- if err != FormatError {
- t.Errorf("sigs: error=%v, want %v", err, FormatError)
+ _, err = NewReader(bytes.NewReader(b), size)
+ if err != ErrFormat {
+ t.Errorf("sigs: error=%v, want %v", err, ErrFormat)
}
}
-type sliceReaderAt []byte
+func messWith(fileName string, corrupter func(b []byte)) (r io.ReaderAt, size int64) {
+ data, err := ioutil.ReadFile(filepath.Join("testdata", fileName))
+ if err != nil {
+ panic("Error reading " + fileName + ": " + err.Error())
+ }
+ corrupter(data)
+ return bytes.NewReader(data), int64(len(data))
+}
+
+func returnCorruptCRC32Zip() (r io.ReaderAt, size int64) {
+ return messWith("go-with-datadesc-sig.zip", func(b []byte) {
+ // Corrupt one of the CRC32s in the data descriptor:
+ b[0x2d]++
+ })
+}
+
+func returnCorruptNotStreamedZip() (r io.ReaderAt, size int64) {
+ return messWith("crc32-not-streamed.zip", func(b []byte) {
+ // Corrupt foo.txt's final crc32 byte, in both
+ // the file header and TOC. (0x7e -> 0x7f)
+ b[0x11]++
+ b[0x9d]++
+
+ // TODO(bradfitz): add a new test that only corrupts
+ // one of these values, and verify that that's also an
+ // error. Currently, the reader code doesn't verify the
+ // fileheader and TOC's crc32 match if they're both
+ // non-zero and only the second line above, the TOC,
+ // is what matters.
+ })
+}
+
+// rZipBytes returns the bytes of a recursive zip file, without
+// putting it on disk and triggering certain virus scanners.
+func rZipBytes() []byte {
+ s := `
+0000000 50 4b 03 04 14 00 00 00 08 00 08 03 64 3c f9 f4
+0000010 89 64 48 01 00 00 b8 01 00 00 07 00 00 00 72 2f
+0000020 72 2e 7a 69 70 00 25 00 da ff 50 4b 03 04 14 00
+0000030 00 00 08 00 08 03 64 3c f9 f4 89 64 48 01 00 00
+0000040 b8 01 00 00 07 00 00 00 72 2f 72 2e 7a 69 70 00
+0000050 2f 00 d0 ff 00 25 00 da ff 50 4b 03 04 14 00 00
+0000060 00 08 00 08 03 64 3c f9 f4 89 64 48 01 00 00 b8
+0000070 01 00 00 07 00 00 00 72 2f 72 2e 7a 69 70 00 2f
+0000080 00 d0 ff c2 54 8e 57 39 00 05 00 fa ff c2 54 8e
+0000090 57 39 00 05 00 fa ff 00 05 00 fa ff 00 14 00 eb
+00000a0 ff c2 54 8e 57 39 00 05 00 fa ff 00 05 00 fa ff
+00000b0 00 14 00 eb ff 42 88 21 c4 00 00 14 00 eb ff 42
+00000c0 88 21 c4 00 00 14 00 eb ff 42 88 21 c4 00 00 14
+00000d0 00 eb ff 42 88 21 c4 00 00 14 00 eb ff 42 88 21
+00000e0 c4 00 00 00 00 ff ff 00 00 00 ff ff 00 34 00 cb
+00000f0 ff 42 88 21 c4 00 00 00 00 ff ff 00 00 00 ff ff
+0000100 00 34 00 cb ff 42 e8 21 5e 0f 00 00 00 ff ff 0a
+0000110 f0 66 64 12 61 c0 15 dc e8 a0 48 bf 48 af 2a b3
+0000120 20 c0 9b 95 0d c4 67 04 42 53 06 06 06 40 00 06
+0000130 00 f9 ff 6d 01 00 00 00 00 42 e8 21 5e 0f 00 00
+0000140 00 ff ff 0a f0 66 64 12 61 c0 15 dc e8 a0 48 bf
+0000150 48 af 2a b3 20 c0 9b 95 0d c4 67 04 42 53 06 06
+0000160 06 40 00 06 00 f9 ff 6d 01 00 00 00 00 50 4b 01
+0000170 02 14 00 14 00 00 00 08 00 08 03 64 3c f9 f4 89
+0000180 64 48 01 00 00 b8 01 00 00 07 00 00 00 00 00 00
+0000190 00 00 00 00 00 00 00 00 00 00 00 72 2f 72 2e 7a
+00001a0 69 70 50 4b 05 06 00 00 00 00 01 00 01 00 35 00
+00001b0 00 00 6d 01 00 00 00 00`
+ s = regexp.MustCompile(`[0-9a-f]{7}`).ReplaceAllString(s, "")
+ s = regexp.MustCompile(`\s+`).ReplaceAllString(s, "")
+ b, err := hex.DecodeString(s)
+ if err != nil {
+ panic(err)
+ }
+ return b
+}
-func (r sliceReaderAt) ReadAt(b []byte, off int64) (int, os.Error) {
- copy(b, r[int(off):int(off)+len(b)])
- return len(b), nil
+func returnRecursiveZip() (r io.ReaderAt, size int64) {
+ b := rZipBytes()
+ return bytes.NewReader(b), int64(len(b))
}
diff --git a/src/pkg/archive/zip/struct.go b/src/pkg/archive/zip/struct.go
index 1d6e70f10..55f3dcfb8 100644
--- a/src/pkg/archive/zip/struct.go
+++ b/src/pkg/archive/zip/struct.go
@@ -11,8 +11,11 @@ This package does not support ZIP64 or disk spanning.
*/
package zip
-import "os"
-import "time"
+import (
+ "errors"
+ "os"
+ "time"
+)
// Compression methods.
const (
@@ -24,10 +27,18 @@ const (
fileHeaderSignature = 0x04034b50
directoryHeaderSignature = 0x02014b50
directoryEndSignature = 0x06054b50
- fileHeaderLen = 30 // + filename + extra
- directoryHeaderLen = 46 // + filename + extra + comment
- directoryEndLen = 22 // + comment
- dataDescriptorLen = 12
+ dataDescriptorSignature = 0x08074b50 // de-facto standard; required by OS X Finder
+ fileHeaderLen = 30 // + filename + extra
+ directoryHeaderLen = 46 // + filename + extra + comment
+ directoryEndLen = 22 // + comment
+ dataDescriptorLen = 16 // four uint32: descriptor signature, crc32, compressed size, size
+
+ // Constants for the first byte in CreatorVersion
+ creatorFAT = 0
+ creatorUnix = 3
+ creatorNTFS = 11
+ creatorVFAT = 14
+ creatorMacOSX = 19
)
type FileHeader struct {
@@ -42,9 +53,43 @@ type FileHeader struct {
CompressedSize uint32
UncompressedSize uint32
Extra []byte
+ ExternalAttrs uint32 // Meaning depends on CreatorVersion
Comment string
}
+// FileInfo returns an os.FileInfo for the FileHeader.
+func (h *FileHeader) FileInfo() os.FileInfo {
+ return headerFileInfo{h}
+}
+
+// headerFileInfo implements os.FileInfo.
+type headerFileInfo struct {
+ fh *FileHeader
+}
+
+func (fi headerFileInfo) Name() string { return fi.fh.Name }
+func (fi headerFileInfo) Size() int64 { return int64(fi.fh.UncompressedSize) }
+func (fi headerFileInfo) IsDir() bool { return fi.Mode().IsDir() }
+func (fi headerFileInfo) ModTime() time.Time { return fi.fh.ModTime() }
+func (fi headerFileInfo) Mode() os.FileMode { return fi.fh.Mode() }
+func (fi headerFileInfo) Sys() interface{} { return fi.fh }
+
+// FileInfoHeader creates a partially-populated FileHeader from an
+// os.FileInfo.
+func FileInfoHeader(fi os.FileInfo) (*FileHeader, error) {
+ size := fi.Size()
+ if size > (1<<32 - 1) {
+ return nil, errors.New("zip: file over 4GB")
+ }
+ fh := &FileHeader{
+ Name: fi.Name(),
+ UncompressedSize: uint32(size),
+ }
+ fh.SetModTime(fi.ModTime())
+ fh.SetMode(fi.Mode())
+ return fh, nil
+}
+
type directoryEnd struct {
diskNbr uint16 // unused
dirDiskNbr uint16 // unused
@@ -56,36 +101,165 @@ type directoryEnd struct {
comment string
}
-func recoverError(err *os.Error) {
- if e := recover(); e != nil {
- if osErr, ok := e.(os.Error); ok {
- *err = osErr
- return
- }
- panic(e)
- }
-}
-
// msDosTimeToTime converts an MS-DOS date and time into a time.Time.
// The resolution is 2s.
// See: http://msdn.microsoft.com/en-us/library/ms724247(v=VS.85).aspx
func msDosTimeToTime(dosDate, dosTime uint16) time.Time {
- return time.Time{
+ return time.Date(
// date bits 0-4: day of month; 5-8: month; 9-15: years since 1980
- Year: int64(dosDate>>9 + 1980),
- Month: int(dosDate >> 5 & 0xf),
- Day: int(dosDate & 0x1f),
+ int(dosDate>>9+1980),
+ time.Month(dosDate>>5&0xf),
+ int(dosDate&0x1f),
// time bits 0-4: second/2; 5-10: minute; 11-15: hour
- Hour: int(dosTime >> 11),
- Minute: int(dosTime >> 5 & 0x3f),
- Second: int(dosTime & 0x1f * 2),
- }
+ int(dosTime>>11),
+ int(dosTime>>5&0x3f),
+ int(dosTime&0x1f*2),
+ 0, // nanoseconds
+
+ time.UTC,
+ )
}
-// Mtime_ns returns the modified time in ns since epoch.
+// timeToMsDosTime converts a time.Time to an MS-DOS date and time.
// The resolution is 2s.
-func (h *FileHeader) Mtime_ns() int64 {
- t := msDosTimeToTime(h.ModifiedDate, h.ModifiedTime)
- return t.Seconds() * 1e9
+// See: http://msdn.microsoft.com/en-us/library/ms724274(v=VS.85).aspx
+func timeToMsDosTime(t time.Time) (fDate uint16, fTime uint16) {
+ t = t.In(time.UTC)
+ fDate = uint16(t.Day() + int(t.Month())<<5 + (t.Year()-1980)<<9)
+ fTime = uint16(t.Second()/2 + t.Minute()<<5 + t.Hour()<<11)
+ return
+}
+
+// ModTime returns the modification time.
+// The resolution is 2s.
+func (h *FileHeader) ModTime() time.Time {
+ return msDosTimeToTime(h.ModifiedDate, h.ModifiedTime)
+}
+
+// SetModTime sets the ModifiedTime and ModifiedDate fields to the given time.
+// The resolution is 2s.
+func (h *FileHeader) SetModTime(t time.Time) {
+ h.ModifiedDate, h.ModifiedTime = timeToMsDosTime(t)
+}
+
+const (
+ // Unix constants. The specification doesn't mention them,
+ // but these seem to be the values agreed on by tools.
+ s_IFMT = 0xf000
+ s_IFSOCK = 0xc000
+ s_IFLNK = 0xa000
+ s_IFREG = 0x8000
+ s_IFBLK = 0x6000
+ s_IFDIR = 0x4000
+ s_IFCHR = 0x2000
+ s_IFIFO = 0x1000
+ s_ISUID = 0x800
+ s_ISGID = 0x400
+ s_ISVTX = 0x200
+
+ msdosDir = 0x10
+ msdosReadOnly = 0x01
+)
+
+// Mode returns the permission and mode bits for the FileHeader.
+func (h *FileHeader) Mode() (mode os.FileMode) {
+ switch h.CreatorVersion >> 8 {
+ case creatorUnix, creatorMacOSX:
+ mode = unixModeToFileMode(h.ExternalAttrs >> 16)
+ case creatorNTFS, creatorVFAT, creatorFAT:
+ mode = msdosModeToFileMode(h.ExternalAttrs)
+ }
+ if len(h.Name) > 0 && h.Name[len(h.Name)-1] == '/' {
+ mode |= os.ModeDir
+ }
+ return mode
+}
+
+// SetMode changes the permission and mode bits for the FileHeader.
+func (h *FileHeader) SetMode(mode os.FileMode) {
+ h.CreatorVersion = h.CreatorVersion&0xff | creatorUnix<<8
+ h.ExternalAttrs = fileModeToUnixMode(mode) << 16
+
+ // set MSDOS attributes too, as the original zip does.
+ if mode&os.ModeDir != 0 {
+ h.ExternalAttrs |= msdosDir
+ }
+ if mode&0200 == 0 {
+ h.ExternalAttrs |= msdosReadOnly
+ }
+}
+
+func msdosModeToFileMode(m uint32) (mode os.FileMode) {
+ if m&msdosDir != 0 {
+ mode = os.ModeDir | 0777
+ } else {
+ mode = 0666
+ }
+ if m&msdosReadOnly != 0 {
+ mode &^= 0222
+ }
+ return mode
+}
+
+func fileModeToUnixMode(mode os.FileMode) uint32 {
+ var m uint32
+ switch mode & os.ModeType {
+ default:
+ m = s_IFREG
+ case os.ModeDir:
+ m = s_IFDIR
+ case os.ModeSymlink:
+ m = s_IFLNK
+ case os.ModeNamedPipe:
+ m = s_IFIFO
+ case os.ModeSocket:
+ m = s_IFSOCK
+ case os.ModeDevice:
+ if mode&os.ModeCharDevice != 0 {
+ m = s_IFCHR
+ } else {
+ m = s_IFBLK
+ }
+ }
+ if mode&os.ModeSetuid != 0 {
+ m |= s_ISUID
+ }
+ if mode&os.ModeSetgid != 0 {
+ m |= s_ISGID
+ }
+ if mode&os.ModeSticky != 0 {
+ m |= s_ISVTX
+ }
+ return m | uint32(mode&0777)
+}
+
+func unixModeToFileMode(m uint32) os.FileMode {
+ mode := os.FileMode(m & 0777)
+ switch m & s_IFMT {
+ case s_IFBLK:
+ mode |= os.ModeDevice
+ case s_IFCHR:
+ mode |= os.ModeDevice | os.ModeCharDevice
+ case s_IFDIR:
+ mode |= os.ModeDir
+ case s_IFIFO:
+ mode |= os.ModeNamedPipe
+ case s_IFLNK:
+ mode |= os.ModeSymlink
+ case s_IFREG:
+ // nothing to do
+ case s_IFSOCK:
+ mode |= os.ModeSocket
+ }
+ if m&s_ISGID != 0 {
+ mode |= os.ModeSetgid
+ }
+ if m&s_ISUID != 0 {
+ mode |= os.ModeSetuid
+ }
+ if m&s_ISVTX != 0 {
+ mode |= os.ModeSticky
+ }
+ return mode
}
diff --git a/src/pkg/archive/zip/testdata/crc32-not-streamed.zip b/src/pkg/archive/zip/testdata/crc32-not-streamed.zip
new file mode 100644
index 000000000..f268d8873
--- /dev/null
+++ b/src/pkg/archive/zip/testdata/crc32-not-streamed.zip
Binary files differ
diff --git a/src/pkg/archive/zip/testdata/go-no-datadesc-sig.zip b/src/pkg/archive/zip/testdata/go-no-datadesc-sig.zip
new file mode 100644
index 000000000..c3d593f44
--- /dev/null
+++ b/src/pkg/archive/zip/testdata/go-no-datadesc-sig.zip
Binary files differ
diff --git a/src/pkg/archive/zip/testdata/go-with-datadesc-sig.zip b/src/pkg/archive/zip/testdata/go-with-datadesc-sig.zip
new file mode 100644
index 000000000..bcfe121bb
--- /dev/null
+++ b/src/pkg/archive/zip/testdata/go-with-datadesc-sig.zip
Binary files differ
diff --git a/src/pkg/archive/zip/testdata/r.zip b/src/pkg/archive/zip/testdata/r.zip
deleted file mode 100644
index ea0fa2ffc..000000000
--- a/src/pkg/archive/zip/testdata/r.zip
+++ /dev/null
Binary files differ
diff --git a/src/pkg/archive/zip/testdata/symlink.zip b/src/pkg/archive/zip/testdata/symlink.zip
new file mode 100644
index 000000000..af846938c
--- /dev/null
+++ b/src/pkg/archive/zip/testdata/symlink.zip
Binary files differ
diff --git a/src/pkg/archive/zip/testdata/unix.zip b/src/pkg/archive/zip/testdata/unix.zip
new file mode 100644
index 000000000..ce1a981b2
--- /dev/null
+++ b/src/pkg/archive/zip/testdata/unix.zip
Binary files differ
diff --git a/src/pkg/archive/zip/testdata/winxp.zip b/src/pkg/archive/zip/testdata/winxp.zip
new file mode 100644
index 000000000..3919322f0
--- /dev/null
+++ b/src/pkg/archive/zip/testdata/winxp.zip
Binary files differ
diff --git a/src/pkg/archive/zip/writer.go b/src/pkg/archive/zip/writer.go
index 2065b06da..45eb6bd73 100644
--- a/src/pkg/archive/zip/writer.go
+++ b/src/pkg/archive/zip/writer.go
@@ -8,10 +8,10 @@ import (
"bufio"
"compress/flate"
"encoding/binary"
+ "errors"
"hash"
"hash/crc32"
"io"
- "os"
)
// TODO(adg): support zip file comments
@@ -19,7 +19,7 @@ import (
// Writer implements a zip file writer.
type Writer struct {
- *countWriter
+ cw *countWriter
dir []*header
last *fileWriter
closed bool
@@ -32,69 +32,81 @@ type header struct {
// NewWriter returns a new Writer writing a zip file to w.
func NewWriter(w io.Writer) *Writer {
- return &Writer{countWriter: &countWriter{w: bufio.NewWriter(w)}}
+ return &Writer{cw: &countWriter{w: bufio.NewWriter(w)}}
}
// Close finishes writing the zip file by writing the central directory.
// It does not (and can not) close the underlying writer.
-func (w *Writer) Close() (err os.Error) {
+func (w *Writer) Close() error {
if w.last != nil && !w.last.closed {
- if err = w.last.close(); err != nil {
- return
+ if err := w.last.close(); err != nil {
+ return err
}
w.last = nil
}
if w.closed {
- return os.NewError("zip: writer closed twice")
+ return errors.New("zip: writer closed twice")
}
w.closed = true
- defer recoverError(&err)
-
// write central directory
- start := w.count
+ start := w.cw.count
for _, h := range w.dir {
- write(w, uint32(directoryHeaderSignature))
- write(w, h.CreatorVersion)
- write(w, h.ReaderVersion)
- write(w, h.Flags)
- write(w, h.Method)
- write(w, h.ModifiedTime)
- write(w, h.ModifiedDate)
- write(w, h.CRC32)
- write(w, h.CompressedSize)
- write(w, h.UncompressedSize)
- write(w, uint16(len(h.Name)))
- write(w, uint16(len(h.Extra)))
- write(w, uint16(len(h.Comment)))
- write(w, uint16(0)) // disk number start
- write(w, uint16(0)) // internal file attributes
- write(w, uint32(0)) // external file attributes
- write(w, h.offset)
- writeBytes(w, []byte(h.Name))
- writeBytes(w, h.Extra)
- writeBytes(w, []byte(h.Comment))
+ var buf [directoryHeaderLen]byte
+ b := writeBuf(buf[:])
+ b.uint32(uint32(directoryHeaderSignature))
+ b.uint16(h.CreatorVersion)
+ b.uint16(h.ReaderVersion)
+ b.uint16(h.Flags)
+ b.uint16(h.Method)
+ b.uint16(h.ModifiedTime)
+ b.uint16(h.ModifiedDate)
+ b.uint32(h.CRC32)
+ b.uint32(h.CompressedSize)
+ b.uint32(h.UncompressedSize)
+ b.uint16(uint16(len(h.Name)))
+ b.uint16(uint16(len(h.Extra)))
+ b.uint16(uint16(len(h.Comment)))
+ b = b[4:] // skip disk number start and internal file attr (2x uint16)
+ b.uint32(h.ExternalAttrs)
+ b.uint32(h.offset)
+ if _, err := w.cw.Write(buf[:]); err != nil {
+ return err
+ }
+ if _, err := io.WriteString(w.cw, h.Name); err != nil {
+ return err
+ }
+ if _, err := w.cw.Write(h.Extra); err != nil {
+ return err
+ }
+ if _, err := io.WriteString(w.cw, h.Comment); err != nil {
+ return err
+ }
}
- end := w.count
+ end := w.cw.count
// write end record
- write(w, uint32(directoryEndSignature))
- write(w, uint16(0)) // disk number
- write(w, uint16(0)) // disk number where directory starts
- write(w, uint16(len(w.dir))) // number of entries this disk
- write(w, uint16(len(w.dir))) // number of entries total
- write(w, uint32(end-start)) // size of directory
- write(w, uint32(start)) // start of directory
- write(w, uint16(0)) // size of comment
+ var buf [directoryEndLen]byte
+ b := writeBuf(buf[:])
+ b.uint32(uint32(directoryEndSignature))
+ b = b[4:] // skip over disk number and first disk number (2x uint16)
+ b.uint16(uint16(len(w.dir))) // number of entries this disk
+ b.uint16(uint16(len(w.dir))) // number of entries total
+ b.uint32(uint32(end - start)) // size of directory
+ b.uint32(uint32(start)) // start of directory
+ // skipped size of comment (always zero)
+ if _, err := w.cw.Write(buf[:]); err != nil {
+ return err
+ }
- return w.w.(*bufio.Writer).Flush()
+ return w.cw.w.(*bufio.Writer).Flush()
}
// Create adds a file to the zip file using the provided name.
// It returns a Writer to which the file contents should be written.
// The file's contents must be written to the io.Writer before the next
// call to Create, CreateHeader, or Close.
-func (w *Writer) Create(name string) (io.Writer, os.Error) {
+func (w *Writer) Create(name string) (io.Writer, error) {
header := &FileHeader{
Name: name,
Method: Deflate,
@@ -107,7 +119,7 @@ func (w *Writer) Create(name string) (io.Writer, os.Error) {
// It returns a Writer to which the file contents should be written.
// The file's contents must be written to the io.Writer before the next
// call to Create, CreateHeader, or Close.
-func (w *Writer) CreateHeader(fh *FileHeader) (io.Writer, os.Error) {
+func (w *Writer) CreateHeader(fh *FileHeader) (io.Writer, error) {
if w.last != nil && !w.last.closed {
if err := w.last.close(); err != nil {
return nil, err
@@ -115,32 +127,36 @@ func (w *Writer) CreateHeader(fh *FileHeader) (io.Writer, os.Error) {
}
fh.Flags |= 0x8 // we will write a data descriptor
- fh.CreatorVersion = 0x14
+ fh.CreatorVersion = fh.CreatorVersion&0xff00 | 0x14
fh.ReaderVersion = 0x14
fw := &fileWriter{
- zipw: w,
- compCount: &countWriter{w: w},
+ zipw: w.cw,
+ compCount: &countWriter{w: w.cw},
crc32: crc32.NewIEEE(),
}
switch fh.Method {
case Store:
fw.comp = nopCloser{fw.compCount}
case Deflate:
- fw.comp = flate.NewWriter(fw.compCount, 5)
+ var err error
+ fw.comp, err = flate.NewWriter(fw.compCount, 5)
+ if err != nil {
+ return nil, err
+ }
default:
- return nil, UnsupportedMethod
+ return nil, ErrAlgorithm
}
fw.rawCount = &countWriter{w: fw.comp}
h := &header{
FileHeader: fh,
- offset: uint32(w.count),
+ offset: uint32(w.cw.count),
}
w.dir = append(w.dir, h)
fw.header = h
- if err := writeHeader(w, fh); err != nil {
+ if err := writeHeader(w.cw, fh); err != nil {
return nil, err
}
@@ -148,22 +164,28 @@ func (w *Writer) CreateHeader(fh *FileHeader) (io.Writer, os.Error) {
return fw, nil
}
-func writeHeader(w io.Writer, h *FileHeader) (err os.Error) {
- defer recoverError(&err)
- write(w, uint32(fileHeaderSignature))
- write(w, h.ReaderVersion)
- write(w, h.Flags)
- write(w, h.Method)
- write(w, h.ModifiedTime)
- write(w, h.ModifiedDate)
- write(w, h.CRC32)
- write(w, h.CompressedSize)
- write(w, h.UncompressedSize)
- write(w, uint16(len(h.Name)))
- write(w, uint16(len(h.Extra)))
- writeBytes(w, []byte(h.Name))
- writeBytes(w, h.Extra)
- return nil
+func writeHeader(w io.Writer, h *FileHeader) error {
+ var buf [fileHeaderLen]byte
+ b := writeBuf(buf[:])
+ b.uint32(uint32(fileHeaderSignature))
+ b.uint16(h.ReaderVersion)
+ b.uint16(h.Flags)
+ b.uint16(h.Method)
+ b.uint16(h.ModifiedTime)
+ b.uint16(h.ModifiedDate)
+ b.uint32(h.CRC32)
+ b.uint32(h.CompressedSize)
+ b.uint32(h.UncompressedSize)
+ b.uint16(uint16(len(h.Name)))
+ b.uint16(uint16(len(h.Extra)))
+ if _, err := w.Write(buf[:]); err != nil {
+ return err
+ }
+ if _, err := io.WriteString(w, h.Name); err != nil {
+ return err
+ }
+ _, err := w.Write(h.Extra)
+ return err
}
type fileWriter struct {
@@ -176,21 +198,21 @@ type fileWriter struct {
closed bool
}
-func (w *fileWriter) Write(p []byte) (int, os.Error) {
+func (w *fileWriter) Write(p []byte) (int, error) {
if w.closed {
- return 0, os.NewError("zip: write to closed file")
+ return 0, errors.New("zip: write to closed file")
}
w.crc32.Write(p)
return w.rawCount.Write(p)
}
-func (w *fileWriter) close() (err os.Error) {
+func (w *fileWriter) close() error {
if w.closed {
- return os.NewError("zip: file closed twice")
+ return errors.New("zip: file closed twice")
}
w.closed = true
- if err = w.comp.Close(); err != nil {
- return
+ if err := w.comp.Close(); err != nil {
+ return err
}
// update FileHeader
@@ -200,12 +222,14 @@ func (w *fileWriter) close() (err os.Error) {
fh.UncompressedSize = uint32(w.rawCount.count)
// write data descriptor
- defer recoverError(&err)
- write(w.zipw, fh.CRC32)
- write(w.zipw, fh.CompressedSize)
- write(w.zipw, fh.UncompressedSize)
-
- return nil
+ var buf [dataDescriptorLen]byte
+ b := writeBuf(buf[:])
+ b.uint32(dataDescriptorSignature) // de-facto standard, required by OS X
+ b.uint32(fh.CRC32)
+ b.uint32(fh.CompressedSize)
+ b.uint32(fh.UncompressedSize)
+ _, err := w.zipw.Write(buf[:])
+ return err
}
type countWriter struct {
@@ -213,7 +237,7 @@ type countWriter struct {
count int64
}
-func (w *countWriter) Write(p []byte) (int, os.Error) {
+func (w *countWriter) Write(p []byte) (int, error) {
n, err := w.w.Write(p)
w.count += int64(n)
return n, err
@@ -223,22 +247,18 @@ type nopCloser struct {
io.Writer
}
-func (w nopCloser) Close() os.Error {
+func (w nopCloser) Close() error {
return nil
}
-func write(w io.Writer, data interface{}) {
- if err := binary.Write(w, binary.LittleEndian, data); err != nil {
- panic(err)
- }
+type writeBuf []byte
+
+func (b *writeBuf) uint16(v uint16) {
+ binary.LittleEndian.PutUint16(*b, v)
+ *b = (*b)[2:]
}
-func writeBytes(w io.Writer, b []byte) {
- n, err := w.Write(b)
- if err != nil {
- panic(err)
- }
- if n != len(b) {
- panic(io.ErrShortWrite)
- }
+func (b *writeBuf) uint32(v uint32) {
+ binary.LittleEndian.PutUint32(*b, v)
+ *b = (*b)[4:]
}
diff --git a/src/pkg/archive/zip/writer_test.go b/src/pkg/archive/zip/writer_test.go
index eb2a80c3f..8b1c4dfd2 100644
--- a/src/pkg/archive/zip/writer_test.go
+++ b/src/pkg/archive/zip/writer_test.go
@@ -7,54 +7,108 @@ package zip
import (
"bytes"
"io/ioutil"
- "rand"
+ "math/rand"
+ "os"
"testing"
)
// TODO(adg): a more sophisticated test suite
-const testString = "Rabbits, guinea pigs, gophers, marsupial rats, and quolls."
+type WriteTest struct {
+ Name string
+ Data []byte
+ Method uint16
+ Mode os.FileMode
+}
+
+var writeTests = []WriteTest{
+ {
+ Name: "foo",
+ Data: []byte("Rabbits, guinea pigs, gophers, marsupial rats, and quolls."),
+ Method: Store,
+ Mode: 0666,
+ },
+ {
+ Name: "bar",
+ Data: nil, // large data set in the test
+ Method: Deflate,
+ 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,
+ },
+ {
+ Name: "symlink",
+ Data: []byte("../link/target"),
+ Method: Deflate,
+ Mode: 0755 | os.ModeSymlink,
+ },
+}
func TestWriter(t *testing.T) {
largeData := make([]byte, 1<<17)
for i := range largeData {
largeData[i] = byte(rand.Int())
}
+ writeTests[1].Data = largeData
+ defer func() {
+ writeTests[1].Data = nil
+ }()
// write a zip file
buf := new(bytes.Buffer)
w := NewWriter(buf)
- testCreate(t, w, "foo", []byte(testString), Store)
- testCreate(t, w, "bar", largeData, Deflate)
+
+ for _, wt := range writeTests {
+ testCreate(t, w, &wt)
+ }
+
if err := w.Close(); err != nil {
t.Fatal(err)
}
// read it back
- r, err := NewReader(sliceReaderAt(buf.Bytes()), int64(buf.Len()))
+ r, err := NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
if err != nil {
t.Fatal(err)
}
- testReadFile(t, r.File[0], []byte(testString))
- testReadFile(t, r.File[1], largeData)
+ for i, wt := range writeTests {
+ testReadFile(t, r.File[i], &wt)
+ }
}
-func testCreate(t *testing.T, w *Writer, name string, data []byte, method uint16) {
+func testCreate(t *testing.T, w *Writer, wt *WriteTest) {
header := &FileHeader{
- Name: name,
- Method: method,
+ Name: wt.Name,
+ Method: wt.Method,
+ }
+ if wt.Mode != 0 {
+ header.SetMode(wt.Mode)
}
f, err := w.CreateHeader(header)
if err != nil {
t.Fatal(err)
}
- _, err = f.Write(data)
+ _, err = f.Write(wt.Data)
if err != nil {
t.Fatal(err)
}
}
-func testReadFile(t *testing.T, f *File, data []byte) {
+func testReadFile(t *testing.T, f *File, wt *WriteTest) {
+ if f.Name != wt.Name {
+ t.Fatalf("File name: got %q, want %q", f.Name, wt.Name)
+ }
+ testFileMode(t, wt.Name, f, wt.Mode)
rc, err := f.Open()
if err != nil {
t.Fatal("opening:", err)
@@ -67,7 +121,7 @@ func testReadFile(t *testing.T, f *File, data []byte) {
if err != nil {
t.Fatal("closing:", err)
}
- if !bytes.Equal(b, data) {
- t.Errorf("File contents %q, want %q", b, data)
+ if !bytes.Equal(b, wt.Data) {
+ t.Errorf("File contents %q, want %q", b, wt.Data)
}
}
diff --git a/src/pkg/archive/zip/zip_test.go b/src/pkg/archive/zip/zip_test.go
index 0f71fdfac..d6490c4cb 100644
--- a/src/pkg/archive/zip/zip_test.go
+++ b/src/pkg/archive/zip/zip_test.go
@@ -9,20 +9,12 @@ package zip
import (
"bytes"
"fmt"
- "os"
+ "reflect"
+ "strings"
"testing"
+ "time"
)
-type stringReaderAt string
-
-func (s stringReaderAt) ReadAt(p []byte, off int64) (n int, err os.Error) {
- if off >= int64(len(s)) {
- return 0, os.EOF
- }
- n = copy(p, s[off:])
- return
-}
-
func TestOver65kFiles(t *testing.T) {
if testing.Short() {
t.Logf("slow test; skipping")
@@ -40,8 +32,8 @@ func TestOver65kFiles(t *testing.T) {
if err := w.Close(); err != nil {
t.Fatalf("Writer.Close: %v", err)
}
- rat := stringReaderAt(buf.String())
- zr, err := NewReader(rat, int64(len(rat)))
+ s := buf.String()
+ zr, err := NewReader(strings.NewReader(s), int64(len(s)))
if err != nil {
t.Fatalf("NewReader: %v", err)
}
@@ -55,3 +47,35 @@ func TestOver65kFiles(t *testing.T) {
}
}
}
+
+func TestModTime(t *testing.T) {
+ var testTime = time.Date(2009, time.November, 10, 23, 45, 58, 0, time.UTC)
+ fh := new(FileHeader)
+ fh.SetModTime(testTime)
+ outTime := fh.ModTime()
+ if !outTime.Equal(testTime) {
+ t.Errorf("times don't match: got %s, want %s", outTime, testTime)
+ }
+}
+
+func TestFileHeaderRoundTrip(t *testing.T) {
+ fh := &FileHeader{
+ Name: "foo.txt",
+ UncompressedSize: 987654321,
+ ModifiedTime: 1234,
+ ModifiedDate: 5678,
+ }
+ fi := fh.FileInfo()
+ fh2, err := FileInfoHeader(fi)
+
+ // Ignore these fields:
+ fh2.CreatorVersion = 0
+ fh2.ExternalAttrs = 0
+
+ if !reflect.DeepEqual(fh, fh2) {
+ t.Errorf("mismatch\n input=%#v\noutput=%#v\nerr=%v", fh, fh2, err)
+ }
+ if sysfh, ok := fi.Sys().(*FileHeader); !ok && sysfh != fh {
+ t.Errorf("Sys didn't return original *FileHeader")
+ }
+}