github.com/Serizao/go-winio@v0.0.0-20230906082528-f02f7f4ad6e8/pkg/fs/resolve_test.go (about) 1 //go:build windows 2 3 package fs 4 5 import ( 6 "os" 7 "path/filepath" 8 "strings" 9 "syscall" 10 "testing" 11 12 "golang.org/x/sys/windows" 13 14 "github.com/Serizao/go-winio/internal/computestorage" 15 "github.com/Serizao/go-winio/internal/fs" 16 "github.com/Serizao/go-winio/vhd" 17 ) 18 19 func getWindowsBuildNumber() uint32 { 20 // RtlGetVersion ignores manifest requirements 21 vex := windows.RtlGetVersion() 22 return vex.BuildNumber 23 } 24 25 func makeSymlink(t *testing.T, oldName string, newName string) { 26 t.Helper() 27 28 if err := os.Symlink(oldName, newName); err != nil { 29 t.Fatalf("creating symlink: %s", err) 30 } 31 } 32 33 func getVolumeGUIDPath(t *testing.T, path string) string { 34 t.Helper() 35 36 h, err := openMetadata(path) 37 if err != nil { 38 t.Fatal(err) 39 } 40 defer windows.CloseHandle(h) //nolint:errcheck 41 final, err := fs.GetFinalPathNameByHandle(h, fs.FILE_NAME_OPENED|fs.VOLUME_NAME_GUID) 42 if err != nil { 43 t.Fatal(err) 44 } 45 return final 46 } 47 48 func openDisk(path string) (windows.Handle, error) { 49 h, err := fs.CreateFile( 50 path, 51 windows.GENERIC_READ|windows.GENERIC_WRITE, 52 fs.FILE_SHARE_READ|fs.FILE_SHARE_WRITE, 53 nil, // security attributes 54 fs.OPEN_EXISTING, 55 windows.FILE_ATTRIBUTE_NORMAL|fs.FILE_FLAG_NO_BUFFERING, 56 fs.NullHandle) 57 if err != nil { 58 return 0, &os.PathError{ 59 Op: "CreateFile", 60 Path: path, 61 Err: err, 62 } 63 } 64 return h, nil 65 } 66 67 func formatVHD(vhdHandle windows.Handle) error { 68 h := vhdHandle 69 // Pre-19H1 HcsFormatWritableLayerVhd expects a disk handle. 70 // On newer builds it expects a VHD handle instead. 71 // Open a handle to the VHD's disk object if needed. 72 73 // Windows Server 1903, aka 19H1 74 if getWindowsBuildNumber() < 18362 { 75 diskPath, err := vhd.GetVirtualDiskPhysicalPath(syscall.Handle(h)) 76 if err != nil { 77 return err 78 } 79 diskHandle, err := openDisk(diskPath) 80 if err != nil { 81 return err 82 } 83 defer windows.CloseHandle(diskHandle) //nolint:errcheck // cleanup code 84 h = diskHandle 85 } 86 // Formatting a disk directly in Windows is a pain, so we use FormatWritableLayerVhd to do it. 87 // It has a side effect of creating a sandbox directory on the formatted volume, but it's safe 88 // to just ignore that for our purposes here. 89 return computestorage.FormatWritableLayerVHD(h) 90 } 91 92 // Creates a VHD with a NTFS volume. Returns the volume path. 93 func setupVHDVolume(t *testing.T, vhdPath string) string { 94 t.Helper() 95 96 vhdHandle, err := vhd.CreateVirtualDisk(vhdPath, 97 vhd.VirtualDiskAccessNone, vhd.CreateVirtualDiskFlagNone, 98 &vhd.CreateVirtualDiskParameters{ 99 Version: 2, 100 Version2: vhd.CreateVersion2{ 101 MaximumSize: 5 * 1024 * 1024 * 1024, // 5GB, thin provisioned 102 BlockSizeInBytes: 1 * 1024 * 1024, // 1MB 103 }, 104 }) 105 if err != nil { 106 t.Fatal(err) 107 } 108 t.Cleanup(func() { 109 _ = windows.CloseHandle(windows.Handle(vhdHandle)) 110 }) 111 if err := vhd.AttachVirtualDisk(vhdHandle, vhd.AttachVirtualDiskFlagNone, &vhd.AttachVirtualDiskParameters{Version: 1}); err != nil { 112 t.Fatal(err) 113 } 114 t.Cleanup(func() { 115 if err := vhd.DetachVirtualDisk(vhdHandle); err != nil { 116 t.Fatal(err) 117 } 118 }) 119 if err := formatVHD(windows.Handle(vhdHandle)); err != nil { 120 t.Fatalf("failed to format VHD: %s", err) 121 } 122 // Get the path for the volume that was just created on the disk. 123 volumePath, err := computestorage.GetLayerVHDMountPath(windows.Handle(vhdHandle)) 124 if err != nil { 125 t.Fatal(err) 126 } 127 return volumePath 128 } 129 130 func writeFile(t *testing.T, path string, content []byte) { 131 t.Helper() 132 133 if err := os.WriteFile(path, content, 0644); err != nil { //nolint:gosec // test file, can have permissive mode 134 t.Fatal(err) 135 } 136 } 137 138 func mountVolume(t *testing.T, volumePath string, mountPoint string) { 139 t.Helper() 140 141 // Create the mount point directory. 142 if err := os.Mkdir(mountPoint, 0644); err != nil { 143 t.Fatal(err) 144 } 145 t.Cleanup(func() { 146 if err := os.Remove(mountPoint); err != nil { 147 t.Fatal(err) 148 } 149 }) 150 // Volume path must end in a slash. 151 if !strings.HasSuffix(volumePath, `\`) { 152 volumePath += `\` 153 } 154 volumePathU16, err := windows.UTF16PtrFromString(volumePath) 155 if err != nil { 156 t.Fatal(err) 157 } 158 // Mount point must end in a slash. 159 if !strings.HasSuffix(mountPoint, `\`) { 160 mountPoint += `\` 161 } 162 mountPointU16, err := windows.UTF16PtrFromString(mountPoint) 163 if err != nil { 164 t.Fatal(err) 165 } 166 if err := windows.SetVolumeMountPoint(mountPointU16, volumePathU16); err != nil { 167 t.Fatalf("failed to mount %s onto %s: %s", volumePath, mountPoint, err) 168 } 169 t.Cleanup(func() { 170 if err := windows.DeleteVolumeMountPoint(mountPointU16); err != nil { 171 t.Fatalf("failed to delete mount on %s: %s", mountPoint, err) 172 } 173 }) 174 } 175 176 func TestResolvePath(t *testing.T) { 177 if !windows.GetCurrentProcessToken().IsElevated() { 178 t.Skip("requires elevated privileges") 179 } 180 181 // Set up some data to be used by the test cases. 182 volumePathC := getVolumeGUIDPath(t, `C:\`) 183 dir := t.TempDir() 184 185 makeSymlink(t, `C:\windows`, filepath.Join(dir, "lnk1")) 186 makeSymlink(t, `\\localhost\c$\windows`, filepath.Join(dir, "lnk2")) 187 188 volumePathVHD1 := setupVHDVolume(t, filepath.Join(dir, "foo.vhdx")) 189 writeFile(t, filepath.Join(volumePathVHD1, "data.txt"), []byte("test content 1")) 190 makeSymlink(t, filepath.Join(volumePathVHD1, "data.txt"), filepath.Join(dir, "lnk3")) 191 192 volumePathVHD2 := setupVHDVolume(t, filepath.Join(dir, "bar.vhdx")) 193 writeFile(t, filepath.Join(volumePathVHD2, "data.txt"), []byte("test content 2")) 194 makeSymlink(t, filepath.Join(volumePathVHD2, "data.txt"), filepath.Join(dir, "lnk4")) 195 mountVolume(t, volumePathVHD2, filepath.Join(dir, "mnt")) 196 197 for _, tc := range []struct { 198 input string 199 expected string 200 description string 201 }{ 202 {`C:\windows`, volumePathC + `Windows`, "local path"}, 203 {filepath.Join(dir, "lnk1"), volumePathC + `Windows`, "symlink to local path"}, 204 {`\\localhost\c$\windows`, `\\localhost\c$\Windows`, "UNC path"}, 205 {filepath.Join(dir, "lnk2"), `\\localhost\c$\Windows`, "symlink to UNC path"}, 206 {filepath.Join(volumePathVHD1, "data.txt"), filepath.Join(volumePathVHD1, "data.txt"), "volume with no mount point"}, 207 {filepath.Join(dir, "lnk3"), filepath.Join(volumePathVHD1, "data.txt"), "symlink to volume with no mount point"}, 208 {filepath.Join(dir, "mnt", "data.txt"), filepath.Join(volumePathVHD2, "data.txt"), "volume with mount point"}, 209 {filepath.Join(dir, "lnk4"), filepath.Join(volumePathVHD2, "data.txt"), "symlink to volume with mount point"}, 210 } { 211 t.Run(tc.description, func(t *testing.T) { 212 actual, err := ResolvePath(tc.input) 213 if err != nil { 214 t.Fatalf("resolvePath should return no error, but %v", err) 215 } 216 if actual != tc.expected { 217 t.Fatalf("expected %v but got %v", tc.expected, actual) 218 } 219 // Make sure EvalSymlinks works with the resolved path, as an extra safety measure. 220 p, err := filepath.EvalSymlinks(actual) 221 if err != nil { 222 t.Fatalf("EvalSymlinks should return no error, but %v", err) 223 } 224 // As an extra-extra safety, check that resolvePath(x) == EvalSymlinks(resolvePath(x)). 225 // EvalSymlinks normalizes UNC path casing, but resolvePath may not, so compare with 226 // case-insensitivity here. 227 if !strings.EqualFold(actual, p) { 228 t.Fatalf("EvalSymlinks should resolve to the same path. Expected %v but got %v", actual, p) 229 } 230 }) 231 } 232 }