mirror of
https://github.com/rwinkhart/go-winio.git
synced 2026-08-28 04:46:50 -04:00
Add ResolvePath tests (#276)
Add required computestorage APIs to allow for running tests of `ResolvePath`. Signed-off-by: Hamza El-Saawy <hamzaelsaawy@microsoft.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
//go:build windows
|
||||
|
||||
package computestorage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
"github.com/Microsoft/go-winio/internal/interop"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go computestorage.go
|
||||
|
||||
// https://learn.microsoft.com/en-us/virtualization/api/hcs/reference/hcsformatwritablelayervhd
|
||||
//
|
||||
//sys hcsFormatWritableLayerVhd(handle windows.Handle) (hr error) = computestorage.HcsFormatWritableLayerVhd?
|
||||
|
||||
// FormatWritableLayerVHD formats a virtual disk for use as a writable container layer.
|
||||
//
|
||||
// If the VHD is not mounted it will be temporarily mounted.
|
||||
//
|
||||
// NOTE: This API had a breaking change in the operating system after Windows Server 2019.
|
||||
// On ws2019 the API expects to get passed a file handle from CreateFile for the vhd that
|
||||
// the caller wants to format. On > ws2019, its expected that the caller passes a vhd handle
|
||||
// that can be obtained from the virtdisk APIs.
|
||||
func FormatWritableLayerVHD(vhdHandle windows.Handle) (err error) {
|
||||
err = hcsFormatWritableLayerVhd(vhdHandle)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to format writable layer vhd: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/virtualization/api/hcs/reference/hcsgetlayervhdmountpath
|
||||
//
|
||||
//sys hcsGetLayerVhdMountPath(vhdHandle windows.Handle, mountPath **uint16) (hr error) = computestorage.HcsGetLayerVhdMountPath?
|
||||
|
||||
// GetLayerVHDMountPath returns the volume path for a virtual disk of a writable container layer.
|
||||
func GetLayerVHDMountPath(vhdHandle windows.Handle) (path string, err error) {
|
||||
var mountPath *uint16
|
||||
err = hcsGetLayerVhdMountPath(vhdHandle, &mountPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get vhd mount path: %w", err)
|
||||
}
|
||||
path = interop.ConvertAndFreeCoTaskMemString(mountPath)
|
||||
return path, nil
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package computestorage
|
||||
@@ -0,0 +1,77 @@
|
||||
//go:build windows
|
||||
|
||||
// Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT.
|
||||
|
||||
package computestorage
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
// TODO: add more here, after collecting data on the common
|
||||
// error values see on Windows. (perhaps when running
|
||||
// all.bat?)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modcomputestorage = windows.NewLazySystemDLL("computestorage.dll")
|
||||
|
||||
procHcsFormatWritableLayerVhd = modcomputestorage.NewProc("HcsFormatWritableLayerVhd")
|
||||
procHcsGetLayerVhdMountPath = modcomputestorage.NewProc("HcsGetLayerVhdMountPath")
|
||||
)
|
||||
|
||||
func hcsFormatWritableLayerVhd(handle windows.Handle) (hr error) {
|
||||
hr = procHcsFormatWritableLayerVhd.Find()
|
||||
if hr != nil {
|
||||
return
|
||||
}
|
||||
r0, _, _ := syscall.Syscall(procHcsFormatWritableLayerVhd.Addr(), 1, uintptr(handle), 0, 0)
|
||||
if int32(r0) < 0 {
|
||||
if r0&0x1fff0000 == 0x00070000 {
|
||||
r0 &= 0xffff
|
||||
}
|
||||
hr = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func hcsGetLayerVhdMountPath(vhdHandle windows.Handle, mountPath **uint16) (hr error) {
|
||||
hr = procHcsGetLayerVhdMountPath.Find()
|
||||
if hr != nil {
|
||||
return
|
||||
}
|
||||
r0, _, _ := syscall.Syscall(procHcsGetLayerVhdMountPath.Addr(), 2, uintptr(vhdHandle), uintptr(unsafe.Pointer(mountPath)), 0)
|
||||
if int32(r0) < 0 {
|
||||
if r0&0x1fff0000 == 0x00070000 {
|
||||
r0 &= 0xffff
|
||||
}
|
||||
hr = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package interop
|
||||
@@ -0,0 +1,25 @@
|
||||
//go:build windows
|
||||
|
||||
package interop
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go interop.go
|
||||
|
||||
//sys coTaskMemFree(buffer unsafe.Pointer) = api_ms_win_core_com_l1_1_0.CoTaskMemFree
|
||||
|
||||
func ConvertAndFreeCoTaskMemString(buffer *uint16) string {
|
||||
str := syscall.UTF16ToString((*[1 << 29]uint16)(unsafe.Pointer(buffer))[:])
|
||||
coTaskMemFree(unsafe.Pointer(buffer))
|
||||
return str
|
||||
}
|
||||
|
||||
func Win32FromHresult(hr uintptr) syscall.Errno {
|
||||
if hr&0x1fff0000 == 0x00070000 {
|
||||
return syscall.Errno(hr & 0xffff)
|
||||
}
|
||||
return syscall.Errno(hr)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//go:build windows
|
||||
|
||||
// Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT.
|
||||
|
||||
package interop
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
// TODO: add more here, after collecting data on the common
|
||||
// error values see on Windows. (perhaps when running
|
||||
// all.bat?)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modapi_ms_win_core_com_l1_1_0 = windows.NewLazySystemDLL("api-ms-win-core-com-l1-1-0.dll")
|
||||
|
||||
procCoTaskMemFree = modapi_ms_win_core_com_l1_1_0.NewProc("CoTaskMemFree")
|
||||
)
|
||||
|
||||
func coTaskMemFree(buffer unsafe.Pointer) {
|
||||
syscall.Syscall(procCoTaskMemFree.Addr(), 1, uintptr(buffer), 0, 0)
|
||||
return
|
||||
}
|
||||
+32
-21
@@ -20,28 +20,9 @@ import (
|
||||
// It is intended to address short-comings of [filepath.EvalSymlinks], which does not work
|
||||
// well on Windows.
|
||||
func ResolvePath(path string) (string, error) {
|
||||
// We are not able to use builtin Go functionality for opening a directory path:
|
||||
// - os.Open on a directory returns a os.File where Fd() is a search handle from FindFirstFile.
|
||||
// - syscall.Open does not provide a way to specify FILE_FLAG_BACKUP_SEMANTICS, which is needed to
|
||||
// open a directory.
|
||||
//
|
||||
// We could use os.Open if the path is a file, but it's easier to just use the same code for both.
|
||||
// Therefore, we call windows.CreateFile directly.
|
||||
h, err := fs.CreateFile(
|
||||
path,
|
||||
fs.FILE_ANY_ACCESS, // access
|
||||
fs.FILE_SHARE_READ|fs.FILE_SHARE_WRITE|fs.FILE_SHARE_DELETE,
|
||||
nil, // security attributes
|
||||
fs.OPEN_EXISTING,
|
||||
fs.FILE_FLAG_BACKUP_SEMANTICS, // Needed to open a directory handle.
|
||||
fs.NullHandle, // template file
|
||||
)
|
||||
h, err := openMetadata(path)
|
||||
if err != nil {
|
||||
return "", &os.PathError{
|
||||
Op: "CreateFile",
|
||||
Path: path,
|
||||
Err: err,
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
defer windows.CloseHandle(h) //nolint:errcheck
|
||||
|
||||
@@ -126,3 +107,33 @@ func ResolvePath(path string) (string, error) {
|
||||
}
|
||||
return rPath, err
|
||||
}
|
||||
|
||||
// openMetadata takes a path, opens it with only meta-data access, and returns the resulting handle.
|
||||
// It works for both file and directory paths.
|
||||
func openMetadata(path string) (windows.Handle, error) {
|
||||
// We are not able to use builtin Go functionality for opening a directory path:
|
||||
// - os.Open on a directory returns a os.File where Fd() is a search handle from FindFirstFile.
|
||||
// - syscall.Open does not provide a way to specify FILE_FLAG_BACKUP_SEMANTICS, which is needed to
|
||||
// open a directory.
|
||||
//
|
||||
// We could use os.Open if the path is a file, but it's easier to just use the same code for both.
|
||||
// Therefore, we call windows.CreateFile directly.
|
||||
h, err := fs.CreateFile(
|
||||
path,
|
||||
fs.FILE_ANY_ACCESS,
|
||||
fs.FILE_SHARE_READ|fs.FILE_SHARE_WRITE|fs.FILE_SHARE_DELETE,
|
||||
nil, // security attributes
|
||||
fs.OPEN_EXISTING,
|
||||
fs.FILE_FLAG_BACKUP_SEMANTICS, // Needed to open a directory handle.
|
||||
fs.NullHandle,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return 0, &os.PathError{
|
||||
Op: "CreateFile",
|
||||
Path: path,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
//go:build windows
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
"github.com/Microsoft/go-winio/internal/computestorage"
|
||||
"github.com/Microsoft/go-winio/internal/fs"
|
||||
"github.com/Microsoft/go-winio/vhd"
|
||||
)
|
||||
|
||||
func getWindowsBuildNumber() uint32 {
|
||||
// RtlGetVersion ignores manifest requirements
|
||||
vex := windows.RtlGetVersion()
|
||||
return vex.BuildNumber
|
||||
}
|
||||
|
||||
func makeSymlink(t *testing.T, oldName string, newName string) {
|
||||
if err := os.Symlink(oldName, newName); err != nil {
|
||||
t.Fatalf("creating symlink: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func getVolumeGUIDPath(t *testing.T, path string) string {
|
||||
h, err := openMetadata(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer windows.CloseHandle(h) //nolint:errcheck
|
||||
final, err := fs.GetFinalPathNameByHandle(h, fs.FILE_NAME_OPENED|fs.VOLUME_NAME_GUID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return final
|
||||
}
|
||||
|
||||
func openDisk(path string) (windows.Handle, error) {
|
||||
h, err := fs.CreateFile(
|
||||
path,
|
||||
windows.GENERIC_READ|windows.GENERIC_WRITE,
|
||||
fs.FILE_SHARE_READ|fs.FILE_SHARE_WRITE,
|
||||
nil, // security attributes
|
||||
fs.OPEN_EXISTING,
|
||||
windows.FILE_ATTRIBUTE_NORMAL|fs.FILE_FLAG_NO_BUFFERING,
|
||||
fs.NullHandle)
|
||||
if err != nil {
|
||||
return 0, &os.PathError{
|
||||
Op: "CreateFile",
|
||||
Path: path,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func formatVHD(vhdHandle windows.Handle) error {
|
||||
h := vhdHandle
|
||||
// Pre-19H1 HcsFormatWritableLayerVhd expects a disk handle.
|
||||
// On newer builds it expects a VHD handle instead.
|
||||
// Open a handle to the VHD's disk object if needed.
|
||||
|
||||
// Windows Server 1903, aka 19H1
|
||||
if getWindowsBuildNumber() < 18362 {
|
||||
diskPath, err := vhd.GetVirtualDiskPhysicalPath(syscall.Handle(h))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
diskHandle, err := openDisk(diskPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer windows.CloseHandle(diskHandle) //nolint:errcheck // cleanup code
|
||||
h = diskHandle
|
||||
}
|
||||
// Formatting a disk directly in Windows is a pain, so we use FormatWritableLayerVhd to do it.
|
||||
// It has a side effect of creating a sandbox directory on the formatted volume, but it's safe
|
||||
// to just ignore that for our purposes here.
|
||||
return computestorage.FormatWritableLayerVHD(h)
|
||||
}
|
||||
|
||||
// Creates a VHD with a NTFS volume. Returns the volume path.
|
||||
func setupVHDVolume(t *testing.T, vhdPath string) string {
|
||||
vhdHandle, err := vhd.CreateVirtualDisk(vhdPath,
|
||||
vhd.VirtualDiskAccessNone, vhd.CreateVirtualDiskFlagNone,
|
||||
&vhd.CreateVirtualDiskParameters{
|
||||
Version: 2,
|
||||
Version2: vhd.CreateVersion2{
|
||||
MaximumSize: 5 * 1024 * 1024 * 1024, // 5GB, thin provisioned
|
||||
BlockSizeInBytes: 1 * 1024 * 1024, // 1MB
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = windows.CloseHandle(windows.Handle(vhdHandle))
|
||||
})
|
||||
if err := vhd.AttachVirtualDisk(vhdHandle, vhd.AttachVirtualDiskFlagNone, &vhd.AttachVirtualDiskParameters{Version: 1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := vhd.DetachVirtualDisk(vhdHandle); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
if err := formatVHD(windows.Handle(vhdHandle)); err != nil {
|
||||
t.Fatalf("failed to format VHD: %s", err)
|
||||
}
|
||||
// Get the path for the volume that was just created on the disk.
|
||||
volumePath, err := computestorage.GetLayerVHDMountPath(windows.Handle(vhdHandle))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return volumePath
|
||||
}
|
||||
|
||||
func writeFile(t *testing.T, path string, content []byte) {
|
||||
if err := os.WriteFile(path, content, 0644); err != nil { //nolint:gosec // test file, can have permissive mode
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func mountVolume(t *testing.T, volumePath string, mountPoint string) {
|
||||
// Create the mount point directory.
|
||||
if err := os.Mkdir(mountPoint, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.Remove(mountPoint); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
// Volume path must end in a slash.
|
||||
if !strings.HasSuffix(volumePath, `\`) {
|
||||
volumePath += `\`
|
||||
}
|
||||
volumePathU16, err := windows.UTF16PtrFromString(volumePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Mount point must end in a slash.
|
||||
if !strings.HasSuffix(mountPoint, `\`) {
|
||||
mountPoint += `\`
|
||||
}
|
||||
mountPointU16, err := windows.UTF16PtrFromString(mountPoint)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := windows.SetVolumeMountPoint(mountPointU16, volumePathU16); err != nil {
|
||||
t.Fatalf("failed to mount %s onto %s: %s", volumePath, mountPoint, err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := windows.DeleteVolumeMountPoint(mountPointU16); err != nil {
|
||||
t.Fatalf("failed to delete mount on %s: %s", mountPoint, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestResolvePath(t *testing.T) {
|
||||
if !windows.GetCurrentProcessToken().IsElevated() {
|
||||
t.Fatal("test requires elevated privileges")
|
||||
}
|
||||
|
||||
// Set up some data to be used by the test cases.
|
||||
volumePathC := getVolumeGUIDPath(t, `C:\`)
|
||||
dir := t.TempDir()
|
||||
|
||||
makeSymlink(t, `C:\windows`, filepath.Join(dir, "lnk1"))
|
||||
makeSymlink(t, `\\localhost\c$\windows`, filepath.Join(dir, "lnk2"))
|
||||
|
||||
volumePathVHD1 := setupVHDVolume(t, filepath.Join(dir, "foo.vhdx"))
|
||||
writeFile(t, filepath.Join(volumePathVHD1, "data.txt"), []byte("test content 1"))
|
||||
makeSymlink(t, filepath.Join(volumePathVHD1, "data.txt"), filepath.Join(dir, "lnk3"))
|
||||
|
||||
volumePathVHD2 := setupVHDVolume(t, filepath.Join(dir, "bar.vhdx"))
|
||||
writeFile(t, filepath.Join(volumePathVHD2, "data.txt"), []byte("test content 2"))
|
||||
makeSymlink(t, filepath.Join(volumePathVHD2, "data.txt"), filepath.Join(dir, "lnk4"))
|
||||
mountVolume(t, volumePathVHD2, filepath.Join(dir, "mnt"))
|
||||
|
||||
for _, tc := range []struct {
|
||||
input string
|
||||
expected string
|
||||
description string
|
||||
}{
|
||||
{`C:\windows`, volumePathC + `Windows`, "local path"},
|
||||
{filepath.Join(dir, "lnk1"), volumePathC + `Windows`, "symlink to local path"},
|
||||
{`\\localhost\c$\windows`, `\\localhost\c$\Windows`, "UNC path"},
|
||||
{filepath.Join(dir, "lnk2"), `\\localhost\c$\Windows`, "symlink to UNC path"},
|
||||
{filepath.Join(volumePathVHD1, "data.txt"), filepath.Join(volumePathVHD1, "data.txt"), "volume with no mount point"},
|
||||
{filepath.Join(dir, "lnk3"), filepath.Join(volumePathVHD1, "data.txt"), "symlink to volume with no mount point"},
|
||||
{filepath.Join(dir, "mnt", "data.txt"), filepath.Join(volumePathVHD2, "data.txt"), "volume with mount point"},
|
||||
{filepath.Join(dir, "lnk4"), filepath.Join(volumePathVHD2, "data.txt"), "symlink to volume with mount point"},
|
||||
} {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
actual, err := ResolvePath(tc.input)
|
||||
if err != nil {
|
||||
t.Fatalf("resolvePath should return no error, but %v", err)
|
||||
}
|
||||
if actual != tc.expected {
|
||||
t.Fatalf("expected %v but got %v", tc.expected, actual)
|
||||
}
|
||||
// Make sure EvalSymlinks works with the resolved path, as an extra safety measure.
|
||||
p, err := filepath.EvalSymlinks(actual)
|
||||
if err != nil {
|
||||
t.Fatalf("EvalSymlinks should return no error, but %v", err)
|
||||
}
|
||||
// As an extra-extra safety, check that resolvePath(x) == EvalSymlinks(resolvePath(x)).
|
||||
// EvalSymlinks normalizes UNC path casing, but resolvePath may not, so compare with
|
||||
// case-insensitivity here.
|
||||
if !strings.EqualFold(actual, p) {
|
||||
t.Fatalf("EvalSymlinks should resolve to the same path. Expected %v but got %v", actual, p)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user