github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/filesystem/directory_posix.go (about) 1 //go:build !windows 2 3 package filesystem 4 5 import ( 6 cryptorand "crypto/rand" 7 "encoding/binary" 8 "errors" 9 "fmt" 10 "io" 11 "math/rand" 12 "os" 13 "runtime" 14 "strconv" 15 "strings" 16 "sync" 17 "time" 18 19 "golang.org/x/sys/unix" 20 21 "github.com/mutagen-io/mutagen/pkg/state" 22 ) 23 24 // ensureValidName verifies that the provided name does not reference the 25 // current directory, the parent directory, or contain a path separator 26 // character. 27 func ensureValidName(name string) error { 28 // Verify that the name does not reference the directory itself or the 29 // parent directory. 30 if name == "." { 31 return errors.New("name is directory reference") 32 } else if name == ".." { 33 return errors.New("name is parent directory reference") 34 } 35 36 // Verify that the path separator character does not appear in the name. 37 if strings.IndexByte(name, os.PathSeparator) != -1 { 38 return errors.New("path separator appears in name") 39 } 40 41 // Success. 42 return nil 43 } 44 45 // Directory represents a directory on disk and provides race-free operations on 46 // the directory's contents. All of its operations avoid the traversal of 47 // symbolic links. 48 type Directory struct { 49 // descriptor is the file descriptor for the directory, designed to be used 50 // in conjunction with POSIX *at functions. It is wrapped by the os.File 51 // object below (file) and should not be closed directly. 52 descriptor int 53 // file is an os.File object which wraps the directory descriptor. It is 54 // required for its Readdirnames function, since there's no other portable 55 // way to do this from Go. 56 file *os.File 57 // exhausted indicates that the directory's contents have been read and that 58 // a seek is required before reading them again. 59 exhausted bool 60 // renameatNoReplaceUnsupported is marked if 61 // renameatNoReplaceRetryingOnEINTR is found to be unsupported with this 62 // directory as a target. 63 renameatNoReplaceUnsupported state.Marker 64 } 65 66 // Close closes the directory. 67 func (d *Directory) Close() error { 68 return d.file.Close() 69 } 70 71 // Descriptor provides access to the raw file descriptor underlying the 72 // directory. It should not be used or retained beyond the point in time where 73 // the Close method is called, and it should not be closed externally. Its 74 // usefulness is to code which relies on file-descriptor-based operations. This 75 // method does not exist on Windows systems, so it should only be used in 76 // POSIX-specific code. 77 func (d *Directory) Descriptor() int { 78 return d.descriptor 79 } 80 81 // CreateDirectory creates a new directory with the specified name inside the 82 // directory. The directory will be created with user-only read/write/execute 83 // permissions. 84 func (d *Directory) CreateDirectory(name string) error { 85 // Verify that the name is valid. 86 if err := ensureValidName(name); err != nil { 87 return err 88 } 89 90 // Create the directory. 91 return mkdiratRetryingOnEINTR(d.descriptor, name, 0700) 92 } 93 94 // createTemporaryFilePRNGLock serializes access to createTemporaryFilePRNG. 95 var createTemporaryFilePRNGLock sync.Mutex 96 97 // createTemporaryFilePRNG provides pseudorandom numbers for filenames in 98 // Directory.CreateTemporaryFile. 99 var createTemporaryFilePRNG *rand.Rand 100 101 func init() { 102 // Read random data to compute a seed for the pseudorandom number generator. 103 var seedBytes [8]byte 104 if _, err := cryptorand.Read(seedBytes[:]); err != nil { 105 panic("unable to read random bytes for seed") 106 } 107 108 // Initialize the pseudorandom number generator. 109 createTemporaryFilePRNG = rand.New(rand.NewSource(int64(binary.BigEndian.Uint64(seedBytes[:])))) 110 } 111 112 // CreateTemporaryFile creates a new temporary file using the specified name 113 // pattern inside the directory. Pattern behavior follows that of os.CreateTemp. 114 // The file will be created with user-only read/write permissions. 115 func (d *Directory) CreateTemporaryFile(pattern string) (string, io.WriteCloser, error) { 116 // Verify that the name is valid. This should still be a sensible operation 117 // for pattern specifications. 118 if err := ensureValidName(pattern); err != nil { 119 return "", nil, err 120 } 121 122 // Parse the pattern into prefix and suffix components. 123 var prefix, suffix string 124 if starIndex := strings.LastIndex(pattern, "*"); starIndex != -1 { 125 prefix, suffix = pattern[:starIndex], pattern[starIndex+1:] 126 } else { 127 prefix = pattern 128 } 129 130 // Iterate until we can find a free file name. 131 try := 0 132 for { 133 // Compute the next potential name using a pseudorandom component. 134 createTemporaryFilePRNGLock.Lock() 135 random := createTemporaryFilePRNG.Int() 136 createTemporaryFilePRNGLock.Unlock() 137 name := prefix + strconv.Itoa(random) + suffix 138 139 // Open the file. Note that we needn't specify O_NOFOLLOW here since 140 // we're enforcing that the file doesn't already exist. 141 descriptor, err := openatRetryingOnEINTR(d.descriptor, name, unix.O_RDWR|unix.O_CREAT|unix.O_EXCL|unix.O_CLOEXEC, 0600) 142 if err != nil { 143 if os.IsExist(err) { 144 if try++; try < 10000 { 145 continue 146 } 147 return "", nil, errors.New("exhausted potential file names") 148 } 149 return "", nil, err 150 } 151 152 // Wrap up the descriptor in a file object. 153 file := os.NewFile(uintptr(descriptor), name) 154 155 // Success. 156 return name, file, nil 157 } 158 } 159 160 // CreateSymbolicLink creates a new symbolic link with the specified name and 161 // target inside the directory. The symbolic link is created with the default 162 // system permissions (which, generally speaking, don't apply to the symbolic 163 // link itself). 164 func (d *Directory) CreateSymbolicLink(name, target string) error { 165 // Verify that the name is valid. 166 if err := ensureValidName(name); err != nil { 167 return err 168 } 169 170 // Create the symbolic link. 171 return symlinkatRetryingOnEINTR(target, d.descriptor, name) 172 } 173 174 // SetPermissions sets the permissions on the content within the directory 175 // specified by name. Ownership information is set first, followed by 176 // permissions extracted from the mode using ModePermissionsMask. Ownership 177 // setting can be skipped completely by providing a nil OwnershipSpecification 178 // or a specification with both components unset. An OwnershipSpecification may 179 // also include only certain components, in which case only those components 180 // will be set. Permission setting can be skipped by providing a mode value that 181 // yields 0 after permission bit masking. 182 func (d *Directory) SetPermissions(name string, ownership *OwnershipSpecification, mode Mode) error { 183 // Verify that the name is valid. 184 if err := ensureValidName(name); err != nil { 185 return err 186 } 187 188 // Set ownership information, if specified. 189 if ownership != nil && (ownership.ownerID != -1 || ownership.groupID != -1) { 190 if err := fchownatRetryingOnEINTR(d.descriptor, name, ownership.ownerID, ownership.groupID, unix.AT_SYMLINK_NOFOLLOW); err != nil { 191 return fmt.Errorf("unable to set ownership information: %w", err) 192 } 193 } 194 195 // Set permissions, if specified. 196 // 197 // HACK: On Linux, the AT_SYMLINK_NOFOLLOW flag is not supported by fchmodat 198 // and will result in an ENOTSUP error, so we have to use a workaround that 199 // opens a file and then uses fchmod in order to avoid setting permissions 200 // across a symbolic link. 201 mode &= ModePermissionsMask 202 if mode != 0 { 203 if runtime.GOOS == "linux" { 204 if f, err := openatRetryingOnEINTR(d.descriptor, name, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0); err != nil { 205 return fmt.Errorf("unable to open file: %w", err) 206 } else if err = fchmodRetryingOnEINTR(f, uint32(mode)); err != nil { 207 closeConsideringEINTR(f) 208 return fmt.Errorf("unable to set permission bits on file: %w", err) 209 } else if err = closeConsideringEINTR(f); err != nil { 210 return fmt.Errorf("unable to close file: %w", err) 211 } 212 } else { 213 if err := fchmodatRetryingOnEINTR(d.descriptor, name, uint32(mode), unix.AT_SYMLINK_NOFOLLOW); err != nil { 214 return fmt.Errorf("unable to set permission bits: %w", err) 215 } 216 } 217 } 218 219 // Success. 220 return nil 221 } 222 223 // open is the underlying open implementation shared by OpenDirectory and 224 // OpenFile. It returns the file descriptor corresponding to the target, the 225 // target metadata if the target is a file (nil otherwise), or any error. 226 func (d *Directory) open(name string, wantDirectory bool) (int, *Metadata, error) { 227 // Verify that the name is valid. 228 if wantDirectory && name == "." { 229 // As a special case, we allow directories to be re-opened on POSIX 230 // systems. This is safe since it doesn't allow traversal. 231 } else if err := ensureValidName(name); err != nil { 232 return -1, nil, err 233 } 234 235 // Open the file for reading while avoiding symbolic link traversal. If a 236 // directory has been requested, then enforce its type here. 237 flags := unix.O_RDONLY | unix.O_NOFOLLOW | unix.O_CLOEXEC | extraOpenFlags 238 if wantDirectory { 239 flags |= unix.O_DIRECTORY 240 } 241 descriptor, err := openatRetryingOnEINTR(d.descriptor, name, flags, 0) 242 if err != nil { 243 return -1, nil, err 244 } 245 246 // If a file has been requested, then verify that's what we've received. 247 // This (along with the directory enforcement above) keeps parity with the 248 // Windows implementation, where checking file type is required for the 249 // implementation to work at all. Unfortunately there's no O_FILE flag 250 // analagous to O_DIRECTORY that we can use with openat, so we have to check 251 // this using an fstat operation. There is some overhead to performing this 252 // check, of course, and on POSIX we could probably live without it (simply 253 // allowing other methods on the resulting object to fail due to an 254 // unexpected type), but given the typical filesystem access patterns at 255 // play when using this code, the overhead will be minimal since this 256 // information should still be in the OS's stat cache. 257 var metadata *Metadata 258 if !wantDirectory { 259 var rawMetadata unix.Stat_t 260 if err := fstatRetryingOnEINTR(descriptor, &rawMetadata); err != nil { 261 closeConsideringEINTR(descriptor) 262 return -1, nil, fmt.Errorf("unable to query file metadata: %w", err) 263 } else if Mode(rawMetadata.Mode)&ModeTypeMask != ModeTypeFile { 264 closeConsideringEINTR(descriptor) 265 return -1, nil, errors.New("path is not a file") 266 } 267 metadata = &Metadata{ 268 Name: name, 269 Mode: Mode(rawMetadata.Mode), 270 Size: uint64(rawMetadata.Size), 271 ModificationTime: time.Unix(rawMetadata.Mtim.Unix()), 272 DeviceID: uint64(rawMetadata.Dev), 273 FileID: uint64(rawMetadata.Ino), 274 } 275 } 276 277 // Success. 278 return descriptor, metadata, nil 279 } 280 281 // OpenDirectory opens the directory within the directory specified by name. On 282 // POSIX systems, the directory itself can be re-opened (with a different 283 // underlying file handle pointing to the same directory) by passing "." to this 284 // function. 285 func (d *Directory) OpenDirectory(name string) (*Directory, error) { 286 // Call the underlying open method. 287 descriptor, _, err := d.open(name, true) 288 if err != nil { 289 return nil, err 290 } 291 292 // Success. 293 return &Directory{ 294 descriptor: descriptor, 295 file: os.NewFile(uintptr(descriptor), name), 296 }, nil 297 } 298 299 // ReadContentNames queries the directory contents and returns their base names. 300 // It does not return "." or ".." entries. 301 func (d *Directory) ReadContentNames() ([]string, error) { 302 // If we've already performed a read on the directory's contents, then we 303 // need to rewind the directory before performing another read. 304 if d.exhausted { 305 if offset, err := seekConsideringEINTR(d.descriptor, 0, 0); err != nil { 306 return nil, fmt.Errorf("unable to reset directory read pointer: %w", err) 307 } else if offset != 0 { 308 return nil, errors.New("directory offset is non-zero after seek operation") 309 } 310 d.exhausted = false 311 } 312 313 // Read content names. Fortunately we can use the os.File implementation for 314 // this since it operates on the underlying file descriptor directly. We 315 // always mark the directory as exhausted, even if we fail to read it all 316 // the way to the end. 317 names, err := d.file.Readdirnames(0) 318 d.exhausted = true 319 if err != nil { 320 return nil, err 321 } 322 323 // Filter names (without allocating a new slice). 324 results := names[:0] 325 for _, name := range names { 326 // Watch for names that reference the directory itself or the parent 327 // directory. The implementation underlying os.File.Readdirnames does 328 // filter these out, but that's not guaranteed by its documentation, so 329 // it's better to do this explicitly. 330 if name == "." || name == ".." { 331 continue 332 } 333 334 // Store the name. 335 results = append(results, name) 336 } 337 338 // Success. 339 return names, nil 340 } 341 342 // ReadContentMetadata reads metadata for the content within the directory 343 // specified by name. 344 func (d *Directory) ReadContentMetadata(name string) (*Metadata, error) { 345 // Verify that the name is valid. 346 if err := ensureValidName(name); err != nil { 347 return nil, err 348 } 349 350 // Perform the actual query operation. 351 return d.readContentMetadata(name) 352 } 353 354 // readContentMetadata reads metadata for the content within the directory 355 // specified by name, but does not perform a check for name validity. 356 func (d *Directory) readContentMetadata(name string) (*Metadata, error) { 357 // Query metadata. 358 var metadata unix.Stat_t 359 if err := fstatatRetryingOnEINTR(d.descriptor, name, &metadata, unix.AT_SYMLINK_NOFOLLOW); err != nil { 360 return nil, err 361 } 362 363 // Success. 364 return &Metadata{ 365 Name: name, 366 Mode: Mode(metadata.Mode), 367 Size: uint64(metadata.Size), 368 ModificationTime: time.Unix(metadata.Mtim.Unix()), 369 DeviceID: uint64(metadata.Dev), 370 FileID: uint64(metadata.Ino), 371 }, nil 372 } 373 374 // ReadContents queries the directory contents and their associated metadata. 375 // While the results of this function can be computed as a combination of 376 // ReadContentNames and ReadContentMetadata, this function may be significantly 377 // faster than a naïve combination of the two (e.g. due to the usage of 378 // FindFirstFile/FindNextFile infrastructure on Windows). This function doesn't 379 // return metadata for "." or ".." entries. 380 func (d *Directory) ReadContents() ([]*Metadata, error) { 381 // Read content names. 382 names, err := d.ReadContentNames() 383 if err != nil { 384 return nil, fmt.Errorf("unable to read directory content names: %w", err) 385 } 386 387 // Allocate the result slice with enough capacity to accommodate all 388 // entries. 389 results := make([]*Metadata, 0, len(names)) 390 391 // Loop over names and grab their individual metadata. 392 for _, name := range names { 393 // Grab metadata for this entry. We don't need to validate its name in 394 // this scenario since we just received it from the OS. If the file has 395 // disappeared between listing and the metadata query, then just pretend 396 // that it never existed. 397 if m, err := d.readContentMetadata(name); err != nil { 398 if os.IsNotExist(err) { 399 continue 400 } 401 return nil, fmt.Errorf("unable to access content metadata: %w", err) 402 } else { 403 results = append(results, m) 404 } 405 } 406 407 // Success. 408 return results, nil 409 } 410 411 // OpenFile opens the file within the directory specified by name. 412 func (d *Directory) OpenFile(name string) (io.ReadSeekCloser, *Metadata, error) { 413 // Perform the open operation. 414 descriptor, metadata, err := d.open(name, false) 415 if err != nil { 416 return nil, nil, err 417 } 418 419 // Convert the file descriptor to a usable type. 420 return file(descriptor), metadata, err 421 } 422 423 // readlinkInitialBufferSize specifies the initial buffer size to use for 424 // readlinkat operations. It should be large enough to accommodate most symbolic 425 // links but not so large that every readlinkat operation incurs an inordinate 426 // amount of allocation overhead. This value is pinched from the os.Readlink 427 // implementation. 428 const readlinkInitialBufferSize = 128 429 430 // ReadSymbolicLink reads the target of the symbolic link within the directory 431 // specified by name. 432 func (d *Directory) ReadSymbolicLink(name string) (string, error) { 433 // Verify that the name is valid. 434 if err := ensureValidName(name); err != nil { 435 return "", err 436 } 437 438 // Loop until we encounter a condition where we successfully read the 439 // symbolic link and with buffer space to spare. This is the only way to 440 // approach the problem because readlink and its ilk don't provide any 441 // mechanism for determining the untruncated length of the symbolic link. 442 for size := readlinkInitialBufferSize; ; size *= 2 { 443 // Allocate a buffer. 444 buffer := make([]byte, size) 445 446 // Read the symbolic link target. 447 count, err := readlinkatRetryingOnEINTR(d.descriptor, name, buffer) 448 449 // Handle errors. If we see ERANGE on AIX systems, it's an indication 450 // that the buffer size is too small. 451 if runtime.GOOS == "aix" && err == unix.ERANGE { 452 continue 453 } else if err != nil { 454 return "", err 455 } 456 457 // Verify that the count is sane. We diverge from the os.Readlink 458 // implementation here (which just sets this value to 0 if negative), 459 // because POSIX specifically says a return value of -1 is indicative of 460 // an error. 461 if count < 0 { 462 return "", errors.New("unknown readlinkat failure occurred") 463 } 464 465 // If we've managed to read the target and have buffer space to spare, 466 // then we know that we have the full link. 467 if count < size { 468 return string(buffer[:count]), nil 469 } 470 } 471 } 472 473 // RemoveDirectory deletes a directory with the specified name inside the 474 // directory. The removal target must be empty. 475 func (d *Directory) RemoveDirectory(name string) error { 476 // Verify that the name is valid. 477 if err := ensureValidName(name); err != nil { 478 return err 479 } 480 481 // Remove the directory. 482 return unlinkatRetryingOnEINTR(d.descriptor, name, unix.AT_REMOVEDIR) 483 } 484 485 // RemoveFile deletes a file with the specified name inside the directory. 486 func (d *Directory) RemoveFile(name string) error { 487 // Verify that the name is valid. 488 if err := ensureValidName(name); err != nil { 489 return err 490 } 491 492 // Remove the file. 493 return unlinkatRetryingOnEINTR(d.descriptor, name, 0) 494 } 495 496 // RemoveSymbolicLink deletes a symbolic link with the specified name inside the 497 // directory. 498 func (d *Directory) RemoveSymbolicLink(name string) error { 499 return d.RemoveFile(name) 500 } 501 502 // Rename performs an atomic rename operation from one filesystem location (the 503 // source) to another (the target). Each location can be specified in one of two 504 // ways: either by a combination of directory and (non-path) name or by path 505 // (with corresponding nil Directory object). Different specification mechanisms 506 // can be used for each location. 507 // 508 // This function does not support cross-device renames. To detect whether or not 509 // an error is due to an attempted cross-device rename, use the 510 // IsCrossDeviceError function. 511 func Rename( 512 sourceDirectory *Directory, sourceNameOrPath string, 513 targetDirectory *Directory, targetNameOrPath string, 514 replace bool, 515 ) error { 516 // If a source directory has been provided, then verify that the source name 517 // is valid and extract the source directory descriptor. 518 sourceDescriptor := unix.AT_FDCWD 519 if sourceDirectory != nil { 520 if err := ensureValidName(sourceNameOrPath); err != nil { 521 return fmt.Errorf("source name invalid: %w", err) 522 } 523 sourceDescriptor = sourceDirectory.descriptor 524 } 525 526 // If a target directory has been provided, then verify that the target name 527 // is valid and extract the target directory descriptor. 528 targetDescriptor := unix.AT_FDCWD 529 if targetDirectory != nil { 530 if err := ensureValidName(targetNameOrPath); err != nil { 531 return fmt.Errorf("target name invalid: %w", err) 532 } 533 targetDescriptor = targetDirectory.descriptor 534 } 535 536 // If we're allowing the target to be replaced, then just attempt a standard 537 // rename operation. 538 if replace { 539 return renameatRetryingOnEINTR( 540 sourceDescriptor, sourceNameOrPath, 541 targetDescriptor, targetNameOrPath, 542 ) 543 } 544 545 // Since we're not allowing replacement, we need to ensure that the target 546 // doesn't exist. Some platforms provide specialized renameat variants and 547 // flags for this purpose, so we'll see if that's the case first. We'll skip 548 // this if we've already determined that the target directory's filesystem 549 // doesn't support this mechanism. 550 if targetDirectory == nil || !targetDirectory.renameatNoReplaceUnsupported.Marked() { 551 err := renameatNoReplaceRetryingOnEINTR( 552 sourceDescriptor, sourceNameOrPath, 553 targetDescriptor, targetNameOrPath, 554 ) 555 if err == nil || (err != unix.ENOTSUP && err != unix.ENOSYS) { 556 return err 557 } else if err == unix.ENOTSUP && targetDirectory != nil { 558 targetDirectory.renameatNoReplaceUnsupported.Mark() 559 } 560 } 561 562 // There either isn't a non-replacing variant of renameat available or it 563 // isn't supported on this platform or target filesystem. In any case, we're 564 // falling back to the slower and less atomic method, so check if the target 565 // exists. 566 var probeErr error 567 if targetDirectory != nil { 568 _, probeErr = targetDirectory.ReadContentMetadata(targetNameOrPath) 569 } else { 570 _, probeErr = os.Lstat(targetNameOrPath) 571 } 572 if probeErr == nil { 573 return os.ErrExist 574 } else if !os.IsNotExist(probeErr) { 575 return fmt.Errorf("unable to probe target existence: %w", probeErr) 576 } 577 578 // RACE: There's a race window here between the time of our check and the 579 // time that the file is renamed. This is a limitation of the POSIX API. 580 581 // Attempt the rename operation. 582 return renameatRetryingOnEINTR( 583 sourceDescriptor, sourceNameOrPath, 584 targetDescriptor, targetNameOrPath, 585 ) 586 } 587 588 // IsCrossDeviceError checks whether or not an error returned from rename 589 // represents a cross-device error. 590 func IsCrossDeviceError(err error) bool { 591 return err == unix.EXDEV 592 }