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  }