github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/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/powerman/golang-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/powerman/golang-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  }