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. 4 5// Package zipfs file provides an implementation of the FileSystem 6// interface based on the contents of a .zip file. 7// 8// Assumptions: 9// 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 13// options). 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. 18package zipfs // import "golang.org/x/tools/godoc/vfs/zipfs" 19 20import ( 21 "archive/zip" 22 "fmt" 23 "go/build" 24 "io" 25 "os" 26 "path" 27 "path/filepath" 28 "sort" 29 "strings" 30 "time" 31 32 "golang.org/x/tools/godoc/vfs" 33) 34 35// zipFI is the zip-file based implementation of FileInfo 36type zipFI struct { 37 name string // directory-local name 38 file *zip.File // nil for a directory 39} 40 41func (fi zipFI) Name() string { 42 return fi.name 43} 44 45func (fi zipFI) Size() int64 { 46 if f := fi.file; f != nil { 47 return int64(f.UncompressedSize) 48 } 49 return 0 // directory 50} 51 52func (fi zipFI) ModTime() time.Time { 53 if f := fi.file; f != nil { 54 return f.ModTime() 55 } 56 return time.Time{} // directory has no modified time entry 57} 58 59func (fi zipFI) Mode() os.FileMode { 60 if fi.file == nil { 61 // Unix directories typically are executable, hence 555. 62 return os.ModeDir | 0555 63 } 64 return 0444 65} 66 67func (fi zipFI) IsDir() bool { 68 return fi.file == nil 69} 70 71func (fi zipFI) Sys() interface{} { 72 return nil 73} 74 75// zipFS is the zip-file based implementation of FileSystem 76type zipFS struct { 77 *zip.ReadCloser 78 list zipList 79 name string 80} 81 82func (fs *zipFS) String() string { 83 return "zip(" + fs.name + ")" 84} 85 86func (fs *zipFS) RootType(abspath string) vfs.RootType { 87 var t vfs.RootType 88 switch { 89 case exists(path.Join(vfs.GOROOT, abspath)): 90 t = vfs.RootTypeGoRoot 91 case isGoPath(abspath): 92 t = vfs.RootTypeGoPath 93 } 94 return t 95} 96 97func isGoPath(abspath string) bool { 98 for _, p := range filepath.SplitList(build.Default.GOPATH) { 99 if exists(path.Join(p, abspath)) { 100 return true 101 } 102 } 103 return false 104} 105 106func exists(path string) bool { 107 _, err := os.Stat(path) 108 return err == nil 109} 110 111func (fs *zipFS) Close() error { 112 fs.list = nil 113 return fs.ReadCloser.Close() 114} 115 116func 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) 120 } 121 return name[1:], nil // strip leading '/' 122} 123 124func isRoot(abspath string) bool { 125 return path.Clean(abspath) == "/" 126} 127 128func (fs *zipFS) stat(abspath string) (int, zipFI, error) { 129 if isRoot(abspath) { 130 return 0, zipFI{ 131 name: "", 132 file: nil, 133 }, nil 134 } 135 zippath, err := zipPath(abspath) 136 if err != nil { 137 return 0, zipFI{}, err 138 } 139 i, exact := fs.list.lookup(zippath) 140 if i < 0 { 141 // zippath has leading '/' stripped - print it explicitly 142 return -1, zipFI{}, &os.PathError{Path: "/" + zippath, Err: os.ErrNotExist} 143 } 144 _, name := path.Split(zippath) 145 var file *zip.File 146 if exact { 147 file = fs.list[i] // exact match found - must be a file 148 } 149 return i, zipFI{name, file}, nil 150} 151 152func (fs *zipFS) Open(abspath string) (vfs.ReadSeekCloser, error) { 153 _, fi, err := fs.stat(abspath) 154 if err != nil { 155 return nil, err 156 } 157 if fi.IsDir() { 158 return nil, fmt.Errorf("Open: %s is a directory", abspath) 159 } 160 r, err := fi.file.Open() 161 if err != nil { 162 return nil, err 163 } 164 return &zipSeek{fi.file, r}, nil 165} 166 167type zipSeek struct { 168 file *zip.File 169 io.ReadCloser 170} 171 172func (f *zipSeek) Seek(offset int64, whence int) (int64, error) { 173 if whence == 0 && offset == 0 { 174 r, err := f.file.Open() 175 if err != nil { 176 return 0, err 177 } 178 f.Close() 179 f.ReadCloser = r 180 return 0, nil 181 } 182 return 0, fmt.Errorf("unsupported Seek in %s", f.file.Name) 183} 184 185func (fs *zipFS) Lstat(abspath string) (os.FileInfo, error) { 186 _, fi, err := fs.stat(abspath) 187 return fi, err 188} 189 190func (fs *zipFS) Stat(abspath string) (os.FileInfo, error) { 191 _, fi, err := fs.stat(abspath) 192 return fi, err 193} 194 195func (fs *zipFS) ReadDir(abspath string) ([]os.FileInfo, error) { 196 i, fi, err := fs.stat(abspath) 197 if err != nil { 198 return nil, err 199 } 200 if !fi.IsDir() { 201 return nil, fmt.Errorf("ReadDir: %s is not a directory", abspath) 202 } 203 204 var list []os.FileInfo 205 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. 210 var dirname string 211 if isRoot(abspath) { 212 dirname = "" 213 } else { 214 zippath, err := zipPath(abspath) 215 if err != nil { 216 return nil, err 217 } 218 dirname = zippath + "/" 219 } 220 prevname := "" 221 for _, e := range fs.list[i:] { 222 if !strings.HasPrefix(e.Name, dirname) { 223 break // not in the same directory anymore 224 } 225 name := e.Name[len(dirname):] // local name 226 file := e 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 231 file = nil 232 } 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}) 239 prevname = name 240 } 241 } 242 243 return list, nil 244} 245 246func 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 249 sort.Sort(list) 250 return &zipFS{rc, list, name} 251} 252 253type zipList []*zip.File 254 255// zipList implements sort.Interface 256func (z zipList) Len() int { return len(z) } 257func (z zipList) Less(i, j int) bool { return z[i].Name < z[j].Name } 258func (z zipList) Swap(i, j int) { z[i], z[j] = z[j], z[i] } 259 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. 263func (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 267 }) 268 if i >= len(z) { 269 return -1, false 270 } 271 // 0 <= i < len(z) 272 if z[i].Name == name { 273 return i, true 274 } 275 276 // look for inexact match (must be in z[i:], if present) 277 z = z[i:] 278 name += "/" 279 j := sort.Search(len(z), func(i int) bool { 280 return name <= z[i].Name 281 }) 282 if j >= len(z) { 283 return -1, false 284 } 285 // 0 <= j < len(z) 286 if strings.HasPrefix(z[j].Name, name) { 287 return i + j, false 288 } 289 290 return -1, false 291} 292