mirror of
https://github.com/rwinkhart/go-winio.git
synced 2026-09-06 00:57:19 -04:00
Add fs.ResolvePath to resolve symbolic links (#275)
* Add `fs.ResolvePath` to resolve symbolic links `filepath.EvalSymlinks` does not work well on Windows, and can enter infinite loops in certain situations and error out. Use Win32 API GetFinalPathNameByHandle to handle path resolution. Implementation based off on: https://github.com/containerd/containerd/pull/5411 Signed-off-by: Hamza El-Saawy <hamzaelsaawy@microsoft.com> * PR: types, documentation Signed-off-by: Hamza El-Saawy <hamzaelsaawy@microsoft.com> * remove unneded constant groups Signed-off-by: Hamza El-Saawy <hamzaelsaawy@microsoft.com> * Attempt normalized path first Update logic to try querying for normalized path initially, then use opened path if access is denied. Signed-off-by: Hamza El-Saawy <hamzaelsaawy@microsoft.com> --------- Signed-off-by: Hamza El-Saawy <hamzaelsaawy@microsoft.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
// This package contains Win32 filesystem functionality.
|
||||||
|
package fs
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
|
||||||
|
"github.com/Microsoft/go-winio/internal/stringbuffer"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go fs.go
|
||||||
|
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew
|
||||||
|
//sys CreateFile(name string, access AccessMask, mode FileShareMode, sa *syscall.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) [failretval==windows.InvalidHandle] = CreateFileW
|
||||||
|
|
||||||
|
const NullHandle windows.Handle = 0
|
||||||
|
|
||||||
|
// AccessMask defines standard, specific, and generic rights.
|
||||||
|
//
|
||||||
|
// Bitmask:
|
||||||
|
// 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
|
||||||
|
// 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
|
||||||
|
// +---------------+---------------+-------------------------------+
|
||||||
|
// |G|G|G|G|Resvd|A| StandardRights| SpecificRights |
|
||||||
|
// |R|W|E|A| |S| | |
|
||||||
|
// +-+-------------+---------------+-------------------------------+
|
||||||
|
//
|
||||||
|
// GR Generic Read
|
||||||
|
// GW Generic Write
|
||||||
|
// GE Generic Exectue
|
||||||
|
// GA Generic All
|
||||||
|
// Resvd Reserved
|
||||||
|
// AS Access Security System
|
||||||
|
//
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/secauthz/access-mask
|
||||||
|
//
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/secauthz/generic-access-rights
|
||||||
|
//
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/fileio/file-access-rights-constants
|
||||||
|
type AccessMask = windows.ACCESS_MASK
|
||||||
|
|
||||||
|
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
|
||||||
|
const (
|
||||||
|
// Not actually any.
|
||||||
|
//
|
||||||
|
// For CreateFile: "query certain metadata such as file, directory, or device attributes without accessing that file or device"
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew#parameters
|
||||||
|
FILE_ANY_ACCESS AccessMask = 0
|
||||||
|
|
||||||
|
// Specific Object Access
|
||||||
|
// from ntioapi.h
|
||||||
|
|
||||||
|
FILE_READ_DATA AccessMask = (0x0001) // file & pipe
|
||||||
|
FILE_LIST_DIRECTORY AccessMask = (0x0001) // directory
|
||||||
|
|
||||||
|
FILE_WRITE_DATA AccessMask = (0x0002) // file & pipe
|
||||||
|
FILE_ADD_FILE AccessMask = (0x0002) // directory
|
||||||
|
|
||||||
|
FILE_APPEND_DATA AccessMask = (0x0004) // file
|
||||||
|
FILE_ADD_SUBDIRECTORY AccessMask = (0x0004) // directory
|
||||||
|
FILE_CREATE_PIPE_INSTANCE AccessMask = (0x0004) // named pipe
|
||||||
|
|
||||||
|
FILE_READ_EA AccessMask = (0x0008) // file & directory
|
||||||
|
FILE_READ_PROPERTIES AccessMask = FILE_READ_EA
|
||||||
|
|
||||||
|
FILE_WRITE_EA AccessMask = (0x0010) // file & directory
|
||||||
|
FILE_WRITE_PROPERTIES AccessMask = FILE_WRITE_EA
|
||||||
|
|
||||||
|
FILE_EXECUTE AccessMask = (0x0020) // file
|
||||||
|
FILE_TRAVERSE AccessMask = (0x0020) // directory
|
||||||
|
|
||||||
|
FILE_DELETE_CHILD AccessMask = (0x0040) // directory
|
||||||
|
|
||||||
|
FILE_READ_ATTRIBUTES AccessMask = (0x0080) // all
|
||||||
|
|
||||||
|
FILE_WRITE_ATTRIBUTES AccessMask = (0x0100) // all
|
||||||
|
|
||||||
|
FILE_ALL_ACCESS AccessMask = (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x1FF)
|
||||||
|
FILE_GENERIC_READ AccessMask = (STANDARD_RIGHTS_READ | FILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_READ_EA | SYNCHRONIZE)
|
||||||
|
FILE_GENERIC_WRITE AccessMask = (STANDARD_RIGHTS_WRITE | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA | SYNCHRONIZE)
|
||||||
|
FILE_GENERIC_EXECUTE AccessMask = (STANDARD_RIGHTS_EXECUTE | FILE_READ_ATTRIBUTES | FILE_EXECUTE | SYNCHRONIZE)
|
||||||
|
|
||||||
|
SPECIFIC_RIGHTS_ALL AccessMask = 0x0000FFFF
|
||||||
|
|
||||||
|
// Standard Access
|
||||||
|
// from ntseapi.h
|
||||||
|
|
||||||
|
DELETE AccessMask = 0x0001_0000
|
||||||
|
READ_CONTROL AccessMask = 0x0002_0000
|
||||||
|
WRITE_DAC AccessMask = 0x0004_0000
|
||||||
|
WRITE_OWNER AccessMask = 0x0008_0000
|
||||||
|
SYNCHRONIZE AccessMask = 0x0010_0000
|
||||||
|
|
||||||
|
STANDARD_RIGHTS_REQUIRED AccessMask = 0x000F_0000
|
||||||
|
|
||||||
|
STANDARD_RIGHTS_READ AccessMask = READ_CONTROL
|
||||||
|
STANDARD_RIGHTS_WRITE AccessMask = READ_CONTROL
|
||||||
|
STANDARD_RIGHTS_EXECUTE AccessMask = READ_CONTROL
|
||||||
|
|
||||||
|
STANDARD_RIGHTS_ALL AccessMask = 0x001F_0000
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileShareMode uint32
|
||||||
|
|
||||||
|
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
|
||||||
|
const (
|
||||||
|
FILE_SHARE_NONE FileShareMode = 0x00
|
||||||
|
FILE_SHARE_READ FileShareMode = 0x01
|
||||||
|
FILE_SHARE_WRITE FileShareMode = 0x02
|
||||||
|
FILE_SHARE_DELETE FileShareMode = 0x04
|
||||||
|
FILE_SHARE_VALID_FLAGS FileShareMode = 0x07
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileCreationDisposition uint32
|
||||||
|
|
||||||
|
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
|
||||||
|
const (
|
||||||
|
// from winbase.h
|
||||||
|
|
||||||
|
CREATE_NEW FileCreationDisposition = 0x01
|
||||||
|
CREATE_ALWAYS FileCreationDisposition = 0x02
|
||||||
|
OPEN_EXISTING FileCreationDisposition = 0x03
|
||||||
|
OPEN_ALWAYS FileCreationDisposition = 0x04
|
||||||
|
TRUNCATE_EXISTING FileCreationDisposition = 0x05
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateFile and co. take flags or attributes together as one parameter.
|
||||||
|
// Define alias until we can use generics to allow both
|
||||||
|
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
|
||||||
|
type FileFlagOrAttribute uint32
|
||||||
|
|
||||||
|
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
|
||||||
|
const ( // from winnt.h
|
||||||
|
FILE_FLAG_WRITE_THROUGH FileFlagOrAttribute = 0x8000_0000
|
||||||
|
FILE_FLAG_OVERLAPPED FileFlagOrAttribute = 0x4000_0000
|
||||||
|
FILE_FLAG_NO_BUFFERING FileFlagOrAttribute = 0x2000_0000
|
||||||
|
FILE_FLAG_RANDOM_ACCESS FileFlagOrAttribute = 0x1000_0000
|
||||||
|
FILE_FLAG_SEQUENTIAL_SCAN FileFlagOrAttribute = 0x0800_0000
|
||||||
|
FILE_FLAG_DELETE_ON_CLOSE FileFlagOrAttribute = 0x0400_0000
|
||||||
|
FILE_FLAG_BACKUP_SEMANTICS FileFlagOrAttribute = 0x0200_0000
|
||||||
|
FILE_FLAG_POSIX_SEMANTICS FileFlagOrAttribute = 0x0100_0000
|
||||||
|
FILE_FLAG_OPEN_REPARSE_POINT FileFlagOrAttribute = 0x0020_0000
|
||||||
|
FILE_FLAG_OPEN_NO_RECALL FileFlagOrAttribute = 0x0010_0000
|
||||||
|
FILE_FLAG_FIRST_PIPE_INSTANCE FileFlagOrAttribute = 0x0008_0000
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileSQSFlag = FileFlagOrAttribute
|
||||||
|
|
||||||
|
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
|
||||||
|
const ( // from winbase.h
|
||||||
|
SECURITY_ANONYMOUS FileSQSFlag = FileSQSFlag(SecurityAnonymous << 16)
|
||||||
|
SECURITY_IDENTIFICATION FileSQSFlag = FileSQSFlag(SecurityIdentification << 16)
|
||||||
|
SECURITY_IMPERSONATION FileSQSFlag = FileSQSFlag(SecurityImpersonation << 16)
|
||||||
|
SECURITY_DELEGATION FileSQSFlag = FileSQSFlag(SecurityDelegation << 16)
|
||||||
|
|
||||||
|
SECURITY_SQOS_PRESENT FileSQSFlag = 0x00100000
|
||||||
|
SECURITY_VALID_SQOS_FLAGS FileSQSFlag = 0x001F0000
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetFinalPathNameByHandle flags
|
||||||
|
//
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew#parameters
|
||||||
|
type GetFinalPathFlag uint32
|
||||||
|
|
||||||
|
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
|
||||||
|
const (
|
||||||
|
GetFinalPathDefaultFlag GetFinalPathFlag = 0x0
|
||||||
|
|
||||||
|
FILE_NAME_NORMALIZED GetFinalPathFlag = 0x0
|
||||||
|
FILE_NAME_OPENED GetFinalPathFlag = 0x8
|
||||||
|
|
||||||
|
VOLUME_NAME_DOS GetFinalPathFlag = 0x0
|
||||||
|
VOLUME_NAME_GUID GetFinalPathFlag = 0x1
|
||||||
|
VOLUME_NAME_NT GetFinalPathFlag = 0x2
|
||||||
|
VOLUME_NAME_NONE GetFinalPathFlag = 0x4
|
||||||
|
)
|
||||||
|
|
||||||
|
// getFinalPathNameByHandle facilitates calling the Windows API GetFinalPathNameByHandle
|
||||||
|
// with the given handle and flags. It transparently takes care of creating a buffer of the
|
||||||
|
// correct size for the call.
|
||||||
|
//
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew
|
||||||
|
func GetFinalPathNameByHandle(h windows.Handle, flags GetFinalPathFlag) (string, error) {
|
||||||
|
b := stringbuffer.NewWString()
|
||||||
|
//TODO: can loop infinitely if Win32 keeps returning the same (or a larger) n?
|
||||||
|
for {
|
||||||
|
n, err := windows.GetFinalPathNameByHandle(h, b.Pointer(), b.Cap(), uint32(flags))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// If the buffer wasn't large enough, n will be the total size needed (including null terminator).
|
||||||
|
// Resize and try again.
|
||||||
|
if n > b.Cap() {
|
||||||
|
b.ResizeTo(n)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If the buffer is large enough, n will be the size not including the null terminator.
|
||||||
|
// Convert to a Go string and return.
|
||||||
|
return b.String(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_GetFinalPathNameByHandle(t *testing.T) {
|
||||||
|
d := t.TempDir()
|
||||||
|
// open f via a relative path
|
||||||
|
name := t.Name() + ".txt"
|
||||||
|
fullPath := filepath.Join(d, name)
|
||||||
|
|
||||||
|
w, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not get working directory: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Chdir(d); err != nil {
|
||||||
|
t.Fatalf("could not chdir to %s: %v", d, err)
|
||||||
|
}
|
||||||
|
defer os.Chdir(w) //nolint:errcheck
|
||||||
|
|
||||||
|
f, err := os.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not open %s: %v", fullPath, err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
path, err := GetFinalPathNameByHandle(windows.Handle(f.Fd()), GetFinalPathDefaultFlag)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not get final path for %s: %v", fullPath, err)
|
||||||
|
}
|
||||||
|
if strings.EqualFold(fullPath, path) {
|
||||||
|
t.Fatalf("expected %s, got %s", fullPath, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package fs
|
||||||
|
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-security_impersonation_level
|
||||||
|
type SecurityImpersonationLevel int32 // C default enums underlying type is `int`, which is Go `int32`
|
||||||
|
|
||||||
|
// Impersonation levels
|
||||||
|
const (
|
||||||
|
SecurityAnonymous SecurityImpersonationLevel = 0
|
||||||
|
SecurityIdentification SecurityImpersonationLevel = 1
|
||||||
|
SecurityImpersonation SecurityImpersonationLevel = 2
|
||||||
|
SecurityDelegation SecurityImpersonationLevel = 3
|
||||||
|
)
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
// Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package fs
|
||||||
|
|
||||||
|
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 (
|
||||||
|
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||||
|
|
||||||
|
procCreateFileW = modkernel32.NewProc("CreateFileW")
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateFile(name string, access AccessMask, mode FileShareMode, sa *syscall.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) {
|
||||||
|
var _p0 *uint16
|
||||||
|
_p0, err = syscall.UTF16PtrFromString(name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return _CreateFile(_p0, access, mode, sa, createmode, attrs, templatefile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _CreateFile(name *uint16, access AccessMask, mode FileShareMode, sa *syscall.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) {
|
||||||
|
r0, _, e1 := syscall.Syscall9(procCreateFileW.Addr(), 7, uintptr(unsafe.Pointer(name)), uintptr(access), uintptr(mode), uintptr(unsafe.Pointer(sa)), uintptr(createmode), uintptr(attrs), uintptr(templatefile), 0, 0)
|
||||||
|
handle = windows.Handle(r0)
|
||||||
|
if handle == windows.InvalidHandle {
|
||||||
|
err = errnoErr(e1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package stringbuffer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"unicode/utf16"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: worth exporting and using in mkwinsyscall?
|
||||||
|
|
||||||
|
// Uint16BufferSize is the buffer size in the pool, chosen somewhat arbitrarily to accommodate
|
||||||
|
// large path strings:
|
||||||
|
// MAX_PATH (260) + size of volume GUID prefix (49) + null terminator = 310.
|
||||||
|
const MinWStringCap = 310
|
||||||
|
|
||||||
|
// use *[]uint16 since []uint16 creates an extra allocation where the slice header
|
||||||
|
// is copied to heap and then referenced via pointer in the interface header that sync.Pool
|
||||||
|
// stores.
|
||||||
|
var pathPool = sync.Pool{ //! if go1.18+ adds Pool[T], use that to store []uint16 directly
|
||||||
|
New: func() interface{} {
|
||||||
|
b := make([]uint16, MinWStringCap)
|
||||||
|
return &b
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBuffer() []uint16 { return *(pathPool.Get().(*[]uint16)) }
|
||||||
|
|
||||||
|
// freeBuffer copies the slice header data, and puts a pointer to that in the pool.
|
||||||
|
// This avoids taking a pointer to the slice header in WString, which can be set to nil.
|
||||||
|
func freeBuffer(b []uint16) { pathPool.Put(&b) }
|
||||||
|
|
||||||
|
// WString is a wide string buffer ([]uint16) meant for storing UTF-16 encoded strings
|
||||||
|
// for interacting with Win32 APIs.
|
||||||
|
// Sizes are specified as uint32 and not int.
|
||||||
|
//
|
||||||
|
// It is not thread safe.
|
||||||
|
type WString struct {
|
||||||
|
// type-def allows casting to []uint16 directly, use struct to prevent that and allow adding fields in the future.
|
||||||
|
|
||||||
|
// raw buffer
|
||||||
|
b []uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWString returns a [WString] allocated from a shared pool with an
|
||||||
|
// initial capacity of at least [MinWStringCap].
|
||||||
|
// Since the buffer may have been previously used, its contents are not guaranteed to be empty.
|
||||||
|
//
|
||||||
|
// The buffer should be freed via [WString.Free]
|
||||||
|
func NewWString() *WString {
|
||||||
|
return &WString{
|
||||||
|
b: newBuffer(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WString) Free() {
|
||||||
|
if b.empty() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
freeBuffer(b.b)
|
||||||
|
b.b = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResizeTo grows the buffer to at least c and returns the new capacity, freeing the
|
||||||
|
// previous buffer back into pool.
|
||||||
|
func (b *WString) ResizeTo(c uint32) uint32 {
|
||||||
|
// allready sufficient (or n is 0)
|
||||||
|
if c <= b.Cap() {
|
||||||
|
return b.Cap()
|
||||||
|
}
|
||||||
|
|
||||||
|
if c <= MinWStringCap {
|
||||||
|
c = MinWStringCap
|
||||||
|
}
|
||||||
|
// allocate at-least double buffer size, as is done in [bytes.Buffer] and other places
|
||||||
|
if c <= 2*b.Cap() {
|
||||||
|
c = 2 * b.Cap()
|
||||||
|
}
|
||||||
|
|
||||||
|
b2 := make([]uint16, c)
|
||||||
|
if !b.empty() {
|
||||||
|
copy(b2, b.b)
|
||||||
|
freeBuffer(b.b)
|
||||||
|
}
|
||||||
|
b.b = b2
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer returns the underlying []uint16 buffer.
|
||||||
|
func (b *WString) Buffer() []uint16 {
|
||||||
|
if b.empty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return b.b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pointer returns a pointer to the first uint16 in the buffer.
|
||||||
|
// If the [WString.Free] has already been called, the pointer will be nil.
|
||||||
|
func (b *WString) Pointer() *uint16 {
|
||||||
|
if b.empty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &b.b[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the returns the UTF-8 encoding of the UTF-16 string in the buffer.
|
||||||
|
//
|
||||||
|
// It assumes that the data is null-terminated.
|
||||||
|
func (b *WString) String() string {
|
||||||
|
// Using [windows.UTF16ToString] would require importing "golang.org/x/sys/windows"
|
||||||
|
// and would make this code Windows-only, which makes no sense.
|
||||||
|
// So copy UTF16ToString code into here.
|
||||||
|
//! If other windows-specific code is added, switch to [windows.UTF16ToString]
|
||||||
|
|
||||||
|
s := b.b
|
||||||
|
for i, v := range s {
|
||||||
|
if v == 0 {
|
||||||
|
s = s[:i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(utf16.Decode(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap returns the underlying buffer capacity.
|
||||||
|
func (b *WString) Cap() uint32 {
|
||||||
|
if b.empty() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return b.cap()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WString) cap() uint32 { return uint32(cap(b.b)) }
|
||||||
|
func (b *WString) empty() bool { return b == nil || b.cap() == 0 }
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package stringbuffer
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func Test_BufferCapacity(t *testing.T) {
|
||||||
|
b := NewWString()
|
||||||
|
|
||||||
|
c := b.Cap()
|
||||||
|
if c < MinWStringCap {
|
||||||
|
t.Fatalf("expected capacity >= %d, got %d", MinWStringCap, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(b.b); l != int(c) {
|
||||||
|
t.Fatalf("buffer length (%d) and capacity (%d) mismatch", l, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
n := uint32(1.5 * MinWStringCap)
|
||||||
|
nn := b.ResizeTo(n)
|
||||||
|
if len(b.b) != int(nn) {
|
||||||
|
t.Fatalf("resized buffer should be %d, was %d", nn, len(b.b))
|
||||||
|
}
|
||||||
|
if n > nn {
|
||||||
|
t.Fatalf("resized to a value smaller than requested")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_BufferFree(t *testing.T) {
|
||||||
|
// make sure free-ing doesn't set pooled buffer to nil as well
|
||||||
|
for i := 0; i < 256; i++ {
|
||||||
|
// try allocating and freeing repeatedly since pool does not guarantee item reuse
|
||||||
|
b := NewWString()
|
||||||
|
b.Free()
|
||||||
|
if b.b != nil {
|
||||||
|
t.Fatalf("freed buffer is not nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
b = NewWString()
|
||||||
|
c := b.Cap()
|
||||||
|
if c < MinWStringCap {
|
||||||
|
t.Fatalf("expected capacity >= %d, got %d", MinWStringCap, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,11 +16,12 @@ import (
|
|||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
|
|
||||||
|
"github.com/Microsoft/go-winio/internal/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
//sys connectNamedPipe(pipe syscall.Handle, o *syscall.Overlapped) (err error) = ConnectNamedPipe
|
//sys connectNamedPipe(pipe syscall.Handle, o *syscall.Overlapped) (err error) = ConnectNamedPipe
|
||||||
//sys createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *syscall.SecurityAttributes) (handle syscall.Handle, err error) [failretval==syscall.InvalidHandle] = CreateNamedPipeW
|
//sys createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *syscall.SecurityAttributes) (handle syscall.Handle, err error) [failretval==syscall.InvalidHandle] = CreateNamedPipeW
|
||||||
//sys createFile(name string, access uint32, mode uint32, sa *syscall.SecurityAttributes, createmode uint32, attrs uint32, templatefile syscall.Handle) (handle syscall.Handle, err error) [failretval==syscall.InvalidHandle] = CreateFileW
|
|
||||||
//sys getNamedPipeInfo(pipe syscall.Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) = GetNamedPipeInfo
|
//sys getNamedPipeInfo(pipe syscall.Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) = GetNamedPipeInfo
|
||||||
//sys getNamedPipeHandleState(pipe syscall.Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) = GetNamedPipeHandleStateW
|
//sys getNamedPipeHandleState(pipe syscall.Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) = GetNamedPipeHandleStateW
|
||||||
//sys localAlloc(uFlags uint32, length uint32) (ptr uintptr) = LocalAlloc
|
//sys localAlloc(uFlags uint32, length uint32) (ptr uintptr) = LocalAlloc
|
||||||
@@ -163,19 +164,21 @@ func (s pipeAddress) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// tryDialPipe attempts to dial the pipe at `path` until `ctx` cancellation or timeout.
|
// tryDialPipe attempts to dial the pipe at `path` until `ctx` cancellation or timeout.
|
||||||
func tryDialPipe(ctx context.Context, path *string, access uint32) (syscall.Handle, error) {
|
func tryDialPipe(ctx context.Context, path *string, access fs.AccessMask) (syscall.Handle, error) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return syscall.Handle(0), ctx.Err()
|
return syscall.Handle(0), ctx.Err()
|
||||||
default:
|
default:
|
||||||
h, err := createFile(*path,
|
wh, err := fs.CreateFile(*path,
|
||||||
access,
|
access,
|
||||||
0,
|
0, // mode
|
||||||
nil,
|
nil, // security attributes
|
||||||
syscall.OPEN_EXISTING,
|
fs.OPEN_EXISTING,
|
||||||
windows.FILE_FLAG_OVERLAPPED|windows.SECURITY_SQOS_PRESENT|windows.SECURITY_ANONYMOUS,
|
fs.FILE_FLAG_OVERLAPPED|fs.SECURITY_SQOS_PRESENT|fs.SECURITY_ANONYMOUS,
|
||||||
0)
|
0, //template file handle
|
||||||
|
)
|
||||||
|
h := syscall.Handle(wh)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
@@ -219,7 +222,7 @@ func DialPipeContext(ctx context.Context, path string) (net.Conn, error) {
|
|||||||
func DialPipeAccess(ctx context.Context, path string, access uint32) (net.Conn, error) {
|
func DialPipeAccess(ctx context.Context, path string, access uint32) (net.Conn, error) {
|
||||||
var err error
|
var err error
|
||||||
var h syscall.Handle
|
var h syscall.Handle
|
||||||
h, err = tryDialPipe(ctx, &path, access)
|
h, err = tryDialPipe(ctx, &path, fs.AccessMask(access))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// This package contains Win32 filesystem functionality.
|
||||||
|
package fs
|
||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
|
|
||||||
|
"github.com/Microsoft/go-winio/internal/stringbuffer"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -13,19 +15,18 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GetFileSystemType obtains the type of a file system through GetVolumeInformation.
|
// GetFileSystemType obtains the type of a file system through GetVolumeInformation.
|
||||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa364993(v=vs.85).aspx
|
//
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumeinformationw
|
||||||
func GetFileSystemType(path string) (fsType string, err error) {
|
func GetFileSystemType(path string) (fsType string, err error) {
|
||||||
drive := filepath.VolumeName(path)
|
drive := filepath.VolumeName(path)
|
||||||
if len(drive) != 2 {
|
if len(drive) != 2 {
|
||||||
return "", ErrInvalidPath
|
return "", ErrInvalidPath
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
buf := stringbuffer.NewWString()
|
||||||
buf = make([]uint16, 255)
|
defer buf.Free()
|
||||||
size = uint32(windows.MAX_PATH + 1)
|
|
||||||
)
|
|
||||||
drive += `\`
|
drive += `\`
|
||||||
err = windows.GetVolumeInformation(windows.StringToUTF16Ptr(drive), nil, 0, nil, nil, nil, &buf[0], size)
|
err = windows.GetVolumeInformation(windows.StringToUTF16Ptr(drive), nil, 0, nil, nil, nil, buf.Pointer(), buf.Cap())
|
||||||
fsType = windows.UTF16ToString(buf)
|
return buf.String(), err
|
||||||
return fsType, err
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
|
||||||
|
"github.com/Microsoft/go-winio/internal/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResolvePath returns the final path to a file or directory represented, resolving symlinks,
|
||||||
|
// handling mount points, etc.
|
||||||
|
// The resolution works by using the Windows API GetFinalPathNameByHandle, which takes a
|
||||||
|
// handle and returns the final path to that file.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", &os.PathError{
|
||||||
|
Op: "CreateFile",
|
||||||
|
Path: path,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer windows.CloseHandle(h) //nolint:errcheck
|
||||||
|
|
||||||
|
// We use the Windows API GetFinalPathNameByHandle to handle path resolution. GetFinalPathNameByHandle
|
||||||
|
// returns a resolved path name for a file or directory. The returned path can be in several different
|
||||||
|
// formats, based on the flags passed. There are several goals behind the design here:
|
||||||
|
// - Do as little manual path manipulation as possible. Since Windows path formatting can be quite
|
||||||
|
// complex, we try to just let the Windows APIs handle that for us.
|
||||||
|
// - Retain as much compatibility with existing Go path functions as we can. In particular, we try to
|
||||||
|
// ensure paths returned from resolvePath can be passed to EvalSymlinks.
|
||||||
|
//
|
||||||
|
// First, we query for the VOLUME_NAME_GUID path of the file. This will return a path in the form
|
||||||
|
// "\\?\Volume{8a25748f-cf34-4ac6-9ee2-c89400e886db}\dir\file.txt". If the path is a UNC share
|
||||||
|
// (e.g. "\\server\share\dir\file.txt"), then the VOLUME_NAME_GUID query will fail with ERROR_PATH_NOT_FOUND.
|
||||||
|
// In this case, we will next try a VOLUME_NAME_DOS query. This query will return a path for a UNC share
|
||||||
|
// in the form "\\?\UNC\server\share\dir\file.txt". This path will work with most functions, but EvalSymlinks
|
||||||
|
// fails on it. Therefore, we rewrite the path to the form "\\server\share\dir\file.txt" before returning it.
|
||||||
|
// This path rewrite may not be valid in all cases (see the notes in the next paragraph), but those should
|
||||||
|
// be very rare edge cases, and this case wouldn't have worked with EvalSymlinks anyways.
|
||||||
|
//
|
||||||
|
// The "\\?\" prefix indicates that no path parsing or normalization should be performed by Windows.
|
||||||
|
// Instead the path is passed directly to the object manager. The lack of parsing means that "." and ".." are
|
||||||
|
// interpreted literally and "\"" must be used as a path separator. Additionally, because normalization is
|
||||||
|
// not done, certain paths can only be represented in this format. For instance, "\\?\C:\foo." (with a trailing .)
|
||||||
|
// cannot be written as "C:\foo.", because path normalization will remove the trailing ".".
|
||||||
|
//
|
||||||
|
// FILE_NAME_NORMALIZED can fail on some UNC paths based on access restrictions.
|
||||||
|
// Attempt to query with FILE_NAME_NORMALIZED, and then fall back on FILE_NAME_OPENED if access is denied.
|
||||||
|
//
|
||||||
|
// Querying for VOLUME_NAME_DOS first instead of VOLUME_NAME_GUID would yield a "nicer looking" path in some cases.
|
||||||
|
// For instance, it could return "\\?\C:\dir\file.txt" instead of "\\?\Volume{8a25748f-cf34-4ac6-9ee2-c89400e886db}\dir\file.txt".
|
||||||
|
// However, we query for VOLUME_NAME_GUID first for two reasons:
|
||||||
|
// - The volume GUID path is more stable. A volume's mount point can change when it is remounted, but its
|
||||||
|
// volume GUID should not change.
|
||||||
|
// - If the volume is mounted at a non-drive letter path (e.g. mounted to "C:\mnt"), then VOLUME_NAME_DOS
|
||||||
|
// will return the mount path. EvalSymlinks fails on a path like this due to a bug.
|
||||||
|
//
|
||||||
|
// References:
|
||||||
|
// - GetFinalPathNameByHandle: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlea
|
||||||
|
// - Naming Files, Paths, and Namespaces: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
|
||||||
|
// - Naming a Volume: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-volume
|
||||||
|
|
||||||
|
normalize := true
|
||||||
|
guid := true
|
||||||
|
rPath := ""
|
||||||
|
for i := 1; i <= 4; i++ { // maximum of 4 different cases to try
|
||||||
|
var flags fs.GetFinalPathFlag
|
||||||
|
if normalize {
|
||||||
|
flags |= fs.FILE_NAME_NORMALIZED // nop; for clarity
|
||||||
|
} else {
|
||||||
|
flags |= fs.FILE_NAME_OPENED
|
||||||
|
}
|
||||||
|
|
||||||
|
if guid {
|
||||||
|
flags |= fs.VOLUME_NAME_GUID
|
||||||
|
} else {
|
||||||
|
flags |= fs.VOLUME_NAME_DOS // nop; for clarity
|
||||||
|
}
|
||||||
|
|
||||||
|
rPath, err = fs.GetFinalPathNameByHandle(h, flags)
|
||||||
|
switch {
|
||||||
|
case guid && errors.Is(err, windows.ERROR_PATH_NOT_FOUND):
|
||||||
|
// ERROR_PATH_NOT_FOUND is returned from the VOLUME_NAME_GUID query if the path is a
|
||||||
|
// network share (UNC path). In this case, query for the DOS name instead.
|
||||||
|
guid = false
|
||||||
|
continue
|
||||||
|
case normalize && errors.Is(err, windows.ERROR_ACCESS_DENIED):
|
||||||
|
// normalization failed when accessing individual components along path for SMB share
|
||||||
|
normalize = false
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil && strings.HasPrefix(rPath, `\\?\UNC\`) {
|
||||||
|
// Convert \\?\UNC\server\share -> \\server\share. The \\?\UNC syntax does not work with
|
||||||
|
// some Go filepath functions such as EvalSymlinks. In the future if other components
|
||||||
|
// move away from EvalSymlinks and use GetFinalPathNameByHandle instead, we could remove
|
||||||
|
// this path munging.
|
||||||
|
rPath = `\\` + rPath[len(`\\?\UNC\`):]
|
||||||
|
}
|
||||||
|
return rPath, err
|
||||||
|
}
|
||||||
@@ -63,7 +63,6 @@ var (
|
|||||||
procBackupWrite = modkernel32.NewProc("BackupWrite")
|
procBackupWrite = modkernel32.NewProc("BackupWrite")
|
||||||
procCancelIoEx = modkernel32.NewProc("CancelIoEx")
|
procCancelIoEx = modkernel32.NewProc("CancelIoEx")
|
||||||
procConnectNamedPipe = modkernel32.NewProc("ConnectNamedPipe")
|
procConnectNamedPipe = modkernel32.NewProc("ConnectNamedPipe")
|
||||||
procCreateFileW = modkernel32.NewProc("CreateFileW")
|
|
||||||
procCreateIoCompletionPort = modkernel32.NewProc("CreateIoCompletionPort")
|
procCreateIoCompletionPort = modkernel32.NewProc("CreateIoCompletionPort")
|
||||||
procCreateNamedPipeW = modkernel32.NewProc("CreateNamedPipeW")
|
procCreateNamedPipeW = modkernel32.NewProc("CreateNamedPipeW")
|
||||||
procGetCurrentThread = modkernel32.NewProc("GetCurrentThread")
|
procGetCurrentThread = modkernel32.NewProc("GetCurrentThread")
|
||||||
@@ -305,24 +304,6 @@ func connectNamedPipe(pipe syscall.Handle, o *syscall.Overlapped) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFile(name string, access uint32, mode uint32, sa *syscall.SecurityAttributes, createmode uint32, attrs uint32, templatefile syscall.Handle) (handle syscall.Handle, err error) {
|
|
||||||
var _p0 *uint16
|
|
||||||
_p0, err = syscall.UTF16PtrFromString(name)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return _createFile(_p0, access, mode, sa, createmode, attrs, templatefile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _createFile(name *uint16, access uint32, mode uint32, sa *syscall.SecurityAttributes, createmode uint32, attrs uint32, templatefile syscall.Handle) (handle syscall.Handle, err error) {
|
|
||||||
r0, _, e1 := syscall.Syscall9(procCreateFileW.Addr(), 7, uintptr(unsafe.Pointer(name)), uintptr(access), uintptr(mode), uintptr(unsafe.Pointer(sa)), uintptr(createmode), uintptr(attrs), uintptr(templatefile), 0, 0)
|
|
||||||
handle = syscall.Handle(r0)
|
|
||||||
if handle == syscall.InvalidHandle {
|
|
||||||
err = errnoErr(e1)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func createIoCompletionPort(file syscall.Handle, port syscall.Handle, key uintptr, threadCount uint32) (newport syscall.Handle, err error) {
|
func createIoCompletionPort(file syscall.Handle, port syscall.Handle, key uintptr, threadCount uint32) (newport syscall.Handle, err error) {
|
||||||
r0, _, e1 := syscall.Syscall6(procCreateIoCompletionPort.Addr(), 4, uintptr(file), uintptr(port), uintptr(key), uintptr(threadCount), 0, 0)
|
r0, _, e1 := syscall.Syscall6(procCreateIoCompletionPort.Addr(), 4, uintptr(file), uintptr(port), uintptr(key), uintptr(threadCount), 0, 0)
|
||||||
newport = syscall.Handle(r0)
|
newport = syscall.Handle(r0)
|
||||||
|
|||||||
Reference in New Issue
Block a user