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  }