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