diff --git a/pkg/volmount/deletevolumemountpoint.go b/pkg/volmount/deletevolumemountpoint.go new file mode 100644 index 00000000..a7521d01 --- /dev/null +++ b/pkg/volmount/deletevolumemountpoint.go @@ -0,0 +1,29 @@ +package volmount + +import ( + "path/filepath" + + "github.com/pkg/errors" + "golang.org/x/sys/windows" +) + +// DeleteVolumeMountPoint removes the volume mount at targetPath +// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-deletevolumemountpointa +func DeleteVolumeMountPoint(targetPath string) error { + // Must end in a backslash + slashedTarget := filepath.Clean(targetPath) + if slashedTarget[len(slashedTarget)-1] != filepath.Separator { + slashedTarget = slashedTarget + string(filepath.Separator) + } + + targetP, err := windows.UTF16PtrFromString(slashedTarget) + if err != nil { + return errors.Wrapf(err, "unable to utf16-ise %s", slashedTarget) + } + + if err := windows.DeleteVolumeMountPoint(targetP); err != nil { + return errors.Wrapf(err, "failed calling DeleteVolumeMountPoint('%s')", slashedTarget) + } + + return nil +} diff --git a/pkg/volmount/getvolumenameforvolumemountpoint.go b/pkg/volmount/getvolumenameforvolumemountpoint.go new file mode 100644 index 00000000..03275577 --- /dev/null +++ b/pkg/volmount/getvolumenameforvolumemountpoint.go @@ -0,0 +1,33 @@ +package volmount + +import ( + "path/filepath" + + "github.com/pkg/errors" + "golang.org/x/sys/windows" +) + +// GetVolumeNameForVolumeMountPoint returns a volume path (in format '\\?\Volume{GUID}' +// for the volume mounted at targetPath. +// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumenameforvolumemountpointw +func GetVolumeNameForVolumeMountPoint(targetPath string) (string, error) { + // Must end in a backslash + slashedTarget := filepath.Clean(targetPath) + if slashedTarget[len(slashedTarget)-1] != filepath.Separator { + slashedTarget = slashedTarget + string(filepath.Separator) + } + + targetP, err := windows.UTF16PtrFromString(slashedTarget) + if err != nil { + return "", errors.Wrapf(err, "unable to utf16-ise %s", slashedTarget) + } + + bufferlength := uint32(50) // "A reasonable size for the buffer" per the documentation. + buffer := make([]uint16, bufferlength) + + if err = windows.GetVolumeNameForVolumeMountPoint(targetP, &buffer[0], bufferlength); err != nil { + return "", errors.Wrapf(err, "failed calling GetVolumeNameForVolumeMountPoint('%s', ..., %d)", slashedTarget, bufferlength) + } + + return windows.UTF16ToString(buffer), nil +} diff --git a/pkg/volmount/getvolumepathnamesforvolumename.go b/pkg/volmount/getvolumepathnamesforvolumename.go new file mode 100644 index 00000000..81f8fcd4 --- /dev/null +++ b/pkg/volmount/getvolumepathnamesforvolumename.go @@ -0,0 +1,69 @@ +package volmount + +import ( + "path/filepath" + "syscall" + "unicode/utf16" + + "github.com/pkg/errors" + "golang.org/x/sys/windows" +) + +// utf16ToStringArray returns the UTF-8 encoding of the sequence of UTF-16 sequences s, +// with a terminating NUL removed. The sequences are terminated by an additional NULL. +func utf16ToStringArray(s []uint16) (result []string) { + start := 0 + + for i, v := range s { + if v != 0 { + continue + } + + if start == i { + // Empty string, that's the end. + return + } + + result = append(result, string(utf16.Decode(s[start:i]))) + start = i + 1 + } + + // The buffer was incomplete, the expected "additional NULL" wasn't present. + // Just return the complete values we did decode, assume the caller intended + // to do this, e.g. as a best-effort call without resizing the buffer. + return +} + +// GetVolumePathNamesForVolumeName returns a list of mount points for the volumePath +// (in format '\\?\Volume{GUID}). +// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumenameforvolumemountpointw +func GetVolumePathNamesForVolumeName(volumePath string) ([]string, error) { + // Must end in a backslash + slashedVolume := filepath.Clean(volumePath) + if slashedVolume[len(slashedVolume)-1] != filepath.Separator { + slashedVolume = slashedVolume + string(filepath.Separator) + } + + volumeP, err := windows.UTF16PtrFromString(slashedVolume) + if err != nil { + return nil, errors.Wrapf(err, "unable to utf16-ise %s", slashedVolume) + } + + bufferLength := uint32(256) + + for { + buffer := make([]uint16, bufferLength) + + var returnLength uint32 + err := windows.GetVolumePathNamesForVolumeName(volumeP, &buffer[0], bufferLength, &returnLength) + + if err == nil { + // It's an array! TODO + return utf16ToStringArray(buffer), nil + } else if err != syscall.ERROR_MORE_DATA { + return nil, errors.Wrapf(err, "failed calling GetVolumePathNamesForVolumeName('%s', ..., %d, ...)", slashedVolume, bufferLength) + } + // Try again + bufferLength = returnLength + } +} diff --git a/pkg/volmount/setvolumemountpoint.go b/pkg/volmount/setvolumemountpoint.go new file mode 100644 index 00000000..777e4c31 --- /dev/null +++ b/pkg/volmount/setvolumemountpoint.go @@ -0,0 +1,43 @@ +package volmount + +import ( + "path/filepath" + "strings" + + "github.com/pkg/errors" + "golang.org/x/sys/windows" +) + +// SetVolumeMountPoint mounts volumePath (in format '\\?\Volume{GUID}' at targetPath. +// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setvolumemountpointw +func SetVolumeMountPoint(targetPath string, volumePath string) error { + if !strings.HasPrefix(volumePath, "\\\\?\\Volume{") { + return errors.Errorf("unable to mount non-volume path %s", volumePath) + } + + // Both must end in a backslash + slashedTarget := filepath.Clean(targetPath) + if slashedTarget[len(slashedTarget)-1] != filepath.Separator { + slashedTarget = slashedTarget + string(filepath.Separator) + } + slashedVolume := filepath.Clean(volumePath) + if slashedVolume[len(slashedVolume)-1] != filepath.Separator { + slashedVolume = slashedVolume + string(filepath.Separator) + } + + targetP, err := windows.UTF16PtrFromString(slashedTarget) + if err != nil { + return errors.Wrapf(err, "unable to utf16-ise %s", slashedTarget) + } + + volumeP, err := windows.UTF16PtrFromString(slashedVolume) + if err != nil { + return errors.Wrapf(err, "unable to utf16-ise %s", slashedVolume) + } + + if err := windows.SetVolumeMountPoint(targetP, volumeP); err != nil { + return errors.Wrapf(err, "failed calling SetVolumeMount('%s', '%s')", slashedTarget, slashedVolume) + } + + return nil +}