1 // Copyright 2011 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
5 // Package zipfs file provides an implementation of the FileSystem
6 // interface based on the contents of a .zip file.
10 // - The file paths stored in the zip file must use a slash ('/') as path
11 // separator; and they must be relative (i.e., they must not start with
12 // a '/' - this is usually the case if the file was created w/o special
14 // - The zip file system treats the file paths found in the zip internally
15 // like absolute paths w/o a leading '/'; i.e., the paths are considered
16 // relative to the root of the file system.
17 // - All path arguments to file system methods must be absolute paths.
18 package zipfs // import "golang.org/x/tools/godoc/vfs/zipfs"
32 "golang.org/x/tools/godoc/vfs"
35 // zipFI is the zip-file based implementation of FileInfo
37 name string // directory-local name
38 file *zip.File // nil for a directory
41 func (fi zipFI) Name() string {
45 func (fi zipFI) Size() int64 {
46 if f := fi.file; f != nil {
47 return int64(f.UncompressedSize)
52 func (fi zipFI) ModTime() time.Time {
53 if f := fi.file; f != nil {
56 return time.Time{} // directory has no modified time entry
59 func (fi zipFI) Mode() os.FileMode {
61 // Unix directories typically are executable, hence 555.
62 return os.ModeDir | 0555
67 func (fi zipFI) IsDir() bool {
71 func (fi zipFI) Sys() interface{} {
75 // zipFS is the zip-file based implementation of FileSystem
82 func (fs *zipFS) String() string {
83 return "zip(" + fs.name + ")"
86 func (fs *zipFS) RootType(abspath string) vfs.RootType {
89 case exists(path.Join(vfs.GOROOT, abspath)):
90 t = vfs.RootTypeGoRoot
91 case isGoPath(abspath):
92 t = vfs.RootTypeGoPath
97 func isGoPath(abspath string) bool {
98 for _, p := range filepath.SplitList(build.Default.GOPATH) {
99 if exists(path.Join(p, abspath)) {
106 func exists(path string) bool {
107 _, err := os.Stat(path)
111 func (fs *zipFS) Close() error {
113 return fs.ReadCloser.Close()
116 func zipPath(name string) (string, error) {
117 name = path.Clean(name)
118 if !path.IsAbs(name) {
119 return "", fmt.Errorf("stat: not an absolute path: %s", name)
121 return name[1:], nil // strip leading '/'
124 func isRoot(abspath string) bool {
125 return path.Clean(abspath) == "/"
128 func (fs *zipFS) stat(abspath string) (int, zipFI, error) {
135 zippath, err := zipPath(abspath)
137 return 0, zipFI{}, err
139 i, exact := fs.list.lookup(zippath)
141 // zippath has leading '/' stripped - print it explicitly
142 return -1, zipFI{}, &os.PathError{Path: "/" + zippath, Err: os.ErrNotExist}
144 _, name := path.Split(zippath)
147 file = fs.list[i] // exact match found - must be a file
149 return i, zipFI{name, file}, nil
152 func (fs *zipFS) Open(abspath string) (vfs.ReadSeekCloser, error) {
153 _, fi, err := fs.stat(abspath)
158 return nil, fmt.Errorf("Open: %s is a directory", abspath)
160 r, err := fi.file.Open()
164 return &zipSeek{fi.file, r}, nil
167 type zipSeek struct {
172 func (f *zipSeek) Seek(offset int64, whence int) (int64, error) {
173 if whence == 0 && offset == 0 {
174 r, err := f.file.Open()
182 return 0, fmt.Errorf("unsupported Seek in %s", f.file.Name)
185 func (fs *zipFS) Lstat(abspath string) (os.FileInfo, error) {
186 _, fi, err := fs.stat(abspath)
190 func (fs *zipFS) Stat(abspath string) (os.FileInfo, error) {
191 _, fi, err := fs.stat(abspath)
195 func (fs *zipFS) ReadDir(abspath string) ([]os.FileInfo, error) {
196 i, fi, err := fs.stat(abspath)
201 return nil, fmt.Errorf("ReadDir: %s is not a directory", abspath)
204 var list []os.FileInfo
206 // make dirname the prefix that file names must start with to be considered
207 // in this directory. we must special case the root directory because, per
208 // the spec of this package, zip file entries MUST NOT start with /, so we
209 // should not append /, as we would in every other case.
214 zippath, err := zipPath(abspath)
218 dirname = zippath + "/"
221 for _, e := range fs.list[i:] {
222 if !strings.HasPrefix(e.Name, dirname) {
223 break // not in the same directory anymore
225 name := e.Name[len(dirname):] // local name
227 if i := strings.IndexRune(name, '/'); i >= 0 {
228 // We infer directories from files in subdirectories.
229 // If we have x/y, return a directory entry for x.
230 name = name[0:i] // keep local directory name only
233 // If we have x/y and x/z, don't return two directory entries for x.
234 // TODO(gri): It should be possible to do this more efficiently
235 // by determining the (fs.list) range of local directory entries
236 // (via two binary searches).
237 if name != prevname {
238 list = append(list, zipFI{name, file})
246 func New(rc *zip.ReadCloser, name string) vfs.FileSystem {
247 list := make(zipList, len(rc.File))
248 copy(list, rc.File) // sort a copy of rc.File
250 return &zipFS{rc, list, name}
253 type zipList []*zip.File
255 // zipList implements sort.Interface
256 func (z zipList) Len() int { return len(z) }
257 func (z zipList) Less(i, j int) bool { return z[i].Name < z[j].Name }
258 func (z zipList) Swap(i, j int) { z[i], z[j] = z[j], z[i] }
260 // lookup returns the smallest index of an entry with an exact match
261 // for name, or an inexact match starting with name/. If there is no
262 // such entry, the result is -1, false.
263 func (z zipList) lookup(name string) (index int, exact bool) {
264 // look for exact match first (name comes before name/ in z)
265 i := sort.Search(len(z), func(i int) bool {
266 return name <= z[i].Name
272 if z[i].Name == name {
276 // look for inexact match (must be in z[i:], if present)
279 j := sort.Search(len(z), func(i int) bool {
280 return name <= z[i].Name
286 if strings.HasPrefix(z[j].Name, name) {