1
0
mirror of https://github.com/golang/go synced 2024-09-29 10:24:34 -06:00

archive/zip: make Reader implement fs.FS

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>
This commit is contained in:
Russ Cox 2020-07-06 11:56:19 -04:00
parent b64202bc29
commit 1296ee6b4f
3 changed files with 203 additions and 0 deletions

View 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
}

View 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)
}
}

View 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