github.com/v2fly/tools@v0.100.0/godoc/vfs/zipfs/zipfs.go (about) 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. 18 package zipfs // import "github.com/v2fly/tools/godoc/vfs/zipfs" 19 20 import ( 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 "github.com/v2fly/tools/godoc/vfs" 33 ) 34 35 // zipFI is the zip-file based implementation of FileInfo 36 type zipFI struct { 37 name string // directory-local name 38 file *zip.File // nil for a directory 39 } 40 41 func (fi zipFI) Name() string { 42 return fi.name 43 } 44 45 func (fi zipFI) Size() int64 { 46 if f := fi.file; f != nil { 47 return int64(f.UncompressedSize) 48 } 49 return 0 // directory 50 } 51 52 func (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 59 func (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 67 func (fi zipFI) IsDir() bool { 68 return fi.file == nil 69 } 70 71 func (fi zipFI) Sys() interface{} { 72 return nil 73 } 74 75 // zipFS is the zip-file based implementation of FileSystem 76 type zipFS struct { 77 *zip.ReadCloser 78 list zipList 79 name string 80 } 81 82 func (fs *zipFS) String() string { 83 return "zip(" + fs.name + ")" 84 } 85 86 func (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 97 func 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 106 func exists(path string) bool { 107 _, err := os.Stat(path) 108 return err == nil 109 } 110 111 func (fs *zipFS) Close() error { 112 fs.list = nil 113 return fs.ReadCloser.Close() 114 } 115 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) 120 } 121 return name[1:], nil // strip leading '/' 122 } 123 124 func isRoot(abspath string) bool { 125 return path.Clean(abspath) == "/" 126 } 127 128 func (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 152 func (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 167 type zipSeek struct { 168 file *zip.File 169 io.ReadCloser 170 } 171 172 func (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 185 func (fs *zipFS) Lstat(abspath string) (os.FileInfo, error) { 186 _, fi, err := fs.stat(abspath) 187 return fi, err 188 } 189 190 func (fs *zipFS) Stat(abspath string) (os.FileInfo, error) { 191 _, fi, err := fs.stat(abspath) 192 return fi, err 193 } 194 195 func (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 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 249 sort.Sort(list) 250 return &zipFS{rc, list, name} 251 } 252 253 type zipList []*zip.File 254 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] } 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. 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 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 }