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 }