330 lines
9.1 KiB
Go
330 lines
9.1 KiB
Go
// Copyright (c) 2022 Tailscale Inc & AUTHORS. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
//go:build windows
|
|
|
|
package wingoes
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
// Error represents various error codes that may be encountered when coding
|
|
// against Windows APIs, including HRESULTs, windows.NTStatus, and windows.Errno.
|
|
type Error HRESULT
|
|
|
|
// Errors are HRESULTs under the hood because the HRESULT encoding allows for
|
|
// all the other common types of Windows errors to be encoded within them.
|
|
|
|
const (
|
|
hrS_OK = HRESULT(0)
|
|
hrE_ABORT = HRESULT(-((0x80004004 ^ 0xFFFFFFFF) + 1))
|
|
hrE_FAIL = HRESULT(-((0x80004005 ^ 0xFFFFFFFF) + 1))
|
|
hrE_NOINTERFACE = HRESULT(-((0x80004002 ^ 0xFFFFFFFF) + 1))
|
|
hrE_NOTIMPL = HRESULT(-((0x80004001 ^ 0xFFFFFFFF) + 1))
|
|
hrE_POINTER = HRESULT(-((0x80004003 ^ 0xFFFFFFFF) + 1))
|
|
hrE_UNEXPECTED = HRESULT(-((0x8000FFFF ^ 0xFFFFFFFF) + 1))
|
|
hrTYPE_E_WRONGTYPEKIND = HRESULT(-((0x8002802A ^ 0xFFFFFFFF) + 1))
|
|
)
|
|
|
|
// S_FALSE is a peculiar HRESULT value which means that the call executed
|
|
// successfully, but returned false as its result.
|
|
const S_FALSE = HRESULT(1)
|
|
|
|
var (
|
|
// genericError encodes an Error whose message string is very generic.
|
|
genericError = Error(hresultFromFacilityAndCode(hrFail, facilityWin32, hrCode(windows.ERROR_UNIDENTIFIED_ERROR)))
|
|
)
|
|
|
|
// Common HRESULT codes that don't use Win32 facilities, but have meanings that
|
|
// we can manually translate to Win32 error codes.
|
|
var commonHRESULTToErrno = map[HRESULT]windows.Errno{
|
|
hrE_ABORT: windows.ERROR_REQUEST_ABORTED,
|
|
hrE_FAIL: windows.ERROR_UNIDENTIFIED_ERROR,
|
|
hrE_NOINTERFACE: windows.ERROR_NOINTERFACE,
|
|
hrE_NOTIMPL: windows.ERROR_CALL_NOT_IMPLEMENTED,
|
|
hrE_UNEXPECTED: windows.ERROR_INTERNAL_ERROR,
|
|
}
|
|
|
|
type hrCode uint16
|
|
type hrFacility uint16
|
|
type failBit bool
|
|
|
|
const (
|
|
hrFlagBitsMask = 0xF8000000
|
|
hrFacilityMax = 0x00001FFF
|
|
hrFacilityMask = hrFacilityMax << 16
|
|
hrCodeMax = 0x0000FFFF
|
|
hrCodeMask = hrCodeMax
|
|
hrFailBit = 0x80000000
|
|
hrCustomerBit = 0x20000000 // Also defined as syscall.APPLICATION_ERROR
|
|
hrFacilityNTBit = 0x10000000
|
|
)
|
|
|
|
const (
|
|
facilityWin32 = hrFacility(7)
|
|
)
|
|
|
|
// Succeeded returns true when hr is successful, but its actual error code
|
|
// may include additional status information.
|
|
func (hr HRESULT) Succeeded() bool {
|
|
return hr >= 0
|
|
}
|
|
|
|
// Failed returns true when hr contains a failure code.
|
|
func (hr HRESULT) Failed() bool {
|
|
return hr < 0
|
|
}
|
|
|
|
func (hr HRESULT) String() string {
|
|
return fmt.Sprintf("0x%08X", uint32(hr))
|
|
}
|
|
|
|
func (hr HRESULT) isNT() bool {
|
|
return (hr & (hrCustomerBit | hrFacilityNTBit)) == hrFacilityNTBit
|
|
}
|
|
|
|
func (hr HRESULT) isCustomer() bool {
|
|
return (hr & hrCustomerBit) != 0
|
|
}
|
|
|
|
// isNormal returns true when the customer and NT bits are cleared, ie hr's
|
|
// encoding contains valid facility and code fields.
|
|
func (hr HRESULT) isNormal() bool {
|
|
return (hr & (hrCustomerBit | hrFacilityNTBit)) == 0
|
|
}
|
|
|
|
// facility returns the facility bits of hr. Only valid when isNormal is true.
|
|
func (hr HRESULT) facility() hrFacility {
|
|
return hrFacility((uint32(hr) >> 16) & hrFacilityMax)
|
|
}
|
|
|
|
// facility returns the code bits of hr. Only valid when isNormal is true.
|
|
func (hr HRESULT) code() hrCode {
|
|
return hrCode(uint32(hr) & hrCodeMask)
|
|
}
|
|
|
|
const (
|
|
hrFail = failBit(true)
|
|
hrSuccess = failBit(false)
|
|
)
|
|
|
|
func hresultFromFacilityAndCode(isFail failBit, f hrFacility, c hrCode) HRESULT {
|
|
var r uint32
|
|
if isFail {
|
|
r |= hrFailBit
|
|
}
|
|
r |= (uint32(f) << 16) & hrFacilityMask
|
|
r |= uint32(c) & hrCodeMask
|
|
return HRESULT(r)
|
|
}
|
|
|
|
// ErrorFromErrno creates an Error from e.
|
|
func ErrorFromErrno(e windows.Errno) Error {
|
|
if e == windows.ERROR_SUCCESS {
|
|
return Error(hrS_OK)
|
|
}
|
|
if ue := uint32(e); (ue & hrFlagBitsMask) == hrCustomerBit {
|
|
// syscall.APPLICATION_ERROR == hrCustomerBit, so the only other thing
|
|
// we need to do to transform this into an HRESULT is add the fail flag
|
|
return Error(HRESULT(ue | hrFailBit))
|
|
}
|
|
if uint32(e) > hrCodeMax {
|
|
// Can't be encoded in HRESULT, return generic error instead
|
|
return genericError
|
|
}
|
|
return Error(hresultFromFacilityAndCode(hrFail, facilityWin32, hrCode(e)))
|
|
}
|
|
|
|
// ErrorFromNTStatus creates an Error from s.
|
|
func ErrorFromNTStatus(s windows.NTStatus) Error {
|
|
if s == windows.STATUS_SUCCESS {
|
|
return Error(hrS_OK)
|
|
}
|
|
return Error(HRESULT(s) | hrFacilityNTBit)
|
|
}
|
|
|
|
// ErrorFromHRESULT creates an Error from hr.
|
|
func ErrorFromHRESULT(hr HRESULT) Error {
|
|
return Error(hr)
|
|
}
|
|
|
|
// NewError converts e into an Error if e's type is supported. It returns
|
|
// both the Error and a bool indicating whether the conversion was successful.
|
|
func NewError(e any) (Error, bool) {
|
|
switch v := e.(type) {
|
|
case Error:
|
|
return v, true
|
|
case windows.NTStatus:
|
|
return ErrorFromNTStatus(v), true
|
|
case windows.Errno:
|
|
return ErrorFromErrno(v), true
|
|
case HRESULT:
|
|
return ErrorFromHRESULT(v), true
|
|
default:
|
|
return ErrorFromHRESULT(hrTYPE_E_WRONGTYPEKIND), false
|
|
}
|
|
}
|
|
|
|
// IsOK returns true when the Error is unconditionally successful.
|
|
func (e Error) IsOK() bool {
|
|
return HRESULT(e) == hrS_OK
|
|
}
|
|
|
|
// Succeeded returns true when the Error is successful, but its error code
|
|
// may include additional status information.
|
|
func (e Error) Succeeded() bool {
|
|
return HRESULT(e).Succeeded()
|
|
}
|
|
|
|
// Failed returns true when the Error contains a failure code.
|
|
func (e Error) Failed() bool {
|
|
return HRESULT(e).Failed()
|
|
}
|
|
|
|
// AsHRESULT converts the Error to a HRESULT.
|
|
func (e Error) AsHRESULT() HRESULT {
|
|
return HRESULT(e)
|
|
}
|
|
|
|
type errnoFailHandler func(hr HRESULT) windows.Errno
|
|
|
|
func (e Error) toErrno(f errnoFailHandler) windows.Errno {
|
|
hr := HRESULT(e)
|
|
|
|
if hr == hrS_OK {
|
|
return windows.ERROR_SUCCESS
|
|
}
|
|
|
|
if hr.isCustomer() {
|
|
return windows.Errno(uint32(e) ^ hrFailBit)
|
|
}
|
|
|
|
if hr.isNT() {
|
|
return e.AsNTStatus().Errno()
|
|
}
|
|
|
|
if hr.facility() == facilityWin32 {
|
|
return windows.Errno(hr.code())
|
|
}
|
|
|
|
if errno, ok := commonHRESULTToErrno[hr]; ok {
|
|
return errno
|
|
}
|
|
|
|
return f(hr)
|
|
}
|
|
|
|
// AsError converts the Error to a windows.Errno, but panics if not possible.
|
|
func (e Error) AsErrno() windows.Errno {
|
|
handler := func(hr HRESULT) windows.Errno {
|
|
panic(fmt.Sprintf("wingoes.Error: Called AsErrno on a non-convertable HRESULT 0x%08X", uint32(hr)))
|
|
return windows.ERROR_UNIDENTIFIED_ERROR
|
|
}
|
|
|
|
return e.toErrno(handler)
|
|
}
|
|
|
|
type ntStatusFailHandler func(hr HRESULT) windows.NTStatus
|
|
|
|
func (e Error) toNTStatus(f ntStatusFailHandler) windows.NTStatus {
|
|
hr := HRESULT(e)
|
|
|
|
if hr == hrS_OK {
|
|
return windows.STATUS_SUCCESS
|
|
}
|
|
|
|
if hr.isNT() {
|
|
return windows.NTStatus(hr ^ hrFacilityNTBit)
|
|
}
|
|
|
|
return f(hr)
|
|
}
|
|
|
|
// AsNTStatus converts the Error to a windows.NTStatus, but panics if not possible.
|
|
func (e Error) AsNTStatus() windows.NTStatus {
|
|
handler := func(hr HRESULT) windows.NTStatus {
|
|
panic(fmt.Sprintf("windows.Error: Called AsNTStatus on a non-NTSTATUS HRESULT 0x%08X", uint32(hr)))
|
|
return windows.STATUS_UNSUCCESSFUL
|
|
}
|
|
|
|
return e.toNTStatus(handler)
|
|
}
|
|
|
|
// TryAsErrno converts the Error to a windows.Errno, or returns defval if
|
|
// such a conversion is not possible.
|
|
func (e Error) TryAsErrno(defval windows.Errno) windows.Errno {
|
|
handler := func(hr HRESULT) windows.Errno {
|
|
return defval
|
|
}
|
|
|
|
return e.toErrno(handler)
|
|
}
|
|
|
|
// TryAsNTStatus converts the Error to a windows.NTStatus, or returns defval if
|
|
// such a conversion is not possible.
|
|
func (e Error) TryAsNTStatus(defval windows.NTStatus) windows.NTStatus {
|
|
handler := func(hr HRESULT) windows.NTStatus {
|
|
return defval
|
|
}
|
|
|
|
return e.toNTStatus(handler)
|
|
}
|
|
|
|
// IsAvailableAsHRESULT returns true if e may be converted to an HRESULT.
|
|
func (e Error) IsAvailableAsHRESULT() bool {
|
|
return true
|
|
}
|
|
|
|
// IsAvailableAsErrno returns true if e may be converted to a windows.Errno.
|
|
func (e Error) IsAvailableAsErrno() bool {
|
|
hr := HRESULT(e)
|
|
if hr.isCustomer() || e.IsAvailableAsNTStatus() || (hr.facility() == facilityWin32) {
|
|
return true
|
|
}
|
|
_, convertable := commonHRESULTToErrno[hr]
|
|
return convertable
|
|
}
|
|
|
|
// IsAvailableAsNTStatus returns true if e may be converted to a windows.NTStatus.
|
|
func (e Error) IsAvailableAsNTStatus() bool {
|
|
return HRESULT(e) == hrS_OK || HRESULT(e).isNT()
|
|
}
|
|
|
|
// Error produces a human-readable message describing Error e.
|
|
func (e Error) Error() string {
|
|
if HRESULT(e).isCustomer() {
|
|
return windows.Errno(uint32(e) ^ hrFailBit).Error()
|
|
}
|
|
|
|
buf := make([]uint16, 300)
|
|
const flags = windows.FORMAT_MESSAGE_FROM_SYSTEM | windows.FORMAT_MESSAGE_IGNORE_INSERTS
|
|
lenExclNul, err := windows.FormatMessage(flags, 0, uint32(e), 0, buf, nil)
|
|
if err != nil {
|
|
return fmt.Sprintf("wingoes.Error 0x%08X", uint32(e))
|
|
}
|
|
for ; lenExclNul > 0 && (buf[lenExclNul-1] == '\n' || buf[lenExclNul-1] == '\r'); lenExclNul-- {
|
|
}
|
|
return windows.UTF16ToString(buf[:lenExclNul])
|
|
}
|
|
|
|
// Unwrap permits extraction of underlying windows.NTStatus or windows.Errno
|
|
// errors that are encoded within e.
|
|
func (e Error) Unwrap() error {
|
|
// Order is important! We need earlier checks to exclude certain things that
|
|
// would otherwise be (in this case) false positives in later checks!
|
|
switch {
|
|
case e.IsOK():
|
|
return nil
|
|
case e.IsAvailableAsNTStatus():
|
|
return e.AsNTStatus()
|
|
case e.IsAvailableAsErrno():
|
|
return e.AsErrno()
|
|
default:
|
|
return nil
|
|
}
|
|
}
|