Skip to content

Commit

Permalink
Implement Volume Mount Point API wrappers
Browse files Browse the repository at this point in the history
Fixes: microsoft#176

These provide Go-typed wrappers around the Win32 APIs backing them,
protecting the user from unsafe pointers and UTF-16 conversions.

Signed-off-by: Paul "TBBle" Hampson <Paul.Hampson@Pobox.com>
  • Loading branch information
TBBle committed Jan 2, 2021
1 parent d1ffc52 commit 7f828f9
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 0 deletions.
29 changes: 29 additions & 0 deletions pkg/volmount/deletevolumemountpoint.go
Original file line number Diff line number Diff line change
@@ -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
}
33 changes: 33 additions & 0 deletions pkg/volmount/getvolumenameforvolumemountpoint.go
Original file line number Diff line number Diff line change
@@ -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
}
69 changes: 69 additions & 0 deletions pkg/volmount/getvolumepathnamesforvolumename.go
Original file line number Diff line number Diff line change
@@ -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
}
}
43 changes: 43 additions & 0 deletions pkg/volmount/setvolumemountpoint.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 7f828f9

Please sign in to comment.