348 lines
8.4 KiB
Go
348 lines
8.4 KiB
Go
package winres
|
|
|
|
import (
|
|
"bytes"
|
|
"debug/pe"
|
|
"encoding/binary"
|
|
"errors"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf16"
|
|
)
|
|
|
|
const (
|
|
rtIcon = 3
|
|
rtGroupIcon = 14
|
|
rtVersion = 16
|
|
rtManifest = 24
|
|
langUS = 0x0409
|
|
subdir = 0x80000000
|
|
)
|
|
|
|
var IconSizes = []int{16, 24, 32, 48, 64, 128, 256}
|
|
|
|
type Icon struct {
|
|
Size int
|
|
PNG []byte
|
|
}
|
|
|
|
type Version [4]uint16
|
|
|
|
func ParseVersion(s string) Version {
|
|
var v Version
|
|
for i, part := range strings.SplitN(strings.TrimPrefix(strings.TrimSpace(s), "v"), ".", 4) {
|
|
end := 0
|
|
for end < len(part) && part[end] >= '0' && part[end] <= '9' {
|
|
end++
|
|
}
|
|
n, _ := strconv.Atoi(part[:end])
|
|
v[i] = uint16(min(n, 65535))
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (v Version) String() string {
|
|
return strconv.Itoa(int(v[0])) + "." + strconv.Itoa(int(v[1])) + "." + strconv.Itoa(int(v[2])) + "." + strconv.Itoa(int(v[3]))
|
|
}
|
|
|
|
type Info struct {
|
|
Product string
|
|
Description string
|
|
Company string
|
|
Copyright string
|
|
Original string
|
|
Internal string
|
|
VersionText string
|
|
Version Version
|
|
}
|
|
|
|
type resource struct {
|
|
typ uint16
|
|
id uint16
|
|
data []byte
|
|
}
|
|
|
|
const appManifest = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
|
<windowsSettings>
|
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
|
</windowsSettings>
|
|
</application>
|
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
|
<application>
|
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
|
</application>
|
|
</compatibility>
|
|
</assembly>
|
|
`
|
|
|
|
func machine(arch string) (uint16, uint16, error) {
|
|
switch arch {
|
|
case "amd64":
|
|
return 0x8664, 0x0003, nil
|
|
case "386":
|
|
return 0x014c, 0x0007, nil
|
|
case "arm64":
|
|
return 0xaa64, 0x0002, nil
|
|
}
|
|
return 0, 0, errors.New("winres: unsupported architecture " + arch)
|
|
}
|
|
|
|
func Syso(arch string, icons []Icon, info Info, manifest bool) ([]byte, error) {
|
|
mach, rel, err := machine(arch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var res []resource
|
|
if len(icons) > 0 {
|
|
for i, ic := range icons {
|
|
res = append(res, resource{typ: rtIcon, id: uint16(i + 1), data: ic.PNG})
|
|
}
|
|
res = append(res, resource{typ: rtGroupIcon, id: 1, data: groupIcon(icons)})
|
|
}
|
|
res = append(res, resource{typ: rtVersion, id: 1, data: versionInfo(info)})
|
|
if manifest {
|
|
res = append(res, resource{typ: rtManifest, id: 1, data: []byte(appManifest)})
|
|
}
|
|
sec, relocs := section(res)
|
|
return coff(mach, rel, arch == "386", sec, relocs), nil
|
|
}
|
|
|
|
func groupIcon(icons []Icon) []byte {
|
|
le := binary.LittleEndian
|
|
b := make([]byte, 6+14*len(icons))
|
|
le.PutUint16(b[2:], 1)
|
|
le.PutUint16(b[4:], uint16(len(icons)))
|
|
for i, ic := range icons {
|
|
e := b[6+14*i:]
|
|
w := byte(ic.Size)
|
|
if ic.Size >= 256 {
|
|
w = 0
|
|
}
|
|
e[0], e[1] = w, w
|
|
le.PutUint16(e[4:], 1)
|
|
le.PutUint16(e[6:], 32)
|
|
le.PutUint32(e[8:], uint32(len(ic.PNG)))
|
|
le.PutUint16(e[12:], uint16(i+1))
|
|
}
|
|
return b
|
|
}
|
|
|
|
type vnode struct {
|
|
key string
|
|
value []byte
|
|
text bool
|
|
kids []vnode
|
|
}
|
|
|
|
func utf16z(s string) []byte {
|
|
u := utf16.Encode([]rune(s))
|
|
b := make([]byte, 2*(len(u)+1))
|
|
for i, c := range u {
|
|
binary.LittleEndian.PutUint16(b[2*i:], c)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func pad4(b *bytes.Buffer) {
|
|
for b.Len()%4 != 0 {
|
|
b.WriteByte(0)
|
|
}
|
|
}
|
|
|
|
func (n vnode) bytes() []byte {
|
|
var b bytes.Buffer
|
|
b.Write(make([]byte, 6))
|
|
b.Write(utf16z(n.key))
|
|
pad4(&b)
|
|
b.Write(n.value)
|
|
for _, k := range n.kids {
|
|
pad4(&b)
|
|
b.Write(k.bytes())
|
|
}
|
|
out := b.Bytes()
|
|
vl, typ := len(n.value), 0
|
|
if n.text {
|
|
vl, typ = len(n.value)/2, 1
|
|
}
|
|
binary.LittleEndian.PutUint16(out[0:], uint16(len(out)))
|
|
binary.LittleEndian.PutUint16(out[2:], uint16(vl))
|
|
binary.LittleEndian.PutUint16(out[4:], uint16(typ))
|
|
return out
|
|
}
|
|
|
|
func fixed(v Version) []byte {
|
|
le := binary.LittleEndian
|
|
b := make([]byte, 52)
|
|
ms := uint32(v[0])<<16 | uint32(v[1])
|
|
ls := uint32(v[2])<<16 | uint32(v[3])
|
|
le.PutUint32(b[0:], 0xFEEF04BD)
|
|
le.PutUint32(b[4:], 0x00010000)
|
|
le.PutUint32(b[8:], ms)
|
|
le.PutUint32(b[12:], ls)
|
|
le.PutUint32(b[16:], ms)
|
|
le.PutUint32(b[20:], ls)
|
|
le.PutUint32(b[24:], 0x3F)
|
|
le.PutUint32(b[32:], 0x00040004)
|
|
le.PutUint32(b[36:], 1)
|
|
return b
|
|
}
|
|
|
|
func versionInfo(info Info) []byte {
|
|
text := info.VersionText
|
|
if text == "" {
|
|
text = info.Version.String()
|
|
}
|
|
pairs := [][2]string{
|
|
{"CompanyName", info.Company}, {"FileDescription", info.Description}, {"FileVersion", text},
|
|
{"InternalName", info.Internal}, {"LegalCopyright", info.Copyright}, {"OriginalFilename", info.Original},
|
|
{"ProductName", info.Product}, {"ProductVersion", text},
|
|
}
|
|
var strs []vnode
|
|
for _, kv := range pairs {
|
|
if kv[1] != "" {
|
|
strs = append(strs, vnode{key: kv[0], value: utf16z(kv[1]), text: true})
|
|
}
|
|
}
|
|
tr := make([]byte, 4)
|
|
binary.LittleEndian.PutUint16(tr[0:], langUS)
|
|
binary.LittleEndian.PutUint16(tr[2:], 0x04B0)
|
|
root := vnode{key: "VS_VERSION_INFO", value: fixed(info.Version), kids: []vnode{
|
|
{key: "StringFileInfo", text: true, kids: []vnode{{key: "040904B0", text: true, kids: strs}}},
|
|
{key: "VarFileInfo", text: true, kids: []vnode{{key: "Translation", value: tr}}},
|
|
}}
|
|
return root.bytes()
|
|
}
|
|
|
|
func align(n, a int) int {
|
|
return (n + a - 1) &^ (a - 1)
|
|
}
|
|
|
|
func section(res []resource) ([]byte, []uint32) {
|
|
le := binary.LittleEndian
|
|
sort.SliceStable(res, func(i, j int) bool {
|
|
if res[i].typ != res[j].typ {
|
|
return res[i].typ < res[j].typ
|
|
}
|
|
return res[i].id < res[j].id
|
|
})
|
|
var types []uint16
|
|
count := map[uint16]int{}
|
|
for _, r := range res {
|
|
if count[r.typ] == 0 {
|
|
types = append(types, r.typ)
|
|
}
|
|
count[r.typ]++
|
|
}
|
|
off := 16 + 8*len(types)
|
|
typeOff := map[uint16]int{}
|
|
for _, t := range types {
|
|
typeOff[t] = off
|
|
off += 16 + 8*count[t]
|
|
}
|
|
langOff := make([]int, len(res))
|
|
for i := range res {
|
|
langOff[i] = off
|
|
off += 24
|
|
}
|
|
entryOff := make([]int, len(res))
|
|
for i := range res {
|
|
entryOff[i] = off
|
|
off += 16
|
|
}
|
|
dataOff := make([]int, len(res))
|
|
for i, r := range res {
|
|
off = align(off, 8)
|
|
dataOff[i] = off
|
|
off += len(r.data)
|
|
}
|
|
b := make([]byte, align(off, 8))
|
|
le.PutUint16(b[14:], uint16(len(types)))
|
|
for i, t := range types {
|
|
le.PutUint32(b[16+8*i:], uint32(t))
|
|
le.PutUint32(b[20+8*i:], uint32(typeOff[t])|subdir)
|
|
le.PutUint16(b[typeOff[t]+14:], uint16(count[t]))
|
|
}
|
|
next := map[uint16]int{}
|
|
relocs := make([]uint32, len(res))
|
|
for i, r := range res {
|
|
at := typeOff[r.typ] + 16 + 8*next[r.typ]
|
|
next[r.typ]++
|
|
le.PutUint32(b[at:], uint32(r.id))
|
|
le.PutUint32(b[at+4:], uint32(langOff[i])|subdir)
|
|
le.PutUint16(b[langOff[i]+14:], 1)
|
|
le.PutUint32(b[langOff[i]+16:], langUS)
|
|
le.PutUint32(b[langOff[i]+20:], uint32(entryOff[i]))
|
|
le.PutUint32(b[entryOff[i]:], uint32(dataOff[i]))
|
|
le.PutUint32(b[entryOff[i]+4:], uint32(len(r.data)))
|
|
copy(b[dataOff[i]:], r.data)
|
|
relocs[i] = uint32(entryOff[i])
|
|
}
|
|
return b, relocs
|
|
}
|
|
|
|
func coff(mach, relType uint16, i386 bool, sec []byte, relocs []uint32) []byte {
|
|
const fileHdr, secHdr, relSize, symSize = 20, 40, 10, 18
|
|
le := binary.LittleEndian
|
|
raw := fileHdr + secHdr
|
|
relPtr := raw + len(sec)
|
|
symPtr := relPtr + relSize*len(relocs)
|
|
b := make([]byte, symPtr+symSize+4)
|
|
le.PutUint16(b[0:], mach)
|
|
le.PutUint16(b[2:], 1)
|
|
le.PutUint32(b[8:], uint32(symPtr))
|
|
le.PutUint32(b[12:], 1)
|
|
if i386 {
|
|
le.PutUint16(b[18:], 0x0100)
|
|
}
|
|
s := b[fileHdr:]
|
|
copy(s[0:8], ".rsrc")
|
|
le.PutUint32(s[16:], uint32(len(sec)))
|
|
le.PutUint32(s[20:], uint32(raw))
|
|
le.PutUint32(s[24:], uint32(relPtr))
|
|
le.PutUint16(s[32:], uint16(len(relocs)))
|
|
le.PutUint32(s[36:], 0x40000040)
|
|
copy(b[raw:], sec)
|
|
for i, r := range relocs {
|
|
o := relPtr + relSize*i
|
|
le.PutUint32(b[o:], r)
|
|
le.PutUint16(b[o+8:], relType)
|
|
}
|
|
sym := b[symPtr:]
|
|
copy(sym[0:8], ".rsrc")
|
|
le.PutUint16(sym[12:], 1)
|
|
sym[16] = 3
|
|
le.PutUint32(b[symPtr+symSize:], 4)
|
|
return b
|
|
}
|
|
|
|
func Types(data []byte) ([]uint16, error) {
|
|
f, err := pe.NewFile(bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
s := f.Section(".rsrc")
|
|
if s == nil {
|
|
return nil, errors.New("winres: no .rsrc section")
|
|
}
|
|
raw, err := s.Data()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(raw) < 16 {
|
|
return nil, errors.New("winres: .rsrc is truncated")
|
|
}
|
|
n := int(binary.LittleEndian.Uint16(raw[12:])) + int(binary.LittleEndian.Uint16(raw[14:]))
|
|
var out []uint16
|
|
for i := 0; i < n && 16+8*i+8 <= len(raw); i++ {
|
|
if id := binary.LittleEndian.Uint32(raw[16+8*i:]); id&subdir == 0 {
|
|
out = append(out, uint16(id))
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|