summaryrefslogtreecommitdiffstats
path: root/src/cmd/distpack/archive.go
blob: 24ed077610ffc6fbd2784ff7f626fa61cf7aaedf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"io/fs"
	"log"
	"os"
	"path"
	"path/filepath"
	"sort"
	"strings"
	"time"
)

// An Archive describes an archive to write: a collection of files.
// Directories are implied by the files and not explicitly listed.
type Archive struct {
	Files []File
}

// A File describes a single file to write to an archive.
type File struct {
	Name string    // name in archive
	Time time.Time // modification time
	Mode fs.FileMode
	Size int64
	Src  string // source file in OS file system
}

// Info returns a FileInfo about the file, for use with tar.FileInfoHeader
// and zip.FileInfoHeader.
func (f *File) Info() fs.FileInfo {
	return fileInfo{f}
}

// A fileInfo is an implementation of fs.FileInfo describing a File.
type fileInfo struct {
	f *File
}

func (i fileInfo) Name() string       { return path.Base(i.f.Name) }
func (i fileInfo) ModTime() time.Time { return i.f.Time }
func (i fileInfo) Mode() fs.FileMode  { return i.f.Mode }
func (i fileInfo) IsDir() bool        { return i.f.Mode&fs.ModeDir != 0 }
func (i fileInfo) Size() int64        { return i.f.Size }
func (i fileInfo) Sys() any           { return nil }

func (i fileInfo) String() string {
	return fs.FormatFileInfo(i)
}

// NewArchive returns a new Archive containing all the files in the directory dir.
// The archive can be amended afterward using methods like Add and Filter.
func NewArchive(dir string) (*Archive, error) {
	a := new(Archive)
	err := fs.WalkDir(os.DirFS(dir), ".", func(name string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			return nil
		}
		info, err := d.Info()
		if err != nil {
			return err
		}
		a.Add(name, filepath.Join(dir, name), info)
		return nil
	})
	if err != nil {
		return nil, err
	}
	a.Sort()
	return a, nil
}

// Add adds a file with the given name and info to the archive.
// The content of the file comes from the operating system file src.
// After a sequence of one or more calls to Add,
// the caller should invoke Sort to re-sort the archive's files.
func (a *Archive) Add(name, src string, info fs.FileInfo) {
	a.Files = append(a.Files, File{
		Name: name,
		Time: info.ModTime(),
		Mode: info.Mode(),
		Size: info.Size(),
		Src:  src,
	})
}

// Sort sorts the files in the archive.
// It is only necessary to call Sort after calling Add or RenameGoMod.
// ArchiveDir returns a sorted archive, and the other methods
// preserve the sorting of the archive.
func (a *Archive) Sort() {
	sort.Slice(a.Files, func(i, j int) bool {
		return a.Files[i].Name < a.Files[j].Name
	})
}

// Clone returns a copy of the Archive.
// Method calls like Add and Filter invoked on the copy do not affect the original,
// nor do calls on the original affect the copy.
func (a *Archive) Clone() *Archive {
	b := &Archive{
		Files: make([]File, len(a.Files)),
	}
	copy(b.Files, a.Files)
	return b
}

// AddPrefix adds a prefix to all file names in the archive.
func (a *Archive) AddPrefix(prefix string) {
	for i := range a.Files {
		a.Files[i].Name = path.Join(prefix, a.Files[i].Name)
	}
}

// Filter removes files from the archive for which keep(name) returns false.
func (a *Archive) Filter(keep func(name string) bool) {
	files := a.Files[:0]
	for _, f := range a.Files {
		if keep(f.Name) {
			files = append(files, f)
		}
	}
	a.Files = files
}

// SetMode changes the mode of every file in the archive
// to be mode(name, m), where m is the file's current mode.
func (a *Archive) SetMode(mode func(name string, m fs.FileMode) fs.FileMode) {
	for i := range a.Files {
		a.Files[i].Mode = mode(a.Files[i].Name, a.Files[i].Mode)
	}
}

// Remove removes files matching any of the patterns from the archive.
// The patterns use the syntax of path.Match, with an extension of allowing
// a leading **/ or trailing /**, which match any number of path elements
// (including no path elements) before or after the main match.
func (a *Archive) Remove(patterns ...string) {
	a.Filter(func(name string) bool {
		for _, pattern := range patterns {
			match, err := amatch(pattern, name)
			if err != nil {
				log.Fatalf("archive remove: %v", err)
			}
			if match {
				return false
			}
		}
		return true
	})
}

// SetTime sets the modification time of all files in the archive to t.
func (a *Archive) SetTime(t time.Time) {
	for i := range a.Files {
		a.Files[i].Time = t
	}
}

// RenameGoMod renames the go.mod files in the archive to _go.mod,
// for use with the module form, which cannot contain other go.mod files.
func (a *Archive) RenameGoMod() {
	for i, f := range a.Files {
		if strings.HasSuffix(f.Name, "/go.mod") {
			a.Files[i].Name = strings.TrimSuffix(f.Name, "go.mod") + "_go.mod"
		}
	}
}

func amatch(pattern, name string) (bool, error) {
	// firstN returns the prefix of name corresponding to the first n path elements.
	// If n <= 0, firstN returns the entire name.
	firstN := func(name string, n int) string {
		for i := 0; i < len(name); i++ {
			if name[i] == '/' {
				if n--; n == 0 {
					return name[:i]
				}
			}
		}
		return name
	}

	// lastN returns the suffix of name corresponding to the last n path elements.
	// If n <= 0, lastN returns the entire name.
	lastN := func(name string, n int) string {
		for i := len(name) - 1; i >= 0; i-- {
			if name[i] == '/' {
				if n--; n == 0 {
					return name[i+1:]
				}
			}
		}
		return name
	}

	if p, ok := strings.CutPrefix(pattern, "**/"); ok {
		return path.Match(p, lastN(name, 1+strings.Count(p, "/")))
	}
	if p, ok := strings.CutSuffix(pattern, "/**"); ok {
		return path.Match(p, firstN(name, 1+strings.Count(p, "/")))
	}
	return path.Match(pattern, name)
}