| 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 |