]> Cypherpunks repositories - gostls13.git/commitdiff
archive/zip: make Reader implement fs.FS
authorRuss Cox <rsc@golang.org>
Mon, 6 Jul 2020 15:56:19 +0000 (11:56 -0400)
committerRuss Cox <rsc@golang.org>
Tue, 20 Oct 2020 18:41:12 +0000 (18:41 +0000)
Now a zip.Reader (an open zip file) can be passed to code
that accepts a file system, such as (soon) template parsing.

For #41190.

Change-Id: If51b12e39db3ccc27f643c2453d3300a38035360
Reviewed-on: https://go-review.googlesource.com/c/go/+/243937
Trust: Russ Cox <rsc@golang.org>
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Rob Pike <r@golang.org>
src/archive/zip/reader.go
src/archive/zip/reader_test.go
src/archive/zip/struct.go

index 13ff9ddcf42cc33b36484429652526612fa2887c..5c9f3dea28197133d28e3519c0f7429c5b916417 100644 (file)
@@ -11,7 +11,12 @@ import (
        "hash"
        "hash/crc32"
        "io"
+       "io/fs"
        "os"
+       "path"
+       "sort"
+       "strings"
+       "sync"
        "time"
 )
 
@@ -21,18 +26,28 @@ var (
        ErrChecksum  = errors.New("zip: checksum error")
 )
 
+// A Reader serves content from a ZIP archive.
 type Reader struct {
        r             io.ReaderAt
        File          []*File
        Comment       string
        decompressors map[uint16]Decompressor
+
+       // fileList is a list of files sorted by ename,
+       // for use by the Open method.
+       fileListOnce sync.Once
+       fileList     []fileListEntry
 }
 
+// A ReadCloser is a Reader that must be closed when no longer needed.
 type ReadCloser struct {
        f *os.File
        Reader
 }
 
+// A File is a single file in a ZIP archive.
+// The file information is in the embedded FileHeader.
+// The file content can be accessed by calling Open.
 type File struct {
        FileHeader
        zip          *Reader
@@ -187,6 +202,10 @@ type checksumReader struct {
        err   error     // sticky error
 }
 
+func (r *checksumReader) Stat() (fs.FileInfo, error) {
+       return headerFileInfo{&r.f.FileHeader}, nil
+}
+
 func (r *checksumReader) Read(b []byte) (n int, err error) {
        if r.err != nil {
                return 0, r.err
@@ -607,3 +626,173 @@ func (b *readBuf) sub(n int) readBuf {
        *b = (*b)[n:]
        return b2
 }
+
+// A fileListEntry is a File and its ename.
+// If file == nil, the fileListEntry describes a directory, without metadata.
+type fileListEntry struct {
+       name string
+       file *File // nil for directories
+}
+
+type fileInfoDirEntry interface {
+       fs.FileInfo
+       fs.DirEntry
+}
+
+func (e *fileListEntry) stat() fileInfoDirEntry {
+       if e.file != nil {
+               return headerFileInfo{&e.file.FileHeader}
+       }
+       return e
+}
+
+// Only used for directories.
+func (f *fileListEntry) Name() string       { _, elem, _ := split(f.name); return elem }
+func (f *fileListEntry) Size() int64        { return 0 }
+func (f *fileListEntry) ModTime() time.Time { return time.Time{} }
+func (f *fileListEntry) Mode() fs.FileMode  { return fs.ModeDir | 0555 }
+func (f *fileListEntry) Type() fs.FileMode  { return fs.ModeDir }
+func (f *fileListEntry) IsDir() bool        { return true }
+func (f *fileListEntry) Sys() interface{}   { return nil }
+
+func (f *fileListEntry) Info() (fs.FileInfo, error) { return f, nil }
+
+// toValidName coerces name to be a valid name for fs.FS.Open.
+func toValidName(name string) string {
+       name = strings.ReplaceAll(name, `\`, `/`)
+       p := path.Clean(name)
+       if strings.HasPrefix(p, "/") {
+               p = p[len("/"):]
+       }
+       for strings.HasPrefix(name, "../") {
+               p = p[len("../"):]
+       }
+       return p
+}
+
+func (r *Reader) initFileList() {
+       r.fileListOnce.Do(func() {
+               dirs := make(map[string]bool)
+               for _, file := range r.File {
+                       name := toValidName(file.Name)
+                       for dir := path.Dir(name); dir != "."; dir = path.Dir(dir) {
+                               dirs[dir] = true
+                       }
+                       r.fileList = append(r.fileList, fileListEntry{name, file})
+               }
+               for dir := range dirs {
+                       r.fileList = append(r.fileList, fileListEntry{dir + "/", nil})
+               }
+
+               sort.Slice(r.fileList, func(i, j int) bool { return fileEntryLess(r.fileList[i].name, r.fileList[j].name) })
+       })
+}
+
+func fileEntryLess(x, y string) bool {
+       xdir, xelem, _ := split(x)
+       ydir, yelem, _ := split(y)
+       return xdir < ydir || xdir == ydir && xelem < yelem
+}
+
+// Open opens the named file in the ZIP archive,
+// using the semantics of io.FS.Open:
+// paths are always slash separated, with no
+// leading / or ../ elements.
+func (r *Reader) Open(name string) (fs.File, error) {
+       r.initFileList()
+
+       e := r.openLookup(name)
+       if e == nil || !fs.ValidPath(name) {
+               return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
+       }
+       if e.file == nil || strings.HasSuffix(e.file.Name, "/") {
+               return &openDir{e, r.openReadDir(name), 0}, nil
+       }
+       rc, err := e.file.Open()
+       if err != nil {
+               return nil, err
+       }
+       return rc.(fs.File), nil
+}
+
+func split(name string) (dir, elem string, isDir bool) {
+       if name[len(name)-1] == '/' {
+               isDir = true
+               name = name[:len(name)-1]
+       }
+       i := len(name) - 1
+       for i >= 0 && name[i] != '/' {
+               i--
+       }
+       if i < 0 {
+               return ".", name, isDir
+       }
+       return name[:i], name[i+1:], isDir
+}
+
+var dotFile = &fileListEntry{name: "./"}
+
+func (r *Reader) openLookup(name string) *fileListEntry {
+       if name == "." {
+               return dotFile
+       }
+
+       dir, elem, _ := split(name)
+       files := r.fileList
+       i := sort.Search(len(files), func(i int) bool {
+               idir, ielem, _ := split(files[i].name)
+               return idir > dir || idir == dir && ielem >= elem
+       })
+       if i < len(files) {
+               fname := files[i].name
+               if fname == name || len(fname) == len(name)+1 && fname[len(name)] == '/' && fname[:len(name)] == name {
+                       return &files[i]
+               }
+       }
+       return nil
+}
+
+func (r *Reader) openReadDir(dir string) []fileListEntry {
+       files := r.fileList
+       i := sort.Search(len(files), func(i int) bool {
+               idir, _, _ := split(files[i].name)
+               return idir >= dir
+       })
+       j := sort.Search(len(files), func(j int) bool {
+               jdir, _, _ := split(files[j].name)
+               return jdir > dir
+       })
+       return files[i:j]
+}
+
+type openDir struct {
+       e      *fileListEntry
+       files  []fileListEntry
+       offset int
+}
+
+func (d *openDir) Close() error               { return nil }
+func (d *openDir) Stat() (fs.FileInfo, error) { return d.e.stat(), nil }
+
+func (d *openDir) Read([]byte) (int, error) {
+       return 0, &fs.PathError{Op: "read", Path: d.e.name, Err: errors.New("is a directory")}
+}
+
+func (d *openDir) ReadDir(count int) ([]fs.DirEntry, error) {
+       n := len(d.files) - d.offset
+       if count > 0 && n > count {
+               n = count
+       }
+       if n == 0 {
+               if count <= 0 {
+                       return nil, nil
+               }
+               return nil, io.EOF
+       }
+       list := make([]fs.DirEntry, n)
+       for i := range list {
+               list[i] = d.files[d.offset+i].stat()
+       }
+       d.offset += n
+       return list, nil
+}
index 8a32d9c7dc9b76413ac8a4c6eba17fd70ad54ef1..38ff7badd0e067603ffc3cf8b66ad91113cac489 100644 (file)
@@ -17,6 +17,7 @@ import (
        "regexp"
        "strings"
        "testing"
+       "testing/fstest"
        "time"
 )
 
@@ -1071,3 +1072,13 @@ func TestIssue12449(t *testing.T) {
                t.Errorf("Error reading the archive: %v", err)
        }
 }
+
+func TestFS(t *testing.T) {
+       z, err := OpenReader("testdata/unix.zip")
+       if err != nil {
+               t.Fatal(err)
+       }
+       if err := fstest.TestFS(z, "hello", "dir/bar", "dir/empty", "readonly"); err != nil {
+               t.Fatal(err)
+       }
+}
index 355c57051bfb13b77a9282b836271ccfcde9ba05..4dd29f35fa63f6e8d898ab2b55faa46b3611d918 100644 (file)
@@ -162,8 +162,11 @@ func (fi headerFileInfo) ModTime() time.Time {
        return fi.fh.Modified.UTC()
 }
 func (fi headerFileInfo) Mode() fs.FileMode { return fi.fh.Mode() }
+func (fi headerFileInfo) Type() fs.FileMode { return fi.fh.Mode().Type() }
 func (fi headerFileInfo) Sys() interface{}  { return fi.fh }
 
+func (fi headerFileInfo) Info() (fs.FileInfo, error) { return fi, nil }
+
 // FileInfoHeader creates a partially-populated FileHeader from an
 // fs.FileInfo.
 // Because fs.FileInfo's Name method returns only the base name of