Files
2024-11-01 17:43:06 +00:00

169 lines
4.5 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build windows
package pe
import (
"errors"
"fmt"
"unsafe"
"golang.org/x/sys/windows"
)
var (
errFixedFileInfoBadSig = errors.New("bad VS_FIXEDFILEINFO signature")
errFixedFileInfoTooShort = errors.New("buffer smaller than VS_FIXEDFILEINFO")
)
// VersionNumber encapsulates a four-component version number that is stored
// in Windows VERSIONINFO resources.
type VersionNumber struct {
Major uint16
Minor uint16
Patch uint16
Build uint16
}
func (vn VersionNumber) String() string {
return fmt.Sprintf("%d.%d.%d.%d", vn.Major, vn.Minor, vn.Patch, vn.Build)
}
type langAndCodePage struct {
language uint16
codePage uint16
}
// VersionInfo encapsulates a buffer containing the VERSIONINFO resources that
// have been successfully extracted from a PE binary.
type VersionInfo struct {
buf []byte
fixed *windows.VS_FIXEDFILEINFO
translationIDs []langAndCodePage
}
const (
langEnUS = 0x0409
codePageUTF16LE = 0x04B0
langNeutral = 0
codePageNeutral = 0
)
// NewVersionInfo extracts any VERSIONINFO resource from filepath, parses its
// fixed-size information, and returns a *VersionInfo for further querying.
// It returns ErrNotPresent if no VERSIONINFO resources are found.
func NewVersionInfo(filepath string) (*VersionInfo, error) {
bufSize, err := windows.GetFileVersionInfoSize(filepath, nil)
if err != nil {
if errors.Is(err, windows.ERROR_RESOURCE_TYPE_NOT_FOUND) {
err = ErrNotPresent
}
return nil, err
}
buf := make([]byte, bufSize)
if err := windows.GetFileVersionInfo(filepath, 0, bufSize, unsafe.Pointer(unsafe.SliceData(buf))); err != nil {
return nil, err
}
var fixed *windows.VS_FIXEDFILEINFO
var fixedLen uint32
if err := windows.VerQueryValue(unsafe.Pointer(unsafe.SliceData(buf)), `\`, unsafe.Pointer(&fixed), &fixedLen); err != nil {
return nil, err
}
if fixedLen < uint32(unsafe.Sizeof(windows.VS_FIXEDFILEINFO{})) {
return nil, errFixedFileInfoTooShort
}
if fixed.Signature != 0xFEEF04BD {
return nil, errFixedFileInfoBadSig
}
return &VersionInfo{
buf: buf,
fixed: fixed,
}, nil
}
func (vi *VersionInfo) VersionNumber() VersionNumber {
f := vi.fixed
return VersionNumber{
Major: uint16(f.FileVersionMS >> 16),
Minor: uint16(f.FileVersionMS & 0xFFFF),
Patch: uint16(f.FileVersionLS >> 16),
Build: uint16(f.FileVersionLS & 0xFFFF),
}
}
func (vi *VersionInfo) maybeLoadTranslationIDs() {
if vi.translationIDs != nil {
// Already loaded
return
}
// Preferred translations, in order of preference.
preferredTranslationIDs := []langAndCodePage{
langAndCodePage{
language: langEnUS,
codePage: codePageUTF16LE,
},
langAndCodePage{
language: langNeutral,
codePage: codePageNeutral,
},
}
var ids *langAndCodePage
var idsNumBytes uint32
if err := windows.VerQueryValue(
unsafe.Pointer(unsafe.SliceData(vi.buf)),
`\VarFileInfo\Translation`,
unsafe.Pointer(&ids),
&idsNumBytes,
); err != nil {
// If nothing is listed, then just try to use our preferred translation IDs.
vi.translationIDs = preferredTranslationIDs
return
}
idsSlice := unsafe.Slice(ids, idsNumBytes/uint32(unsafe.Sizeof(*ids)))
vi.translationIDs = append(preferredTranslationIDs, idsSlice...)
}
func (vi *VersionInfo) queryWithLangAndCodePage(key string, lcp langAndCodePage) (string, error) {
fq := fmt.Sprintf("\\StringFileInfo\\%04x%04x\\%s", lcp.language, lcp.codePage, key)
var value *uint16
var valueLen uint32
if err := windows.VerQueryValue(unsafe.Pointer(unsafe.SliceData(vi.buf)), fq, unsafe.Pointer(&value), &valueLen); err != nil {
return "", err
}
return windows.UTF16ToString(unsafe.Slice(value, valueLen)), nil
}
// Field queries the version information for a field named key and either
// returns the field's value, or an error. It attempts to resolve strings using
// the following order of language preference: en-US, language-neutral, followed
// by the first entry in version info's list of supported languages that
// successfully resolves the key.
// If the key cannot be resolved, it returns ErrNotPresent.
func (vi *VersionInfo) Field(key string) (string, error) {
vi.maybeLoadTranslationIDs()
for _, lcp := range vi.translationIDs {
value, err := vi.queryWithLangAndCodePage(key, lcp)
if err == nil {
return value, nil
}
if !errors.Is(err, windows.ERROR_RESOURCE_TYPE_NOT_FOUND) {
return "", err
}
// Otherwise we continue looping and try the next language
}
return "", ErrNotPresent
}