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 "golang.org/x/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 | "golang.org/x/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 | } |
292 |
Members