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..12be4258 --- /dev/null +++ b/pkg/volmount/getvolumepathnamesforvolumename.go @@ -0,0 +1,66 @@ +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) ([]string, error) { + var results []string + prev := 0 + for i := range s { + if s[i] == 0 { + if prev == i { + // found two null characters in a row, return result + return results, nil + } + results = append(results, string(utf16.Decode(s[prev:i]))) + prev = i + 1 + } + } + return nil, errors.New("string set malformed: missing null terminator at end of buffer") +} + +// GetMountPathsFromVolumeName 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-getvolumepathnamesforvolumenamew +func GetMountPathsFromVolumeName(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) + } + + var bufferLength uint32 + err = windows.GetVolumePathNamesForVolumeName(volumeP, nil, 0, &bufferLength) + if err == nil { + // This should never happen. An empty list would have a single 0 in it. + return nil, errors.Errorf("unexpected success of GetVolumePathNamesForVolumeName('%s', nil, 0, ...)", slashedVolume) + } else if err != syscall.ERROR_MORE_DATA { + return nil, errors.Wrapf(err, "failed calling GetVolumePathNamesForVolumeName('%s', nil, 0, ...)", slashedVolume) + } + + buffer := make([]uint16, bufferLength) + err = windows.GetVolumePathNamesForVolumeName(volumeP, &buffer[0], bufferLength, nil) + if err != nil { + return nil, errors.Wrapf(err, "failed calling GetVolumePathNamesForVolumeName('%s', ..., %d, nil)", slashedVolume, bufferLength) + } + + result, err := utf16ToStringArray(buffer) + if err != nil { + return nil, errors.Wrapf(err, "failed decoding result of GetVolumePathNamesForVolumeName('%s', ..., %d, nil)", slashedVolume, bufferLength) + } + + return result, nil +} 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 +}