github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/filesystem/behavior/executability.go (about)

     1  package behavior
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"runtime"
     7  
     8  	"github.com/mutagen-io/mutagen/pkg/filesystem"
     9  )
    10  
    11  const (
    12  	// executabilityProbeFileNamePrefix is the prefix used for temporary files
    13  	// created by the executability preservation test.
    14  	executabilityProbeFileNamePrefix = filesystem.TemporaryNamePrefix + "executability-test"
    15  )
    16  
    17  // PreservesExecutabilityByPath determines whether or not the filesystem on
    18  // which the directory at the specified path resides preserves POSIX
    19  // executability bits. It allows for the path leaf to be a symbolic link. The
    20  // second value returned by this function indicates whether or not probe files
    21  // were used in determining behavior.
    22  func PreservesExecutabilityByPath(path string, probeMode ProbeMode) (bool, bool, error) {
    23  	// Check the filesystem probing mode and see if we can return an assumption.
    24  	if probeMode == ProbeMode_ProbeModeAssume {
    25  		return assumeExecutabilityPreservation, false, nil
    26  	} else if !probeMode.Supported() {
    27  		panic("invalid probe mode")
    28  	}
    29  
    30  	// Check if we have a fast test that will work. If we're on Windows, we
    31  	// enforce that the fast path was used. There is some code below, namely the
    32  	// use of os.File's Chmod method (and possibly the os.File's Stat method,
    33  	// which may be racey on Windows), that won't work on Windows (though it
    34  	// could possibly be adapted in case we add a force-probe probe mode), which
    35  	// is why we require that the fast path succeeds on Windows.
    36  	if result, ok := probeExecutabilityPreservationFastByPath(path); ok {
    37  		return result, false, nil
    38  	} else if runtime.GOOS == "windows" {
    39  		panic("fast path not used on Windows")
    40  	}
    41  
    42  	// Create a temporary file.
    43  	file, err := os.CreateTemp(path, executabilityProbeFileNamePrefix)
    44  	if err != nil {
    45  		return false, true, fmt.Errorf("unable to create test file: %w", err)
    46  	}
    47  
    48  	// Ensure that the file is cleaned up and removed when we're done.
    49  	defer func() {
    50  		file.Close()
    51  		os.Remove(file.Name())
    52  	}()
    53  
    54  	// Mark the file as user-executable. We use the os.File-based Chmod here
    55  	// since this code only runs on POSIX systems where this is supported.
    56  	if err = file.Chmod(0700); err != nil {
    57  		return false, true, fmt.Errorf("unable to mark test file as executable: %w", err)
    58  	}
    59  
    60  	// Grab the file statistics and check for executability. We enforce that
    61  	// only the user-executable bit is set, because filesystems that don't
    62  	// preserve executability on POSIX systems (e.g. FAT32 on Darwin) sometimes
    63  	// mark every file as having every executable bit set, which is another type
    64  	// of non-preserving behavior. This behavior is not universal (e.g. FAT32 on
    65  	// Linux marks every file as having no executable bit set), but this test
    66  	// should be.
    67  	if info, err := file.Stat(); err != nil {
    68  		return false, true, fmt.Errorf("unable to check test file executability: %w", err)
    69  	} else {
    70  		return info.Mode()&0111 == 0100, true, nil
    71  	}
    72  }
    73  
    74  // PreservesExecutability determines whether or not the specified directory (and
    75  // its underlying filesystem) preserves POSIX executability bits. The second
    76  // value returned by this function indicates whether or not probe files were
    77  // used in determining behavior.
    78  func PreservesExecutability(directory *filesystem.Directory, probeMode ProbeMode) (bool, bool, error) {
    79  	// Check the filesystem probing mode and see if we can return an assumption.
    80  	if probeMode == ProbeMode_ProbeModeAssume {
    81  		return assumeExecutabilityPreservation, false, nil
    82  	} else if !probeMode.Supported() {
    83  		panic("invalid probe mode")
    84  	}
    85  
    86  	// Check if we have a fast test that will work. If we're on Windows, we
    87  	// enforce that the fast path was used. There is some code below, namely the
    88  	// use of os.File's Chmod method (and possibly the os.File's Stat method,
    89  	// which may be racey on Windows), that won't work on Windows (though it
    90  	// could possibly be adapted in case we add a force-probe probe mode), which
    91  	// is why we require that the fast path succeeds on Windows.
    92  	if result, ok := probeExecutabilityPreservationFast(directory); ok {
    93  		return result, false, nil
    94  	} else if runtime.GOOS == "windows" {
    95  		panic("fast path not used on Windows")
    96  	}
    97  
    98  	// Create a temporary file.
    99  	name, file, err := directory.CreateTemporaryFile(executabilityProbeFileNamePrefix)
   100  	if err != nil {
   101  		return false, true, fmt.Errorf("unable to create test file: %w", err)
   102  	}
   103  
   104  	// Ensure that the file is cleaned up and removed when we're done.
   105  	defer func() {
   106  		file.Close()
   107  		directory.RemoveFile(name)
   108  	}()
   109  
   110  	// HACK: Convert the file to an os.File object for race-free Chmod and Stat
   111  	// access. This is an acceptable hack since we live inside the same package
   112  	// as the Directory implementation.
   113  	osFile, ok := file.(*os.File)
   114  	if !ok {
   115  		panic("opened file is not an os.File object")
   116  	}
   117  
   118  	// Mark the file as user-executable. We use the os.File-based Chmod here
   119  	// since this code only runs on POSIX systems where this is supported.
   120  	if err = osFile.Chmod(0700); err != nil {
   121  		return false, true, fmt.Errorf("unable to mark test file as executable: %w", err)
   122  	}
   123  
   124  	// Grab the file statistics and check for executability. We enforce that
   125  	// only the user-executable bit is set, because filesystems that don't
   126  	// preserve executability on POSIX systems (e.g. FAT32 on Darwin) sometimes
   127  	// mark every file as having every executable bit set, which is another type
   128  	// of non-preserving behavior. This behavior is not universal (e.g. FAT32 on
   129  	// Linux marks every file as having no executable bit set), but this test
   130  	// should be.
   131  	if info, err := osFile.Stat(); err != nil {
   132  		return false, true, fmt.Errorf("unable to check test file executability: %w", err)
   133  	} else {
   134  		return info.Mode()&0111 == 0100, true, nil
   135  	}
   136  }