github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/filesystem/directory_windows.go (about) 1 package filesystem 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "strings" 10 "syscall" 11 12 "golang.org/x/sys/windows" 13 14 aclapi "github.com/hectane/go-acl/api" 15 16 osvendor "github.com/mutagen-io/mutagen/pkg/filesystem/internal/third_party/os" 17 ) 18 19 // ensureValidName verifies that the provided name does not reference the 20 // current directory, the parent directory, or contain a path separator 21 // character. 22 func ensureValidName(name string) error { 23 // Verify that the name does not reference the directory itself or the 24 // parent directory. 25 if name == "." { 26 return errors.New("name is directory reference") 27 } else if name == ".." { 28 return errors.New("name is parent directory reference") 29 } 30 31 // Verify that neither of the path separator characters appears in the name. 32 if strings.IndexByte(name, os.PathSeparator) != -1 { 33 return errors.New("path separator appears in name") 34 } else if strings.IndexByte(name, '/') != -1 { 35 return errors.New("alternate path separator appears in name") 36 } 37 38 // Success. 39 return nil 40 } 41 42 // Directory represents a directory on disk and provides race-free operations on 43 // the directory's contents. All of its operations avoid the traversal of 44 // symbolic links. 45 type Directory struct { 46 // handle is the underlying Windows HANDLE object that has been opened 47 // without the FILE_SHARE_DELETE to ensure that the directory is immovable. 48 handle windows.Handle 49 // file is the underlying os.File object corresponding to the directory. On 50 // Windows systems, it is not actually a wrapper around the handle object, 51 // but rather around a search handle generated by the FindFirstFileExW 52 // function, hence the reason we need to hold open a separate HANDLE. It is 53 // guaranteed that the value returned from the file's Name function will be 54 // an absolute path. 55 file *os.File 56 } 57 58 // Close closes the directory. 59 func (d *Directory) Close() error { 60 // Close the file object. 61 if err := d.file.Close(); err != nil { 62 windows.CloseHandle(d.handle) 63 return fmt.Errorf("unable to close file object: %w", err) 64 } 65 66 // Close the handle. 67 if err := windows.CloseHandle(d.handle); err != nil { 68 return fmt.Errorf("unable to close file handle: %w", err) 69 } 70 71 // Success. 72 return nil 73 } 74 75 // Handle provides access to the raw Windows handle underlying the directory. It 76 // should not be used or retained beyond the point in time where the Close 77 // method is called, and it should not be closed externally. Its usefulness is 78 // to code which relies on handle-based operations. This method does not exist 79 // on POSIX systems, so it should only be used in Windows-specific code. 80 func (d *Directory) Handle() windows.Handle { 81 return d.handle 82 } 83 84 // CreateDirectory creates a new directory with the specified name inside the 85 // directory. The directory will be created with user-only read/write/execute 86 // permissions. 87 func (d *Directory) CreateDirectory(name string) error { 88 // Verify that the name is valid. 89 if err := ensureValidName(name); err != nil { 90 return err 91 } 92 93 // Create the directory. 94 return os.Mkdir(filepath.Join(d.file.Name(), name), 0700) 95 } 96 97 // CreateTemporaryFile creates a new temporary file using the specified name 98 // pattern inside the directory. Pattern behavior follows that of os.CreateTemp. 99 // The file will be created with user-only read/write permissions. 100 func (d *Directory) CreateTemporaryFile(pattern string) (string, io.WriteCloser, error) { 101 // Verify that the name is valid. This should still be a sensible operation 102 // for pattern specifications. 103 if err := ensureValidName(pattern); err != nil { 104 return "", nil, err 105 } 106 107 // Create the temporary file using the standard os implementation. 108 file, err := os.CreateTemp(d.file.Name(), pattern) 109 if err != nil { 110 return "", nil, err 111 } 112 113 // Extract the base name of the file. 114 name := filepath.Base(file.Name()) 115 116 // Success. 117 return name, file, nil 118 } 119 120 // CreateSymbolicLink creates a new symbolic link with the specified name and 121 // target inside the directory. The symbolic link is created with the default 122 // system permissions (which, generally speaking, don't apply to the symbolic 123 // link itself). 124 func (d *Directory) CreateSymbolicLink(name, target string) error { 125 // Verify that the name is valid. 126 if err := ensureValidName(name); err != nil { 127 return err 128 } 129 130 // Create the symbolic link. 131 return os.Symlink(target, filepath.Join(d.file.Name(), name)) 132 } 133 134 // SetPermissions sets the permissions on the content within the directory 135 // specified by name. Ownership information is set first, followed by 136 // permissions extracted from the mode using ModePermissionsMask. Ownership 137 // setting can be skipped completely by providing a nil OwnershipSpecification 138 // or a specification with both components unset. An OwnershipSpecification may 139 // also include only certain components, in which case only those components 140 // will be set. Permission setting can be skipped by providing a mode value that 141 // yields 0 after permission bit masking. 142 func (d *Directory) SetPermissions(name string, ownership *OwnershipSpecification, mode Mode) error { 143 // Verify that the name is valid. 144 if err := ensureValidName(name); err != nil { 145 return err 146 } 147 148 // Compute the target path. 149 path := filepath.Join(d.file.Name(), name) 150 151 // Fix long paths. 152 path = osvendor.FixLongPath(path) 153 154 // Set ownership information, if specified. 155 if ownership != nil && (ownership.ownerSID != nil || ownership.groupSID != nil) { 156 // Compute the information that we're going to set. 157 var information uint32 158 if ownership.ownerSID != nil { 159 information |= aclapi.OWNER_SECURITY_INFORMATION 160 } 161 if ownership.groupSID != nil { 162 information |= aclapi.GROUP_SECURITY_INFORMATION 163 } 164 165 // Set the information. 166 // 167 // NOTE: As with other Windows API functions, calling 168 // SetNamedSecurityInfoW with a path name that exceeds the default 169 // Windows path length limit (without long-path formatting) will result 170 // in failure, and SetNamedSecurityInfoW will return a non-zero error 171 // code to indicate this failure. However, at least on some versions of 172 // Windows, SetNamedSecurityInfoW (and probably SetNamedSecurityInfoA as 173 // well) doesn't seem to call SetLastError to record the error (at least 174 // in this particular failure mode), and thus when Go's Windows syscall 175 // implementation invokes GetLastError to construct the error that it 176 // returns (see https://golang.org/pkg/syscall/?GOOS=windows#Proc.Call), 177 // it will receive ERROR_SUCCESS. The SetNamedSecurityInfo wrapper 178 // function will check for a non-zero return code, see that an error 179 // occurred, and thus return the Go-constructed error that wraps 180 // ERROR_SUCCESS, resulting in a very confusing error message. It's 181 // unfortunate, but at least the error condition is trackable. 182 if err := aclapi.SetNamedSecurityInfo( 183 path, 184 aclapi.SE_FILE_OBJECT, 185 information, 186 ownership.ownerSID, 187 ownership.groupSID, 188 0, 189 0, 190 ); err != nil { 191 return fmt.Errorf("unable to set ownership information: %w", err) 192 } 193 } 194 195 // Set permissions, if specified. 196 mode = mode & ModePermissionsMask 197 if mode != 0 { 198 if err := os.Chmod(path, os.FileMode(mode)); err != nil { 199 return fmt.Errorf("unable to set permission bits: %w", err) 200 } 201 } 202 203 // Success. 204 return nil 205 } 206 207 // openHandle is the underlying open implementation shared by OpenDirectory and 208 // OpenFile. It returns the full target path, the Windows file handle 209 // corresponding to the target, the target metadata, or any error. 210 func (d *Directory) openHandle(name string, wantDirectory bool) (string, windows.Handle, *Metadata, error) { 211 // Verify that the name is valid. 212 if err := ensureValidName(name); err != nil { 213 return "", 0, nil, err 214 } 215 216 // Compute the full path. 217 path := filepath.Join(d.file.Name(), name) 218 219 // Fix long paths. 220 path = osvendor.FixLongPath(path) 221 222 // Convert the path to UTF-16. 223 path16, err := windows.UTF16PtrFromString(path) 224 if err != nil { 225 return "", 0, nil, fmt.Errorf("unable to convert path to UTF-16: %w", err) 226 } 227 228 // Open the path in a manner that is suitable for reading, doesn't allow for 229 // other threads or processes to delete or rename the file while open, 230 // avoids symbolic link traversal (at the path leaf), and has suitable 231 // semantics for both files and directories. 232 handle, err := windows.CreateFile( 233 path16, 234 windows.GENERIC_READ, 235 windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE, 236 nil, 237 windows.OPEN_EXISTING, 238 windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OPEN_REPARSE_POINT, 239 0, 240 ) 241 if err != nil { 242 if os.IsNotExist(err) { 243 return "", 0, nil, err 244 } 245 return "", 0, nil, fmt.Errorf("unable to open path: %w", err) 246 } 247 248 // Query handle metadata. 249 metadata, err := queryHandleMetadata(name, handle) 250 if err != nil { 251 windows.CloseHandle(handle) 252 return "", 0, nil, fmt.Errorf("unable to query file handle metadata: %w", err) 253 } 254 255 // Verify that we're not dealing with a symbolic link. 256 if metadata.Mode&ModeTypeSymbolicLink != 0 { 257 windows.CloseHandle(handle) 258 return "", 0, nil, errors.New("path pointed to symbolic link") 259 } 260 261 // Verify that the handle corresponds to a directory (if requested). 262 if wantDirectory && metadata.Mode&ModeTypeDirectory == 0 { 263 windows.CloseHandle(handle) 264 return "", 0, nil, errors.New("path pointed to non-directory location") 265 } 266 267 // Success. 268 return path, handle, metadata, nil 269 } 270 271 // OpenDirectory opens the directory within the directory specified by name. 272 func (d *Directory) OpenDirectory(name string) (*Directory, error) { 273 // Open the directory handle. 274 path, handle, _, err := d.openHandle(name, true) 275 if err != nil { 276 return nil, fmt.Errorf("unable to open directory handle: %w", err) 277 } 278 279 // Open the corresponding file object. Unfortunately we can't force the file 280 // to use the base name in order to keep consistency with file os.File 281 // objects on Windows and file/directory os.File POSIX, but it's okay since 282 // this object (and its Name method) isn't exposed anyway. 283 file, err := os.Open(path) 284 if err != nil { 285 windows.CloseHandle(handle) 286 return nil, fmt.Errorf("unable to open file object for directory: %w", err) 287 } 288 289 // Success. 290 return &Directory{ 291 handle: handle, 292 file: file, 293 }, nil 294 } 295 296 // ReadContentNames queries the directory contents and returns their base names. 297 // It does not return "." or ".." entries. 298 func (d *Directory) ReadContentNames() ([]string, error) { 299 // Read content names. Fortunately we can use the os.File implementation for 300 // this since it operates on the underlying file descriptor directly. 301 names, err := d.file.Readdirnames(0) 302 if err != nil { 303 return nil, err 304 } 305 306 // Filter names (without allocating a new slice). 307 results := names[:0] 308 for _, name := range names { 309 // Watch for names that reference the directory itself or the parent 310 // directory. The implementation underlying os.File.Readdirnames does 311 // filter these out, but that's not guaranteed by its documentation, so 312 // it's better to do this explicitly. 313 if name == "." || name == ".." { 314 continue 315 } 316 317 // Store the name. 318 results = append(results, name) 319 } 320 321 // Success. 322 return names, nil 323 } 324 325 // ReadContentMetadata reads metadata for the content within the directory 326 // specified by name. 327 func (d *Directory) ReadContentMetadata(name string) (*Metadata, error) { 328 // Verify that the name is valid. 329 if err := ensureValidName(name); err != nil { 330 return nil, err 331 } 332 333 // Query metadata. 334 metadata, err := os.Lstat(filepath.Join(d.file.Name(), name)) 335 if err != nil { 336 return nil, err 337 } 338 339 // Success. 340 return &Metadata{ 341 Name: name, 342 Mode: Mode(metadata.Mode()), 343 Size: uint64(metadata.Size()), 344 ModificationTime: metadata.ModTime(), 345 }, nil 346 } 347 348 // ReadContents queries the directory contents and their associated metadata. 349 // While the results of this function can be computed as a combination of 350 // ReadContentNames and ReadContentMetadata, this function may be significantly 351 // faster than a naïve combination of the two (e.g. due to the usage of 352 // FindFirstFile/FindNextFile infrastructure on Windows). This function doesn't 353 // return metadata for "." or ".." entries. 354 func (d *Directory) ReadContents() ([]*Metadata, error) { 355 // Read directory content. On Windows, we use the os.File implementation to 356 // read names and (an acceptable amount of) metadata in one fell swoop, 357 // rather than using a "read names + loop and query" construct. The reason 358 // for this is that Windows file metadata queries are extremely slow, 359 // requiring use of either GetFileInformationByHandle (which requires 360 // opening the file) or GetFileAttributesEx (which I'm fairly sure uses the 361 // first function under the hood). Instead, os.File.Readdir uses 362 // FindFirstFile/FindNextFile infrastructure under the hood (in fact os.File 363 // is just a search handle for directory objects on Windows), which is much 364 // faster and retrieves just enough of the necessary metadata. 365 contents, err := d.file.Readdir(0) 366 if err != nil { 367 return nil, err 368 } 369 370 // Allocate the result slice with enough capacity to accommodate all 371 // entries. 372 results := make([]*Metadata, 0, len(contents)) 373 374 // Loop over contents. 375 for _, content := range contents { 376 // Watch for names that reference the directory itself or the parent 377 // directory. The implementation underlying os.File.Readdir does seem to 378 // filter these out, but that's not guaranteed by its documentation, so 379 // it's better to do this explicitly. 380 name := content.Name() 381 if name == "." || name == ".." { 382 continue 383 } 384 385 // Convert and append the metadata. Unfortunately we can't populate 386 // FileID and DeviceID because the FindFirstFile/FindNextFile 387 // infrastructure used by the os package doesn't provide access to this 388 // information. We'd have to open each file and use 389 // GetFileInformationByHandle, which is just way too expensive. 390 results = append(results, &Metadata{ 391 Name: name, 392 Mode: Mode(content.Mode()), 393 Size: uint64(content.Size()), 394 ModificationTime: content.ModTime(), 395 }) 396 } 397 398 // Success. 399 return results, nil 400 } 401 402 // OpenFile opens the file within the directory specified by name. 403 func (d *Directory) OpenFile(name string) (io.ReadSeekCloser, *Metadata, error) { 404 // Open the file handle. 405 _, handle, metadata, err := d.openHandle(name, false) 406 if err != nil { 407 return nil, nil, fmt.Errorf("unable to open file handle: %w", err) 408 } 409 410 // Wrap the file handle in an os.File object. We use the base name for the 411 // file since that's the name that was used to "open" the file, which is 412 // what os.File.Name is supposed to return (even though we don't expose 413 // os.File.Name). 414 file := os.NewFile(uintptr(handle), name) 415 416 // Success. 417 return file, metadata, nil 418 } 419 420 // ReadSymbolicLink reads the target of the symbolic link within the directory 421 // specified by name. 422 func (d *Directory) ReadSymbolicLink(name string) (string, error) { 423 // Verify that the name is valid. 424 if err := ensureValidName(name); err != nil { 425 return "", err 426 } 427 428 // Read the symbolic link. 429 return os.Readlink(filepath.Join(d.file.Name(), name)) 430 } 431 432 // RemoveDirectory deletes a directory with the specified name inside the 433 // directory. The removal target must be empty. 434 func (d *Directory) RemoveDirectory(name string) error { 435 // Verify that the name is valid. 436 if err := ensureValidName(name); err != nil { 437 return err 438 } 439 440 // Compute the full path. 441 path := filepath.Join(d.file.Name(), name) 442 443 // Fix long paths. 444 path = osvendor.FixLongPath(path) 445 446 // Convert the path to UTF-16. 447 path16, err := windows.UTF16PtrFromString(path) 448 if err != nil { 449 return fmt.Errorf("unable to convert path to UTF-16: %w", err) 450 } 451 452 // Remove the directory. 453 return windows.RemoveDirectory(path16) 454 } 455 456 // RemoveFile deletes a file with the specified name inside the directory. 457 func (d *Directory) RemoveFile(name string) error { 458 // Verify that the name is valid. 459 if err := ensureValidName(name); err != nil { 460 return err 461 } 462 463 // Compute the full path. 464 path := filepath.Join(d.file.Name(), name) 465 466 // Fix long paths. 467 path = osvendor.FixLongPath(path) 468 469 // Convert the path to UTF-16. 470 path16, err := windows.UTF16PtrFromString(path) 471 if err != nil { 472 return fmt.Errorf("unable to convert path to UTF-16: %w", err) 473 } 474 475 // Remove the file. 476 return windows.DeleteFile(path16) 477 } 478 479 // RemoveSymbolicLink deletes a symbolic link with the specified name inside the 480 // directory. 481 func (d *Directory) RemoveSymbolicLink(name string) error { 482 // Verify that the name is valid. 483 if err := ensureValidName(name); err != nil { 484 return err 485 } 486 487 // Compute the full path. 488 path := filepath.Join(d.file.Name(), name) 489 490 // On Windows, we need the same type-based fallback logic used in os.Remove 491 // (i.e. trying file removal first and then directory removal), so we just 492 // use that. This is necessary because Windows symbolic links are typed and 493 // have to be removed with the appropriate removal function. 494 return os.Remove(path) 495 } 496 497 // Rename performs an atomic rename operation from one filesystem location (the 498 // source) to another (the target). Each location can be specified in one of two 499 // ways: either by a combination of directory and (non-path) name or by path 500 // (with corresponding nil Directory object). Different specification mechanisms 501 // can be used for each location. 502 // 503 // This function does not support cross-device renames. To detect whether or not 504 // an error is due to an attempted cross-device rename, use the 505 // IsCrossDeviceError function. 506 func Rename( 507 sourceDirectory *Directory, sourceNameOrPath string, 508 targetDirectory *Directory, targetNameOrPath string, 509 replace bool, 510 ) error { 511 // Adjust the source path if necessary. 512 if sourceDirectory != nil { 513 if err := ensureValidName(sourceNameOrPath); err != nil { 514 return fmt.Errorf("source name invalid: %w", err) 515 } 516 sourceNameOrPath = filepath.Join(sourceDirectory.file.Name(), sourceNameOrPath) 517 } 518 519 // Adjust the target path if necessary. 520 if targetDirectory != nil { 521 if err := ensureValidName(targetNameOrPath); err != nil { 522 return fmt.Errorf("target name invalid: %w", err) 523 } 524 targetNameOrPath = filepath.Join(targetDirectory.file.Name(), targetNameOrPath) 525 } 526 527 // Convert paths to UTF-16. 528 sourceNameOrPathUTF16, err := windows.UTF16PtrFromString(sourceNameOrPath) 529 if err != nil { 530 return fmt.Errorf("unable to convert source path to UTF-16: %w", err) 531 } 532 targetNameOrPathUTF16, err := windows.UTF16PtrFromString(targetNameOrPath) 533 if err != nil { 534 return fmt.Errorf("unable to convert targt path to UTF-16: %w", err) 535 } 536 537 // Compute flags. 538 var flags uint32 539 if replace { 540 flags = uint32(windows.MOVEFILE_REPLACE_EXISTING) 541 } 542 543 // Attempt the rename operation. 544 return windows.MoveFileEx(sourceNameOrPathUTF16, targetNameOrPathUTF16, flags) 545 } 546 547 const ( 548 // _ERROR_NOT_SAME_DEVICE is the error code returned by MoveFileEx on 549 // Windows when attempting to move a file across devices (without the 550 // MOVEFILE_COPY_ALLOWED flag being specified). 551 _ERROR_NOT_SAME_DEVICE = 0x11 552 ) 553 554 // IsCrossDeviceError checks whether or not an error returned from rename 555 // represents a cross-device error. 556 func IsCrossDeviceError(err error) bool { 557 if errno, ok := err.(syscall.Errno); !ok { 558 return false 559 } else { 560 return errno == _ERROR_NOT_SAME_DEVICE 561 } 562 }