// Copyright 2011 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 zipfs file provides an implementation of the FileSystem // interface based on the contents of a .zip file. // // Assumptions: // // - The file paths stored in the zip file must use a slash ('/') as path // separator; and they must be relative (i.e., they must not start with // a '/' - this is usually the case if the file was created w/o special // options). // - The zip file system treats the file paths found in the zip internally // like absolute paths w/o a leading '/'; i.e., the paths are considered // relative to the root of the file system. // - All path arguments to file system methods must be absolute paths. package zipfs // import "golang.org/x/tools/godoc/vfs/zipfs" import ( "archive/zip" "fmt" "io" "os" "path" "sort" "strings" "time" "golang.org/x/tools/godoc/vfs" ) // zipFI is the zip-file based implementation of FileInfo type zipFI struct { name string // directory-local name file *zip.File // nil for a directory } func (fi zipFI) Name() string { return fi.name } func (fi zipFI) Size() int64 { if f := fi.file; f != nil { return int64(f.UncompressedSize) } return 0 // directory } func (fi zipFI) ModTime() time.Time { if f := fi.file; f != nil { return f.ModTime() } return time.Time{} // directory has no modified time entry } func (fi zipFI) Mode() os.FileMode { if fi.file == nil { // Unix directories typically are executable, hence 555. return os.ModeDir | 0555 } return 0444 } func (fi zipFI) IsDir() bool { return fi.file == nil } func (fi zipFI) Sys() interface{} { return nil } // zipFS is the zip-file based implementation of FileSystem type zipFS struct { *zip.ReadCloser list zipList name string } func (fs *zipFS) String() string { return "zip(" + fs.name + ")" } func (fs *zipFS) Close() error { fs.list = nil return fs.ReadCloser.Close() } func zipPath(name string) (string, error) { name = path.Clean(name) if !path.IsAbs(name) { return "", fmt.Errorf("stat: not an absolute path: %s", name) } return name[1:], nil // strip leading '/' } func isRoot(abspath string) bool { return path.Clean(abspath) == "/" } func (fs *zipFS) stat(abspath string) (int, zipFI, error) { if isRoot(abspath) { return 0, zipFI{ name: "", file: nil, }, nil } zippath, err := zipPath(abspath) if err != nil { return 0, zipFI{}, err } i, exact := fs.list.lookup(zippath) if i < 0 { // zippath has leading '/' stripped - print it explicitly return -1, zipFI{}, &os.PathError{Path: "/" + zippath, Err: os.ErrNotExist} } _, name := path.Split(zippath) var file *zip.File if exact { file = fs.list[i] // exact match found - must be a file } return i, zipFI{name, file}, nil } func (fs *zipFS) Open(abspath string) (vfs.ReadSeekCloser, error) { _, fi, err := fs.stat(abspath) if err != nil { return nil, err } if fi.IsDir() { return nil, fmt.Errorf("Open: %s is a directory", abspath) } r, err := fi.file.Open() if err != nil { return nil, err } return &zipSeek{fi.file, r}, nil } type zipSeek struct { file *zip.File io.ReadCloser } func (f *zipSeek) Seek(offset int64, whence int) (int64, error) { if whence == 0 && offset == 0 { r, err := f.file.Open() if err != nil { return 0, err } f.Close() f.ReadCloser = r return 0, nil } return 0, fmt.Errorf("unsupported Seek in %s", f.file.Name) } func (fs *zipFS) Lstat(abspath string) (os.FileInfo, error) { _, fi, err := fs.stat(abspath) return fi, err } func (fs *zipFS) Stat(abspath string) (os.FileInfo, error) { _, fi, err := fs.stat(abspath) return fi, err } func (fs *zipFS) ReadDir(abspath string) ([]os.FileInfo, error) { i, fi, err := fs.stat(abspath) if err != nil { return nil, err } if !fi.IsDir() { return nil, fmt.Errorf("ReadDir: %s is not a directory", abspath) } var list []os.FileInfo // make dirname the prefix that file names must start with to be considered // in this directory. we must special case the root directory because, per // the spec of this package, zip file entries MUST NOT start with /, so we // should not append /, as we would in every other case. var dirname string if isRoot(abspath) { dirname = "" } else { zippath, err := zipPath(abspath) if err != nil { return nil, err } dirname = zippath + "/" } prevname := "" for _, e := range fs.list[i:] { if !strings.HasPrefix(e.Name, dirname) { break // not in the same directory anymore } name := e.Name[len(dirname):] // local name file := e if i := strings.IndexRune(name, '/'); i >= 0 { // We infer directories from files in subdirectories. // If we have x/y, return a directory entry for x. name = name[0:i] // keep local directory name only file = nil } // If we have x/y and x/z, don't return two directory entries for x. // TODO(gri): It should be possible to do this more efficiently // by determining the (fs.list) range of local directory entries // (via two binary searches). if name != prevname { list = append(list, zipFI{name, file}) prevname = name } } return list, nil } func New(rc *zip.ReadCloser, name string) vfs.FileSystem { list := make(zipList, len(rc.File)) copy(list, rc.File) // sort a copy of rc.File sort.Sort(list) return &zipFS{rc, list, name} } type zipList []*zip.File // zipList implements sort.Interface func (z zipList) Len() int { return len(z) } func (z zipList) Less(i, j int) bool { return z[i].Name < z[j].Name } func (z zipList) Swap(i, j int) { z[i], z[j] = z[j], z[i] } // lookup returns the smallest index of an entry with an exact match // for name, or an inexact match starting with name/. If there is no // such entry, the result is -1, false. func (z zipList) lookup(name string) (index int, exact bool) { // look for exact match first (name comes before name/ in z) i := sort.Search(len(z), func(i int) bool { return name <= z[i].Name }) if i >= len(z) { return -1, false } // 0 <= i < len(z) if z[i].Name == name { return i, true } // look for inexact match (must be in z[i:], if present) z = z[i:] name += "/" j := sort.Search(len(z), func(i int) bool { return name <= z[i].Name }) if j >= len(z) { return -1, false } // 0 <= j < len(z) if strings.HasPrefix(z[j].Name, name) { return i + j, false } return -1, false }