diff --git a/.gitignore b/.gitignore deleted file mode 100644 index b6a8084..0000000 --- a/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.fuzz/ -*.zip - diff --git a/.travis.yml b/.travis.yml index 580243c..7189a09 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ language: go go: - - 1.4.3 - - 1.5.4 - - 1.6.4 - - 1.7.6 - - 1.8.3 - -script: make check +- 1.17 +- 1.16 +- 1.15 +- 1.14 +- 1.13 +- 1.12 +- 1.11 diff --git a/Makefile b/Makefile deleted file mode 100644 index 8294588..0000000 --- a/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -PACKAGE = github.com/cavaliercoder/go-cpio - -all: check - -check: - go test -v - -cpio-fuzz.zip: *.go - go-fuzz-build $(PACKAGE) - -fuzz: cpio-fuzz.zip - go-fuzz -bin=./cpio-fuzz.zip -workdir=.fuzz/ - -clean-fuzz: - rm -rf cpio-fuzz.zip .fuzz/crashers/* .fuzz/suppressions/* - - -.PHONY: all check diff --git a/README.md b/README.md index 0a322ed..6613d23 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,9 @@ -# go-cpio [![GoDoc](https://godoc.org/github.com/cavaliercoder/go-cpio?status.svg)](https://godoc.org/github.com/cavaliercoder/go-cpio) [![Build Status](https://travis-ci.org/cavaliercoder/go-cpio.svg?branch=master)](https://travis-ci.org/cavaliercoder/go-cpio) [![Go Report Card](https://goreportcard.com/badge/github.com/cavaliercoder/go-cpio)](https://goreportcard.com/report/github.com/cavaliercoder/go-cpio) +# cpio +[![Go Reference](https://pkg.go.dev/badge/github.com/cavaliergopher/cpio.svg)](https://pkg.go.dev/github.com/cavaliergopher/cpio) [![Build Status](https://app.travis-ci.com/cavaliergopher/cpio.svg?branch=main)](https://app.travis-ci.com/cavaliergopher/cpio) [![Go Report Card](https://goreportcard.com/badge/github.com/cavaliergopher/cpio)](https://goreportcard.com/report/github.com/cavaliergopher/cpio) -This package provides a Go native implementation of the CPIO archive file -format. +Package cpio provides readers and writers for the CPIO archive file format. Currently, only the SVR4 (New ASCII) format is supported, both with and without checksums. -```go -// Create a buffer to write our archive to. -buf := new(bytes.Buffer) - -// Create a new cpio archive. -w := cpio.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 license."}, -} -for _, file := range files { - hdr := &cpio.Header{ - Name: file.Name, - Mode: 0600, - Size: int64(len(file.Body)), - } - if err := w.WriteHeader(hdr); err != nil { - log.Fatalln(err) - } - if _, err := w.Write([]byte(file.Body)); err != nil { - log.Fatalln(err) - } -} -// Make sure to check the error on Close. -if err := w.Close(); err != nil { - log.Fatalln(err) -} - -// Open the cpio archive for reading. -b := bytes.NewReader(buf.Bytes()) -r := cpio.NewReader(b) - -// Iterate through the files in the archive. -for { - hdr, err := r.Next() - if err == io.EOF { - // end of cpio archive - break - } - if err != nil { - log.Fatalln(err) - } - fmt.Printf("Contents of %s:\n", hdr.Name) - if _, err := io.Copy(os.Stdout, r); err != nil { - log.Fatalln(err) - } - fmt.Println() -} -``` +Copyright 2021, Ryan Armstrong diff --git a/cpio.go b/cpio.go deleted file mode 100644 index beebf39..0000000 --- a/cpio.go +++ /dev/null @@ -1,8 +0,0 @@ -/* -Package cpio implements access to CPIO archives. Currently, only the SVR4 (New -ASCII) format is supported, both with and without checksums. - -References: - https://www.freebsd.org/cgi/man.cgi?query=cpio&sektion=5 -*/ -package cpio diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..1b0eeac --- /dev/null +++ b/doc.go @@ -0,0 +1,9 @@ +/* +Package cpio providers readers and writers for CPIO archives. Currently, only +the SVR4 (New ASCII) format is supported, both with and without checksums. + +This package aims to be feel like Go's archive/tar package. + +See the [CPIO man page](https://www.freebsd.org/cgi/man.cgi?query=cpio&sektion=5). +*/ +package cpio diff --git a/example_test.go b/example_test.go index b7ed518..5f2f723 100644 --- a/example_test.go +++ b/example_test.go @@ -7,7 +7,7 @@ import ( "log" "os" - "github.com/cavaliercoder/go-cpio" + "github.com/cavaliergopher/cpio" ) func Example() { @@ -38,14 +38,14 @@ func Example() { log.Fatalln(err) } } + // Make sure to check the error on Close. if err := w.Close(); err != nil { log.Fatalln(err) } // Open the cpio archive for reading. - b := bytes.NewReader(buf.Bytes()) - r := cpio.NewReader(b) + r := cpio.NewReader(buf) // Iterate through the files in the archive. for { diff --git a/fileinfo.go b/fileinfo.go index 55adab3..cf6d12c 100644 --- a/fileinfo.go +++ b/fileinfo.go @@ -6,70 +6,53 @@ import ( "time" ) -// headerFileInfo implements os.FileInfo. -type headerFileInfo struct { +// fileInfo implements fs.FileInfo. +type fileInfo struct { h *Header } // Name returns the base name of the file. -func (fi headerFileInfo) Name() string { +func (fi fileInfo) Name() string { if fi.IsDir() { return path.Base(path.Clean(fi.h.Name)) } return path.Base(fi.h.Name) } -func (fi headerFileInfo) Size() int64 { return fi.h.Size } -func (fi headerFileInfo) IsDir() bool { return fi.Mode().IsDir() } -func (fi headerFileInfo) ModTime() time.Time { return fi.h.ModTime } -func (fi headerFileInfo) Sys() interface{} { return fi.h } +func (fi fileInfo) Size() int64 { return fi.h.Size } +func (fi fileInfo) IsDir() bool { return fi.Mode().IsDir() } +func (fi fileInfo) ModTime() time.Time { return fi.h.ModTime } +func (fi fileInfo) Sys() interface{} { return fi.h } -func (fi headerFileInfo) Mode() (mode os.FileMode) { - // Set file permission bits. +func (fi fileInfo) Mode() (mode os.FileMode) { mode = os.FileMode(fi.h.Mode).Perm() - - // Set setuid, setgid and sticky bits. if fi.h.Mode&ModeSetuid != 0 { - // setuid mode |= os.ModeSetuid } if fi.h.Mode&ModeSetgid != 0 { - // setgid mode |= os.ModeSetgid } if fi.h.Mode&ModeSticky != 0 { - // sticky mode |= os.ModeSticky } - - // Set file mode bits. - // clear perm, setuid, setgid and sticky bits. - m := os.FileMode(fi.h.Mode) & 0170000 + m := os.FileMode(fi.h.Mode) & ModeType if m == ModeDir { - // directory mode |= os.ModeDir } if m == ModeNamedPipe { - // named pipe (FIFO) mode |= os.ModeNamedPipe } if m == ModeSymlink { - // symbolic link mode |= os.ModeSymlink } if m == ModeDevice { - // device file mode |= os.ModeDevice } if m == ModeCharDevice { - // Unix character device - mode |= os.ModeDevice - mode |= os.ModeCharDevice + mode |= os.ModeDevice | os.ModeCharDevice } if m == ModeSocket { - // Unix domain socket mode |= os.ModeSocket } - return mode } diff --git a/fuzz.go b/fuzz.go deleted file mode 100644 index 7d13d4c..0000000 --- a/fuzz.go +++ /dev/null @@ -1,35 +0,0 @@ -// +build gofuzz - -package cpio - -import "bytes" -import "io" - -// Fuzz tests the parsing and error handling of random byte arrays using -// https://github.com/dvyukov/go-fuzz. -func Fuzz(data []byte) int { - r := NewReader(bytes.NewReader(data)) - h := NewHash() - for { - hdr, err := r.Next() - if err != nil { - if hdr != nil { - panic("hdr != nil on error") - } - if err == io.EOF { - // everything worked with random input... interesting - return 1 - } - // error returned for random input. Good! - return -1 - } - - // hash file - h.Reset() - io.CopyN(h, r, hdr.Size) - h.Sum32() - - // convert file header - FileInfoHeader(hdr.FileInfo()) - } -} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7d75bdd --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/cavaliergopher/cpio + +go 1.17 diff --git a/hash.go b/hash.go index 5eae7e8..0af86d9 100644 --- a/hash.go +++ b/hash.go @@ -9,7 +9,7 @@ type digest struct { sum uint32 } -// NewHash returns a new hash.Hash32 computing the SVR4 checksum. +// NewHash returns a new hash.Hash32 for computing SVR4 checksums. func NewHash() hash.Hash32 { return &digest{} } diff --git a/header.go b/header.go index cba24e3..5654436 100644 --- a/header.go +++ b/header.go @@ -8,6 +8,7 @@ import ( ) // Mode constants from the cpio spec. +// TODO: rename to Type const ( ModeSetuid = 04000 // Set uid ModeSetgid = 02000 // Set gid @@ -30,6 +31,7 @@ const ( ) var ( + // ErrHeader indicates there was an error decoding a CPIO header entry. ErrHeader = errors.New("cpio: invalid cpio header") ) @@ -57,49 +59,49 @@ func (m FileMode) Perm() FileMode { return m & ModePerm } -// Checksum is the sum of all bytes in the file data. This sum is computed -// treating all bytes as unsigned values and using unsigned arithmetic. Only -// the least-significant 32 bits of the sum are stored. Use NewHash to compute -// the actual checksum of an archived file. -type Checksum uint32 +// A Header represents a single header in a CPIO archive. Some fields may not be +// populated. +// +// For forward compatibility, users that retrieve a Header from Reader.Next, +// mutate it in some ways, and then pass it back to Writer.WriteHeader should do +// so by creating a new Header and copying the fields that they are interested +// in preserving. +type Header struct { + Name string // Name of the file entry + Linkname string // Target name of link (valid for TypeLink or TypeSymlink) + Links int // Number of inbound links -func (c Checksum) String() string { - return fmt.Sprintf("%08X", uint32(c)) -} + Size int64 // Size in bytes + Mode FileMode // Permission and mode bits + Uid int // User id of the owner + Guid int // Group id of the owner + + ModTime time.Time // Modification time + + Checksum uint32 // Computed checksum -// A Header represents a single header in a CPIO archive. -type Header struct { DeviceID int - Inode int64 // inode number - Mode FileMode // permission and mode bits - UID int // user id of the owner - GID int // group id of the owner - Links int // number of inbound links - ModTime time.Time // modified time - Size int64 // size in bytes - Name string // filename - Linkname string // target name of link - Checksum Checksum // computed checksum + Inode int64 // Inode number pad int64 // bytes to pad before next header } -// FileInfo returns an os.FileInfo for the Header. +// FileInfo returns an fs.FileInfo for the Header. func (h *Header) FileInfo() os.FileInfo { - return headerFileInfo{h} + return fileInfo{h} } -// FileInfoHeader creates a partially-populated Header from fi. -// If fi describes a symlink, FileInfoHeader records link as the link target. -// If fi describes a directory, a slash is appended to the name. -// Because os.FileInfo's Name method returns only the base name of -// the file it describes, it may be necessary to modify the Name field -// of the returned header to provide the full path name of the file. +// FileInfoHeader creates a partially-populated Header from fi. If fi describes +// a symlink, FileInfoHeader records link as the link target. If fi describes a +// directory, a slash is appended to the name. +// +// Since fs.FileInfo's Name method returns only the base name of the file it +// describes, it may be necessary to modify Header.Name to provide the full path +// name of the file. func FileInfoHeader(fi os.FileInfo, link string) (*Header, error) { if fi == nil { return nil, errors.New("cpio: FileInfo is nil") } - if sys, ok := fi.Sys().(*Header); ok { // This FileInfo came from a Header (not the OS). Return a copy of the // original Header. @@ -107,7 +109,6 @@ func FileInfoHeader(fi os.FileInfo, link string) (*Header, error) { *h = *sys return h, nil } - fm := fi.Mode() h := &Header{ Name: fi.Name(), @@ -115,7 +116,6 @@ func FileInfoHeader(fi os.FileInfo, link string) (*Header, error) { ModTime: fi.ModTime(), Size: fi.Size(), } - switch { case fm.IsRegular(): h.Mode |= ModeRegular @@ -148,6 +148,5 @@ func FileInfoHeader(fi os.FileInfo, link string) (*Header, error) { if fm&os.ModeSticky != 0 { h.Mode |= ModeSticky } - return h, nil } diff --git a/reader.go b/reader.go index 81912b6..f2e5bec 100644 --- a/reader.go +++ b/reader.go @@ -5,10 +5,9 @@ import ( "io/ioutil" ) -// A Reader provides sequential access to the contents of a CPIO archive. A CPIO -// archive consists of a sequence of files. The Next method advances to the next -// file in the archive (including the first), and then it can be treated as an -// io.Reader to access the file's data. +// Reader provides sequential access to the contents of a CPIO archive. +// Reader.Next advances to the next file in the archive (including the first), +// and then Reader can be treated as an io.Reader to access the file's data. type Reader struct { r io.Reader // underlying file reader hdr *Header // current Header @@ -22,9 +21,13 @@ func NewReader(r io.Reader) *Reader { } } -// Read reads from the current entry in the CPIO archive. It returns 0, io.EOF -// when it reaches the end of that entry, until Next is called to advance to the -// next entry. +// Read reads from the current file in the CPIO archive. It returns (0, io.EOF) +// when it reaches the end of that file, until Next is called to advance to the +// next file. +// +// Calling Read on special types like TypeLink, TypeSymlink, TypeChar, +// TypeBlock, TypeDir, and TypeFifo returns (0, io.EOF) regardless of what the +// Header.Size claims. func (r *Reader) Read(p []byte) (n int, err error) { if r.hdr == nil || r.eof == 0 { return 0, io.EOF @@ -38,7 +41,10 @@ func (r *Reader) Read(p []byte) (n int, err error) { return } -// Next advances to the next entry in the CPIO archive. +// Next advances to the next entry in the CPIO archive. The Header.Size +// determines how many bytes can be read for the next file. Any remaining data +// in the current file is automatically discarded. +// // io.EOF is returned at the end of the input. func (r *Reader) Next() (*Header, error) { if r.hdr == nil { @@ -56,7 +62,7 @@ func (r *Reader) Next() (*Header, error) { func (r *Reader) next() (*Header, error) { r.eof = 0 - hdr, err := readHeader(r.r) + hdr, err := readSVR4Header(r.r) if err != nil { return nil, err } @@ -64,9 +70,3 @@ func (r *Reader) next() (*Header, error) { r.eof = hdr.Size return hdr, nil } - -// ReadHeader creates a new Header, reading from r. -func readHeader(r io.Reader) (*Header, error) { - // currently only SVR4 format is supported - return readSVR4Header(r) -} diff --git a/svr4.go b/svr4.go index f965554..43b0522 100644 --- a/svr4.go +++ b/svr4.go @@ -39,22 +39,22 @@ func readSVR4Header(r io.Reader) (*Header, error) { if !bytes.HasPrefix(buf[:], svr4Magic[:5]) { return nil, ErrHeader } - if buf[5] == 0x32 { // '2' + if buf[5] == '2' { hasCRC = true - } else if buf[5] != 0x31 { // '1' + } else if buf[5] != '1' { return nil, ErrHeader } asc := string(buf[:]) - hdr := &Header{} - - hdr.Inode = readHex(asc[6:14]) - hdr.Mode = FileMode(readHex(asc[14:22])) - hdr.UID = int(readHex(asc[22:30])) - hdr.GID = int(readHex(asc[30:38])) - hdr.Links = int(readHex(asc[38:46])) - hdr.ModTime = time.Unix(readHex(asc[46:54]), 0) - hdr.Size = readHex(asc[54:62]) + hdr := &Header{ + Inode: readHex(asc[6:14]), + Mode: FileMode(readHex(asc[14:22])), + Uid: int(readHex(asc[22:30])), + Guid: int(readHex(asc[30:38])), + Links: int(readHex(asc[38:46])), + ModTime: time.Unix(readHex(asc[46:54]), 0), + Size: readHex(asc[54:62]), + } if hdr.Size > svr4MaxFileSize { return nil, ErrHeader } @@ -62,7 +62,7 @@ func readSVR4Header(r io.Reader) (*Header, error) { if nameSize < 1 || nameSize > svr4MaxNameSize { return nil, ErrHeader } - hdr.Checksum = Checksum(readHex(asc[102:110])) + hdr.Checksum = uint32(readHex(asc[102:110])) if !hasCRC && hdr.Checksum != 0 { return nil, ErrHeader } @@ -115,8 +115,8 @@ func writeSVR4Header(w io.Writer, hdr *Header) (pad int64, err error) { copy(hdrBuf[:], magic) writeHex(hdrBuf[6:14], hdr.Inode) writeHex(hdrBuf[14:22], int64(hdr.Mode)) - writeHex(hdrBuf[22:30], int64(hdr.UID)) - writeHex(hdrBuf[30:38], int64(hdr.GID)) + writeHex(hdrBuf[22:30], int64(hdr.Uid)) + writeHex(hdrBuf[30:38], int64(hdr.Guid)) writeHex(hdrBuf[38:46], int64(hdr.Links)) if !hdr.ModTime.IsZero() { writeHex(hdrBuf[46:54], hdr.ModTime.Unix()) diff --git a/svr4_test.go b/svr4_test.go index 4dafcec..72fd193 100644 --- a/svr4_test.go +++ b/svr4_test.go @@ -8,21 +8,12 @@ import ( "testing" ) -var files = []struct { - Name, Body string -}{ - {"./gophers.txt", "Gopher names:\nGeorge\nGeoffrey\nGonzo"}, - {"./readme.txt", "This archive contains some text files."}, - {"./todo.txt", "Get animal handling license."}, -} - func TestRead(t *testing.T) { f, err := os.Open("testdata/test_svr4_crc.cpio") if err != nil { t.Fatalf("error opening test file: %v", err) } defer f.Close() - r := NewReader(f) for { _, err := r.Next() @@ -43,7 +34,6 @@ func TestSVR4CRC(t *testing.T) { t.Fatalf("error opening test file: %v", err) } defer f.Close() - w := NewHash() r := NewReader(f) for { @@ -54,15 +44,13 @@ func TestSVR4CRC(t *testing.T) { } return } - if hdr.Mode.IsRegular() { w.Reset() _, err = io.CopyN(w, r, hdr.Size) if err != nil { t.Fatalf("error writing to checksum hash: %v", err) } - - sum := Checksum(w.Sum32()) + sum := w.Sum32() if sum != hdr.Checksum { t.Errorf("expected checksum %v, got %v for %v", hdr.Checksum, sum, hdr.Name) } @@ -79,9 +67,6 @@ func ExampleNewHash() { defer f.Close() r := NewReader(f) - // create a Hash - h := NewHash() - // Iterate through the files in the archive. for { hdr, err := r.Next() @@ -99,18 +84,18 @@ func ExampleNewHash() { } // read file into hash - h.Reset() + h := NewHash() _, err = io.CopyN(h, r, hdr.Size) if err != nil { log.Fatal(err) } // check hash matches header checksum - sum := Checksum(h.Sum32()) + sum := h.Sum32() if sum == hdr.Checksum { - fmt.Printf("Checksum OK: %v (%v)\n", hdr.Name, hdr.Checksum) + fmt.Printf("Checksum OK: %s (%08X)\n", hdr.Name, hdr.Checksum) } else { - fmt.Printf("Checksum FAIL: %v - expected %v, got %v\n", hdr.Name, hdr.Checksum, sum) + fmt.Printf("Checksum FAIL: %s - expected %08X, got %08X\n", hdr.Name, hdr.Checksum, sum) } } diff --git a/writer.go b/writer.go index e8ed127..fb7f9ae 100644 --- a/writer.go +++ b/writer.go @@ -7,21 +7,25 @@ import ( ) var ( - ErrWriteTooLong = errors.New("cpio: write too long") + // ErrWriteTooLong indicates that an attempt was made to write more than + // Header.Size bytes to the current file. + ErrWriteTooLong = errors.New("cpio: write too long") + + // ErrWriteAfterClose indicates that an attempt was made to write to the + // CPIO archive after it was closed. ErrWriteAfterClose = errors.New("cpio: write after close") ) var trailer = &Header{ - Name: string(headerEOF), + Name: headerEOF, Links: 1, } var zeroBlock [4]byte -// A Writer provides sequential writing of a CPIO archive. A CPIO archive -// consists of a sequence of files. Call WriteHeader to begin a new file, and -// then call Write to supply that file's data, writing at most hdr.Size bytes in -// total. +// Writer provides sequential writing of a CPIO archive. Write.WriteHeader +// begins a new file with the provided Header, and then Writer can be treated as +// an io.Writer to supply that file's data. type Writer struct { w io.Writer nb int64 // number of unwritten bytes for current file entry @@ -36,7 +40,11 @@ func NewWriter(w io.Writer) *Writer { return &Writer{w: w} } -// Flush finishes writing the current file (optional). +// Flush finishes writing the current file's block padding. The current file +// must be fully written before Flush can be called. +// +// This is unnecessary as the next call to WriteHeader or Close will implicitly +// flush out the file's padding. func (w *Writer) Flush() error { if w.nb > 0 { w.err = fmt.Errorf("cpio: missed writing %d bytes", w.nb) @@ -51,9 +59,10 @@ func (w *Writer) Flush() error { return w.err } -// WriteHeader writes hdr and prepares to accept the file's contents. -// WriteHeader calls Flush if it is not the first header. Calling after a Close -// will return ErrWriteAfterClose. +// WriteHeader writes hdr and prepares to accept the file's contents. The +// Header.Size determines how many bytes can be written for the next file. If +// the current file is not fully written, then this returns an error. This +// implicitly flushes any padding necessary before writing the header. func (w *Writer) WriteHeader(hdr *Header) (err error) { if w.closed { return ErrWriteAfterClose @@ -64,7 +73,6 @@ func (w *Writer) WriteHeader(hdr *Header) (err error) { if w.err != nil { return w.err } - if hdr.Name != headerEOF { // TODO: should we be mutating hdr here? // ensure all inodes are unique @@ -89,9 +97,12 @@ func (w *Writer) WriteHeader(hdr *Header) (err error) { return } -// Write writes to the current entry in the CPIO archive. Write returns the -// error ErrWriteTooLong if more than hdr.Size bytes are written after -// WriteHeader. +// Write writes to the current file in the CPIO archive. Write returns the error +// ErrWriteTooLong if more than Header.Size bytes are written after WriteHeader. +// +// Calling Write on special types like TypeLink, TypeSymlink, TypeChar, +// TypeBlock, TypeDir, and TypeFifo returns (0, ErrWriteTooLong) regardless of +// what the Header.Size claims. func (w *Writer) Write(p []byte) (n int, err error) { if w.closed { err = ErrWriteAfterClose @@ -112,8 +123,9 @@ func (w *Writer) Write(p []byte) (n int, err error) { return } -// Close closes the CPIO archive, flushing any unwritten data to the underlying -// writer. +// Close closes the CPIO archive by flushing the padding, and writing the +// footer. If the current file (from a prior call to WriteHeader) is not fully +// written, then this returns an error. func (w *Writer) Close() error { if w.err != nil || w.closed { return w.err diff --git a/writer_test.go b/writer_test.go index 2a935b1..5b163fd 100644 --- a/writer_test.go +++ b/writer_test.go @@ -6,7 +6,7 @@ import ( "os" "testing" - cpio "github.com/cavaliercoder/go-cpio" + "github.com/cavaliergopher/cpio" ) func store(w *cpio.Writer, fn string) error { @@ -31,22 +31,18 @@ func store(w *cpio.Writer, fn string) error { return err } } - return err } func TestWriter(t *testing.T) { var buf bytes.Buffer w := cpio.NewWriter(&buf) - if err := store(w, "testdata/etc"); err != nil { t.Fatalf("store: %v", err) } - if err := store(w, "testdata/etc/hosts"); err != nil { t.Fatalf("store: %v", err) } - if err := w.Close(); err != nil { t.Fatalf("Close: %v", err) }