diff options
Diffstat (limited to 'src/pkg/archive/zip')
-rw-r--r-- | src/pkg/archive/zip/Makefile | 13 | ||||
-rw-r--r-- | src/pkg/archive/zip/example_test.go | 75 | ||||
-rw-r--r-- | src/pkg/archive/zip/reader.go | 252 | ||||
-rw-r--r-- | src/pkg/archive/zip/reader_test.go | 339 | ||||
-rw-r--r-- | src/pkg/archive/zip/struct.go | 230 | ||||
-rw-r--r-- | src/pkg/archive/zip/testdata/crc32-not-streamed.zip | bin | 0 -> 314 bytes | |||
-rw-r--r-- | src/pkg/archive/zip/testdata/go-no-datadesc-sig.zip | bin | 0 -> 330 bytes | |||
-rw-r--r-- | src/pkg/archive/zip/testdata/go-with-datadesc-sig.zip | bin | 0 -> 242 bytes | |||
-rw-r--r-- | src/pkg/archive/zip/testdata/r.zip | bin | 440 -> 0 bytes | |||
-rw-r--r-- | src/pkg/archive/zip/testdata/symlink.zip | bin | 0 -> 173 bytes | |||
-rw-r--r-- | src/pkg/archive/zip/testdata/unix.zip | bin | 0 -> 620 bytes | |||
-rw-r--r-- | src/pkg/archive/zip/testdata/winxp.zip | bin | 0 -> 412 bytes | |||
-rw-r--r-- | src/pkg/archive/zip/writer.go | 202 | ||||
-rw-r--r-- | src/pkg/archive/zip/writer_test.go | 82 | ||||
-rw-r--r-- | src/pkg/archive/zip/zip_test.go | 50 |
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 Binary files differnew file mode 100644 index 000000000..f268d8873 --- /dev/null +++ b/src/pkg/archive/zip/testdata/crc32-not-streamed.zip diff --git a/src/pkg/archive/zip/testdata/go-no-datadesc-sig.zip b/src/pkg/archive/zip/testdata/go-no-datadesc-sig.zip Binary files differnew file mode 100644 index 000000000..c3d593f44 --- /dev/null +++ b/src/pkg/archive/zip/testdata/go-no-datadesc-sig.zip diff --git a/src/pkg/archive/zip/testdata/go-with-datadesc-sig.zip b/src/pkg/archive/zip/testdata/go-with-datadesc-sig.zip Binary files differnew file mode 100644 index 000000000..bcfe121bb --- /dev/null +++ b/src/pkg/archive/zip/testdata/go-with-datadesc-sig.zip diff --git a/src/pkg/archive/zip/testdata/r.zip b/src/pkg/archive/zip/testdata/r.zip Binary files differdeleted file mode 100644 index ea0fa2ffc..000000000 --- a/src/pkg/archive/zip/testdata/r.zip +++ /dev/null diff --git a/src/pkg/archive/zip/testdata/symlink.zip b/src/pkg/archive/zip/testdata/symlink.zip Binary files differnew file mode 100644 index 000000000..af846938c --- /dev/null +++ b/src/pkg/archive/zip/testdata/symlink.zip diff --git a/src/pkg/archive/zip/testdata/unix.zip b/src/pkg/archive/zip/testdata/unix.zip Binary files differnew file mode 100644 index 000000000..ce1a981b2 --- /dev/null +++ b/src/pkg/archive/zip/testdata/unix.zip diff --git a/src/pkg/archive/zip/testdata/winxp.zip b/src/pkg/archive/zip/testdata/winxp.zip Binary files differnew file mode 100644 index 000000000..3919322f0 --- /dev/null +++ b/src/pkg/archive/zip/testdata/winxp.zip 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") + } +} |