github.com/jfrog/jfrog-cli-go@v1.22.1-0.20200318093948-4826ef344ffd/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 return fmt.Errorf("version %q is not canonical (should be %q)", m.Version, vers) 194 } 195 if err := module.Check(m.Path, m.Version); err != nil { 196 return err 197 } 198 199 // Find directories containing go.mod files (other than the root). 200 // These directories will not be included in the output zip. 201 haveGoMod := make(map[string]bool) 202 for _, f := range files { 203 dir, base := path.Split(f.Path()) 204 if strings.EqualFold(base, "go.mod") { 205 info, err := f.Lstat() 206 if err != nil { 207 return err 208 } 209 if info.Mode().IsRegular() { 210 haveGoMod[dir] = true 211 } 212 } 213 } 214 215 inSubmodule := func(p string) bool { 216 for { 217 dir, _ := path.Split(p) 218 if dir == "" { 219 return false 220 } 221 if haveGoMod[dir] { 222 return true 223 } 224 p = dir[:len(dir)-1] 225 } 226 } 227 228 // Create the module zip file. 229 zw := zip.NewWriter(w) 230 prefix := fmt.Sprintf("%s@%s/", m.Path, m.Version) 231 232 addFile := func(f File, path string, size int64) error { 233 rc, err := f.Open() 234 if err != nil { 235 return err 236 } 237 defer rc.Close() 238 w, err := zw.Create(prefix + path) 239 if err != nil { 240 return err 241 } 242 lr := &io.LimitedReader{R: rc, N: size + 1} 243 if _, err := io.Copy(w, lr); err != nil { 244 return err 245 } 246 if lr.N <= 0 { 247 return fmt.Errorf("file %q is larger than declared size", path) 248 } 249 return nil 250 } 251 252 collisions := make(collisionChecker) 253 maxSize := int64(MaxZipFile) 254 for _, f := range files { 255 p := f.Path() 256 if p != path.Clean(p) { 257 return fmt.Errorf("file path %s is not clean", p) 258 } 259 if path.IsAbs(p) { 260 return fmt.Errorf("file path %s is not relative", p) 261 } 262 if isVendoredPackage(p) || inSubmodule(p) { 263 continue 264 } 265 if p == ".hg_archival.txt" { 266 // Inserted by hg archive. 267 // The go command drops this regardless of the VCS being used. 268 continue 269 } 270 if err := module.CheckFilePath(p); err != nil { 271 return err 272 } 273 if strings.ToLower(p) == "go.mod" && p != "go.mod" { 274 return fmt.Errorf("found file named %s, want all lower-case go.mod", p) 275 } 276 info, err := f.Lstat() 277 if err != nil { 278 return err 279 } 280 if err := collisions.check(p, info.IsDir()); err != nil { 281 return err 282 } 283 if !info.Mode().IsRegular() { 284 // Skip symbolic links (golang.org/issue/27093). 285 continue 286 } 287 size := info.Size() 288 if size < 0 || maxSize < size { 289 return fmt.Errorf("module source tree too large (max size is %d bytes)", MaxZipFile) 290 } 291 maxSize -= size 292 if p == "go.mod" && size > MaxGoMod { 293 return fmt.Errorf("go.mod file too large (max size is %d bytes)", MaxGoMod) 294 } 295 if p == "LICENSE" && size > MaxLICENSE { 296 return fmt.Errorf("LICENSE file too large (max size is %d bytes)", MaxLICENSE) 297 } 298 299 if err := addFile(f, p, size); err != nil { 300 return err 301 } 302 } 303 if err := zw.Close(); err != nil { 304 return err 305 } 306 return 307 } 308 309 type dirFile struct { 310 filePath, slashPath string 311 info os.FileInfo 312 } 313 314 func (f dirFile) Path() string { return f.slashPath } 315 func (f dirFile) Lstat() (os.FileInfo, error) { return f.info, nil } 316 func (f dirFile) Open() (io.ReadCloser, error) { return os.Open(f.filePath) } 317 318 // collisionChecker finds case-insensitive name collisions and paths that 319 // are listed as both files and directories. 320 // 321 // The keys of this map are processed with strToFold. pathInfo has the original 322 // path for each folded path. 323 type collisionChecker map[string]pathInfo 324 325 type pathInfo struct { 326 path string 327 isDir bool 328 } 329 330 // File provides an abstraction for a file in a directory, zip, or anything 331 // else that looks like a file. 332 type File interface { 333 // Path returns a clean slash-separated relative path from the module root 334 // directory to the file. 335 Path() string 336 337 // Lstat returns information about the file. If the file is a symbolic link, 338 // Lstat returns information about the link itself, not the file it points to. 339 Lstat() (os.FileInfo, error) 340 341 // Open provides access to the data within a regular file. Open may return 342 // an error if called on a directory or symbolic link. 343 Open() (io.ReadCloser, error) 344 } 345 346 func (cc collisionChecker) check(p string, isDir bool) error { 347 fold := strToFold(p) 348 if other, ok := cc[fold]; ok { 349 if p != other.path { 350 return fmt.Errorf("case-insensitive file name collision: %q and %q", other.path, p) 351 } 352 if isDir != other.isDir { 353 return fmt.Errorf("entry %q is both a file and a directory", p) 354 } 355 if !isDir { 356 return fmt.Errorf("multiple entries for file %q", p) 357 } 358 // It's not an error if check is called with the same directory multiple 359 // times. check is called recursively on parent directories, so check 360 // may be called on the same directory many times. 361 } else { 362 cc[fold] = pathInfo{path: p, isDir: isDir} 363 } 364 365 if parent := path.Dir(p); parent != "." { 366 return cc.check(parent, true) 367 } 368 return nil 369 } 370 371 // strToFold returns a string with the property that 372 // strings.EqualFold(s, t) iff strToFold(s) == strToFold(t) 373 // This lets us test a large set of strings for fold-equivalent 374 // duplicates without making a quadratic number of calls 375 // to EqualFold. Note that strings.ToUpper and strings.ToLower 376 // do not have the desired property in some corner cases. 377 func strToFold(s string) string { 378 // Fast path: all ASCII, no upper case. 379 // Most paths look like this already. 380 for i := 0; i < len(s); i++ { 381 c := s[i] 382 if c >= utf8.RuneSelf || 'A' <= c && c <= 'Z' { 383 goto Slow 384 } 385 } 386 return s 387 388 Slow: 389 var buf bytes.Buffer 390 for _, r := range s { 391 // SimpleFold(x) cycles to the next equivalent rune > x 392 // or wraps around to smaller values. Iterate until it wraps, 393 // and we've found the minimum value. 394 for { 395 r0 := r 396 r = unicode.SimpleFold(r0) 397 if r <= r0 { 398 break 399 } 400 } 401 // Exception to allow fast path above: A-Z => a-z 402 if 'A' <= r && r <= 'Z' { 403 r += 'a' - 'A' 404 } 405 buf.WriteRune(r) 406 } 407 return buf.String() 408 }