github.com/jfrog/jfrog-cli-core@v1.12.1/artifactory/utils/golang/project/archive.go (about)

     1  package project
     2  
     3  // The code in this file was copied from https://github.com/golang/go
     4  // which is under this license https://github.com/golang/go/blob/master/LICENSE
     5  // Copyright (c) 2009 The Go Authors. All rights reserved.
     6  
     7  // Redistribution and use in source and binary forms, with or without
     8  // modification, are permitted provided that the following conditions are
     9  // met:
    10  
    11  //    * Redistributions of source code must retain the above copyright
    12  // notice, this list of conditions and the following disclaimer.
    13  //    * Redistributions in binary form must reproduce the above
    14  // copyright notice, this list of conditions and the following disclaimer
    15  // in the documentation and/or other materials provided with the
    16  // distribution.
    17  //    * Neither the name of Google Inc. nor the names of its
    18  // contributors may be used to endorse or promote products derived from
    19  // this software without specific prior written permission.
    20  
    21  // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    22  // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    23  // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    24  // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
    25  // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    26  // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
    27  // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    28  // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
    29  // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    30  // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    31  // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    32  import (
    33  	"archive/zip"
    34  	"bytes"
    35  	"fmt"
    36  	"io"
    37  	"os"
    38  	"path"
    39  	"path/filepath"
    40  	"strings"
    41  	"unicode"
    42  	"unicode/utf8"
    43  
    44  	"golang.org/x/mod/module"
    45  )
    46  
    47  // Package zip provides functions for creating and extracting module zip files.
    48  //
    49  // Module zip files have several restrictions listed below. These are necessary
    50  // to ensure that module zip files can be extracted consistently on supported
    51  // platforms and file systems.
    52  //
    53  // • No two file paths may be equal under Unicode case-folding (see
    54  // strings.EqualFold).
    55  //
    56  // • A go.mod file may or may not appear in the top-level directory. If present,
    57  // it must be named "go.mod", not any other case. Files named "go.mod"
    58  // are not allowed in any other directory.
    59  //
    60  // • The total size in bytes of a module zip file may be at most MaxZipFile
    61  // bytes (500 MiB). The total uncompressed size of the files within the
    62  // zip may also be at most MaxZipFile bytes.
    63  //
    64  // • Each file's uncompressed size must match its declared 64-bit uncompressed
    65  // size in the zip file header.
    66  //
    67  // • If the zip contains files named "<module>@<version>/go.mod" or
    68  // "<module>@<version>/LICENSE", their sizes in bytes may be at most
    69  // MaxGoMod or MaxLICENSE, respectively (both are 16 MiB).
    70  //
    71  // • Empty directories are ignored. File permissions and timestamps are also
    72  // ignored.
    73  //
    74  // • Symbolic links and other irregular files are not allowed.
    75  //
    76  // Note that this package does not provide hashing functionality. See
    77  // golang.org/x/mod/sumdb/dirhash.
    78  
    79  const (
    80  	// MaxZipFile is the maximum size in bytes of a module zip file. The
    81  	// go command will report an error if either the zip file or its extracted
    82  	// content is larger than this.
    83  	MaxZipFile = 500 << 20
    84  
    85  	// MaxGoMod is the maximum size in bytes of a go.mod file within a
    86  	// module zip file.
    87  	MaxGoMod = 16 << 20
    88  
    89  	// MaxLICENSE is the maximum size in bytes of a LICENSE file within a
    90  	// module zip file.
    91  	MaxLICENSE = 16 << 20
    92  )
    93  
    94  // Archive project files according to the go project standard
    95  func archiveProject(writer io.Writer, dir, mod, version string) error {
    96  	m := module.Version{Version: version, Path: mod}
    97  	//ignore, gitIgnoreErr := gitignore.NewFromFile(sourcePath + "/.gitignore") ??
    98  	var files []File
    99  
   100  	err := filepath.Walk(dir, func(filePath string, info os.FileInfo, err error) error {
   101  		relPath, err := filepath.Rel(dir, filePath)
   102  		if err != nil {
   103  			return err
   104  		}
   105  		slashPath := filepath.ToSlash(relPath)
   106  		if info.IsDir() {
   107  			if filePath == dir {
   108  				// Don't skip the top-level directory.
   109  				return nil
   110  			}
   111  
   112  			// Skip VCS directories.
   113  			// fossil repos are regular files with arbitrary names, so we don't try
   114  			// to exclude them.
   115  			switch filepath.Base(filePath) {
   116  			case ".bzr", ".git", ".hg", ".svn":
   117  				return filepath.SkipDir
   118  			}
   119  
   120  			// Skip some subdirectories inside vendor, but maintain bug
   121  			// golang.org/issue/31562, described in isVendoredPackage.
   122  			// We would like Create and CreateFromDir to produce the same result
   123  			// for a set of files, whether expressed as a directory tree or zip.
   124  
   125  			if isVendoredPackage(slashPath) {
   126  				return filepath.SkipDir
   127  			}
   128  
   129  			// Skip submodules (directories containing go.mod files).
   130  			if goModInfo, err := os.Lstat(filepath.Join(filePath, "go.mod")); err == nil && !goModInfo.IsDir() {
   131  				return filepath.SkipDir
   132  			}
   133  			return nil
   134  		}
   135  		if info.Mode().IsRegular() {
   136  			if !isVendoredPackage(slashPath) {
   137  				files = append(files, dirFile{
   138  					filePath:  filePath,
   139  					slashPath: slashPath,
   140  					info:      info,
   141  				})
   142  			}
   143  			return nil
   144  		}
   145  		// Not a regular file or a directory. Probably a symbolic link.
   146  		// Irregular files are ignored, so skip it.
   147  		return nil
   148  	})
   149  	if err != nil {
   150  		return err
   151  	}
   152  
   153  	return Create(writer, m, files)
   154  }
   155  
   156  func isVendoredPackage(name string) bool {
   157  	var i int
   158  	if strings.HasPrefix(name, "vendor/") {
   159  		i += len("vendor/")
   160  	} else if j := strings.Index(name, "/vendor/"); j >= 0 {
   161  		// This offset looks incorrect; this should probably be
   162  		//
   163  		// 	i = j + len("/vendor/")
   164  		//
   165  		// Unfortunately, we can't fix it without invalidating checksums.
   166  		// Fortunately, the error appears to be strictly conservative: we'll retain
   167  		// vendored packages that we should have pruned, but we won't prune
   168  		// non-vendored packages that we should have retained.
   169  		//
   170  		// Since this defect doesn't seem to break anything, it's not worth fixing
   171  		// for now.
   172  		i += len("/vendor/")
   173  	} else {
   174  		return false
   175  	}
   176  	return strings.Contains(name[i:], "/")
   177  }
   178  
   179  // Create builds a zip archive for module m from an abstract list of files
   180  // and writes it to w.
   181  //
   182  // Create verifies the restrictions described in the package documentation
   183  // and should not produce an archive that Unzip cannot extract. Create does not
   184  // include files in the output archive if they don't belong in the module zip.
   185  // In particular, Create will not include files in modules found in
   186  // subdirectories, most files in vendor directories, or irregular files (such
   187  // as symbolic links) in the output archive.
   188  func Create(w io.Writer, m module.Version, files []File) (err error) {
   189  
   190  	// Check that the version is canonical, the module path is well-formed, and
   191  	// the major version suffix matches the major version.
   192  	if vers := module.CanonicalVersion(m.Version); vers != m.Version {
   193  		if vers == "" {
   194  			vers = "the version structure to be vX.Y.Z"
   195  		}
   196  		return fmt.Errorf("version %q is not canonical (expected %s)", m.Version, vers)
   197  	}
   198  	if err := module.Check(m.Path, m.Version); err != nil {
   199  		return err
   200  	}
   201  
   202  	// Find directories containing go.mod files (other than the root).
   203  	// These directories will not be included in the output zip.
   204  	haveGoMod := make(map[string]bool)
   205  	for _, f := range files {
   206  		dir, base := path.Split(f.Path())
   207  		if strings.EqualFold(base, "go.mod") {
   208  			info, err := f.Lstat()
   209  			if err != nil {
   210  				return err
   211  			}
   212  			if info.Mode().IsRegular() {
   213  				haveGoMod[dir] = true
   214  			}
   215  		}
   216  	}
   217  
   218  	inSubmodule := func(p string) bool {
   219  		for {
   220  			dir, _ := path.Split(p)
   221  			if dir == "" {
   222  				return false
   223  			}
   224  			if haveGoMod[dir] {
   225  				return true
   226  			}
   227  			p = dir[:len(dir)-1]
   228  		}
   229  	}
   230  
   231  	// Create the module zip file.
   232  	zw := zip.NewWriter(w)
   233  	prefix := fmt.Sprintf("%s@%s/", m.Path, m.Version)
   234  
   235  	addFile := func(f File, path string, size int64) error {
   236  		rc, err := f.Open()
   237  		if err != nil {
   238  			return err
   239  		}
   240  		defer rc.Close()
   241  		w, err := zw.Create(prefix + path)
   242  		if err != nil {
   243  			return err
   244  		}
   245  		lr := &io.LimitedReader{R: rc, N: size + 1}
   246  		if _, err := io.Copy(w, lr); err != nil {
   247  			return err
   248  		}
   249  		if lr.N <= 0 {
   250  			return fmt.Errorf("file %q is larger than declared size", path)
   251  		}
   252  		return nil
   253  	}
   254  
   255  	collisions := make(collisionChecker)
   256  	maxSize := int64(MaxZipFile)
   257  	for _, f := range files {
   258  		p := f.Path()
   259  		if p != path.Clean(p) {
   260  			return fmt.Errorf("file path %s is not clean", p)
   261  		}
   262  		if path.IsAbs(p) {
   263  			return fmt.Errorf("file path %s is not relative", p)
   264  		}
   265  		if isVendoredPackage(p) || inSubmodule(p) {
   266  			continue
   267  		}
   268  		if p == ".hg_archival.txt" {
   269  			// Inserted by hg archive.
   270  			// The go command drops this regardless of the VCS being used.
   271  			continue
   272  		}
   273  		if err := module.CheckFilePath(p); err != nil {
   274  			return err
   275  		}
   276  		if strings.ToLower(p) == "go.mod" && p != "go.mod" {
   277  			return fmt.Errorf("found file named %s, want all lower-case go.mod", p)
   278  		}
   279  		info, err := f.Lstat()
   280  		if err != nil {
   281  			return err
   282  		}
   283  		if err := collisions.check(p, info.IsDir()); err != nil {
   284  			return err
   285  		}
   286  		if !info.Mode().IsRegular() {
   287  			// Skip symbolic links (golang.org/issue/27093).
   288  			continue
   289  		}
   290  		size := info.Size()
   291  		if size < 0 || maxSize < size {
   292  			return fmt.Errorf("module source tree too large (max size is %d bytes)", MaxZipFile)
   293  		}
   294  		maxSize -= size
   295  		if p == "go.mod" && size > MaxGoMod {
   296  			return fmt.Errorf("go.mod file too large (max size is %d bytes)", MaxGoMod)
   297  		}
   298  		if p == "LICENSE" && size > MaxLICENSE {
   299  			return fmt.Errorf("LICENSE file too large (max size is %d bytes)", MaxLICENSE)
   300  		}
   301  
   302  		if err := addFile(f, p, size); err != nil {
   303  			return err
   304  		}
   305  	}
   306  	if err := zw.Close(); err != nil {
   307  		return err
   308  	}
   309  	return
   310  }
   311  
   312  type dirFile struct {
   313  	filePath, slashPath string
   314  	info                os.FileInfo
   315  }
   316  
   317  func (f dirFile) Path() string                 { return f.slashPath }
   318  func (f dirFile) Lstat() (os.FileInfo, error)  { return f.info, nil }
   319  func (f dirFile) Open() (io.ReadCloser, error) { return os.Open(f.filePath) }
   320  
   321  // collisionChecker finds case-insensitive name collisions and paths that
   322  // are listed as both files and directories.
   323  //
   324  // The keys of this map are processed with strToFold. pathInfo has the original
   325  // path for each folded path.
   326  type collisionChecker map[string]pathInfo
   327  
   328  type pathInfo struct {
   329  	path  string
   330  	isDir bool
   331  }
   332  
   333  // File provides an abstraction for a file in a directory, zip, or anything
   334  // else that looks like a file.
   335  type File interface {
   336  	// Path returns a clean slash-separated relative path from the module root
   337  	// directory to the file.
   338  	Path() string
   339  
   340  	// Lstat returns information about the file. If the file is a symbolic link,
   341  	// Lstat returns information about the link itself, not the file it points to.
   342  	Lstat() (os.FileInfo, error)
   343  
   344  	// Open provides access to the data within a regular file. Open may return
   345  	// an error if called on a directory or symbolic link.
   346  	Open() (io.ReadCloser, error)
   347  }
   348  
   349  func (cc collisionChecker) check(p string, isDir bool) error {
   350  	fold := strToFold(p)
   351  	if other, ok := cc[fold]; ok {
   352  		if p != other.path {
   353  			return fmt.Errorf("case-insensitive file name collision: %q and %q", other.path, p)
   354  		}
   355  		if isDir != other.isDir {
   356  			return fmt.Errorf("entry %q is both a file and a directory", p)
   357  		}
   358  		if !isDir {
   359  			return fmt.Errorf("multiple entries for file %q", p)
   360  		}
   361  		// It's not an error if check is called with the same directory multiple
   362  		// times. check is called recursively on parent directories, so check
   363  		// may be called on the same directory many times.
   364  	} else {
   365  		cc[fold] = pathInfo{path: p, isDir: isDir}
   366  	}
   367  
   368  	if parent := path.Dir(p); parent != "." {
   369  		return cc.check(parent, true)
   370  	}
   371  	return nil
   372  }
   373  
   374  // strToFold returns a string with the property that
   375  //	strings.EqualFold(s, t) iff strToFold(s) == strToFold(t)
   376  // This lets us test a large set of strings for fold-equivalent
   377  // duplicates without making a quadratic number of calls
   378  // to EqualFold. Note that strings.ToUpper and strings.ToLower
   379  // do not have the desired property in some corner cases.
   380  func strToFold(s string) string {
   381  	// Fast path: all ASCII, no upper case.
   382  	// Most paths look like this already.
   383  	for i := 0; i < len(s); i++ {
   384  		c := s[i]
   385  		if c >= utf8.RuneSelf || 'A' <= c && c <= 'Z' {
   386  			goto Slow
   387  		}
   388  	}
   389  	return s
   390  
   391  Slow:
   392  	var buf bytes.Buffer
   393  	for _, r := range s {
   394  		// SimpleFold(x) cycles to the next equivalent rune > x
   395  		// or wraps around to smaller values. Iterate until it wraps,
   396  		// and we've found the minimum value.
   397  		for {
   398  			r0 := r
   399  			r = unicode.SimpleFold(r0)
   400  			if r <= r0 {
   401  				break
   402  			}
   403  		}
   404  		// Exception to allow fast path above: A-Z => a-z
   405  		if 'A' <= r && r <= 'Z' {
   406  			r += 'a' - 'A'
   407  		}
   408  		buf.WriteRune(r)
   409  	}
   410  	return buf.String()
   411  }