carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package archive
2
3import (
4 "archive/tar"
5 "errors"
6 "fmt"
7 "io"
8 "io/fs"
9 "os"
10 "path/filepath"
11 "runtime"
12 "sort"
13 "strings"
14 "sync"
15)
16
17// Extract writes an opened archive into filesDir and envPath, refusing
18// anything a tar could smuggle: absolute paths, "..", the protocol marker,
19// links, devices and FIFOs, two entries naming one destination, more
20// entries than MaxEntries, content over Cap and a decoded stream over
21// MaxDecoded. Every directory must appear before anything inside it (Build's
22// sorted order always does), so each one carries its archived mode.
23//
24// Nothing reaches the destination unless the whole archive is accepted:
25// entries are written into a private staging directory beside filesDir
26// (and env into a temporary file beside envPath), made durable, and moved
27// into place only after the stream has been read to its end. filesDir must be absent
28// or an empty directory (ErrNotEmpty otherwise; a non-empty directory
29// would keep files the archive does not name), and its parent and
30// envPath's parent must exist. envPath is replaced when the archive
31// carries env and removed when it does not, so afterwards PlainDigest of
32// the pair equals the archive's. The files move first and env second; a
33// failure between the two renames leaves the files in place and is
34// returned.
35func Extract(r io.Reader, filesDir, envPath string) error {
36 if filesDir == "" {
37 return errors.New("archive: no files directory named")
38 }
39 filesDir = filepath.Clean(filesDir)
40 if envPath != "" {
41 envPath = filepath.Clean(envPath)
42 if rel, err := filepath.Rel(filesDir, envPath); err == nil && filepath.IsLocal(rel) {
43 return fmt.Errorf("%w: the env file %s is inside the files directory", ErrBadPath, envPath)
44 }
45 }
46 rootMode, existed, err := checkDestination(filesDir)
47 if err != nil {
48 return err
49 }
50 stage, err := os.MkdirTemp(filepath.Dir(filesDir), "."+filepath.Base(filesDir)+".extract-")
51 if err != nil {
52 return err
53 }
54 x := &extraction{stage: stage, envPath: envPath, files: newSyncPool(syncWorkers)}
55 defer x.cleanup()
56 if x.root, err = os.OpenRoot(stage); err != nil {
57 return err
58 }
59 if err := x.read(r); err != nil {
60 return err
61 }
62 return x.commit(filesDir, rootMode, existed)
63}
64
65// checkDestination requires filesDir to be absent or an empty directory
66// (not a symlink to one) and returns the mode the extracted root takes:
67// the existing directory's, else 0755.
68func checkDestination(filesDir string) (fs.FileMode, bool, error) {
69 info, err := os.Lstat(filesDir)
70 if errors.Is(err, fs.ErrNotExist) {
71 return 0o755, false, nil
72 }
73 if err != nil {
74 return 0, false, err
75 }
76 if !info.IsDir() {
77 return 0, false, fmt.Errorf("%w: the destination %s is not a directory", ErrBadPath, filesDir)
78 }
79 f, err := os.Open(filesDir)
80 if err != nil {
81 return 0, false, err
82 }
83 names, err := f.Readdirnames(1)
84 f.Close()
85 if len(names) > 0 {
86 return 0, false, fmt.Errorf("%w: %s", ErrNotEmpty, filesDir)
87 }
88 if err != nil && err != io.EOF {
89 return 0, false, err
90 }
91 return info.Mode().Perm(), true, nil
92}
93
94type modeFix struct {
95 rel string
96 mode fs.FileMode
97}
98
99// extraction is one Extract in progress.
100type extraction struct {
101 stage string // the staging directory, removed unless committed
102 root *os.Root // stage, for every write
103 envPath string
104 envTmp string // the staged env file, if the archive carried one
105 dirs []modeFix
106 files *syncPool // fsyncs and closes each staged file as it is written
107}
108
109func (x *extraction) cleanup() {
110 x.files.wait()
111 if x.root != nil {
112 x.root.Close()
113 }
114 if x.stage != "" {
115 removeStage(x.stage)
116 }
117 if x.envTmp != "" {
118 os.Remove(x.envTmp)
119 }
120}
121
122// removeStage removes a staging directory whose directories may already
123// carry their final, restrictive modes. It holds only what Extract
124// created: regular files and directories.
125func removeStage(stage string) {
126 filepath.WalkDir(stage, func(p string, d fs.DirEntry, err error) error {
127 if err == nil && d.IsDir() {
128 os.Chmod(p, 0o700)
129 }
130 return nil
131 })
132 os.RemoveAll(stage)
133}
134
135// read writes every entry into the staging area and reads the stream to
136// its end.
137func (x *extraction) read(r io.Reader) error {
138 zr, err := newDecoder(r)
139 if err != nil {
140 return err
141 }
142 defer zr.Close()
143 dec := &boundedReader{r: zr, left: decodedLimit}
144 tr := tar.NewReader(dec)
145 rules := newStreamRules()
146 for {
147 hdr, err := tr.Next()
148 if err == io.EOF {
149 break
150 }
151 if err != nil {
152 return err
153 }
154 _, rel, isEnv, err := rules.admit(hdr)
155 if err != nil {
156 return err
157 }
158 mode := fs.FileMode(hdr.Mode).Perm()
159 if hdr.Typeflag == tar.TypeDir {
160 // Created writable so children can land; the archived mode is
161 // set in commit, before the directory is synced.
162 if err := x.root.Mkdir(rel, 0o700); err != nil {
163 return err
164 }
165 x.dirs = append(x.dirs, modeFix{rel, mode})
166 continue
167 }
168 if isEnv {
169 err = x.writeEnv(tr, hdr.Size, mode)
170 } else {
171 err = x.writeFile(tr, rel, hdr.Size, mode)
172 }
173 if err != nil {
174 return err
175 }
176 }
177 if _, err := io.Copy(io.Discard, dec); err != nil {
178 return err
179 }
180 return requireEOF(r)
181}
182
183func (x *extraction) writeFile(src io.Reader, rel string, size int64, mode fs.FileMode) error {
184 // O_EXCL: a name that already exists in the fresh staging tree (on a
185 // case-insensitive filesystem, "A" after "a") is refused, not merged.
186 f, err := x.root.OpenFile(rel, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
187 if err != nil {
188 return err
189 }
190 return x.finishFile(f, src, size, mode)
191}
192
193func (x *extraction) writeEnv(src io.Reader, size int64, mode fs.FileMode) error {
194 if x.envPath == "" {
195 return fmt.Errorf("%w: the archive carries env and no env path was given", ErrBadPath)
196 }
197 f, err := os.CreateTemp(filepath.Dir(x.envPath), "."+filepath.Base(x.envPath)+".extract-")
198 if err != nil {
199 return err
200 }
201 x.envTmp = f.Name()
202 return x.finishFile(f, src, size, mode)
203}
204
205// finishFile copies exactly size bytes and sets the archived mode (fchmod,
206// so the umask does not apply; §8.1), then hands the still-open file to
207// the sync pool, which fsyncs and closes it. Syncing the open descriptor
208// means a mode that refuses every open (0000) cannot stop the sync, and
209// the pool overlaps the syncs with the rest of the extraction.
210func (x *extraction) finishFile(f *os.File, src io.Reader, size int64, mode fs.FileMode) error {
211 if _, err := io.CopyN(f, src, size); err != nil {
212 f.Close()
213 return err
214 }
215 if err := f.Chmod(mode); err != nil {
216 f.Close()
217 return err
218 }
219 x.files.submit(func() error { return syncAndClose(f, fsyncFile) })
220 return nil
221}
222
223// commit makes the staged tree durable with its archived modes, then
224// publishes it.
225//
226// Crash ordering. Every staged file is fsynced with its archived mode
227// (finishFile) and every staged directory is fsynced after its archived
228// mode is set, deepest level first, then the staged root with its final
229// mode: all before the rename that publishes the tree. The rename is made
230// durable by fsyncing the destination's parent, then env is published the
231// same way. So a crash at any point leaves either no published tree (a
232// hidden .extract- directory to sweep) or a complete one carrying the
233// archived modes, never a published tree whose directories still have
234// their staging modes. The landing writes its marker only after Extract
235// returns nil.
236//
237// Cost. Scoped fsyncs only, never a filesystem-wide flush: other tenants'
238// dirty data on the box is not this landing's to wait for. They run on
239// syncWorkers workers, since concurrent fsyncs on ext4 and xfs share
240// journal commits. The cut budget prices this time under its Archive and
241// RestoreStaged terms (moves.Estimate).
242func (x *extraction) commit(filesDir string, rootMode fs.FileMode, existed bool) error {
243 if err := x.files.wait(); err != nil {
244 return err
245 }
246 if err := x.settleDirs(); err != nil {
247 return err
248 }
249 if err := settleDir(func() (*os.File, error) { return os.Open(x.stage) }, rootMode); err != nil {
250 return err
251 }
252 if existed {
253 // os.Remove refuses a directory that is no longer empty.
254 if err := os.Remove(filesDir); err != nil {
255 return err
256 }
257 }
258 if err := os.Rename(x.stage, filesDir); err != nil {
259 return err
260 }
261 x.stage = ""
262 if err := syncDir(filepath.Dir(filesDir)); err != nil {
263 return err
264 }
265 if x.envPath == "" {
266 return nil
267 }
268 if x.envTmp == "" {
269 if err := os.Remove(x.envPath); err != nil && !errors.Is(err, fs.ErrNotExist) {
270 return err
271 }
272 } else {
273 if err := os.Rename(x.envTmp, x.envPath); err != nil {
274 return err
275 }
276 x.envTmp = ""
277 }
278 return syncDir(filepath.Dir(x.envPath))
279}
280
281// settleDirs sets every staged directory's archived mode through an open
282// descriptor and fsyncs it, one depth level at a time, deepest first: a
283// directory is opened by a path through its ancestors, which are still
284// 0700 until their own level, so a restrictive mode (0300, 0000) never
285// blocks a deeper directory, and the descriptor syncs whatever the mode.
286func (x *extraction) settleDirs() error {
287 levels := map[int][]modeFix{}
288 for _, d := range x.dirs {
289 depth := strings.Count(d.rel, "/")
290 levels[depth] = append(levels[depth], d)
291 }
292 depths := make([]int, 0, len(levels))
293 for depth := range levels {
294 depths = append(depths, depth)
295 }
296 sort.Sort(sort.Reverse(sort.IntSlice(depths)))
297 for _, depth := range depths {
298 p := newSyncPool(syncWorkers)
299 for _, d := range levels[depth] {
300 p.submit(func() error {
301 return settleDir(func() (*os.File, error) { return x.root.Open(d.rel) }, d.mode)
302 })
303 }
304 if err := p.wait(); err != nil {
305 return err
306 }
307 }
308 return nil
309}
310
311func settleDir(open func() (*os.File, error), mode fs.FileMode) error {
312 f, err := open()
313 if err != nil {
314 return err
315 }
316 if err := f.Chmod(mode); err != nil {
317 f.Close()
318 return err
319 }
320 return syncAndClose(f, syncDirFile)
321}
322
323// fsyncFile is the one fsync every staged file and directory goes
324// through. A variable so a test can see each one and the mode it carried.
325var fsyncFile = func(f *os.File) error { return f.Sync() }
326
327// syncDirFile fsyncs a directory. Windows cannot sync a directory handle;
328// the platform's boxes are Linux, and this only keeps that build whole.
329func syncDirFile(f *os.File) error {
330 if runtime.GOOS == "windows" {
331 return nil
332 }
333 return fsyncFile(f)
334}
335
336func syncAndClose(f *os.File, sync func(*os.File) error) error {
337 err := sync(f)
338 if cerr := f.Close(); err == nil {
339 err = cerr
340 }
341 return err
342}
343
344// syncDir makes a rename in dir durable.
345func syncDir(dir string) error {
346 d, err := os.Open(dir)
347 if err != nil {
348 return err
349 }
350 return syncAndClose(d, syncDirFile)
351}
352
353// syncWorkers is how many fsyncs run at once (defaultSyncWorkers, per
354// platform). A variable so the timing test can compare serial and parallel.
355var syncWorkers = defaultSyncWorkers
356
357// syncPool runs jobs on a bounded set of workers and keeps the first
358// error. At most workers jobs run and workers more wait, so the staged
359// files held open for syncing stay bounded.
360type syncPool struct {
361 jobs chan func() error
362 wg sync.WaitGroup
363 once sync.Once
364 mu sync.Mutex
365 err error
366}
367
368func newSyncPool(workers int) *syncPool {
369 workers = max(workers, 1)
370 p := &syncPool{jobs: make(chan func() error, workers)}
371 p.wg.Add(workers)
372 for range workers {
373 go func() {
374 defer p.wg.Done()
375 for job := range p.jobs {
376 if err := job(); err != nil {
377 p.mu.Lock()
378 if p.err == nil {
379 p.err = err
380 }
381 p.mu.Unlock()
382 }
383 }
384 }()
385 }
386 return p
387}
388
389func (p *syncPool) submit(job func() error) { p.jobs <- job }
390
391// wait closes the pool, waits for every job and returns the first error.
392// Safe to call more than once.
393func (p *syncPool) wait() error {
394 p.once.Do(func() {
395 close(p.jobs)
396 p.wg.Wait()
397 })
398 p.mu.Lock()
399 defer p.mu.Unlock()
400 return p.err
401}
402