Flamenco/internal/find_blender/windows.go
Sybren A. Stüvel 642ef36778 Blender finder: fix compatibility with Windows Home
For some reason, calling `AssocQueryStringW` on Windows Home returns error
code 122, "The data area passed to a system call is too small", even when
the data area is large enough. Furthermore, the API actually describes that
in such cases `S_FALSE` is supposed to be returned, with `*pcchOut` set to
the required size. Because of this apparent violation of the documentation,
and because it just works, Flamenco now ignores this particular error and
just returns the obtained string.
2022-08-01 16:00:49 +02:00

178 lines
5.5 KiB
Go
Executable File

//go:build windows
package find_blender
// SPDX-License-Identifier: GPL-3.0-or-later
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"syscall"
"unsafe"
)
const blenderExeName = "blender.exe"
// fileAssociation returns the full path of `blender.exe` associated with ".blend" files.
func fileAssociation() (string, error) {
exe, err := getFileAssociation(".blend")
if err != nil {
return "", err
}
// Often the association will be with blender-launcher.exe, which is
// unsuitable for use in Flamenco. Use its path to find its `blender.exe`.
dir, file := filepath.Split(exe)
if file != "blender-launcher.exe" {
return exe, nil
}
blenderPath := filepath.Join(dir, "blender.exe")
_, err = os.Stat(blenderPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return "", fmt.Errorf("blender-launcher found at %s but not its blender.exe", exe)
}
return "", fmt.Errorf("investigating %s: %w", blenderPath, err)
}
return blenderPath, nil
}
// getFileAssociation finds the executable associated with the given extension.
// The extension must be a string like ".blend".
func getFileAssociation(extension string) (string, error) {
// Load library.
libname := "shlwapi.dll"
libshlwapi, err := syscall.LoadLibrary(libname)
if err != nil {
return "", fmt.Errorf("loading %s: %w", libname, err)
}
defer func() { _ = syscall.FreeLibrary(libshlwapi) }()
// Find function.
funcname := "AssocQueryStringW"
assocQueryString, err := syscall.GetProcAddress(libshlwapi, funcname)
if err != nil {
return "", fmt.Errorf("finding function %s in %s: %w", funcname, libname, err)
}
// https://docs.microsoft.com/en-gb/windows/win32/api/shlwapi/nf-shlwapi-assocquerystringw
pszAssoc, err := syscall.UTF16PtrFromString(extension)
if err != nil {
return "", fmt.Errorf("converting string to UTF16: %w", err)
}
pszExtra, err := syscall.UTF16PtrFromString("open")
if err != nil {
return "", fmt.Errorf("converting string to UTF16: %w", err)
}
var (
bufferSize uint32 = 1024
result1 uintptr
errno syscall.Errno
)
buf := make([]uint16, bufferSize)
pszOut := unsafe.Pointer(&buf[0])
result1, _, errno = syscall.SyscallN(
assocQueryString,
uintptr(ASSOCF_NONE), // [in] ASSOCF flags
uintptr(ASSOCSTR_EXECUTABLE), // [in] ASSOCSTR str
uintptr(unsafe.Pointer(pszAssoc)), // [in] LPCWSTR pszAssoc
uintptr(unsafe.Pointer(pszExtra)), // [in, optional] LPCWSTR pszExtra
uintptr(pszOut), // [out, optional] LPWSTR pszOut
uintptr(unsafe.Pointer(&bufferSize)), // [in, out] DWORD *pcchOut
)
// Somehow Windows Home returns an error 122 "The data area passed to a system
// call is too small" even when the data area is big enough. So, for the sake
// of sanity of this software developer, let's just ignore that particular
// error.
if errno != 0 && errno != 122 {
exe := syscall.UTF16ToString(buf)
return "", fmt.Errorf("error calling %s from %s: %d; %w (with bufsize=%v and pszOut=%q)",
funcname, libname, errno, errno, bufferSize, exe)
}
switch result1 {
case S_OK:
// Continue below with the happy flow.
case ERROR_NO_ASSOCIATION:
// No association with .blend files exists, and that's fine.
return "", ErrAssociationNotFound
case S_FALSE:
return "", fmt.Errorf("error calling %s from %s: buffer too small, should be %v", funcname, libname, bufferSize)
case E_POINTER:
return "", fmt.Errorf("error calling %s from %s: invalid pointer", funcname, libname)
default:
return "", fmt.Errorf("unknown result %#x calling %s from %s", result1, funcname, libname)
}
exe := syscall.UTF16ToString(buf)
return exe, nil
}
// Source: https://docs.microsoft.com/en-us/windows/win32/shell/assocf_str
const (
ASSOCF_NONE = ASSOCF(0x00000000)
ASSOCF_INIT_NOREMAPCLSID = ASSOCF(0x00000001)
ASSOCF_INIT_BYEXENAME = ASSOCF(0x00000002)
ASSOCF_OPEN_BYEXENAME = ASSOCF(0x00000002)
ASSOCF_INIT_DEFAULTTOSTAR = ASSOCF(0x00000004)
ASSOCF_INIT_DEFAULTTOFOLDER = ASSOCF(0x00000008)
ASSOCF_NOUSERSETTINGS = ASSOCF(0x00000010)
ASSOCF_NOTRUNCATE = ASSOCF(0x00000020)
ASSOCF_VERIFY = ASSOCF(0x00000040)
ASSOCF_REMAPRUNDLL = ASSOCF(0x00000080)
ASSOCF_NOFIXUPS = ASSOCF(0x00000100)
ASSOCF_IGNOREBASECLASS = ASSOCF(0x00000200)
ASSOCF_INIT_IGNOREUNKNOWN = ASSOCF(0x00000400)
ASSOCF_INIT_FIXED_PROGID = ASSOCF(0x00000800)
ASSOCF_IS_PROTOCOL = ASSOCF(0x00001000)
ASSOCF_INIT_FOR_FILE = ASSOCF(0x00002000)
)
type ASSOCF uint32
// Source: https://docs.microsoft.com/en-us/windows/win32/api/shlwapi/ne-shlwapi-assocstr
const (
ASSOCSTR_COMMAND ASSOCSTR = iota + 1
ASSOCSTR_EXECUTABLE
ASSOCSTR_FRIENDLYDOCNAME
ASSOCSTR_FRIENDLYAPPNAME
ASSOCSTR_NOOPEN
ASSOCSTR_SHELLNEWVALUE
ASSOCSTR_DDECOMMAND
ASSOCSTR_DDEIFEXEC
ASSOCSTR_DDEAPPLICATION
ASSOCSTR_DDETOPIC
ASSOCSTR_INFOTIP
ASSOCSTR_QUICKTIP
ASSOCSTR_TILEINFO
ASSOCSTR_CONTENTTYPE
ASSOCSTR_DEFAULTICON
ASSOCSTR_SHELLEXTENSION
ASSOCSTR_DROPTARGET
ASSOCSTR_DELEGATEEXECUTE
ASSOCSTR_SUPPORTED_URI_PROTOCOLS
ASSOCSTR_PROGID
ASSOCSTR_APPID
ASSOCSTR_APPPUBLISHER
ASSOCSTR_APPICONREFERENCE
ASSOCSTR_MAX
)
type ASSOCSTR uint32
// Source: https://docs.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values
// and some other random Duck Duck Go searches.
const (
S_OK uintptr = 0x00000000
E_POINTER uintptr = 0x80004003
S_FALSE uintptr = 0x00000001
ERROR_NO_ASSOCIATION uintptr = 0x80070483
)