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 }