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