OLD | NEW |
| (Empty) |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 package cipd | |
6 | |
7 import ( | |
8 "fmt" | |
9 "io" | |
10 "io/ioutil" | |
11 "os" | |
12 "path/filepath" | |
13 "strings" | |
14 ) | |
15 | |
16 // File defines a single file to be added or extracted from a package. All paths | |
17 // are slash separated (including symlink targets). | |
18 type File interface { | |
19 // Name returns slash separated file path relative to a package root, e.
g. "dir/dir/file". | |
20 Name() string | |
21 // Size returns size of the file. 0 for symlinks. | |
22 Size() uint64 | |
23 // Executable returns true if the file is executable. Only used for Linu
x\Mac archives. false for symlinks. | |
24 Executable() bool | |
25 // Symlink returns true if the file is a symlink. | |
26 Symlink() bool | |
27 // SymlinkTarget return a path the symlink is pointing to. | |
28 SymlinkTarget() (string, error) | |
29 // Open opens the regular file for reading. Returns error for symlink fi
les. | |
30 Open() (io.ReadCloser, error) | |
31 } | |
32 | |
33 // Destination knows how to create files when extracting a package. It supports | |
34 // transactional semantic by providing 'Begin' and 'End' methods. No changes | |
35 // should be applied until End(true) is called. A call to End(false) should | |
36 // discard any pending changes. All paths are slash separated. | |
37 type Destination interface { | |
38 // Begin initiates a new write transaction. Called before first CreateFi
le. | |
39 Begin() error | |
40 // CreateFile opens a writer to extract some package file to. 'name' mus
t | |
41 // be a slash separated path relative to the destination root. | |
42 CreateFile(name string, executable bool) (io.WriteCloser, error) | |
43 // CreateSymlink creates a symlink (with absolute or relative target). '
name' | |
44 // must be a slash separated path relative to the destination root. | |
45 CreateSymlink(name string, target string) error | |
46 // End finalizes package extraction (commit or rollback, based on succes
s). | |
47 End(success bool) error | |
48 } | |
49 | |
50 //////////////////////////////////////////////////////////////////////////////// | |
51 // File system source. | |
52 | |
53 type fileSystemFile struct { | |
54 absPath string | |
55 name string | |
56 size uint64 | |
57 executable bool | |
58 symlinkTarget string | |
59 } | |
60 | |
61 func (f *fileSystemFile) Name() string { return f.name } | |
62 func (f *fileSystemFile) Size() uint64 { return f.size } | |
63 func (f *fileSystemFile) Executable() bool { return f.executable } | |
64 func (f *fileSystemFile) Symlink() bool { return f.symlinkTarget != "" } | |
65 | |
66 func (f *fileSystemFile) SymlinkTarget() (string, error) { | |
67 if f.symlinkTarget != "" { | |
68 return f.symlinkTarget, nil | |
69 } | |
70 return "", fmt.Errorf("Not a symlink: %s", f.Name()) | |
71 } | |
72 | |
73 func (f *fileSystemFile) Open() (io.ReadCloser, error) { | |
74 if f.Symlink() { | |
75 return nil, fmt.Errorf("Opening a symlink is not allowed: %s", f
.Name()) | |
76 } | |
77 return os.Open(f.absPath) | |
78 } | |
79 | |
80 // ScanFilter is predicate used by ScanFileSystem to decide what files to skip. | |
81 type ScanFilter func(abs string) bool | |
82 | |
83 // ScanFileSystem returns all files in some file system directory in | |
84 // an alphabetical order. It returns only files, skipping directory entries | |
85 // (i.e. empty directories are completely invisible). ScanFileSystem does not | |
86 // follow symbolic links, but recognizes them as such (see Symlink() method | |
87 // of File interface). It scans "dir" path, returning File objects that have | |
88 // paths relative to "root". It skips files and directories for which | |
89 // 'exclude(absolute path)' returns true. | |
90 func ScanFileSystem(dir string, root string, exclude ScanFilter) ([]File, error)
{ | |
91 root, err := filepath.Abs(filepath.Clean(root)) | |
92 if err != nil { | |
93 return nil, err | |
94 } | |
95 dir, err = filepath.Abs(filepath.Clean(dir)) | |
96 if err != nil { | |
97 return nil, err | |
98 } | |
99 if !isSubpath(dir, root) { | |
100 return nil, fmt.Errorf("Scanned directory must be under root dir
ectory") | |
101 } | |
102 | |
103 files := []File{} | |
104 | |
105 err = filepath.Walk(dir, func(abs string, info os.FileInfo, err error) e
rror { | |
106 if err != nil { | |
107 return err | |
108 } | |
109 if exclude != nil && abs != dir && exclude(abs) { | |
110 if info.Mode().IsDir() { | |
111 return filepath.SkipDir | |
112 } | |
113 return nil | |
114 } | |
115 if info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 { | |
116 f, err := WrapFile(abs, root, &info) | |
117 if err != nil { | |
118 return err | |
119 } | |
120 files = append(files, f) | |
121 } | |
122 return nil | |
123 }) | |
124 | |
125 if err != nil { | |
126 return nil, err | |
127 } | |
128 return files, nil | |
129 } | |
130 | |
131 // WrapFile constructs File object for some file in the file system, specified | |
132 // by its native absolute path 'abs' (subpath of 'root', also specified as | |
133 // a native absolute path). Returned File object has path relative to 'root'. | |
134 // If fileInfo is given, it will be used to grab file mode and size, otherwise | |
135 // os.Lstat will be used to get it. Recognizes symlinks. | |
136 func WrapFile(abs string, root string, fileInfo *os.FileInfo) (File, error) { | |
137 if !filepath.IsAbs(abs) { | |
138 return nil, fmt.Errorf("Expecting absolute path, got this: %q",
abs) | |
139 } | |
140 if !filepath.IsAbs(root) { | |
141 return nil, fmt.Errorf("Expecting absolute path, got this: %q",
root) | |
142 } | |
143 if !isSubpath(abs, root) { | |
144 return nil, fmt.Errorf("Path %q is not under %q", abs, root) | |
145 } | |
146 | |
147 var info os.FileInfo | |
148 if fileInfo == nil { | |
149 // Use Lstat to NOT follow symlinks (as os.Stat does). | |
150 var err error | |
151 info, err = os.Lstat(abs) | |
152 if err != nil { | |
153 return nil, err | |
154 } | |
155 } else { | |
156 info = *fileInfo | |
157 } | |
158 | |
159 rel, err := filepath.Rel(root, abs) | |
160 if err != nil { | |
161 return nil, err | |
162 } | |
163 | |
164 // Recognize symlinks as such, convert target to relative path, if neede
d. | |
165 if info.Mode()&os.ModeSymlink != 0 { | |
166 target, err := os.Readlink(abs) | |
167 if err != nil { | |
168 return nil, err | |
169 } | |
170 if filepath.IsAbs(target) { | |
171 // Convert absolute path pointing somewhere in "root" in
to a path | |
172 // relative to the symlink file itself. Store other abso
lute paths as | |
173 // they are. For example, it allows to package virtual e
nv directory | |
174 // that symlinks python binary from /usr/bin. | |
175 if isSubpath(target, root) { | |
176 target, err = filepath.Rel(filepath.Dir(abs), ta
rget) | |
177 if err != nil { | |
178 return nil, err | |
179 } | |
180 } | |
181 } else { | |
182 // Only relative paths that do not go outside "root" are
allowed. | |
183 // A package must not depend on its installation path. | |
184 targetAbs := filepath.Clean(filepath.Join(filepath.Dir(a
bs), target)) | |
185 if !isSubpath(targetAbs, root) { | |
186 return nil, fmt.Errorf( | |
187 "Invalid symlink %s: a relative symlink
pointing to a file outside of the package root", rel) | |
188 } | |
189 } | |
190 return &fileSystemFile{ | |
191 absPath: abs, | |
192 name: filepath.ToSlash(rel), | |
193 symlinkTarget: filepath.ToSlash(target), | |
194 }, nil | |
195 } | |
196 | |
197 // Regular file. | |
198 if info.Mode().IsRegular() { | |
199 return &fileSystemFile{ | |
200 absPath: abs, | |
201 name: filepath.ToSlash(rel), | |
202 size: uint64(info.Size()), | |
203 executable: (info.Mode().Perm() & 0111) != 0, | |
204 }, nil | |
205 } | |
206 | |
207 return nil, fmt.Errorf("Not a regular file or symlink: %s", abs) | |
208 } | |
209 | |
210 // isSubpath returns true if 'path' is 'root' or is inside a subdirectory of | |
211 // 'root'. Both 'path' and 'root' should be given as a native paths. If any of | |
212 // paths can't be converted to an absolute path returns false. | |
213 func isSubpath(path, root string) bool { | |
214 path, err := filepath.Abs(filepath.Clean(path)) | |
215 if err != nil { | |
216 return false | |
217 } | |
218 root, err = filepath.Abs(filepath.Clean(root)) | |
219 if err != nil { | |
220 return false | |
221 } | |
222 if root == path { | |
223 return true | |
224 } | |
225 if root[len(root)-1] != filepath.Separator { | |
226 root += string(filepath.Separator) | |
227 } | |
228 return strings.HasPrefix(path, root) | |
229 } | |
230 | |
231 //////////////////////////////////////////////////////////////////////////////// | |
232 // FileSystemDestination implementation. | |
233 | |
234 type fileSystemDestination struct { | |
235 // Destination directory. | |
236 dir string | |
237 // Root temporary directory. | |
238 tempDir string | |
239 // Where to extract all temp files, subdirectory of tempDir. | |
240 outDir string | |
241 // Currently open files. | |
242 openFiles map[string]*os.File | |
243 } | |
244 | |
245 // NewFileSystemDestination returns a destination in the file system (directory) | |
246 // to extract a package to. | |
247 func NewFileSystemDestination(dir string) Destination { | |
248 return &fileSystemDestination{ | |
249 dir: dir, | |
250 openFiles: map[string]*os.File{}, | |
251 } | |
252 } | |
253 | |
254 func (d *fileSystemDestination) Begin() (err error) { | |
255 if d.tempDir != "" { | |
256 return fmt.Errorf("Destination is already open") | |
257 } | |
258 | |
259 // Ensure parent directory of destination directory exists. | |
260 d.dir, err = filepath.Abs(filepath.Clean(d.dir)) | |
261 if err != nil { | |
262 return err | |
263 } | |
264 err = os.MkdirAll(filepath.Dir(d.dir), 0777) | |
265 if err != nil { | |
266 return err | |
267 } | |
268 | |
269 // Called in case something below fails. | |
270 cleanup := func() { | |
271 if d.tempDir != "" { | |
272 os.RemoveAll(d.tempDir) | |
273 } | |
274 d.tempDir = "" | |
275 d.outDir = "" | |
276 } | |
277 | |
278 // Create root temp dir, on the same level as destination directory. | |
279 d.tempDir, err = ioutil.TempDir(filepath.Dir(d.dir), filepath.Base(d.dir
)+"_") | |
280 if err != nil { | |
281 cleanup() | |
282 return err | |
283 } | |
284 | |
285 // Create a staging output directory where everything will be extracted. | |
286 d.outDir = filepath.Join(d.tempDir, "out") | |
287 err = os.MkdirAll(d.outDir, 0777) | |
288 if err != nil { | |
289 cleanup() | |
290 return err | |
291 } | |
292 | |
293 return nil | |
294 } | |
295 | |
296 func (d *fileSystemDestination) CreateFile(name string, executable bool) (io.Wri
teCloser, error) { | |
297 _, ok := d.openFiles[name] | |
298 if ok { | |
299 return nil, fmt.Errorf("File %s is already open", name) | |
300 } | |
301 | |
302 path, err := d.prepareFilePath(name) | |
303 if err != nil { | |
304 return nil, err | |
305 } | |
306 | |
307 // Let the umask trim the file mode. Do not set 'writable' bit though. | |
308 var mode os.FileMode | |
309 if executable { | |
310 mode = 0555 | |
311 } else { | |
312 mode = 0444 | |
313 } | |
314 | |
315 file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_EXCL, mode) | |
316 if err != nil { | |
317 return nil, err | |
318 } | |
319 d.openFiles[name] = file | |
320 return &fileSystemDestinationFile{ | |
321 nested: file, | |
322 parent: d, | |
323 closeCallback: func() { | |
324 delete(d.openFiles, name) | |
325 }, | |
326 }, nil | |
327 } | |
328 | |
329 func (d *fileSystemDestination) CreateSymlink(name string, target string) error
{ | |
330 path, err := d.prepareFilePath(name) | |
331 if err != nil { | |
332 return err | |
333 } | |
334 | |
335 // Forbid relative symlinks to files outside of the destination root. | |
336 target = filepath.FromSlash(target) | |
337 if !filepath.IsAbs(target) { | |
338 targetAbs := filepath.Clean(filepath.Join(filepath.Dir(path), ta
rget)) | |
339 if !isSubpath(targetAbs, d.outDir) { | |
340 return fmt.Errorf("Relative symlink is pointing outside
of the destination dir: %s", name) | |
341 } | |
342 } | |
343 | |
344 return os.Symlink(target, path) | |
345 } | |
346 | |
347 func (d *fileSystemDestination) End(success bool) error { | |
348 if d.tempDir == "" { | |
349 return fmt.Errorf("Destination is not open") | |
350 } | |
351 if len(d.openFiles) != 0 { | |
352 return fmt.Errorf("Not all files were closed. Leaking.") | |
353 } | |
354 | |
355 // Clean up temp dir and the state no matter what. | |
356 defer func() { | |
357 os.RemoveAll(d.tempDir) | |
358 d.tempDir = "" | |
359 d.outDir = "" | |
360 }() | |
361 | |
362 if success { | |
363 // Move existing directory away, if it is there. | |
364 old := filepath.Join(d.tempDir, "old") | |
365 if os.Rename(d.dir, old) != nil { | |
366 old = "" | |
367 } | |
368 | |
369 // Move new directory in place. | |
370 err := os.Rename(d.outDir, d.dir) | |
371 if err != nil { | |
372 // Try to return the original directory back... | |
373 if old != "" { | |
374 os.Rename(old, d.dir) | |
375 } | |
376 return err | |
377 } | |
378 } | |
379 | |
380 return nil | |
381 } | |
382 | |
383 // prepareFilePath performs steps common to CreateFile and CreateSymlink: it | |
384 // does some validation, expands "name" to an absolute path and creates parent | |
385 // directories for a future file. Returns absolute path where the file should | |
386 // be put. | |
387 func (d *fileSystemDestination) prepareFilePath(name string) (string, error) { | |
388 if d.tempDir == "" { | |
389 return "", fmt.Errorf("Destination is not open") | |
390 } | |
391 path := filepath.Clean(filepath.Join(d.outDir, filepath.FromSlash(name))
) | |
392 if !isSubpath(path, d.outDir) { | |
393 return "", fmt.Errorf("Invalid relative file name: %s", name) | |
394 } | |
395 err := os.MkdirAll(filepath.Dir(path), 0777) | |
396 if err != nil { | |
397 return "", err | |
398 } | |
399 return path, nil | |
400 } | |
401 | |
402 type fileSystemDestinationFile struct { | |
403 nested io.WriteCloser | |
404 parent *fileSystemDestination | |
405 closeCallback func() | |
406 } | |
407 | |
408 func (f *fileSystemDestinationFile) Write(p []byte) (n int, err error) { | |
409 return f.nested.Write(p) | |
410 } | |
411 | |
412 func (f *fileSystemDestinationFile) Close() error { | |
413 f.closeCallback() | |
414 return f.nested.Close() | |
415 } | |
OLD | NEW |