carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build unix
2
3package hostfiles
4
5import (
6 "errors"
7 "io"
8 "os"
9 "sort"
10 "strconv"
11 "strings"
12
13 "golang.org/x/sys/unix"
14)
15
16// restoreDirFlags is how Restore opens a directory it is about to sync:
17// read-only, a directory only, never through a link at the last
18// component, and never waiting on a FIFO.
19const restoreDirFlags = unix.O_RDONLY | unix.O_DIRECTORY | unix.O_NONBLOCK | unix.O_NOFOLLOW
20
21// dirHandle is a directory held open by descriptor. Everything beneath it
22// is reached one path component at a time with openat and O_NOFOLLOW, so
23// no symlink is ever followed, inside the tree or out of it, and a
24// directory swapped for something else after it was looked at is refused
25// rather than entered. Every open is non-blocking, so a FIFO swapped in
26// for a file or a directory cannot hang the publisher.
27type dirHandle struct{ f *os.File }
28
29// openTree opens the root of a files directory. The path to the root is
30// the platform's own and may contain symlinks; nothing beneath it may.
31func openTree(path string) (*dirHandle, error) {
32 fd, err := unix.Open(path, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NONBLOCK, 0)
33 if err != nil {
34 return nil, &os.PathError{Op: "open", Path: path, Err: err}
35 }
36 return &dirHandle{os.NewFile(uintptr(fd), path)}, nil
37}
38
39// handleOf wraps a directory the caller opened, and keeps, as the root of
40// a walk: SyncDirAt never closes it. Anything but a directory is refused.
41func handleOf(f *os.File) (*dirHandle, error) {
42 if f == nil {
43 return nil, errors.New("hostfiles: sync of a nil directory")
44 }
45 info, err := f.Stat()
46 if err != nil {
47 return nil, err
48 }
49 if !info.IsDir() {
50 return nil, &os.PathError{Op: "sync", Path: f.Name(), Err: unix.ENOTDIR}
51 }
52 return &dirHandle{f}, nil
53}
54
55func (d *dirHandle) Close() error { return d.f.Close() }
56func (d *dirHandle) Name() string { return d.f.Name() }
57
58// openDir opens the directory name inside d. A name that is not a real
59// directory (a symlink, a file, a FIFO) fails with ENOTDIR or ELOOP
60// without blocking; a missing one with ENOENT.
61func (d *dirHandle) openDir(name string) (*dirHandle, error) {
62 fd, err := unix.Openat(int(d.f.Fd()), name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0)
63 if err != nil {
64 return nil, &os.PathError{Op: "openat", Path: name, Err: err}
65 }
66 return &dirHandle{os.NewFile(uintptr(fd), name)}, nil
67}
68
69// names lists d's entries, sorted.
70func (d *dirHandle) names() ([]string, error) {
71 if _, err := d.f.Seek(0, io.SeekStart); err != nil {
72 return nil, err
73 }
74 names, err := d.f.Readdirnames(-1)
75 if err != nil {
76 return nil, err
77 }
78 sort.Strings(names)
79 return names, nil
80}
81
82// look inspects name inside d without following a symlink.
83func (d *dirHandle) look(name string) (unix.Stat_t, error) {
84 var st unix.Stat_t
85 err := unix.Fstatat(int(d.f.Fd()), name, &st, unix.AT_SYMLINK_NOFOLLOW)
86 return st, err
87}
88
89func isDir(st unix.Stat_t) bool { return st.Mode&unix.S_IFMT == unix.S_IFDIR }
90func isRegular(st unix.Stat_t) bool { return st.Mode&unix.S_IFMT == unix.S_IFREG }
91
92func sameFile(a, b unix.Stat_t) bool { return a.Dev == b.Dev && a.Ino == b.Ino }
93
94func sameImage(a, b unix.Stat_t) bool {
95 return sameFile(a, b) && a.Size == b.Size && a.Mtim == b.Mtim
96}
97
98// notADirectory reports the errors openDir gives for a name that exists
99// but is not a real directory.
100func notADirectory(err error) bool {
101 return errors.Is(err, unix.ENOTDIR) || errors.Is(err, unix.ELOOP)
102}
103
104// descend opens the directory rel ("" or "a/b") beneath d, one component
105// at a time. gone is true when some component is missing or is not a real
106// directory: nothing of ours lives at a path under it.
107//
108// A component that is "", "." or ".." is refused with an error, never
109// reported as gone: publishOne relies on descend never leaving the tree.
110// descend("") returns d itself, so the caller must not close it.
111func (d *dirHandle) descend(rel string) (sub *dirHandle, gone bool, err error) {
112 if rel == "" {
113 return d, false, nil
114 }
115 parts := strings.Split(rel, "/")
116 for _, part := range parts {
117 if part == "" || part == "." || part == ".." {
118 return nil, false, errors.New("hostfiles: path component " + strconv.Quote(part) + " in " + strconv.Quote(rel) + " is not a plain name")
119 }
120 }
121 cur, owned := d, false
122 for _, part := range parts {
123 betweenLookAndOpen()
124 next, err := cur.openDir(part)
125 if owned {
126 cur.Close()
127 }
128 if err != nil {
129 if errors.Is(err, unix.ENOENT) || notADirectory(err) {
130 return nil, true, nil
131 }
132 return nil, false, err
133 }
134 cur, owned = next, true
135 }
136 return cur, false, nil
137}
138
139// readStable reads the file name inside d for publishing.
140//
141// It reads only what it opened without following a link and found to be
142// the same regular file it had just inspected. A file over MaxFileBytes is
143// refused on its size alone, before any read. What it reports:
144//
145// - fileGone: nothing by that name.
146// - fileNotRegular: something by that name that is not a regular file.
147// - fileUnreadable: a regular file that could not be opened or read. It
148// still exists, so the caller must keep what the manifest has for it.
149// - fileRetry: the regular file was replaced by another regular file
150// between the look and the open. Nothing was read; ask again.
151// - fileTorn: read, but its size, mtime or identity changed meanwhile.
152// The bytes are a possibly inconsistent image.
153func readStable(d *dirHandle, name string) ([]byte, uint32, fileState, error) {
154 before, err := d.look(name)
155 if errors.Is(err, unix.ENOENT) {
156 return nil, 0, fileGone, nil
157 }
158 if err != nil {
159 return nil, 0, fileUnreadable, nil
160 }
161 if !isRegular(before) {
162 return nil, 0, fileNotRegular, nil
163 }
164 if before.Size > MaxFileBytes {
165 return nil, 0, fileTooLarge, nil
166 }
167 betweenLookAndOpen()
168 fd, err := unix.Openat(int(d.f.Fd()), name, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0)
169 switch {
170 case errors.Is(err, unix.ENOENT):
171 return nil, 0, fileGone, nil
172 case errors.Is(err, unix.ELOOP):
173 return nil, 0, fileNotRegular, nil // it became a symlink
174 case err != nil:
175 return nil, 0, fileUnreadable, nil
176 }
177 f := os.NewFile(uintptr(fd), name)
178 defer f.Close()
179 var opened unix.Stat_t
180 if err := unix.Fstat(fd, &opened); err != nil {
181 return nil, 0, fileUnreadable, nil
182 }
183 if !isRegular(opened) {
184 return nil, 0, fileNotRegular, nil // swapped for a FIFO, a device, a directory
185 }
186 if !sameFile(before, opened) {
187 return nil, 0, fileRetry, nil
188 }
189 if opened.Size > MaxFileBytes {
190 return nil, 0, fileTooLarge, nil
191 }
192 body, err := readAll(io.LimitReader(f, MaxFileBytes+1))
193 if err != nil {
194 return nil, 0, fileUnreadable, nil
195 }
196 if int64(len(body)) > MaxFileBytes {
197 return nil, 0, fileTooLarge, nil // it grew past the cap while being read
198 }
199 state := fileOK
200 if after, err := d.look(name); err != nil || !sameImage(before, after) {
201 state = fileTorn
202 }
203 return body, uint32(before.Mode & 0o777), state, nil
204}
205