Source file src/cmd/distpack/archive.go

     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  
     5  package main
     6  
     7  import (
     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.
    20  type Archive struct {
    21  	Files []File
    22  }
    23  
    24  // A File describes a single file to write to an archive.
    25  type 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.
    35  func (f *File) Info() fs.FileInfo {
    36  	return fileInfo{f}
    37  }
    38  
    39  // A fileInfo is an implementation of fs.FileInfo describing a File.
    40  type fileInfo struct {
    41  	f *File
    42  }
    43  
    44  func (i fileInfo) Name() string       { return path.Base(i.f.Name) }
    45  func (i fileInfo) ModTime() time.Time { return i.f.Time }
    46  func (i fileInfo) Mode() fs.FileMode  { return i.f.Mode }
    47  func (i fileInfo) IsDir() bool        { return i.f.Mode&fs.ModeDir != 0 }
    48  func (i fileInfo) Size() int64        { return i.f.Size }
    49  func (i fileInfo) Sys() any           { return nil }
    50  
    51  func (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.
    57  func 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.
    84  func (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  
    94  func 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.
   114  func (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.
   123  func (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.
   132  func (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.
   139  func (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.
   151  func (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.
   161  func (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.
   177  func (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.
   185  func (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  
   193  func 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  

View as plain text