1// Copyright 2023 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 5package main 6 7import ( 8 "io/fs" 9 "log" 10 "os" 11 "path" 12 "path/filepath" 13 "sort" 14 "strings" 15 "time" 16) 17 18// An Archive describes an archive to write: a collection of files. 19// Directories are implied by the files and not explicitly listed. 20type Archive struct { 21 Files []File 22} 23 24// A File describes a single file to write to an archive. 25type File struct { 26 Name string // name in archive 27 Time time.Time // modification time 28 Mode fs.FileMode 29 Size int64 30 Src string // source file in OS file system 31} 32 33// Info returns a FileInfo about the file, for use with tar.FileInfoHeader 34// and zip.FileInfoHeader. 35func (f *File) Info() fs.FileInfo { 36 return fileInfo{f} 37} 38 39// A fileInfo is an implementation of fs.FileInfo describing a File. 40type fileInfo struct { 41 f *File 42} 43 44func (i fileInfo) Name() string { return path.Base(i.f.Name) } 45func (i fileInfo) ModTime() time.Time { return i.f.Time } 46func (i fileInfo) Mode() fs.FileMode { return i.f.Mode } 47func (i fileInfo) IsDir() bool { return i.f.Mode&fs.ModeDir != 0 } 48func (i fileInfo) Size() int64 { return i.f.Size } 49func (i fileInfo) Sys() any { return nil } 50 51func (i fileInfo) String() string { 52 return fs.FormatFileInfo(i) 53} 54 55// NewArchive returns a new Archive containing all the files in the directory dir. 56// The archive can be amended afterward using methods like Add and Filter. 57func NewArchive(dir string) (*Archive, error) { 58 a := new(Archive) 59 err := fs.WalkDir(os.DirFS(dir), ".", func(name string, d fs.DirEntry, err error) error { 60 if err != nil { 61 return err 62 } 63 if d.IsDir() { 64 return nil 65 } 66 info, err := d.Info() 67 if err != nil { 68 return err 69 } 70 a.Add(name, filepath.Join(dir, name), info) 71 return nil 72 }) 73 if err != nil { 74 return nil, err 75 } 76 a.Sort() 77 return a, nil 78} 79 80// Add adds a file with the given name and info to the archive. 81// The content of the file comes from the operating system file src. 82// After a sequence of one or more calls to Add, 83// the caller should invoke Sort to re-sort the archive's files. 84func (a *Archive) Add(name, src string, info fs.FileInfo) { 85 a.Files = append(a.Files, File{ 86 Name: name, 87 Time: info.ModTime(), 88 Mode: info.Mode(), 89 Size: info.Size(), 90 Src: src, 91 }) 92} 93 94func nameLess(x, y string) bool { 95 for i := 0; i < len(x) && i < len(y); i++ { 96 if x[i] != y[i] { 97 // foo/bar/baz before foo/bar.go, because foo/bar is before foo/bar.go 98 if x[i] == '/' { 99 return true 100 } 101 if y[i] == '/' { 102 return false 103 } 104 return x[i] < y[i] 105 } 106 } 107 return len(x) < len(y) 108} 109 110// Sort sorts the files in the archive. 111// It is only necessary to call Sort after calling Add or RenameGoMod. 112// NewArchive returns a sorted archive, and the other methods 113// preserve the sorting of the archive. 114func (a *Archive) Sort() { 115 sort.Slice(a.Files, func(i, j int) bool { 116 return nameLess(a.Files[i].Name, a.Files[j].Name) 117 }) 118} 119 120// Clone returns a copy of the Archive. 121// Method calls like Add and Filter invoked on the copy do not affect the original, 122// nor do calls on the original affect the copy. 123func (a *Archive) Clone() *Archive { 124 b := &Archive{ 125 Files: make([]File, len(a.Files)), 126 } 127 copy(b.Files, a.Files) 128 return b 129} 130 131// AddPrefix adds a prefix to all file names in the archive. 132func (a *Archive) AddPrefix(prefix string) { 133 for i := range a.Files { 134 a.Files[i].Name = path.Join(prefix, a.Files[i].Name) 135 } 136} 137 138// Filter removes files from the archive for which keep(name) returns false. 139func (a *Archive) Filter(keep func(name string) bool) { 140 files := a.Files[:0] 141 for _, f := range a.Files { 142 if keep(f.Name) { 143 files = append(files, f) 144 } 145 } 146 a.Files = files 147} 148 149// SetMode changes the mode of every file in the archive 150// to be mode(name, m), where m is the file's current mode. 151func (a *Archive) SetMode(mode func(name string, m fs.FileMode) fs.FileMode) { 152 for i := range a.Files { 153 a.Files[i].Mode = mode(a.Files[i].Name, a.Files[i].Mode) 154 } 155} 156 157// Remove removes files matching any of the patterns from the archive. 158// The patterns use the syntax of path.Match, with an extension of allowing 159// a leading **/ or trailing /**, which match any number of path elements 160// (including no path elements) before or after the main match. 161func (a *Archive) Remove(patterns ...string) { 162 a.Filter(func(name string) bool { 163 for _, pattern := range patterns { 164 match, err := amatch(pattern, name) 165 if err != nil { 166 log.Fatalf("archive remove: %v", err) 167 } 168 if match { 169 return false 170 } 171 } 172 return true 173 }) 174} 175 176// SetTime sets the modification time of all files in the archive to t. 177func (a *Archive) SetTime(t time.Time) { 178 for i := range a.Files { 179 a.Files[i].Time = t 180 } 181} 182 183// RenameGoMod renames the go.mod files in the archive to _go.mod, 184// for use with the module form, which cannot contain other go.mod files. 185func (a *Archive) RenameGoMod() { 186 for i, f := range a.Files { 187 if strings.HasSuffix(f.Name, "/go.mod") { 188 a.Files[i].Name = strings.TrimSuffix(f.Name, "go.mod") + "_go.mod" 189 } 190 } 191} 192 193func amatch(pattern, name string) (bool, error) { 194 // firstN returns the prefix of name corresponding to the first n path elements. 195 // If n <= 0, firstN returns the entire name. 196 firstN := func(name string, n int) string { 197 for i := 0; i < len(name); i++ { 198 if name[i] == '/' { 199 if n--; n == 0 { 200 return name[:i] 201 } 202 } 203 } 204 return name 205 } 206 207 // lastN returns the suffix of name corresponding to the last n path elements. 208 // If n <= 0, lastN returns the entire name. 209 lastN := func(name string, n int) string { 210 for i := len(name) - 1; i >= 0; i-- { 211 if name[i] == '/' { 212 if n--; n == 0 { 213 return name[i+1:] 214 } 215 } 216 } 217 return name 218 } 219 220 if p, ok := strings.CutPrefix(pattern, "**/"); ok { 221 return path.Match(p, lastN(name, 1+strings.Count(p, "/"))) 222 } 223 if p, ok := strings.CutSuffix(pattern, "/**"); ok { 224 return path.Match(p, firstN(name, 1+strings.Count(p, "/"))) 225 } 226 return path.Match(pattern, name) 227} 228