internal/nativewin/folder_windows.go
Rama main · réplica de lectura del repositorio original.
//go:build windows
package nativewin
import (
"errors"
"fmt"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
// The native "pick a folder" dialog, driven through COM.
//
// This is IFileDialog with FOS_PICKFOLDERS, which is the modern Explorer-style
// browser -- the same one Windows' own installers show. The alternative,
// SHBrowseForFolder, is ten lines shorter and looks like Windows 2000: no
// address bar, no typing a path, no pinned places. Since the whole point of the
// window is that the wizard reads as a native app, the dialog it opens cannot
// be the one that gives that away.
//
// Shelling out to PowerShell's FolderBrowserDialog was the other option and is
// worse in every direction: a console flash, a second of startup, and a
// dependency on an execution policy that may be locked down.
const (
coinitApartmentThreaded = 0x2
rpcEChangedMode = 0x80010106
fosPickFolders = 0x00000020
fosForceFileSystem = 0x00000040
fosPathMustExist = 0x00000800
fosNoChangeDir = 0x00000008
sigdnFileSysPath = 0x80058000
// HRESULT_FROM_WIN32(ERROR_CANCELLED). Not an error: it is the user
// answering "no folder", which the caller reports as an empty string.
hresultCancelled = 0x800704C7
)
// Vtable slots. IFileOpenDialog inherits IFileDialog inherits IModalWindow
// inherits IUnknown, so the indices are fixed by that chain and are the only
// way to reach the methods without generating COM stubs.
const (
vtRelease = 2
vtShow = 3
vtSetOptions = 9
vtSetFolder = 12
vtSetTitle = 17
vtGetResult = 20
vtGetDisplayName = 5 // IShellItem
)
var (
ole32 = syscall.NewLazyDLL("ole32.dll")
shell32 = syscall.NewLazyDLL("shell32.dll")
coInitializeEx = ole32.NewProc("CoInitializeEx")
coUninitialize = ole32.NewProc("CoUninitialize")
coCreateInstance = ole32.NewProc("CoCreateInstance")
coTaskMemFree = ole32.NewProc("CoTaskMemFree")
shCreateItemFromParsingName = shell32.NewProc("SHCreateItemFromParsingName")
)
var (
clsidFileOpenDialog = windows.GUID{Data1: 0xDC1C5A9C, Data2: 0xE88A, Data3: 0x4DDE,
Data4: [8]byte{0xA5, 0xA1, 0x60, 0xF8, 0x2A, 0x20, 0xAE, 0xF7}}
iidFileOpenDialog = windows.GUID{Data1: 0xD57C7288, Data2: 0xD4AD, Data3: 0x4768,
Data4: [8]byte{0xBE, 0x02, 0x9D, 0x96, 0x95, 0x32, 0xD9, 0x60}}
iidShellItem = windows.GUID{Data1: 0x43826D1E, Data2: 0xE718, Data3: 0x42EE,
Data4: [8]byte{0xBC, 0x55, 0xA1, 0xE2, 0x61, 0xC3, 0x7B, 0xFE}}
)
// comCall invokes the idx'th method on a COM interface pointer.
//
// Every COM object begins with a pointer to its vtable, and the vtable is a
// flat array of function pointers in interface-declaration order, so a method
// call is two dereferences and a stdcall with the object as the first argument.
func comCall(obj uintptr, idx int, args ...uintptr) uintptr {
vtbl := *(*uintptr)(unsafe.Pointer(obj))
fn := *(*uintptr)(unsafe.Pointer(vtbl + uintptr(idx)*unsafe.Sizeof(uintptr(0))))
ret, _, _ := syscall.SyscallN(fn, append([]uintptr{obj}, args...)...)
return ret
}
func comRelease(obj uintptr) {
if obj != 0 {
comCall(obj, vtRelease)
}
}
// PickFolder shows the folder browser and returns the chosen path, or "" if the
// user cancelled.
//
// owner makes the dialog modal to the calling window. Passing 0 would let the
// user click back to the window behind a dialog that is still open, which on
// Windows leaves the dialog stranded behind its own parent with no obvious way
// back.
func PickFolder(owner uintptr, title, start string) (string, error) {
// The caller's thread is already an STA (WebView2 requires one), so this
// almost always returns S_FALSE -- "already initialised", which still counts
// as a reference and still has to be balanced. RPC_E_CHANGED_MODE means the
// thread is an MTA and the call took no reference at all, so it must NOT be.
hr, _, _ := coInitializeEx.Call(0, coinitApartmentThreaded)
if uint32(hr) != rpcEChangedMode {
defer coUninitialize.Call()
}
var dlg uintptr
hr, _, _ = coCreateInstance.Call(
uintptr(unsafe.Pointer(&clsidFileOpenDialog)),
0,
1, // CLSCTX_INPROC_SERVER
uintptr(unsafe.Pointer(&iidFileOpenDialog)),
uintptr(unsafe.Pointer(&dlg)),
)
if hr != 0 || dlg == 0 {
return "", fmt.Errorf("could not open the folder browser (0x%08x)", uint32(hr))
}
defer comRelease(dlg)
// FOS_FORCEFILESYSTEM alongside FOS_PICKFOLDERS: without it the dialog
// happily returns a virtual place like "This PC" or a library, which has no
// path to install into and would surface as a confusing error later.
comCall(dlg, vtSetOptions, fosPickFolders|fosForceFileSystem|fosPathMustExist|fosNoChangeDir)
if title != "" {
if p, err := windows.UTF16PtrFromString(title); err == nil {
comCall(dlg, vtSetTitle, uintptr(unsafe.Pointer(p)))
}
}
// Best effort: a start folder that does not exist yet (the proposed install
// dir usually does not) simply leaves the dialog wherever it would have
// opened, which is better than refusing to open.
if item := shellItem(start); item != 0 {
comCall(dlg, vtSetFolder, item)
comRelease(item)
}
if hr := comCall(dlg, vtShow, owner); hr != 0 {
if uint32(hr) == hresultCancelled {
return "", nil
}
return "", fmt.Errorf("folder browser failed (0x%08x)", uint32(hr))
}
var item uintptr
if hr := comCall(dlg, vtGetResult, uintptr(unsafe.Pointer(&item))); hr != 0 || item == 0 {
return "", errors.New("no folder was returned")
}
defer comRelease(item)
var pathPtr *uint16
if hr := comCall(item, vtGetDisplayName, sigdnFileSysPath,
uintptr(unsafe.Pointer(&pathPtr))); hr != 0 || pathPtr == nil {
return "", errors.New("that folder has no file-system path")
}
// The string is allocated by the shell, so it is freed with the shell's
// allocator and not Go's.
defer coTaskMemFree.Call(uintptr(unsafe.Pointer(pathPtr)))
return windows.UTF16PtrToString(pathPtr), nil
}
// shellItem resolves a path to an IShellItem, or 0 if it cannot be resolved.
func shellItem(path string) uintptr {
if path == "" {
return 0
}
p, err := windows.UTF16PtrFromString(path)
if err != nil {
return 0
}
var item uintptr
hr, _, _ := shCreateItemFromParsingName.Call(
uintptr(unsafe.Pointer(p)),
0,
uintptr(unsafe.Pointer(&iidShellItem)),
uintptr(unsafe.Pointer(&item)),
)
if hr != 0 {
return 0
}
return item
}