From 0a88d65a20884717ad6ce838490d69e2f57f2243 Mon Sep 17 00:00:00 2001 From: yanghy Date: Mon, 2 Oct 2023 16:46:45 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4winres,=20=E5=9C=A8go.mod?= =?UTF-8?q?=E5=BC=95=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cef/ipc/argument/list_test.go | 25 + go.mod | 3 +- go.sum | 4 + pkgs/winres/LICENSE | 12 - pkgs/winres/README.md | 45 -- pkgs/winres/coff.go | 126 ---- pkgs/winres/coff_test.go | 77 -- pkgs/winres/cursor.go | 336 --------- pkgs/winres/cursor_test.go | 503 ------------- pkgs/winres/errors.go | 47 -- pkgs/winres/exe.go | 684 ----------------- pkgs/winres/helper_test.go | 234 ------ pkgs/winres/icon.go | 356 --------- pkgs/winres/icon_test.go | 558 -------------- pkgs/winres/id.go | 48 -- pkgs/winres/manifest.go | 533 -------------- pkgs/winres/manifest_test.go | 853 --------------------- pkgs/winres/pesum.go | 41 -- pkgs/winres/pesum_test.go | 33 - pkgs/winres/reader.go | 25 - pkgs/winres/rsrc.go | 550 -------------- pkgs/winres/rsrc_test.go | 304 -------- pkgs/winres/version/binary.go | 413 ----------- pkgs/winres/version/binary_test.go | 40 - pkgs/winres/version/errors.go | 13 - pkgs/winres/version/json.go | 112 --- pkgs/winres/version/json_test.go | 99 --- pkgs/winres/version/timestamp.go | 26 - pkgs/winres/version/version.go | 313 -------- pkgs/winres/version/version_test.go | 659 ----------------- pkgs/winres/winres.go | 357 --------- pkgs/winres/winres_test.go | 1062 --------------------------- 32 files changed, 31 insertions(+), 8460 deletions(-) create mode 100644 cef/ipc/argument/list_test.go delete mode 100644 pkgs/winres/LICENSE delete mode 100644 pkgs/winres/README.md delete mode 100644 pkgs/winres/coff.go delete mode 100644 pkgs/winres/coff_test.go delete mode 100644 pkgs/winres/cursor.go delete mode 100644 pkgs/winres/cursor_test.go delete mode 100644 pkgs/winres/errors.go delete mode 100644 pkgs/winres/exe.go delete mode 100644 pkgs/winres/helper_test.go delete mode 100644 pkgs/winres/icon.go delete mode 100644 pkgs/winres/icon_test.go delete mode 100644 pkgs/winres/id.go delete mode 100644 pkgs/winres/manifest.go delete mode 100644 pkgs/winres/manifest_test.go delete mode 100644 pkgs/winres/pesum.go delete mode 100644 pkgs/winres/pesum_test.go delete mode 100644 pkgs/winres/reader.go delete mode 100644 pkgs/winres/rsrc.go delete mode 100644 pkgs/winres/rsrc_test.go delete mode 100644 pkgs/winres/version/binary.go delete mode 100644 pkgs/winres/version/binary_test.go delete mode 100644 pkgs/winres/version/errors.go delete mode 100644 pkgs/winres/version/json.go delete mode 100644 pkgs/winres/version/json_test.go delete mode 100644 pkgs/winres/version/timestamp.go delete mode 100644 pkgs/winres/version/version.go delete mode 100644 pkgs/winres/version/version_test.go delete mode 100644 pkgs/winres/winres.go delete mode 100644 pkgs/winres/winres_test.go diff --git a/cef/ipc/argument/list_test.go b/cef/ipc/argument/list_test.go new file mode 100644 index 0000000..868e404 --- /dev/null +++ b/cef/ipc/argument/list_test.go @@ -0,0 +1,25 @@ +package argument + +import ( + "testing" +) + +func TestList(t *testing.T) { + list := &List{ + Data: make([]string, 0), + } + json := list.JSON() + json.Add("value1") + json.Add("value2") + json.Add("value3") + if json.Size() != 3 { + t.Fail() + } + if json.GetStringByIndex(1) != "value2" { + t.Fail() + } + data := json.Bytes() + if data == nil { + t.Fail() + } +} diff --git a/go.mod b/go.mod index b1e7798..9e00ab0 100644 --- a/go.mod +++ b/go.mod @@ -14,8 +14,8 @@ require ( github.com/jessevdk/go-flags v1.5.0 github.com/json-iterator/go v1.1.12 github.com/lithammer/fuzzysearch v1.1.8 - github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/pterm/pterm v0.12.66 + github.com/tc-hib/winres v0.2.0 github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c golang.org/x/image v0.12.0 golang.org/x/sys v0.12.0 @@ -28,6 +28,7 @@ require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/term v0.11.0 // indirect diff --git a/go.sum b/go.sum index 095568d..d7e5ce7 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/tc-hib/winres v0.2.0 h1:gly/ivDWGvlhl7ENtEmA7wPQ6dWab1LlLq/DgcZECKE= +github.com/tc-hib/winres v0.2.0/go.mod h1:uG6S5M2Q0/kThoqsCSYvGJODUQP9O9R0SNxUPmFIegw= github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c h1:coVla7zpsycc+kA9NXpcvv2E4I7+ii6L5hZO2S6C3kw= github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= @@ -83,6 +85,7 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c= golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ= golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -117,6 +120,7 @@ golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= diff --git a/pkgs/winres/LICENSE b/pkgs/winres/LICENSE deleted file mode 100644 index 281cb6a..0000000 --- a/pkgs/winres/LICENSE +++ /dev/null @@ -1,12 +0,0 @@ -Copyright (c) 2021 Thomas Combeléran - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. diff --git a/pkgs/winres/README.md b/pkgs/winres/README.md deleted file mode 100644 index 57f2dad..0000000 --- a/pkgs/winres/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# winres - - -Put this file in your project directory, name it "something.syso" or, preferably, -"something_windows_amd64.syso", and you're done : -the `go build` command will detect it and automatically use it. - -You should have a look at the [command line tool](https://github.com/tc-hib/go-winres) to try it. Using the library -gives you more control, though. - -Here is a quick example: - -```go -package main - -import ( - "io/ioutil" - "os" - - "github.com/tc-hib/winres" -) - -func main() { - // Start by creating an empty resource set - rs := winres.ResourceSet{} - - // Add resources - // This is a cursor named ID(1) - cursorData, _ := ioutil.ReadFile("cursor.cur") - rs.Set(winres.RT_CURSOR, winres.ID(1), 0, cursorData) - - // This is a custom data type, translated in english (0x409) and french (0x40C) - // You can find more language IDs by searching for LCID - rs.Set(winres.Name("CUSTOM"), winres.Name("COOLDATA"), 0x409, []byte("Hello World")) - rs.Set(winres.Name("CUSTOM"), winres.Name("COOLDATA"), 0x40C, []byte("Bonjour Monde")) - - // Compile to a COFF object file - // It is recommended to use the target suffix "_window_amd64" - // so that `go build` knows when not to include it. - out, _ := os.Create("rsrc_windows_amd64.syso") - rs.WriteObject(out, winres.ArchAMD64) -} -``` - -# From: github.com/tc-hib/winres \ No newline at end of file diff --git a/pkgs/winres/coff.go b/pkgs/winres/coff.go deleted file mode 100644 index 3259c94..0000000 --- a/pkgs/winres/coff.go +++ /dev/null @@ -1,126 +0,0 @@ -package winres - -import ( - "debug/pe" - "encoding/binary" - "errors" - "io" -) - -// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#other-contents-of-the-file - -const ( - _IMAGE_SCN_MEM_READ = 0x00000040 - _IMAGE_SCN_CNT_INITIALIZED_DATA = 0x40000000 -) - -const sizeOfReloc = 10 - -func writeObject(w io.Writer, r *ResourceSet, arch Arch) error { - file := pe.FileHeader{ - Machine: pe.IMAGE_FILE_MACHINE_UNKNOWN, - NumberOfSections: 1, - NumberOfSymbols: 1, - } - section := pe.SectionHeader32{ - Name: [8]byte{'.', 'r', 's', 'r', 'c'}, - PointerToLineNumbers: 0, - Characteristics: _IMAGE_SCN_MEM_READ | _IMAGE_SCN_CNT_INITIALIZED_DATA, - } - - section.PointerToRawData = uint32(binary.Size(file) + binary.Size(section)) - section.SizeOfRawData = uint32(r.fullSize()) - section.PointerToRelocations = section.PointerToRawData + section.SizeOfRawData - section.NumberOfRelocations = uint16(r.numDataEntries()) - - switch arch { - case ArchI386: - file.Machine = pe.IMAGE_FILE_MACHINE_I386 - case ArchAMD64: - file.Machine = pe.IMAGE_FILE_MACHINE_AMD64 - case ArchARM: - file.Machine = pe.IMAGE_FILE_MACHINE_ARMNT - case ArchARM64: - file.Machine = pe.IMAGE_FILE_MACHINE_ARM64 - default: - return errors.New(errUnknownArch) - } - file.PointerToSymbolTable = section.PointerToRelocations + uint32(section.NumberOfRelocations)*sizeOfReloc - - if err := binary.Write(w, binary.LittleEndian, file); err != nil { - return err - } - if err := binary.Write(w, binary.LittleEndian, section); err != nil { - return err - } - addr, err := r.write(w) - if err != nil { - return err - } - if err := writeRelocTable(w, 0, arch, addr); err != nil { - return err - } - if err := writeSymbol(w, 1); err != nil { - return err - } - // Empty string table - if err := binary.Write(w, binary.LittleEndian, uint32(4)); err != nil { - return err - } - return nil -} - -// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#type-indicators - -const ( - _IMAGE_REL_I386_DIR32NB uint16 = 0x7 - _IMAGE_REL_AMD64_ADDR32NB uint16 = 0x3 - _IMAGE_REL_ARM_ADDR32NB uint16 = 0x2 - _IMAGE_REL_ARM64_ADDR32NB uint16 = 0x2 -) - -func writeRelocTable(w io.Writer, symbolIndex int, arch Arch, addr []int) error { - var t uint16 - - switch arch { - case ArchI386: - t = _IMAGE_REL_I386_DIR32NB - case ArchAMD64: - t = _IMAGE_REL_AMD64_ADDR32NB - case ArchARM: - t = _IMAGE_REL_ARM_ADDR32NB - case ArchARM64: - t = _IMAGE_REL_ARM64_ADDR32NB - default: - return errors.New(errUnknownArch) - } - - for _, a := range addr { - err := binary.Write(w, binary.LittleEndian, &pe.Reloc{ - VirtualAddress: uint32(a), - SymbolTableIndex: uint32(symbolIndex), - Type: t, - }) - if err != nil { - return err - } - } - return nil -} - -// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#coff-symbol-table - -const _IMAGE_SYM_TYPE_NULL = 0 - -const _IMAGE_SYM_CLASS_STATIC = 3 - -func writeSymbol(w io.Writer, sectionNumber int) error { - // The symbol is a section name because Value == 0 and StorageClass == IMAGE_SYM_CLASS_STATIC - return binary.Write(w, binary.LittleEndian, &pe.COFFSymbol{ - Name: [8]byte{'.', 'r', 's', 'r', 'c'}, - Value: 0, - SectionNumber: int16(sectionNumber), - Type: _IMAGE_SYM_TYPE_NULL, - StorageClass: _IMAGE_SYM_CLASS_STATIC, - }) -} diff --git a/pkgs/winres/coff_test.go b/pkgs/winres/coff_test.go deleted file mode 100644 index bbd3faf..0000000 --- a/pkgs/winres/coff_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package winres - -import ( - "io" - "testing" -) - -func Test_writeObject_WriteErrFileHeader(t *testing.T) { - bw := newBadWriter(19) - rs := &ResourceSet{} - - err := writeObject(bw, rs, ArchAMD64) - if !isExpectedWriteErr(err) { - t.Fatal("expected write error, got", err) - } -} - -func Test_writeObject_WriteErrSectionHeader(t *testing.T) { - bw := newBadWriter(58) - rs := &ResourceSet{} - - err := writeObject(bw, rs, ArchAMD64) - if !isExpectedWriteErr(err) { - t.Fatal("expected write error, got", err) - } -} - -func Test_writeObject_WriteErrReloc(t *testing.T) { - bw := newBadWriter(165) - rs := &ResourceSet{} - rs.Set(RT_RCDATA, ID(1), 0, make([]byte, 1)) - - err := writeObject(bw, rs, ArchAMD64) - if !isExpectedWriteErr(err) { - t.Fatal("expected write error, got", err) - } -} - -func Test_writeObject_WriteErrSymbol(t *testing.T) { - bw := newBadWriter(182) - rs := &ResourceSet{} - rs.Set(RT_RCDATA, ID(1), 0, make([]byte, 1)) - - err := writeObject(bw, rs, ArchAMD64) - if !isExpectedWriteErr(err) { - t.Fatal("expected write error, got", err) - } -} - -func Test_writeObject_WriteErrStringTable(t *testing.T) { - bw := newBadWriter(185) - rs := &ResourceSet{} - rs.Set(RT_RCDATA, ID(1), 0, make([]byte, 1)) - - err := writeObject(bw, rs, ArchAMD64) - if !isExpectedWriteErr(err) { - t.Fatal("expected write error, got", err) - } -} - -func Test_writeObject_WriteErrSection(t *testing.T) { - bw := newBadWriter(61) - rs := &ResourceSet{} - rs.Set(RT_RCDATA, ID(1), 0, make([]byte, 1)) - - err := writeObject(bw, rs, ArchAMD64) - if !isExpectedWriteErr(err) { - t.Fatal("expected write error, got", err) - } -} - -func Test_writeRelocTable_UnknownArch(t *testing.T) { - err := writeRelocTable(io.Discard, 1, "*", []int{1, 2, 3, 4}) - if err == nil || err.Error() != errUnknownArch { - t.Fail() - } -} diff --git a/pkgs/winres/cursor.go b/pkgs/winres/cursor.go deleted file mode 100644 index c0ea9e6..0000000 --- a/pkgs/winres/cursor.go +++ /dev/null @@ -1,336 +0,0 @@ -package winres - -import ( - "bytes" - "encoding/binary" - "errors" - "image" - "io" - "sort" - - "golang.org/x/image/bmp" -) - -// Cursor describes a mouse cursor. -// -// This structure must only be created by constructors: -// NewCursorFromImages, LoadCUR -type Cursor struct { - images []cursorImage -} - -// CursorImage defines an image to import into a cursor. -// -// It is an Image with hot spot coordinates. -type CursorImage struct { - Image image.Image - HotSpot HotSpot -} - -// HotSpot is the coordinates of a cursor's hot spot. -type HotSpot struct { - X uint16 - Y uint16 -} - -// NewCursorFromImages makes a cursor from a list of images and hot spots. -func NewCursorFromImages(images []CursorImage) (*Cursor, error) { - cursor := &Cursor{} - - for _, img := range images { - if err := cursor.addImage(img.Image, img.HotSpot); err != nil { - return nil, err - } - } - - return cursor, nil -} - -// LoadCUR loads a CUR file and returns a cursor, ready to embed in a resource set. -func LoadCUR(cur io.ReadSeeker) (*Cursor, error) { - hdr := cursorDirHeader{} - if err := binaryRead(cur, &hdr); err != nil { - return nil, err - } - - if hdr.Type != 2 || hdr.Reserved != 0 { - return nil, errors.New(errNotCUR) - } - - entries := make([]cursorFileDirEntry, hdr.Count) - if err := binaryRead(cur, entries); err != nil { - return nil, err - } - - cursor := &Cursor{} - for _, e := range entries { - // Arbitrary limit: no more than 10MB per image, so we can blindly allocate bytes and try to read them. - if e.BytesInRes > 0xA00000 { - return nil, errors.New(errImageLengthTooBig) - } - if _, err := cur.Seek(int64(e.ImageOffset), io.SeekStart); err != nil { - return nil, err - } - img := make([]byte, e.BytesInRes) - if err := readFull(cur, img); err != nil { - return nil, err - } - - planes, bitCount, err := readDIBBitCount(img) - if err != nil { - return nil, err - } - - cursor.images = append(cursor.images, cursorImage{ - info: cursorInfo{ - Width: uint16(e.Width-1) + 1, - Height: uint16(e.Height-1) + 1, - Planes: planes, - BitCount: bitCount, - BytesInRes: e.BytesInRes, - }, - hotSpot: HotSpot{ - X: e.XHotSpot, - Y: e.YHotSpot, - }, - image: img, - }) - } - - return cursor, nil -} - -// SaveCUR saves a cursor as a CUR file. -func (cursor *Cursor) SaveCUR(ico io.Writer) error { - err := binary.Write(ico, binary.LittleEndian, &cursorDirHeader{ - Type: 2, - Count: uint16(len(cursor.images)), - }) - if err != nil { - return err - } - - var ( - pos = sizeOfCursorDirHeader - offset = sizeOfCursorDirHeader + len(cursor.images)*sizeOfCursorFileDirEntry - ) - - cursor.order() - for i := range cursor.images { - err = binary.Write(ico, binary.LittleEndian, &cursorFileDirEntry{ - Width: uint8(cursor.images[i].info.Width), - Height: uint8(cursor.images[i].info.Height), - XHotSpot: cursor.images[i].hotSpot.X, - YHotSpot: cursor.images[i].hotSpot.Y, - BytesInRes: uint32(len(cursor.images[i].image)), - ImageOffset: uint32(offset), - }) - if err != nil { - return err - } - - offset += len(cursor.images[i].image) - pos += sizeOfCursorFileDirEntry - } - - for i := range cursor.images { - _, err = ico.Write(cursor.images[i].image) - if err != nil { - return err - } - } - - return nil -} - -// SetCursor adds the cursor to the resource set. -func (rs *ResourceSet) SetCursor(resID Identifier, cursor *Cursor) error { - return rs.SetCursorTranslation(resID, LCIDNeutral, cursor) -} - -// SetCursorTranslation adds the cursor to a specific language in the resource set. -func (rs *ResourceSet) SetCursorTranslation(resID Identifier, langID uint16, cursor *Cursor) error { - b := &bytes.Buffer{} - binary.Write(b, binary.LittleEndian, cursorDirHeader{ - Type: 2, - Count: uint16(len(cursor.images)), - }) - - cursor.order() - for _, img := range cursor.images { - id := rs.lastCursorID + 1 - - binary.Write(b, binary.LittleEndian, cursorResDirEntry{ - cursorInfo: img.info, - Id: id, - }) - - if err := rs.Set(RT_CURSOR, ID(id), LCIDNeutral, img.resData()); err != nil { - return err - } - } - return rs.Set(RT_GROUP_CURSOR, resID, langID, b.Bytes()) -} - -// GetCursor extracts a cursor from a resource set. -func (rs *ResourceSet) GetCursor(resID Identifier) (*Cursor, error) { - return rs.GetCursorTranslation(resID, rs.firstLang(RT_GROUP_CURSOR, resID)) -} - -// GetCursorTranslation extracts a cursor from a specific language of the resource set. -func (rs *ResourceSet) GetCursorTranslation(resID Identifier, langID uint16) (*Cursor, error) { - data := rs.Get(RT_GROUP_CURSOR, resID, langID) - if data == nil { - return nil, errors.New(errGroupNotFound) - } - - in := bytes.NewReader(data) - dir := cursorDirHeader{} - err := binaryRead(in, &dir) - if err != nil || dir.Type != 2 || dir.Reserved != 0 { - return nil, errors.New(errInvalidGroup) - } - - g := &Cursor{} - for i := 0; i < int(dir.Count); i++ { - entry := cursorResDirEntry{} - err := binaryRead(in, &entry) - if err != nil { - return nil, errors.New(errInvalidGroup) - } - img := rs.Get(RT_CURSOR, ID(entry.Id), rs.firstLang(RT_CURSOR, ID(entry.Id))) - if img == nil { - return nil, errors.New(errCursorMissing) - } - g.images = append(g.images, cursorImage{ - info: entry.cursorInfo, - image: img[4:], - hotSpot: HotSpot{ - X: uint16(img[1])<<8 | uint16(img[0]), - Y: uint16(img[3])<<8 | uint16(img[2]), - }, - }) - } - return g, nil -} - -type cursorDirHeader struct { - Reserved uint16 - Type uint16 - Count uint16 -} - -const sizeOfCursorDirHeader = 6 - -type cursorFileDirEntry struct { - Width uint8 - Height uint8 - Reserved uint16 - XHotSpot uint16 - YHotSpot uint16 - BytesInRes uint32 - ImageOffset uint32 -} - -const sizeOfCursorFileDirEntry = 16 - -type cursorResDirEntry struct { - cursorInfo - Id uint16 -} - -type cursorInfo struct { - Width uint16 - Height uint16 - Planes uint16 - BitCount uint16 - BytesInRes uint32 -} - -type cursorImage struct { - info cursorInfo - hotSpot HotSpot - image []byte -} - -func (ci *cursorImage) resData() []byte { - // As a resource, image data includes the hot spot - buf := bytes.NewBuffer(make([]byte, 0, len(ci.image)+4)) - binary.Write(buf, binary.LittleEndian, ci.hotSpot) - buf.Write(ci.image) - return buf.Bytes() -} - -// This makes a testing error reporting possible -var bmpEncode = bmp.Encode - -func (cursor *Cursor) addImage(img image.Image, hotSpot HotSpot) error { - bounds := img.Bounds() - if bounds.Empty() { - return errors.New(errInvalidImageDimensions) - } - if bounds.Size().X > 256 || bounds.Size().Y > 256 { - return errors.New(errImageTooBig) - } - - // PNG seems to be supported, and simpler. (no need to double the height or to skip a part of the header) - // But I've never seen PNG cursors and I would not take the risk. - // PNG icons, on the other hand, are very common. - buf := &bytes.Buffer{} - curImg := imageInSquareNRGBA(img, false) - if err := bmpEncode(buf, curImg); err != nil { - return err - } - - // 14 is the size of a BMPFILEHEADER, which we want to skip. - // A BMP file is a BMPFILEHEADER followed by a DIB. - dib := buf.Bytes()[14:] - - width, height := curImg.Bounds().Size().X, curImg.Bounds().Size().Y - // Height must be doubled in the DIB header, as if there was an AND mask for transparency. - // In a 32 bits DIB, the mask can be the alpha channel, therefore there is no AND mask. - dib[8] = byte(height << 1) - dib[9] = byte(height >> 7) - - cursor.images = append(cursor.images, cursorImage{ - info: cursorInfo{ - Width: uint16(width), - Height: uint16(height), - Planes: 1, - BitCount: 32, - BytesInRes: uint32(len(dib) + 4), // +4 for the hot spot - }, - hotSpot: hotSpot, - image: dib, - }) - - return nil -} - -func (cursor *Cursor) order() { - sort.SliceStable(cursor.images, func(i, j int) bool { - img1, img2 := &cursor.images[i].info, &cursor.images[j].info - return img1.BitCount > img2.BitCount || img1.BitCount == img2.BitCount && img1.Width > img2.Width - }) -} - -func readDIBBitCount(data []byte) (uint16, uint16, error) { - // Icons and cursor may contain PNG instead of DIB - _, s, _ := image.DecodeConfig(bytes.NewReader(data)) - if s == "png" { - return 1, 32, nil - } - - hdrSize := uint32(data[3])<<24 | uint32(data[2])<<16 | uint32(data[1])<<8 | uint32(data[0]) - - if hdrSize != 40 && hdrSize != 108 && hdrSize != 124 { - return 0, 0, errors.New(errUnknownImageFormat) - } - - var ( - planes = uint16(data[13])<<8 | uint16(data[12]) - bitCount = uint16(data[15])<<8 | uint16(data[14]) - ) - - return planes, bitCount, nil -} diff --git a/pkgs/winres/cursor_test.go b/pkgs/winres/cursor_test.go deleted file mode 100644 index b9cfc2c..0000000 --- a/pkgs/winres/cursor_test.go +++ /dev/null @@ -1,503 +0,0 @@ -package winres - -import ( - "bytes" - "errors" - "image" - "io" - "os" - "path/filepath" - "reflect" - "testing" -) - -func TestLoadCUR(t *testing.T) { - f, err := os.Open(filepath.Join(testDataDir, "cursor.cur")) - if err != nil { - t.Fatal("missing test data") - } - defer f.Close() - - cursor, err := LoadCUR(f) - if err != nil { - t.Fatal(err) - } - - cursor.order() - - expected := []cursorImage{ - { - info: cursorInfo{ - Width: 32, - Height: 32, - Planes: 1, - BitCount: 32, - BytesInRes: 0x10A8, - }, - hotSpot: HotSpot{14, 8}, - }, - { - info: cursorInfo{ - Width: 256, - Height: 256, - Planes: 1, - BitCount: 24, - BytesInRes: 0x32028, - }, - hotSpot: HotSpot{112, 64}, - }, - { - info: cursorInfo{ - Width: 32, - Height: 32, - Planes: 1, - BitCount: 8, - BytesInRes: 0x710, - }, - hotSpot: HotSpot{14, 8}, - }, - { - info: cursorInfo{ - Width: 32, - Height: 32, - Planes: 1, - BitCount: 4, - BytesInRes: 0x2E8, - }, - hotSpot: HotSpot{21, 10}, - }, - { - info: cursorInfo{ - Width: 32, - Height: 32, - Planes: 1, - BitCount: 1, - BytesInRes: 0x130, - }, - hotSpot: HotSpot{21, 10}, - }, - } - - for i := range expected { - if !reflect.DeepEqual(cursor.images[i].info, expected[i].info) { - t.Errorf("%s - image %d: expected %v got %v", t.Name(), i, expected[i].info, cursor.images[i].info) - } - if cursor.images[i].hotSpot != expected[i].hotSpot { - t.Errorf("%s - hotspot %d: expected %v got %v", t.Name(), i, expected[i].hotSpot, cursor.images[i].hotSpot) - } - } -} - -func TestLoadCUR_ErrEOF1(t *testing.T) { - cursor, err := LoadCUR(bytes.NewReader([]byte{0, 0, 2, 0, 1})) - if err != io.ErrUnexpectedEOF || cursor != nil { - t.Fail() - } -} - -func TestLoadCUR_ErrEOF2(t *testing.T) { - cursor, err := LoadCUR(bytes.NewReader([]byte{ - 0, 0, 2, 0, 0xFF, 0xFF, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - })) - if err != io.ErrUnexpectedEOF || cursor != nil { - t.Fail() - } -} - -func TestLoadCUR_ErrImageOffset(t *testing.T) { - temp, err := os.ReadFile(filepath.Join(testDataDir, "cursor.cur")) - if err != nil { - t.Fatal(err) - } - - temp[21] = 0x04 - cursor, err := LoadCUR(bytes.NewReader(temp)) - if err != io.ErrUnexpectedEOF || cursor != nil { - t.Fail() - } -} - -func TestLoadCUR_ErrNotCUR(t *testing.T) { - temp, err := os.ReadFile(filepath.Join(testDataDir, "cursor.cur")) - if err != nil { - t.Fatal(err) - } - - temp[0] = 1 - cursor, err := LoadCUR(bytes.NewReader(temp)) - if err == nil || cursor != nil || err.Error() != errNotCUR { - t.Fail() - } - - temp[0] = 0 - temp[2] = 1 - cursor, err = LoadCUR(bytes.NewReader(temp)) - if err == nil || cursor != nil || err.Error() != errNotCUR { - t.Fail() - } -} - -func TestLoadCUR_ErrNotDIB(t *testing.T) { - temp, err := os.ReadFile(filepath.Join(testDataDir, "cursor.cur")) - if err != nil { - t.Fatal(err) - } - - temp[0x56] = 41 - cursor, err := LoadCUR(bytes.NewReader(temp)) - if err == nil || cursor != nil || err.Error() != errUnknownImageFormat { - t.Fail() - } -} - -func TestLoadCUR_ErrSeek(t *testing.T) { - data, err := os.ReadFile(filepath.Join(testDataDir, "cursor.cur")) - if err != nil { - t.Fatal("missing test data") - } - - r := &badSeeker{br: bytes.NewReader(data)} - - cursor, err := LoadCUR(r) - if !isExpectedSeekErr(err) || cursor != nil { - t.Fatal("expected seek error, got", err) - } -} - -func TestLoadCUR_ImageLengthLimit(t *testing.T) { - _, err := LoadCUR(bytes.NewReader([]byte{ - 0, 0, 2, 0, 1, 0, 32, 32, 0, 0, 0, 0, 0, 0, - 0x00, 0x00, 0xA0, 0x00, // image data length = 0xA00000 - 0, 0, 0, 22, - })) - if err == nil || err.Error() == errImageLengthTooBig { - t.Fail() - } - - _, err = LoadCUR(bytes.NewReader([]byte{ - 0, 0, 2, 0, 1, 0, 32, 32, 0, 0, 0, 0, 0, 0, - 0x01, 0x00, 0xA0, 0x00, // image data length = 0xA00001 - 0, 0, 0, 22, - })) - if err == nil || err.Error() != errImageLengthTooBig { - t.Fail() - } -} - -func TestLoadCUR_PNG(t *testing.T) { - data, err := os.ReadFile(filepath.Join(testDataDir, "png.cur")) - if err != nil { - t.Fatal(err) - } - - cursor, err := LoadCUR(bytes.NewReader(data)) - if err != nil { - t.Fatal(err) - } - - if cursor.images[0].info.BitCount != 32 { - t.Fail() - } -} - -func TestCursor_SaveCUR(t *testing.T) { - data, err := os.ReadFile(filepath.Join(testDataDir, "cursor.cur")) - if err != nil { - t.Fatal(err) - } - - cursor, err := LoadCUR(bytes.NewReader(data)) - if err != nil { - t.Fatal(err) - } - - buf := &bytes.Buffer{} - err = cursor.SaveCUR(buf) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, buf.Bytes()) -} - -func TestCursor_SaveCUR_ErrWrite(t *testing.T) { - data, err := os.ReadFile(filepath.Join(testDataDir, "cursor.cur")) - if err != nil { - t.Fatal(err) - } - - cursor, err := LoadCUR(bytes.NewReader(data)) - if err != nil { - t.Fatal(err) - } - - w := newBadWriter(5) - err = cursor.SaveCUR(w) - if !isExpectedWriteErr(err) { - t.Fatal("expected write error, got", err) - } - - w = newBadWriter(20) - err = cursor.SaveCUR(w) - if !isExpectedWriteErr(err) { - t.Fatal("expected write error, got", err) - } - - w = newBadWriter(9190) - err = cursor.SaveCUR(w) - if !isExpectedWriteErr(err) { - t.Fatal("expected write error, got", err) - } -} - -func TestNewCursorFromImages(t *testing.T) { - cursor, err := NewCursorFromImages([]CursorImage{ - { - Image: loadImage(t, "cur-16x8.png"), - HotSpot: HotSpot{1, 2}, - }, - { - Image: shiftImage(loadImage(t, "cur-16x32.png"), -10, -100), - HotSpot: HotSpot{3, 4}, - }, - { - Image: shiftImage(loadImage(t, "cur-32x64.png"), 2, -1), - HotSpot: HotSpot{5, 6}, - }, - { - Image: loadImage(t, "cur-64x128.png"), - HotSpot: HotSpot{7, 8}, - }, - }) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, curToBinary(cursor)) -} - -func TestNewCursorFromImages_ErrDimensions(t *testing.T) { - _, err := NewCursorFromImages([]CursorImage{ - { - Image: image.NewNRGBA(image.Rectangle{}), - HotSpot: HotSpot{0, 0}, - }, - }) - if err == nil || err.Error() != errInvalidImageDimensions { - t.Fail() - } -} - -func TestNewCursorFromImages_ErrEncode(t *testing.T) { - enc := bmpEncode - defer func() { bmpEncode = enc }() - bmpEncode = func(w io.Writer, m image.Image) error { - return errors.New("oops") - } - - _, err := NewCursorFromImages([]CursorImage{ - { - Image: loadImage(t, "cur-32x64.png"), - HotSpot: HotSpot{5, 6}, - }, - { - Image: loadImage(t, "cur-64x128.png"), - HotSpot: HotSpot{7, 8}, - }, - }) - if err == nil || err.Error() != "oops" { - t.Fail() - } -} - -func TestNewCursorFromImages_ErrTooBig(t *testing.T) { - _, err := NewCursorFromImages([]CursorImage{ - { - Image: image.NewNRGBA(image.Rectangle{ - Max: image.Point{ - X: 257, - Y: 256, - }, - }), - HotSpot: HotSpot{1, 2}, - }, - }) - if err == nil || err.Error() != errImageTooBig { - t.Fail() - } - - _, err = NewCursorFromImages([]CursorImage{ - { - Image: image.NewNRGBA(image.Rectangle{ - Max: image.Point{ - X: 256, - Y: 257, - }, - }), - HotSpot: HotSpot{1, 2}, - }, - }) - if err == nil || err.Error() != errImageTooBig { - t.Fail() - } - - _, err = NewCursorFromImages([]CursorImage{ - { - Image: image.NewNRGBA(image.Rectangle{ - Max: image.Point{ - X: 256, - Y: 256, - }, - }), - HotSpot: HotSpot{1, 2}, - }, - }) - if err != nil { - t.Fail() - } -} - -func TestResourceSet_SetCursor(t *testing.T) { - rs := ResourceSet{} - - id := Name("CURSOR") - cursor := loadCURFile(t, "cursor.cur") - err := rs.SetCursor(id, cursor) - if err != nil { - t.Fatal(err) - } - if int(rs.lastCursorID) != len(cursor.images) { - t.Fail() - } - - checkCursorResource(t, &rs, id, 0, cursor) -} - -func TestResourceSet_SetCursor_IDOverflow(t *testing.T) { - rs := ResourceSet{} - rs.lastCursorID = 0xFFFD - - cursor := loadCURFile(t, "cursor.cur") - err := rs.SetCursor(Name("CURSOR"), cursor) - if err == nil || err.Error() != errZeroID { - t.Fail() - } -} - -func TestResourceSet_SetCursorTranslation(t *testing.T) { - rs := ResourceSet{} - - cursor1 := loadCURFile(t, "en.cur") - cursor2 := loadCURFile(t, "fr.cur") - cursor3, err := NewCursorFromImages([]CursorImage{ - { - Image: loadImage(t, "cur-16x8.png"), - HotSpot: HotSpot{1, 2}, - }, - }) - if err != nil { - t.Fatal(err) - } - - err = rs.SetCursorTranslation(Name("LOCALCUR"), 0x409, cursor1) - if err != nil { - t.Fatal(err) - } - err = rs.SetCursorTranslation(Name("ANOTHERCUR"), 0x409, cursor3) - if err != nil { - t.Fatal(err) - } - err = rs.SetCursorTranslation(Name("LOCALCUR"), 0x40C, cursor2) - if err != nil { - t.Fatal(err) - } - - checkCursorResource(t, &rs, Name("LOCALCUR"), 0x409, cursor1) - checkCursorResource(t, &rs, Name("LOCALCUR"), 0x40C, cursor2) - checkCursorResource(t, &rs, Name("ANOTHERCUR"), 0x409, cursor3) -} - -func TestResourceSet_GetCursorTranslation_Err(t *testing.T) { - rs := ResourceSet{} - - bin0 := []byte{0, 0, 2, 0, 1} - bin1 := []byte{0, 0, 1, 0, 1, 0, 32, 0, 32, 0, 1, 0, 32, 0, 42, 0, 0, 0, 1, 0} - bin2 := []byte{0, 0, 2, 0, 1, 0, 32, 0, 32, 0, 1, 0, 32, 0, 42, 0, 0, 0, 1} - bin3 := []byte{0, 0, 2, 0, 1, 0, 32, 0, 32, 0, 1, 0, 32, 0, 42, 0, 0, 0, 1, 0} - - err := rs.Set(RT_GROUP_CURSOR, Name("LOCALCUR"), 0, bin0) - if err != nil { - t.Fatal(err) - } - err = rs.Set(RT_GROUP_CURSOR, Name("LOCALCUR"), 0x401, bin1) - if err != nil { - t.Fatal(err) - } - err = rs.Set(RT_GROUP_CURSOR, Name("LOCALCUR"), 0x402, bin2) - if err != nil { - t.Fatal(err) - } - err = rs.Set(RT_GROUP_CURSOR, Name("LOCALCUR"), 0x403, bin3) - if err != nil { - t.Fatal(err) - } - - var cursor *Cursor - - cursor, err = rs.GetCursor(ID(0)) - if err == nil || cursor != nil || err.Error() != errGroupNotFound { - t.Fail() - } - cursor, err = rs.GetCursorTranslation(Name("LOCALCU"), 0x401) - if err == nil || cursor != nil || err.Error() != errGroupNotFound { - t.Fail() - } - cursor, err = rs.GetCursorTranslation(Name("LOCALCUR"), 0x409) - if err == nil || cursor != nil || err.Error() != errGroupNotFound { - t.Fail() - } - - cursor, err = rs.GetCursorTranslation(Name("LOCALCUR"), 0) - if err == nil || cursor != nil || err.Error() != errInvalidGroup { - t.Fail() - } - cursor, err = rs.GetCursorTranslation(Name("LOCALCUR"), 0x401) - if err == nil || cursor != nil || err.Error() != errInvalidGroup { - t.Fail() - } - cursor, err = rs.GetCursorTranslation(Name("LOCALCUR"), 0x402) - if err == nil || cursor != nil || err.Error() != errInvalidGroup { - t.Fail() - } - cursor, err = rs.GetCursorTranslation(Name("LOCALCUR"), 0x403) - if err == nil || cursor != nil || err.Error() != errCursorMissing { - t.Fail() - } -} - -func checkCursorResource(t *testing.T, rs *ResourceSet, ident Identifier, langID uint16, source *Cursor) { - cursor, err := rs.GetCursorTranslation(ident, langID) - if err != nil { - t.Fatal(err) - } - buf1, buf2 := &bytes.Buffer{}, &bytes.Buffer{} - err = source.SaveCUR(buf1) - if err != nil { - t.Fatal(err) - } - err = cursor.SaveCUR(buf2) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(buf1.Bytes(), buf2.Bytes()) { - t.Fail() - } -} - -func curToBinary(cursor *Cursor) []byte { - buf := &bytes.Buffer{} - cursor.SaveCUR(buf) - return buf.Bytes() -} diff --git a/pkgs/winres/errors.go b/pkgs/winres/errors.go deleted file mode 100644 index 7f501c9..0000000 --- a/pkgs/winres/errors.go +++ /dev/null @@ -1,47 +0,0 @@ -package winres - -import "errors" - -const ( - errZeroID = "ordinal identifier must not be zero" - errEmptyName = "string identifier must not be empty" - errNameContainsNUL = "string identifier must not contain NUL char" - - errUnknownArch = "unknown architecture" - - errNotICO = "not a valid ICO file" - errImageLengthTooBig = "image size found in ICONDIRENTRY is too big (above 10 MB)" - errTooManyIconSizes = "too many sizes" - errGroupNotFound = "group does not exist" - errInvalidGroup = "invalid group" - errIconMissing = "icon missing from group" - errCursorMissing = "cursor missing from group" - errInvalidImageDimensions = "invalid image dimensions" - errImageTooBig = "image size too big, must fit in 256x256" - errNotCUR = "not a valid CUR file" - errUnknownImageFormat = "unknown image format" - - errInvalidResDir = "invalid resource directory" - errDataEntryOutOfBounds = "data entry out of bounds" - - errNotPEImage = "not a valid PE image" - errSignedPE = "cannot modify a signed PE image" - errUnknownPE = "unknown PE format" - errNoRSRC = "image doesn't have a resource directory" // This is when the data directory entry is zero - errRSRCNotFound = "resource section not found" // This is when the data directory entry is not zero - errSectionTooFar = "invalid section header points too far" - errNoRoomForRSRC = "not enough room to add .rsrc section header" - errRSRCTwice = "found resource section twice" - errRelocTwice = "found reloc section twice" - - errInvalidVersion = "invalid version number" - errUnknownSupportedOS = "unknown minimum-os value" - errUnknownDPIAwareness = "unknown dpi-awareness value" - errUnknownExecLevel = "unknown execution-level value" -) - -// ErrNoResources is the error returned by LoadFromEXE when it didn't find a .rsrc section. -var ErrNoResources = errors.New(errNoRSRC) - -// ErrSignedPE is the error returned by WriteToEXE when it refused to touch signed code. (Authenticode) -var ErrSignedPE = errors.New(errSignedPE) diff --git a/pkgs/winres/exe.go b/pkgs/winres/exe.go deleted file mode 100644 index ef05518..0000000 --- a/pkgs/winres/exe.go +++ /dev/null @@ -1,684 +0,0 @@ -package winres - -import ( - "bytes" - "debug/pe" - "encoding/binary" - "errors" - "io" - "os" -) - -type authenticodeHandling int - -const ( - // ErrorIfSigned means winres won't patch a signed executable. - ErrorIfSigned authenticodeHandling = 0 - // RemoveSignature means winres will patch a signed executable, - // and remove the signature. - RemoveSignature authenticodeHandling = 1 - // IgnoreSignature means winres will patch a signed executable, - // and leave the now invalid signature as is. - IgnoreSignature authenticodeHandling = 2 -) - -type exeOptions struct { - forceCheckSum bool - authenticodeHandling authenticodeHandling -} - -type exeOption func(opt *exeOptions) - -// ForceCheckSum forces updating the PE checksum, even when the original file didn't have one -func ForceCheckSum() exeOption { - return func(opt *exeOptions) { - opt.forceCheckSum = true - } -} - -// WithAuthenticode allows patching signed executables, either by removing the signature or by ignoring it (and making it wrong) -func WithAuthenticode(handling authenticodeHandling) exeOption { - return func(opt *exeOptions) { - opt.authenticodeHandling = handling - } -} - -type peHeaders struct { - file pe.FileHeader - opt peOptionalHeader - dirs []pe.DataDirectory - sections []pe.SectionHeader32 - stubLength int64 - length int64 - hasChecksum bool -} - -const sizeOfSectionHeader = 40 - -type peOptionalHeader interface { - getSizeOfInitializedData() uint32 - getSectionAlignment() uint32 - getFileAlignment() uint32 - getNumberOfRvaAndSizes() uint32 - getCheckSum() uint32 - - setSizeOfInitializedData(uint32) - setSizeOfImage(uint32) - setCheckSum(uint32) -} - -type peOptionalHeader32 struct { - Magic uint16 - MajorLinkerVersion uint8 - MinorLinkerVersion uint8 - SizeOfCode uint32 - SizeOfInitializedData uint32 - SizeOfUninitializedData uint32 - AddressOfEntryPoint uint32 - BaseOfCode uint32 - BaseOfData uint32 - ImageBase uint32 - SectionAlignment uint32 - FileAlignment uint32 - MajorOperatingSystemVersion uint16 - MinorOperatingSystemVersion uint16 - MajorImageVersion uint16 - MinorImageVersion uint16 - MajorSubsystemVersion uint16 - MinorSubsystemVersion uint16 - Win32VersionValue uint32 - SizeOfImage uint32 - SizeOfHeaders uint32 - CheckSum uint32 - Subsystem uint16 - DllCharacteristics uint16 - SizeOfStackReserve uint32 - SizeOfStackCommit uint32 - SizeOfHeapReserve uint32 - SizeOfHeapCommit uint32 - LoaderFlags uint32 - NumberOfRvaAndSizes uint32 -} - -func (h *peOptionalHeader32) getSizeOfInitializedData() uint32 { - return h.SizeOfInitializedData -} - -func (h *peOptionalHeader32) getSectionAlignment() uint32 { - return h.SectionAlignment -} - -func (h *peOptionalHeader32) getFileAlignment() uint32 { - return h.FileAlignment -} - -func (h *peOptionalHeader32) getNumberOfRvaAndSizes() uint32 { - return h.NumberOfRvaAndSizes -} - -func (h *peOptionalHeader32) getCheckSum() uint32 { - return h.CheckSum -} - -func (h *peOptionalHeader32) setSizeOfInitializedData(s uint32) { - h.SizeOfInitializedData = s -} - -func (h *peOptionalHeader32) setSizeOfImage(s uint32) { - h.SizeOfImage = s -} - -func (h *peOptionalHeader32) setCheckSum(c uint32) { - h.CheckSum = c -} - -type peOptionalHeader64 struct { - Magic uint16 - MajorLinkerVersion uint8 - MinorLinkerVersion uint8 - SizeOfCode uint32 - SizeOfInitializedData uint32 - SizeOfUninitializedData uint32 - AddressOfEntryPoint uint32 - BaseOfCode uint32 - ImageBase uint64 - SectionAlignment uint32 - FileAlignment uint32 - MajorOperatingSystemVersion uint16 - MinorOperatingSystemVersion uint16 - MajorImageVersion uint16 - MinorImageVersion uint16 - MajorSubsystemVersion uint16 - MinorSubsystemVersion uint16 - Win32VersionValue uint32 - SizeOfImage uint32 - SizeOfHeaders uint32 - CheckSum uint32 - Subsystem uint16 - DllCharacteristics uint16 - SizeOfStackReserve uint64 - SizeOfStackCommit uint64 - SizeOfHeapReserve uint64 - SizeOfHeapCommit uint64 - LoaderFlags uint32 - NumberOfRvaAndSizes uint32 -} - -func (h *peOptionalHeader64) getSizeOfInitializedData() uint32 { - return h.SizeOfInitializedData -} - -func (h *peOptionalHeader64) getSectionAlignment() uint32 { - return h.SectionAlignment -} - -func (h *peOptionalHeader64) getFileAlignment() uint32 { - return h.FileAlignment -} - -func (h *peOptionalHeader64) getNumberOfRvaAndSizes() uint32 { - return h.NumberOfRvaAndSizes -} - -func (h *peOptionalHeader64) getCheckSum() uint32 { - return h.CheckSum -} - -func (h *peOptionalHeader64) setSizeOfInitializedData(s uint32) { - h.SizeOfInitializedData = s -} - -func (h *peOptionalHeader64) setSizeOfImage(s uint32) { - h.SizeOfImage = s -} - -func (h *peOptionalHeader64) setCheckSum(c uint32) { - h.CheckSum = c -} - -func extractRSRCSection(r io.ReadSeeker) ([]byte, uint32, error) { - r.Seek(0, io.SeekStart) - - fileSize := getSeekerSize(r) - - h, err := readPEHeaders(r) - if err != nil { - return nil, 0, err - } - - if h.dirs[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress == 0 && h.dirs[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE].Size == 0 { - return nil, 0, ErrNoResources - } - - var sec *pe.SectionHeader32 - for i := range h.sections { - if h.sections[i].VirtualAddress == h.dirs[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress { - sec = &h.sections[i] - break - } - } - if sec == nil { - return nil, 0, errors.New(errRSRCNotFound) - } - - if int64(sec.PointerToRawData)+int64(sec.SizeOfRawData) > fileSize { - return nil, 0, errors.New(errSectionTooFar) - } - data := make([]byte, sec.SizeOfRawData) - - r.Seek(int64(sec.PointerToRawData), io.SeekStart) - err = readFull(r, data) - if err != nil { - return nil, 0, err - } - - return data, sec.VirtualAddress, nil -} - -type peWriter struct { - h *peHeaders - rsrcData []byte - rsrcHdr *pe.SectionHeader32 - relocHdr *pe.SectionHeader32 - src struct { - r io.ReadSeeker - fileSize int64 - sigSize int64 // size of a code signature we'd want to skip (only if it is at the end of the file) - dataOffset uint32 - dataEnd uint32 - virtEnd uint32 - rsrcEnd int64 - } -} - -func replaceRSRCSection(dst io.Writer, src io.ReadSeeker, rsrcData []byte, reloc []int, options exeOptions) error { - src.Seek(0, io.SeekStart) - - pew, err := preparePEWriter(src, rsrcData, options.authenticodeHandling) - if err != nil { - return err - } - - pew.applyReloc(reloc) - - if options.forceCheckSum || pew.h.hasChecksum { - c := peCheckSum{} - pew.writeEXE(&c) - pew.h.opt.setCheckSum(c.Sum()) - } - - return pew.writeEXE(dst) -} - -func preparePEWriter(src io.ReadSeeker, rsrcData []byte, sigHandling authenticodeHandling) (*peWriter, error) { - var ( - pew peWriter - err error - ) - - pew.src.r = src - pew.rsrcData = rsrcData - - pew.src.fileSize = getSeekerSize(src) - - pew.h, err = readPEHeaders(src) - if err != nil { - return nil, err - } - - if len(pew.h.dirs) > pe.IMAGE_DIRECTORY_ENTRY_SECURITY && pew.h.dirs[pe.IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress > 0 { - switch sigHandling { - case RemoveSignature: - entry := pew.h.dirs[pe.IMAGE_DIRECTORY_ENTRY_SECURITY] - pew.h.dirs[pe.IMAGE_DIRECTORY_ENTRY_SECURITY] = pe.DataDirectory{} - // The certificate entry actually contains a raw data offset, not a virtual address. - // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#the-attribute-certificate-table-image-only - if int64(entry.VirtualAddress)+int64(entry.Size) == pew.src.fileSize { - pew.src.sigSize = pew.src.fileSize - int64(entry.VirtualAddress) - } - case IgnoreSignature: - default: - return nil, ErrSignedPE - } - } - - err = pew.fillSectionsInfo() - if err != nil { - return nil, err - } - - if pew.src.fileSize < int64(pew.src.dataEnd) { - return nil, io.ErrUnexpectedEOF - } - if pew.rsrcHdr == nil && (int64(pew.src.dataOffset) < pew.h.length+sizeOfSectionHeader || pew.h.file.NumberOfSections == 0xFFFF) { - return nil, errors.New(errNoRoomForRSRC) - } - - if pew.requiresNewSection() { - // Play it safe, abandon the existing .rsrc section and create a new one. - pew.rsrcHdr.Name = [8]byte{'o', 'l', 'd', '.', 'r', 's', 'r', 'c'} - pew.rsrcHdr = nil - } - - pew.updateHeaders() - - return &pew, nil -} - -func (pew *peWriter) fillSectionsInfo() error { - pew.src.dataOffset = 0xFFFFFFFF - pew.src.rsrcEnd = pew.src.fileSize - - for i := range pew.h.sections { - if pew.h.sections[i].VirtualAddress == pew.h.dirs[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress { - if pew.rsrcHdr != nil { - return errors.New(errRSRCTwice) - } - pew.rsrcHdr = &pew.h.sections[i] - pew.src.rsrcEnd = int64(pew.roundRaw(pew.rsrcHdr.PointerToRawData + pew.rsrcHdr.SizeOfRawData)) - } - if pew.h.sections[i].VirtualAddress == pew.h.dirs[pe.IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress { - if pew.relocHdr != nil { - return errors.New(errRelocTwice) - } - pew.relocHdr = &pew.h.sections[i] - } - } - - for i := range pew.h.sections { - if pew.h.sections[i].PointerToRawData < pew.src.dataOffset { - pew.src.dataOffset = pew.h.sections[i].PointerToRawData - } - if pew.h.sections[i].PointerToRawData+pew.h.sections[i].SizeOfRawData > pew.src.dataEnd { - pew.src.dataEnd = pew.h.sections[i].PointerToRawData + pew.h.sections[i].SizeOfRawData - } - if pew.h.sections[i].VirtualAddress+pew.h.sections[i].VirtualSize > pew.src.virtEnd { - pew.src.virtEnd = pew.h.sections[i].VirtualAddress + pew.h.sections[i].VirtualSize - } - } - pew.src.virtEnd = pew.roundVirt(pew.src.virtEnd) - - return nil -} - -func (pew *peWriter) requiresNewSection() bool { - if pew.rsrcHdr == nil { - return false - } - endOfRSRC := pew.roundVirt(pew.rsrcHdr.VirtualAddress + pew.rsrcHdr.VirtualSize) - if endOfRSRC >= pew.src.virtEnd { - return false - } - if pew.relocHdr.VirtualAddress == endOfRSRC && - pew.roundVirt(pew.relocHdr.VirtualAddress+pew.relocHdr.VirtualSize) >= pew.src.virtEnd { - return false - } - - // From here, we should not shift data after the existing .rsrc section - if pew.rsrcHdr.SizeOfRawData >= uint32(len(pew.rsrcData)) { - // The .rsrc section won't grow, so we only have to ensure it won't shrink too much either - buf := make([]byte, pew.rsrcHdr.SizeOfRawData) - copy(buf, pew.rsrcData) - pew.rsrcData = buf - return false - } - - return true -} - -func (pew *peWriter) updateHeaders() { - var ( - rsrcLen = uint32(len(pew.rsrcData)) - lastSection *pe.SectionHeader32 - oldSize uint32 - virtDelta uint32 - ) - - if pew.rsrcHdr == nil { - // Add .rsrc section - pew.h.sections = append(pew.h.sections, pe.SectionHeader32{ - Name: [8]uint8{'.', 'r', 's', 'r', 'c'}, - VirtualSize: rsrcLen, - VirtualAddress: pew.roundVirt(pew.src.virtEnd), - SizeOfRawData: pew.roundRaw(uint32(len(pew.rsrcData))), - PointerToRawData: pew.roundRaw(pew.src.dataEnd), - Characteristics: _IMAGE_SCN_MEM_READ | _IMAGE_SCN_CNT_INITIALIZED_DATA, - }) - pew.rsrcHdr = &pew.h.sections[len(pew.h.sections)-1] - pew.h.file.NumberOfSections++ - pew.h.length += sizeOfSectionHeader - lastSection = pew.rsrcHdr - pew.h.opt.setSizeOfInitializedData(pew.h.opt.getSizeOfInitializedData() + rsrcLen) - } else { - oldSize = pew.rsrcHdr.SizeOfRawData - virtDelta = pew.roundVirt(rsrcLen) - pew.roundVirt(pew.rsrcHdr.VirtualSize) - rawDelta := pew.roundRaw(rsrcLen) - pew.roundRaw(pew.rsrcHdr.SizeOfRawData) - pew.rsrcHdr.VirtualSize = rsrcLen - pew.rsrcHdr.SizeOfRawData = pew.roundRaw(rsrcLen) - lastSection = pew.rsrcHdr - for i := range pew.h.sections { - if pew.h.sections[i].VirtualAddress > pew.rsrcHdr.VirtualAddress { - pew.h.sections[i].VirtualAddress += virtDelta - if pew.h.sections[i].VirtualAddress > lastSection.VirtualAddress { - lastSection = &pew.h.sections[i] - } - } - if pew.h.sections[i].PointerToRawData > pew.rsrcHdr.PointerToRawData { - pew.h.sections[i].PointerToRawData += rawDelta - } - } - } - - if pew.h.dirs[pe.IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress >= pew.src.dataEnd { - pew.h.dirs[pe.IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress += lastSection.PointerToRawData + lastSection.SizeOfRawData - pew.src.dataEnd - } - - pew.h.dirs[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress = pew.rsrcHdr.VirtualAddress - pew.h.dirs[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE].Size = rsrcLen - for i := range pew.h.dirs { - if i != pe.IMAGE_DIRECTORY_ENTRY_SECURITY && pew.h.dirs[i].VirtualAddress > pew.rsrcHdr.VirtualAddress { - pew.h.dirs[i].VirtualAddress += virtDelta - } - } - - pew.h.opt.setCheckSum(0) - pew.h.opt.setSizeOfImage(lastSection.VirtualAddress + pew.roundVirt(lastSection.VirtualSize)) - pew.h.opt.setSizeOfInitializedData(pew.h.opt.getSizeOfInitializedData() - oldSize + pew.rsrcHdr.SizeOfRawData) -} - -func (pew *peWriter) roundRaw(p uint32) uint32 { - a := pew.h.opt.getFileAlignment() - x := p + a - 1 - return x - x%a -} - -func (pew *peWriter) roundVirt(p uint32) uint32 { - a := pew.h.opt.getSectionAlignment() - x := p + a - 1 - return x - x%a -} - -func (pew *peWriter) applyReloc(reloc []int) { - for _, o := range reloc { - addr := uint32(pew.rsrcData[o+3])<<24 | - uint32(pew.rsrcData[o+2])<<16 | - uint32(pew.rsrcData[o+1])<<8 | - uint32(pew.rsrcData[o]) - addr += pew.rsrcHdr.VirtualAddress - pew.rsrcData[o+3] = uint8(addr >> 24) - pew.rsrcData[o+2] = uint8(addr >> 16) - pew.rsrcData[o+1] = uint8(addr >> 8) - pew.rsrcData[o] = uint8(addr) - } -} - -func (pew *peWriter) writeEXE(w io.Writer) error { - var err error - - _, err = pew.src.r.Seek(0, io.SeekStart) - if err != nil { - return err - } - - // MS-DOS Stub + PE signature - _, err = io.CopyN(w, pew.src.r, pew.h.stubLength+4) - if err != nil { - return err - } - - // Headers - err = binary.Write(w, binary.LittleEndian, &pew.h.file) - if err != nil { - return err - } - err = binary.Write(w, binary.LittleEndian, pew.h.opt) - if err != nil { - return err - } - err = binary.Write(w, binary.LittleEndian, pew.h.dirs) - if err != nil { - return err - } - err = binary.Write(w, binary.LittleEndian, pew.h.sections) - if err != nil { - return err - } - err = writeBlank(w, int64(pew.src.dataOffset)-pew.h.length) - if err != nil { - return err - } - _, err = pew.src.r.Seek(int64(pew.src.dataOffset), io.SeekStart) - if err != nil { - return err - } - - // Sections before .rsrc - end := int64(pew.src.dataEnd) - if int64(pew.rsrcHdr.PointerToRawData) < end { - end = int64(pew.rsrcHdr.PointerToRawData) - } - _, err = io.CopyN(w, pew.src.r, end-int64(pew.src.dataOffset)) - if err != nil { - return err - } - err = writeBlank(w, int64(pew.rsrcHdr.PointerToRawData)-end) - if err != nil { - return err - } - - // .rsrc - _, err = w.Write(pew.rsrcData) - if err != nil { - return err - } - err = writeBlank(w, int64(pew.rsrcHdr.SizeOfRawData)-int64(len(pew.rsrcData))) - if err != nil { - return err - } - - // Remainder - _, err = pew.src.r.Seek(pew.src.rsrcEnd, io.SeekStart) - if err != nil { - return err - } - _, err = io.CopyN(w, pew.src.r, pew.src.fileSize-pew.src.sigSize-pew.src.rsrcEnd) - if err != nil { - return err - } - - return nil -} - -func writeBlank(w io.Writer, length int64) error { - if length <= 0 { - return nil - } - - if ws, ok := w.(io.WriteSeeker); ok { - ws.Seek(length-1, io.SeekCurrent) - var b [1]byte - _, err := w.Write(b[:]) - if err != nil { - return err - } - return nil - } - - const bufLen = 0x100 - var b [bufLen]byte - for length > 0 { - l := length - if l > bufLen { - l = bufLen - } - _, err := w.Write(b[:l]) - if err != nil { - return err - } - length -= l - } - return nil -} - -func readPEOffset(r io.Reader) (int64, error) { - stubHead := make([]byte, 0x40) - - err := readFull(r, stubHead) - if err != nil { - return 0, err - } - - if string(stubHead[:2]) != "MZ" { - return 0, errors.New(errNotPEImage) - } - - return int64(stubHead[0x3F])<<24 | int64(stubHead[0x3E])<<16 | int64(stubHead[0x3D])<<8 | int64(stubHead[0x3C]), nil -} - -func readPEHeaders(r io.ReadSeeker) (*peHeaders, error) { - var ( - h peHeaders - err error - ) - - h.stubLength, err = readPEOffset(r) - if err != nil { - return nil, err - } - r.Seek(h.stubLength, io.SeekStart) - - var sig [4]byte - err = readFull(r, sig[:]) - if err != nil { - return nil, err - } - if sig != [4]byte{'P', 'E'} { - return nil, errors.New(errNotPEImage) - } - - err = binaryRead(r, &h.file) - if err != nil { - return nil, err - } - - optHdr := make([]byte, h.file.SizeOfOptionalHeader) - err = readFull(r, optHdr) - if err != nil { - return nil, err - } - if optHdr[0] != 11 { - return nil, errors.New(errUnknownPE) - } - switch optHdr[1] { - case 1: - h.opt = &peOptionalHeader32{} - case 2: - h.opt = &peOptionalHeader64{} - default: - return nil, errors.New(errUnknownPE) - } - optRead := bytes.NewReader(optHdr) - err = binaryRead(optRead, h.opt) - if err != nil { - return nil, err - } - numDirs := int(h.opt.getNumberOfRvaAndSizes()) - if numDirs < 6 { - return nil, errors.New(errUnknownPE) - } - if int(h.file.SizeOfOptionalHeader) != binary.Size(h.opt)+numDirs*8 { - return nil, errors.New(errNotPEImage) - } - - h.dirs = make([]pe.DataDirectory, numDirs) - binaryRead(optRead, &h.dirs) - - h.sections = make([]pe.SectionHeader32, h.file.NumberOfSections, h.file.NumberOfSections+1) - err = binaryRead(r, &h.sections) - if err != nil { - return nil, err - } - - h.hasChecksum = h.opt.getCheckSum() != 0 - h.length, err = r.Seek(0, io.SeekCurrent) - if err != nil { - return nil, err - } - return &h, nil -} - -func getSeekerSize(r io.ReadSeeker) int64 { - switch r := r.(type) { - case *os.File: - stat, err := r.Stat() - if err == nil { - return stat.Size() - } - case interface{ Size() int64 }: - return r.Size() - } - pos, _ := r.Seek(0, io.SeekCurrent) - size, _ := r.Seek(0, io.SeekEnd) - r.Seek(pos, io.SeekStart) - return size -} diff --git a/pkgs/winres/helper_test.go b/pkgs/winres/helper_test.go deleted file mode 100644 index 0fcbf4f..0000000 --- a/pkgs/winres/helper_test.go +++ /dev/null @@ -1,234 +0,0 @@ -package winres - -import ( - "bytes" - "errors" - "image" - "image/png" - "io" - "os" - "path/filepath" - "testing" -) - -const testDataDir = "testdata" - -func checkResourceSet(t *testing.T, rs *ResourceSet, arch Arch) { - buf := &bytes.Buffer{} - if err := rs.WriteObject(buf, arch); err != nil { - t.Fatal(err) - } - checkBinary(t, buf.Bytes()) -} - -func golden(t *testing.T) string { - return filepath.Join(testDataDir, t.Name()+".golden") -} - -func checkBinary(t *testing.T, data []byte) { - refFile := golden(t) - ref, _ := os.ReadFile(refFile) - - if !bytes.Equal(ref, data) { - t.Error(t.Name() + " output is different") - bugFile := refFile[:len(refFile)-7] + ".bug" - err := os.WriteFile(bugFile, data, 0666) - if err != nil { - t.Error(err) - return - } - t.Log("dumped output to", bugFile) - } -} - -func loadBinary(t *testing.T, filename string) []byte { - data, err := os.ReadFile(filepath.Join(testDataDir, filename)) - if err != nil { - t.Fatal(err) - } - return data -} - -func loadPNGFileAsIcon(t *testing.T, name string, sizes []int) *Icon { - f, err := os.Open(filepath.Join(testDataDir, name)) - if err != nil { - t.Fatal(err) - } - img, err := png.Decode(f) - if err != nil { - t.Fatal(err) - } - icon, err := NewIconFromResizedImage(img, sizes) - if err != nil { - t.Fatal(err) - } - return icon -} - -func loadPNGFileAsCursor(t *testing.T, name string, spotX, spotY uint16) *Cursor { - f, err := os.Open(filepath.Join(testDataDir, name)) - if err != nil { - t.Fatal(err) - } - img, err := png.Decode(f) - if err != nil { - t.Fatal(err) - } - cursor, err := NewCursorFromImages([]CursorImage{{img, HotSpot{spotX, spotY}}}) - if err != nil { - t.Fatal(err) - } - return cursor -} - -func loadICOFile(t *testing.T, name string) *Icon { - f, err := os.Open(filepath.Join(testDataDir, name)) - if err != nil { - t.Fatal(err) - } - icon, err := LoadICO(f) - if err != nil { - t.Fatal(err) - } - return icon -} - -func loadCURFile(t *testing.T, name string) *Cursor { - f, err := os.Open(filepath.Join(testDataDir, name)) - if err != nil { - t.Fatal(err) - } - cursor, err := LoadCUR(f) - if err != nil { - t.Fatal(err) - } - return cursor -} - -func loadImage(t *testing.T, name string) image.Image { - f, err := os.Open(filepath.Join(testDataDir, name)) - if err != nil { - t.Fatal(err) - } - - img, _, err := image.Decode(f) - if err != nil { - t.Fatal(err) - } - - return img -} - -func shiftImage(img image.Image, x, y int) image.Image { - shifted := image.NewNRGBA(image.Rectangle{ - Min: image.Point{ - X: img.Bounds().Min.X + x, - Y: img.Bounds().Min.Y + y, - }, - Max: image.Point{ - X: img.Bounds().Max.X + x, - Y: img.Bounds().Max.Y + y, - }, - }) - - for srcY := img.Bounds().Min.Y; srcY < img.Bounds().Max.Y; srcY++ { - for srcX := img.Bounds().Min.X; srcX < img.Bounds().Max.X; srcX++ { - shifted.Set(srcX+x, srcY+y, img.At(srcX, srcY)) - } - } - - return shifted -} - -type badReader struct { - br *bytes.Reader - errPos int64 - returned bool -} - -type badSeeker struct { - br *bytes.Reader - errIter int - returned bool -} - -type badWriter struct { - badLen int - returned bool -} - -func newBadWriter(badLen int) *badWriter { - return &badWriter{badLen: badLen} -} - -const ( - errRead = "expected read error" - errReadOn = "reading on after error" - errSeek = "expected seek error" - errSeekOn = "seeking on after error" - errWrite = "expected write error" - errWriteOn = "writing on after error" -) - -func (r *badReader) Read(b []byte) (int, error) { - if r.returned { - return 0, errors.New(errReadOn) - } - p, _ := r.br.Seek(0, io.SeekCurrent) - if p <= r.errPos && r.errPos < p+int64(len(b)) { - n, _ := r.br.Read(b[:r.errPos-p]) - r.returned = true - return n, errors.New(errRead) - } - return r.br.Read(b) -} - -func (r *badReader) Seek(offset int64, whence int) (int64, error) { - if r.returned { - return 0, errors.New(errSeekOn) - } - return r.br.Seek(offset, whence) -} - -func isExpectedReadErr(err error) bool { - return err != nil && err.Error() == errRead -} - -func (s *badSeeker) Read(b []byte) (int, error) { - if s.returned { - return 0, errors.New(errReadOn) - } - return s.br.Read(b) -} - -func (s *badSeeker) Seek(offset int64, whence int) (int64, error) { - if s.returned { - return 0, errors.New(errSeekOn) - } - if s.errIter <= 0 { - s.returned = true - return 0, errors.New(errSeek) - } - s.errIter-- - return s.br.Seek(offset, whence) -} - -func isExpectedSeekErr(err error) bool { - return err != nil && err.Error() == errSeek -} - -func (w *badWriter) Write(b []byte) (n int, err error) { - if w.returned { - return 0, errors.New(errWriteOn) - } - w.badLen -= len(b) - if w.badLen <= 0 { - w.returned = true - return 0, errors.New(errWrite) - } - return len(b), nil -} - -func isExpectedWriteErr(err error) bool { - return err != nil && err.Error() == errWrite -} diff --git a/pkgs/winres/icon.go b/pkgs/winres/icon.go deleted file mode 100644 index fb634d3..0000000 --- a/pkgs/winres/icon.go +++ /dev/null @@ -1,356 +0,0 @@ -package winres - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "image" - "image/color" - "image/png" - "io" - "sort" - - "github.com/nfnt/resize" -) - -// Icon describes a Windows icon. -// -// This structure must only be created by constructors: -// NewIconFromImages, NewIconFromResizedImage, LoadICO -type Icon struct { - images []iconImage -} - -var DefaultIconSizes = []int{256, 64, 48, 32, 16} - -// NewIconFromImages makes an icon from a list of images. -// -// This converts every image to 32bpp PNG. -func NewIconFromImages(images []image.Image) (*Icon, error) { - icon := Icon{} - - for _, img := range images { - if err := icon.addImage(img); err != nil { - return nil, err - } - } - - return &icon, nil -} - -// NewIconFromResizedImage makes an icon from a single Image by resizing it. -// -// If sizes is nil, the icon will be resized to: 256px, 64px, 48px, 32px, 16px. -func NewIconFromResizedImage(img image.Image, sizes []int) (*Icon, error) { - if sizes == nil { - sizes = DefaultIconSizes - } - if len(sizes) > 30 { - return nil, errors.New(errTooManyIconSizes) - } - - icon := Icon{} - for _, s := range sizes { - if err := icon.addImage(resizeImage(img, s)); err != nil { - return nil, err - } - } - - return &icon, nil -} - -// LoadICO loads an ICO file and returns an icon, ready to embed in a resource set. -func LoadICO(ico io.ReadSeeker) (*Icon, error) { - hdr := iconDirHeader{} - if err := binaryRead(ico, &hdr); err != nil { - return nil, err - } - - if hdr.Type != 1 || hdr.Reserved != 0 { - return nil, errors.New(errNotICO) - } - - entries := make([]iconFileDirEntry, hdr.Count) - if err := binaryRead(ico, entries); err != nil { - return nil, err - } - - icon := &Icon{} - for _, e := range entries { - // Arbitrary limit: no more than 10MB per image, so we can blindly allocate bytes and try to read them. - if e.BytesInRes > 0xA00000 { - return nil, fmt.Errorf(errImageLengthTooBig) - } - if _, err := ico.Seek(int64(e.ImageOffset), io.SeekStart); err != nil { - return nil, err - } - img := make([]byte, e.BytesInRes) - if err := readFull(ico, img); err != nil { - return nil, err - } - icon.images = append(icon.images, iconImage{ - info: e.iconInfo, - image: img, - }) - } - - return icon, nil -} - -// SaveICO saves an icon as an ICO file. -func (icon *Icon) SaveICO(ico io.Writer) error { - err := binary.Write(ico, binary.LittleEndian, &iconDirHeader{ - Type: 1, - Count: uint16(len(icon.images)), - }) - if err != nil { - return err - } - - var ( - pos = sizeOfIconDirHeader - hdrLen = sizeOfIconDirHeader + len(icon.images)*sizeOfIconFileDirEntry - offset = hdrLen - ) - - icon.order() - for i := range icon.images { - err = binary.Write(ico, binary.LittleEndian, &iconFileDirEntry{ - iconInfo: icon.images[i].info, - ImageOffset: uint32(offset), - }) - if err != nil { - return err - } - offset += len(icon.images[i].image) - pos += sizeOfIconFileDirEntry - } - - for i := range icon.images { - _, err = ico.Write(icon.images[i].image) - if err != nil { - return err - } - } - - return nil -} - -// SetIcon adds the icon to the resource set. -// -// The first icon will be the application's icon, as shown in Windows Explorer. -// That means: -// 1. First name in case-sensitive ascending order, or else... -// 2. First ID in ascending order -// -func (rs *ResourceSet) SetIcon(resID Identifier, icon *Icon) error { - return rs.SetIconTranslation(resID, LCIDNeutral, icon) -} - -// SetIconTranslation adds the icon to a specific language in the resource set. -// -// The first icon will be the application's icon, as shown in Windows Explorer. -// That means: -// 1. First name in case-sensitive ascending order, or else... -// 2. First ID in ascending order -// -func (rs *ResourceSet) SetIconTranslation(resID Identifier, langID uint16, icon *Icon) error { - b := &bytes.Buffer{} - binary.Write(b, binary.LittleEndian, iconDirHeader{ - Type: 1, - Count: uint16(len(icon.images)), - }) - - icon.order() - - for _, img := range icon.images { - id := rs.lastIconID + 1 - - binary.Write(b, binary.LittleEndian, iconResDirEntry{ - iconInfo: img.info, - Id: id, - }) - - if err := rs.Set(RT_ICON, ID(id), LCIDNeutral, img.image); err != nil { - return err - } - } - return rs.Set(RT_GROUP_ICON, resID, langID, b.Bytes()) -} - -// GetIcon extracts an icon from a resource set. -func (rs *ResourceSet) GetIcon(resID Identifier) (*Icon, error) { - return rs.GetIconTranslation(resID, rs.firstLang(RT_GROUP_ICON, resID)) -} - -// GetIconTranslation extracts an icon from a specific language of the resource set. -func (rs *ResourceSet) GetIconTranslation(resID Identifier, langID uint16) (*Icon, error) { - data := rs.Get(RT_GROUP_ICON, resID, langID) - if data == nil { - return nil, errors.New(errGroupNotFound) - } - - in := bytes.NewReader(data) - hdr := iconDirHeader{} - err := binaryRead(in, &hdr) - if err != nil || hdr.Type != 1 || hdr.Reserved != 0 { - return nil, errors.New(errInvalidGroup) - } - - icon := &Icon{} - for i := 0; i < int(hdr.Count); i++ { - entry := iconResDirEntry{} - err := binaryRead(in, &entry) - if err != nil { - return nil, errors.New(errInvalidGroup) - } - img := rs.Get(RT_ICON, ID(entry.Id), rs.firstLang(RT_ICON, ID(entry.Id))) - if img == nil { - return nil, errors.New(errIconMissing) - } - icon.images = append(icon.images, iconImage{ - info: entry.iconInfo, - image: img, - }) - } - - return icon, nil -} - -// An icon is made of an icon directory and actual icons. -// -// The icon directory is made of a header and entries. -// -// Directory entries are slightly different between ICO files and RT_GROUP_ICON resources. -// -// https://devblogs.microsoft.com/oldnewthing/20120720-00/?p=7083 -// https://docs.microsoft.com/en-us/previous-versions/ms997538 - -// iconDirHeader is the binary format of an icon directory header. -type iconDirHeader struct { - Reserved uint16 - Type uint16 - Count uint16 -} - -const sizeOfIconDirHeader = 6 - -// iconFileDirEntry is the binary format of an icon directory entry, in an ICO file. -type iconFileDirEntry struct { - iconInfo - ImageOffset uint32 -} - -const sizeOfIconFileDirEntry = 16 - -// iconResDirEntry is the binary format of an icon directory entry, in an RT_GROUP_ICON resource. -type iconResDirEntry struct { - iconInfo - Id uint16 -} - -// iconInfo is the common part of iconResDirEntry and iconFileDirEntry. -type iconInfo struct { - Width uint8 - Height uint8 - ColorCount uint8 - Reserved uint8 - Planes uint16 - BitCount uint16 - BytesInRes uint32 -} - -type iconImage struct { - info iconInfo - image []byte -} - -// This makes a testing error reporting possible -var pngEncode = png.Encode - -func (icon *Icon) addImage(img image.Image) error { - bounds := img.Bounds() - if bounds.Empty() { - return errors.New(errInvalidImageDimensions) - } - if bounds.Size().X > 256 || bounds.Size().Y > 256 { - return errors.New(errImageTooBig) - } - - img = imageInSquareNRGBA(img, true) - bounds = img.Bounds() - buf := &bytes.Buffer{} - if err := pngEncode(buf, img); err != nil { - return err - } - - icon.images = append(icon.images, iconImage{ - info: iconInfo{ - Width: uint8(bounds.Size().X), // 0 means 256 - Height: uint8(bounds.Size().Y), // 0 means 256 - ColorCount: 0, // should be defined as 1 << BitCount only if BitCount < 8 - Reserved: 0, - Planes: 1, - BitCount: 32, - BytesInRes: uint32(buf.Len()), - }, - image: buf.Bytes(), - }) - - return nil -} - -func (icon *Icon) order() { - // Sort images by descending size and quality - sort.SliceStable(icon.images, func(i, j int) bool { - img1, img2 := &icon.images[i].info, &icon.images[j].info - return img1.BitCount > img2.BitCount || - img1.BitCount == img2.BitCount && int(img1.Width-1)+1 > int(img2.Width-1)+1 - }) -} - -func resizeImage(img image.Image, size int) image.Image { - var ( - sz = img.Bounds().Size() - w, h = size, size - ) - - if sz.X < sz.Y { - w = 0 - } else if sz.X > sz.Y { - h = 0 - } - - return resize.Resize(uint(w), uint(h), img, resize.Lanczos2) -} - -func imageInSquareNRGBA(img image.Image, center bool) image.Image { - w, h := img.Bounds().Size().X, img.Bounds().Size().Y - if w == h && img.ColorModel() == color.NRGBAModel { - return img - } - - length := w - if length < h { - length = h - } - - offset := image.Point{ - X: -img.Bounds().Min.X, - Y: -img.Bounds().Min.Y, - } - if center { - offset.X -= (w - length) / 2 - offset.Y -= (h - length) / 2 - } - - square := image.NewNRGBA(image.Rectangle{Max: image.Point{X: length, Y: length}}) - for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { - for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { - square.Set(x+offset.X, y+offset.Y, img.At(x, y)) - } - } - - return square -} diff --git a/pkgs/winres/icon_test.go b/pkgs/winres/icon_test.go deleted file mode 100644 index df04126..0000000 --- a/pkgs/winres/icon_test.go +++ /dev/null @@ -1,558 +0,0 @@ -package winres - -import ( - "bytes" - "errors" - "image" - "io" - "os" - "path/filepath" - "reflect" - "testing" -) - -func TestLoadICO(t *testing.T) { - f, err := os.Open(filepath.Join(testDataDir, "icon.ico")) - if err != nil { - t.Fatal("missing test data") - } - defer f.Close() - - cursor, err := LoadICO(f) - if err != nil { - t.Fatal(err) - } - - cursor.order() - - expected := []iconImage{ - { - info: iconInfo{ - Width: 0, - Height: 0, - ColorCount: 0, - Planes: 1, - BitCount: 32, - BytesInRes: 0x7D30, - }, - }, - { - info: iconInfo{ - Width: 48, - Height: 48, - ColorCount: 0, - Planes: 1, - BitCount: 32, - BytesInRes: 0x25A8, - }, - }, - { - info: iconInfo{ - Width: 32, - Height: 32, - ColorCount: 0, - Planes: 1, - BitCount: 32, - BytesInRes: 0x10A8, - }, - }, - { - info: iconInfo{ - Width: 16, - Height: 16, - ColorCount: 0, - Planes: 1, - BitCount: 32, - BytesInRes: 0x468, - }, - }, - { - info: iconInfo{ - Width: 0, - Height: 0, - ColorCount: 0, - Planes: 1, - BitCount: 8, - BytesInRes: 0x5824, - }, - }, - { - info: iconInfo{ - Width: 48, - Height: 48, - ColorCount: 0, - Planes: 1, - BitCount: 8, - BytesInRes: 0xEA8, - }, - }, - { - info: iconInfo{ - Width: 32, - Height: 32, - ColorCount: 0, - Planes: 1, - BitCount: 8, - BytesInRes: 0x8A8, - }, - }, - { - info: iconInfo{ - Width: 16, - Height: 16, - ColorCount: 0, - Planes: 1, - BitCount: 8, - BytesInRes: 0x568, - }, - }, - { - info: iconInfo{ - Width: 0, - Height: 0, - ColorCount: 16, - Planes: 1, - BitCount: 4, - BytesInRes: 0x55FC, - }, - }, - { - info: iconInfo{ - Width: 48, - Height: 48, - ColorCount: 16, - Planes: 1, - BitCount: 4, - BytesInRes: 0x668, - }, - }, - { - info: iconInfo{ - Width: 32, - Height: 32, - ColorCount: 16, - Planes: 1, - BitCount: 4, - BytesInRes: 0x2E8, - }, - }, - { - info: iconInfo{ - Width: 16, - Height: 16, - ColorCount: 16, - Planes: 1, - BitCount: 4, - BytesInRes: 0x128, - }, - }, - } - - for i := range expected { - if !reflect.DeepEqual(cursor.images[i].info, expected[i].info) { - t.Errorf("%s - image %d: expected %v got %v", t.Name(), i, expected[i].info, cursor.images[i].info) - } - } -} - -func TestLoadICO_ErrEOF1(t *testing.T) { - cursor, err := LoadICO(bytes.NewReader([]byte{0, 0, 1, 0, 1})) - if err != io.ErrUnexpectedEOF || cursor != nil { - t.Fail() - } -} - -func TestLoadICO_ErrEOF2(t *testing.T) { - icon, err := LoadICO(bytes.NewReader([]byte{ - 0, 0, 1, 0, 0xFF, 0xFF, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - })) - if err != io.ErrUnexpectedEOF || icon != nil { - t.Fail() - } -} - -func TestLoadICO_ErrImageOffset(t *testing.T) { - temp, err := os.ReadFile(filepath.Join(testDataDir, "icon.ico")) - if err != nil { - t.Fatal(err) - } - - temp[21] = 0x01 - icon, err := LoadICO(bytes.NewReader(temp)) - if err != io.ErrUnexpectedEOF || icon != nil { - t.Fail() - } -} - -func TestLoadCUR_ErrNotICO(t *testing.T) { - temp, err := os.ReadFile(filepath.Join(testDataDir, "icon.ico")) - if err != nil { - t.Fatal(err) - } - - temp[0] = 1 - icon, err := LoadICO(bytes.NewReader(temp)) - if err == nil || icon != nil || err.Error() != errNotICO { - t.Fail() - } - - temp[0] = 0 - temp[2] = 2 - icon, err = LoadICO(bytes.NewReader(temp)) - if err == nil || icon != nil || err.Error() != errNotICO { - t.Fail() - } -} - -func TestLoadICO_ErrSeek(t *testing.T) { - data, err := os.ReadFile(filepath.Join(testDataDir, "icon.ico")) - if err != nil { - t.Fatal("missing test data") - } - - r := &badSeeker{br: bytes.NewReader(data)} - - icon, err := LoadICO(r) - if !isExpectedSeekErr(err) || icon != nil { - t.Fatal("expected seek error, got", err) - } -} - -func TestLoadICO_ImageLengthLimit(t *testing.T) { - _, err := LoadICO(bytes.NewReader([]byte{ - 0, 0, 1, 0, 1, 0, 32, 32, 0, 0, 1, 0, 32, 0, - 0x00, 0x00, 0xA0, 0x00, // image data length = 0xA00000 - 0, 0, 0, 22, - })) - if err == nil || err.Error() == errImageLengthTooBig { - t.Fail() - } - - _, err = LoadICO(bytes.NewReader([]byte{ - 0, 0, 1, 0, 1, 0, 32, 32, 0, 0, 0, 0, 0, 0, - 0x01, 0x00, 0xA0, 0x00, // image data length = 0xA00001 - 0, 0, 0, 22, - })) - if err == nil || err.Error() != errImageLengthTooBig { - t.Fail() - } -} - -func TestIcon_SaveICO(t *testing.T) { - data, err := os.ReadFile(filepath.Join(testDataDir, "icon.ico")) - if err != nil { - t.Fatal(err) - } - - icon, err := LoadICO(bytes.NewReader(data)) - if err != nil { - t.Fatal(err) - } - - buf := &bytes.Buffer{} - err = icon.SaveICO(buf) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, buf.Bytes()) -} - -func TestIcon_SaveICO_ErrWrite(t *testing.T) { - data, err := os.ReadFile(filepath.Join(testDataDir, "icon.ico")) - if err != nil { - t.Fatal(err) - } - - icon, err := LoadICO(bytes.NewReader(data)) - if err != nil { - t.Fatal(err) - } - - w := newBadWriter(5) - err = icon.SaveICO(w) - if !isExpectedWriteErr(err) { - t.Fatal("expected write error, got", err) - } - - w = newBadWriter(20) - err = icon.SaveICO(w) - if !isExpectedWriteErr(err) { - t.Fatal("expected write error, got", err) - } - - w = newBadWriter(41885) - err = icon.SaveICO(w) - if !isExpectedWriteErr(err) { - t.Fatal("expected write error, got", err) - } -} - -func TestNewIconFromImages(t *testing.T) { - icon, err := NewIconFromImages([]image.Image{ - shiftImage(loadImage(t, "cur-16x8.png"), 30, 0), - loadImage(t, "cur-16x32.png"), - loadImage(t, "cur-32x64.png"), - loadImage(t, "cur-64x128.png"), - shiftImage(loadImage(t, "cur-16x8.png"), 1, -100), - }) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, icoToBinary(icon)) -} - -func TestNewIconFromImages_ErrDimensions(t *testing.T) { - _, err := NewIconFromImages([]image.Image{image.NewNRGBA(image.Rectangle{})}) - if err == nil || err.Error() != errInvalidImageDimensions { - t.Fail() - } -} - -func TestNewIconFromImages_ErrEncode(t *testing.T) { - enc := pngEncode - defer func() { pngEncode = enc }() - pngEncode = func(w io.Writer, m image.Image) error { - return errors.New("oops") - } - - _, err := NewIconFromImages([]image.Image{ - loadImage(t, "cur-32x64.png"), - loadImage(t, "cur-64x128.png"), - }) - if err == nil || err.Error() != "oops" { - t.Fail() - } -} - -func TestNewIconFromImages_ErrTooBig(t *testing.T) { - _, err := NewIconFromImages([]image.Image{ - image.NewNRGBA(image.Rectangle{ - Max: image.Point{ - X: 257, - Y: 256, - }, - }), - }) - if err == nil || err.Error() != errImageTooBig { - t.Fail() - } - - _, err = NewIconFromImages([]image.Image{ - image.NewNRGBA(image.Rectangle{ - Max: image.Point{ - X: 256, - Y: 257, - }, - }), - }) - if err == nil || err.Error() != errImageTooBig { - t.Fail() - } - - _, err = NewIconFromImages([]image.Image{ - image.NewNRGBA(image.Rectangle{ - Max: image.Point{ - X: 256, - Y: 256, - }, - }), - }) - if err != nil { - t.Fail() - } -} - -func TestNewIconFromResizedImage(t *testing.T) { - icon, err := NewIconFromResizedImage(shiftImage(loadImage(t, "img.png"), -100, -10), nil) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, icoToBinary(icon)) -} - -func TestNewIconFromResizedImage_Ratio1(t *testing.T) { - icon, err := NewIconFromResizedImage(shiftImage(loadImage(t, "cur-32x64.png"), 5, -3), []int{32, 64}) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, icoToBinary(icon)) -} - -func TestNewIconFromResizedImage_Ratio2(t *testing.T) { - icon, err := NewIconFromResizedImage(shiftImage(loadImage(t, "cur-16x8.png"), -2, 8), []int{8, 16}) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, icoToBinary(icon)) -} - -func TestNewIconFromResizedImage_InvalidSize(t *testing.T) { - icon, err := NewIconFromResizedImage(shiftImage(loadImage(t, "img.png"), 100, 100), []int{16, 257}) - if err == nil || icon != nil || err.Error() != errImageTooBig { - t.Fail() - } -} - -func TestNewIconFromResizedImage_TooManySizes(t *testing.T) { - s := make([]int, 30) - for i := range s { - s[i] = i + 1 - } - - _, err := NewIconFromResizedImage(shiftImage(loadImage(t, "img.png"), 100, 100), s) - if err != nil { - t.Fail() - } - - s = append(s, len(s)+1) - - icon, err := NewIconFromResizedImage(shiftImage(loadImage(t, "img.png"), 100, 100), s) - if err == nil || icon != nil || err.Error() != errTooManyIconSizes { - t.Fail() - } -} - -func TestResourceSet_SetIcon(t *testing.T) { - rs := ResourceSet{} - - id := Name("ICON") - icon := loadICOFile(t, "icon.ico") - err := rs.SetIcon(id, icon) - if err != nil { - t.Fatal(err) - } - if int(rs.lastIconID) != len(icon.images) { - t.Fail() - } - - checkIconResource(t, &rs, id, 0, icon) -} - -func TestResourceSet_SetIcon_IDOverflow(t *testing.T) { - rs := ResourceSet{} - rs.lastIconID = 0xFFF4 - - icon := loadICOFile(t, "icon.ico") - err := rs.SetIcon(Name("ICON"), icon) - if err == nil || err.Error() != errZeroID { - t.Fail() - } -} - -func TestResourceSet_SetIconTranslation(t *testing.T) { - rs := ResourceSet{} - - icon1 := loadICOFile(t, "en.ico") - icon2 := loadICOFile(t, "fr.ico") - icon3, err := NewIconFromImages([]image.Image{loadImage(t, "cur-16x8.png")}) - if err != nil { - t.Fatal(err) - } - - err = rs.SetIconTranslation(Name("LOCALICO"), 0x409, icon1) - if err != nil { - t.Fatal(err) - } - err = rs.SetIconTranslation(Name("ANOTHERICO"), 0x409, icon3) - if err != nil { - t.Fatal(err) - } - err = rs.SetIconTranslation(Name("LOCALICO"), 0x40C, icon2) - if err != nil { - t.Fatal(err) - } - - checkIconResource(t, &rs, Name("LOCALICO"), 0x409, icon1) - checkIconResource(t, &rs, Name("LOCALICO"), 0x40C, icon2) - checkIconResource(t, &rs, Name("ANOTHERICO"), 0x409, icon3) -} - -func TestResourceSet_GetIconTranslation_Err(t *testing.T) { - rs := ResourceSet{} - - bin0 := []byte{0, 0, 1, 0, 1} - bin1 := []byte{0, 0, 2, 0, 1, 0, 32, 32, 0, 0, 1, 0, 32, 0, 42, 0, 0, 0, 1, 0} - bin2 := []byte{0, 0, 1, 0, 1, 0, 32, 32, 0, 0, 1, 0, 32, 0, 42, 0, 0, 0, 1} - bin3 := []byte{0, 0, 1, 0, 1, 0, 32, 32, 0, 0, 1, 0, 32, 0, 42, 0, 0, 0, 1, 0} - - err := rs.Set(RT_GROUP_ICON, Name("LOCALICO"), 0, bin0) - if err != nil { - t.Fatal(err) - } - err = rs.Set(RT_GROUP_ICON, Name("LOCALICO"), 0x401, bin1) - if err != nil { - t.Fatal(err) - } - err = rs.Set(RT_GROUP_ICON, Name("LOCALICO"), 0x402, bin2) - if err != nil { - t.Fatal(err) - } - err = rs.Set(RT_GROUP_ICON, Name("LOCALICO"), 0x403, bin3) - if err != nil { - t.Fatal(err) - } - - var icon *Icon - - icon, err = rs.GetIcon(ID(0)) - if err == nil || icon != nil || err.Error() != errGroupNotFound { - t.Fail() - } - icon, err = rs.GetIconTranslation(Name("LOCALIC"), 0x401) - if err == nil || icon != nil || err.Error() != errGroupNotFound { - t.Fail() - } - icon, err = rs.GetIconTranslation(Name("LOCALICO"), 0x409) - if err == nil || icon != nil || err.Error() != errGroupNotFound { - t.Fail() - } - - icon, err = rs.GetIconTranslation(Name("LOCALICO"), 0) - if err == nil || icon != nil || err.Error() != errInvalidGroup { - t.Fail() - } - icon, err = rs.GetIconTranslation(Name("LOCALICO"), 0x401) - if err == nil || icon != nil || err.Error() != errInvalidGroup { - t.Fail() - } - icon, err = rs.GetIconTranslation(Name("LOCALICO"), 0x402) - if err == nil || icon != nil || err.Error() != errInvalidGroup { - t.Fail() - } - icon, err = rs.GetIconTranslation(Name("LOCALICO"), 0x403) - if err == nil || icon != nil || err.Error() != errIconMissing { - t.Fail() - } -} - -func checkIconResource(t *testing.T, rs *ResourceSet, ident Identifier, langID uint16, source *Icon) { - icon, err := rs.GetIconTranslation(ident, langID) - if err != nil { - t.Fatal(err) - } - buf1, buf2 := &bytes.Buffer{}, &bytes.Buffer{} - err = source.SaveICO(buf1) - if err != nil { - t.Fatal(err) - } - err = icon.SaveICO(buf2) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(buf1.Bytes(), buf2.Bytes()) { - t.Fail() - } -} - -func icoToBinary(icon *Icon) []byte { - buf := &bytes.Buffer{} - icon.SaveICO(buf) - return buf.Bytes() -} diff --git a/pkgs/winres/id.go b/pkgs/winres/id.go deleted file mode 100644 index 191d5dd..0000000 --- a/pkgs/winres/id.go +++ /dev/null @@ -1,48 +0,0 @@ -package winres - -import ( - "errors" - "strings" -) - -// ID is the type of a resource id, or resource type id. -type ID uint16 - -// Name is the type of a resource name, or a resource type name. -type Name string - -// Identifier is either an ID or a Name. -// -// When you are asked for an Identifier, you can pass an int cast to an ID or a string cast to a Name. -type Identifier interface { - // This method serves both to seal the interface and help order identifiers the standard way - // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#resource-directory-entries - lessThan(ident Identifier) bool -} - -func (id ID) lessThan(ident Identifier) bool { - right, ok := ident.(ID) - return ok && id < right -} - -func (n Name) lessThan(ident Identifier) bool { - right, ok := ident.(Name) - return !ok || n < right -} - -func checkIdentifier(ident Identifier) error { - switch ident := ident.(type) { - case ID: - if ident == 0 { - return errors.New(errZeroID) - } - case Name: - if ident == "" { - return errors.New(errEmptyName) - } - if strings.ContainsRune(string(ident), 0) { - return errors.New(errNameContainsNUL) - } - } - return nil -} diff --git a/pkgs/winres/manifest.go b/pkgs/winres/manifest.go deleted file mode 100644 index 08c037f..0000000 --- a/pkgs/winres/manifest.go +++ /dev/null @@ -1,533 +0,0 @@ -package winres - -import ( - "bytes" - "encoding/json" - "encoding/xml" - "errors" - "fmt" - "strconv" - "strings" - "text/template" -) - -// AppManifest describes an application manifest. -// -// Its zero value corresponds to the most common case. -type AppManifest struct { - Identity AssemblyIdentity `json:"identity"` - Description string `json:"description"` - Compatibility SupportedOS `json:"minimum-os"` - ExecutionLevel ExecutionLevel `json:"execution-level"` - UIAccess bool `json:"ui-access"` // Require access to other applications' UI elements - AutoElevate bool `json:"auto-elevate"` - DPIAwareness DPIAwareness `json:"dpi-awareness"` - DisableTheming bool `json:"disable-theming"` - DisableWindowFiltering bool `json:"disable-window-filtering"` - HighResolutionScrollingAware bool `json:"high-resolution-scrolling-aware"` - UltraHighResolutionScrollingAware bool `json:"ultra-high-resolution-scrolling-aware"` - LongPathAware bool `json:"long-path-aware"` - PrinterDriverIsolation bool `json:"printer-driver-isolation"` - GDIScaling bool `json:"gdi-scaling"` - SegmentHeap bool `json:"segment-heap"` - UseCommonControlsV6 bool `json:"use-common-controls-v6"` // Application requires Common Controls V6 (V5 remains the default) -} - -// AssemblyIdentity defines the side-by-side assembly identity of the executable. -// -// It should not be needed unless another assembly depends on this one. -// -// If the Name field is empty, the element will be omitted. -type AssemblyIdentity struct { - Name string - Version [4]uint16 -} - -// DPIAwareness is an enumeration which corresponds to the and the elements. -// -// When it is set to DPIPerMonitorV2, it will fallback to DPIAware if the OS does not support it. -// -// DPIPerMonitor would not scale windows on secondary monitors. -type DPIAwareness int - -const ( - DPIAware DPIAwareness = iota - DPIUnaware - DPIPerMonitor - DPIPerMonitorV2 -) - -// SupportedOS is an enumeration that provides a simplified way to fill the -// compatibility element in an application manifest, by only setting a minimum OS. -// -// Its zero value is Win7AndAbove, which matches Go's requirements. -// -// https://github.com/golang/go/wiki/MinimumRequirements#windows -type SupportedOS int - -const ( - WinVistaAndAbove SupportedOS = iota - 1 - Win7AndAbove - Win8AndAbove - Win81AndAbove - Win10AndAbove -) - -// ExecutionLevel is used in an AppManifest to set the required execution level. -type ExecutionLevel int - -const ( - AsInvoker ExecutionLevel = iota - HighestAvailable - RequireAdministrator -) - -const ( - osWin10 = "{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" - osWin81 = "{1f676c76-80e1-4239-95bb-83d0f6d0da78}" - osWin8 = "{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" - osWin7 = "{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" - osWinVista = "{e2011457-1546-43c5-a5fe-008deee3d3f0}" -) - -// language=GoTemplate -var manifestTemplate = ` - -{{- if .AssemblyName}} - - -{{- end}} -{{- if .Description}} - {{.Description | html}} -{{- end}} - - - - {{- range $osID := .SupportedOS}} - - {{- end}} - - - - - - {{.DPIAware}} - {{.DPIAwareness}} - {{- if .AutoElevate}} - true - {{- end}} - {{- if .DisableTheming}} - true - {{- end}} - {{- if .DisableWindowFiltering}} - true - {{- end}} - {{- if .HighResolutionScrollingAware}} - true - {{- end}} - {{- if .PrinterDriverIsolation}} - true - {{- end}} - {{- if .UltraHighResolutionScrollingAware}} - true - {{- end}} - {{- if .LongPathAware}} - true - {{- end}} - {{- if .GDIScaling}} - true - {{- end}} - {{- if .SegmentHeap}} - SegmentHeap - {{- end}} - - - - - - - - - - - {{- if .UseCommonControlsV6}} - - - - - - - {{- end}} - - -` - -func makeManifest(manifest AppManifest) []byte { - vars := struct { - AppManifest - AssemblyName string - AssemblyVersion string - SupportedOS []string - DPIAware string - DPIAwareness string - ExecutionLevel string - }{AppManifest: manifest} - - if manifest.Identity.Name != "" { - vars.AssemblyName = manifest.Identity.Name - v := manifest.Identity.Version - vars.AssemblyVersion = fmt.Sprintf("%d.%d.%d.%d", v[0], v[1], v[2], v[3]) - } - - vars.SupportedOS = []string{ - osWin10, - osWin81, - osWin8, - osWin7, - osWinVista, - } - switch manifest.Compatibility { - case Win7AndAbove: - vars.SupportedOS = vars.SupportedOS[:4] - case Win8AndAbove: - vars.SupportedOS = vars.SupportedOS[:3] - case Win81AndAbove: - vars.SupportedOS = vars.SupportedOS[:2] - case Win10AndAbove: - vars.SupportedOS = vars.SupportedOS[:1] - } - - switch manifest.ExecutionLevel { - case RequireAdministrator: - vars.ExecutionLevel = "requireAdministrator" - case HighestAvailable: - vars.ExecutionLevel = "highestAvailable" - default: - vars.ExecutionLevel = "asInvoker" - } - - switch manifest.DPIAwareness { - case DPIAware: - vars.DPIAware = "true" - vars.DPIAwareness = "system" - case DPIPerMonitor: - vars.DPIAware = "true/pm" - vars.DPIAwareness = "permonitor" - case DPIPerMonitorV2: - // PerMonitorV2 fixes the scale on secondary monitors - // If not available, the closest option seems to be System - vars.DPIAware = "true" - vars.DPIAwareness = "permonitorv2,system" - case DPIUnaware: - vars.DPIAware = "false" - vars.DPIAwareness = "unaware" - } - - buf := &bytes.Buffer{} - tmpl := template.Must(template.New("manifest").Parse(manifestTemplate)) - err := tmpl.Execute(buf, vars) - if err != nil { - panic(err) - } - - return buf.Bytes() -} - -type appManifestXML struct { - Identity struct { - Name string `xml:"name,attr"` - Version string `xml:"version,attr"` - } `xml:"assemblyIdentity"` - Description string `xml:"description"` - Compatibility struct { - Application struct { - SupportedOS []struct { - Id string `xml:"Id,attr"` - } `xml:"supportedOS"` - } `xml:"application"` - } `xml:"compatibility"` - Application struct { - WindowsSettings struct { - DPIAware string `xml:"dpiAware"` - DPIAwareness string `xml:"dpiAwareness"` - AutoElevate string `xml:"autoElevate"` - DisableTheming string `xml:"disableTheming"` - DisableWindowFiltering string `xml:"disableWindowFiltering"` - HighResolutionScrollingAware string `xml:"highResolutionScrollingAware"` - PrinterDriverIsolation string `xml:"printerDriverIsolation"` - UltraHighResolutionScrollingAware string `xml:"ultraHighResolutionScrollingAware"` - LongPathAware string `xml:"longPathAware"` - GDIScaling string `xml:"gdiScaling"` - HeapType string `xml:"heapType"` - } `xml:"windowsSettings"` - } `xml:"application"` - TrustInfo struct { - Security struct { - RequestedPrivileges struct { - RequestedExecutionLevel struct { - Level string `xml:"level,attr"` - UIAccess string `xml:"uiAccess,attr"` - } `xml:"requestedExecutionLevel"` - } `xml:"requestedPrivileges"` - } `xml:"security"` - } `xml:"trustInfo"` - Dependency struct { - DependentAssembly []struct { - Identity struct { - Name string `xml:"name,attr"` - Version string `xml:"version,attr"` - PublicKeyToken string `xml:"publicKeyToken,attr"` - } `xml:"assemblyIdentity"` - } `xml:"dependentAssembly"` - } `xml:"dependency"` -} - -// AppManifestFromXML makes an AppManifest from an xml manifest, -// trying to retrieve as much valid information as possible. -// -// If the xml contains other data, they are ignored. -// -// This function can only return xml syntax errors, other errors are ignored. -func AppManifestFromXML(data []byte) (AppManifest, error) { - x := appManifestXML{} - err := xml.Unmarshal(data, &x) - if err != nil { - return AppManifest{}, err - } - var m AppManifest - - m.Identity.Name = x.Identity.Name - v := strings.Split(x.Identity.Version, ".") - if len(v) > 4 { - v = v[:4] - } - for i := range v { - n, _ := strconv.ParseUint(v[i], 10, 16) - m.Identity.Version[i] = uint16(n) - } - m.Description = x.Description - - m.Compatibility = Win10AndAbove + 1 - for _, os := range x.Compatibility.Application.SupportedOS { - c := osIDToEnum(os.Id) - if c < m.Compatibility { - m.Compatibility = c - } - } - if m.Compatibility > Win10AndAbove { - m.Compatibility = Win7AndAbove - } - - settings := x.Application.WindowsSettings - m.DPIAwareness = readDPIAwareness(settings.DPIAware, settings.DPIAwareness) - m.AutoElevate = manifestBool(settings.AutoElevate) - m.DisableTheming = manifestBool(settings.DisableTheming) - m.DisableWindowFiltering = manifestBool(settings.DisableWindowFiltering) - m.HighResolutionScrollingAware = manifestBool(settings.HighResolutionScrollingAware) - m.PrinterDriverIsolation = manifestBool(settings.PrinterDriverIsolation) - m.UltraHighResolutionScrollingAware = manifestBool(settings.UltraHighResolutionScrollingAware) - m.LongPathAware = manifestBool(settings.LongPathAware) - m.GDIScaling = manifestBool(settings.GDIScaling) - m.SegmentHeap = manifestString(settings.HeapType) == "segmentheap" - - for _, dep := range x.Dependency.DependentAssembly { - if manifestString(dep.Identity.Name) == "microsoft.windows.common-controls" && - strings.HasPrefix(manifestString(dep.Identity.Version), "6.") && - manifestString(dep.Identity.PublicKeyToken) == "6595b64144ccf1df" { - m.UseCommonControlsV6 = true - } - } - - m.UIAccess = manifestBool(x.TrustInfo.Security.RequestedPrivileges.RequestedExecutionLevel.UIAccess) - switch manifestString(x.TrustInfo.Security.RequestedPrivileges.RequestedExecutionLevel.Level) { - case "requireadministrator": - m.ExecutionLevel = RequireAdministrator - case "highestavailable": - m.ExecutionLevel = HighestAvailable - } - - return m, nil -} - -func readDPIAwareness(dpiAware string, dpiAwareness string) DPIAwareness { - for _, s := range strings.Split(dpiAwareness, ",") { - switch manifestString(s) { - case "permonitorv2": - return DPIPerMonitorV2 - case "permonitor": - return DPIPerMonitor - case "system": - return DPIAware - case "unaware": - return DPIUnaware - } - } - switch manifestString(dpiAware) { - case "true": - return DPIAware - case "true/pm": - return DPIPerMonitor - } - return DPIUnaware -} - -func manifestString(s string) string { - return strings.ToLower(strings.TrimSpace(s)) -} - -func manifestBool(s string) bool { - return manifestString(s) == "true" -} - -func osIDToEnum(osID string) SupportedOS { - switch osID { - case osWinVista: - return WinVistaAndAbove - case osWin7: - return Win7AndAbove - case osWin8: - return Win8AndAbove - case osWin81: - return Win81AndAbove - } - return Win10AndAbove -} - -// JSON marshalling: - -func (os SupportedOS) MarshalText() ([]byte, error) { - switch os { - case WinVistaAndAbove: - return []byte("vista"), nil - case Win7AndAbove: - return []byte("win7"), nil - case Win8AndAbove: - return []byte("win8"), nil - case Win81AndAbove: - return []byte("win8.1"), nil - case Win10AndAbove: - return []byte("win10"), nil - } - return nil, errors.New(errUnknownSupportedOS) -} - -func (os *SupportedOS) UnmarshalText(b []byte) error { - switch strings.ToLower(strings.TrimSpace(string(b))) { - case "vista": - *os = WinVistaAndAbove - return nil - case "win7": - *os = Win7AndAbove - return nil - case "win8": - *os = Win8AndAbove - return nil - case "win8.1": - *os = Win81AndAbove - return nil - case "win10": - *os = Win10AndAbove - return nil - } - return errors.New(errUnknownSupportedOS) -} - -func (a DPIAwareness) MarshalText() ([]byte, error) { - switch a { - case DPIAware: - return []byte("system"), nil - case DPIUnaware: - return []byte("unaware"), nil - case DPIPerMonitor: - return []byte("per monitor"), nil - case DPIPerMonitorV2: - return []byte("per monitor v2"), nil - } - return nil, errors.New(errUnknownDPIAwareness) -} - -func (a *DPIAwareness) UnmarshalText(b []byte) error { - switch strings.ToLower(strings.TrimSpace(string(b))) { - case "system", "true", "": - *a = DPIAware - return nil - case "unaware", "false": - *a = DPIUnaware - return nil - case "per monitor", "permonitor", "true/pm": - *a = DPIPerMonitor - return nil - case "per monitor v2", "permonitorv2": - *a = DPIPerMonitorV2 - return nil - } - return errors.New(errUnknownDPIAwareness) -} - -func (level ExecutionLevel) MarshalText() ([]byte, error) { - switch level { - case AsInvoker: - return []byte(""), nil - case HighestAvailable: - return []byte("highest"), nil - case RequireAdministrator: - return []byte("administrator"), nil - } - return nil, errors.New(errUnknownExecLevel) -} - -func (level *ExecutionLevel) UnmarshalText(b []byte) error { - switch strings.ToLower(strings.TrimSpace(string(b))) { - case "", "as invoker", "asinvoker": - *level = AsInvoker - return nil - case "highest", "highest available", "highestavailable": - *level = HighestAvailable - return nil - case "administrator", "require administrator", "requireadministrator": - *level = RequireAdministrator - return nil - } - return errors.New(errUnknownExecLevel) -} - -type assemblyIdentityJSON struct { - Name string `json:"name"` - Version string `json:"version"` -} - -func (ai AssemblyIdentity) MarshalJSON() ([]byte, error) { - if ai.Name == "" { - return []byte(`{}`), nil - } - j := assemblyIdentityJSON{} - j.Name = ai.Name - if ai.Name != "" { - j.Version = fmt.Sprintf("%d.%d.%d.%d", ai.Version[0], ai.Version[1], ai.Version[2], ai.Version[3]) - } - return json.Marshal(j) -} - -func (ai *AssemblyIdentity) UnmarshalJSON(b []byte) error { - j := assemblyIdentityJSON{} - if err := json.Unmarshal(b, &j); err != nil { - return err - } - ai.Name = j.Name - j.Version = strings.TrimSpace(j.Version) - if j.Version == "" { - return nil - } - v := strings.Split(j.Version, ".") - if len(v) > 4 { - return errors.New(errInvalidVersion) - } - for i := range v { - n, err := strconv.ParseUint(v[i], 10, 16) - if err != nil { - return errors.New(errInvalidVersion) - } - ai.Version[i] = uint16(n) - } - return nil -} diff --git a/pkgs/winres/manifest_test.go b/pkgs/winres/manifest_test.go deleted file mode 100644 index 141f175..0000000 --- a/pkgs/winres/manifest_test.go +++ /dev/null @@ -1,853 +0,0 @@ -package winres - -import ( - "encoding/json" - "errors" - "reflect" - "testing" -) - -func Test_makeManifest(t *testing.T) { - type args struct { - manifest AppManifest - } - tests := []struct { - name string - args args - want string - }{ - { - name: "empty", - args: struct{ manifest AppManifest }{}, - // language=manifest - want: ` - - - - - - - - - - - - - - true - system - - - - - - - - - - - - -`}, - { - name: "full", - args: struct{ manifest AppManifest }{AppManifest{ - Identity: AssemblyIdentity{ - Name: "", - Version: [4]uint16{1, 2, 3, 4}, - }, - Description: "", - UIAccess: true, - AutoElevate: true, - DisableTheming: true, - DisableWindowFiltering: true, - HighResolutionScrollingAware: true, - UltraHighResolutionScrollingAware: true, - LongPathAware: true, - PrinterDriverIsolation: true, - GDIScaling: true, - SegmentHeap: true, - UseCommonControlsV6: true, - ExecutionLevel: HighestAvailable, - Compatibility: WinVistaAndAbove, - DPIAwareness: DPIAware, - }}, - // language=manifest - want: ` - - - - <Application Description> - - - - - - - - - - - - - - true - system - true - true - true - true - true - true - true - true - SegmentHeap - - - - - - - - - - - - - - - - - - -`}, - { - name: "win10admin", - args: struct{ manifest AppManifest }{AppManifest{ - Identity: AssemblyIdentity{ - // No name, no identity (empty name is forbidden) - Version: [4]uint16{1, 2, 3, 4}, - }, - Description: "Application Description", - UIAccess: false, - AutoElevate: true, - DisableTheming: false, - DisableWindowFiltering: true, - HighResolutionScrollingAware: false, - UltraHighResolutionScrollingAware: true, - LongPathAware: false, - PrinterDriverIsolation: true, - GDIScaling: false, - SegmentHeap: true, - ExecutionLevel: RequireAdministrator, - Compatibility: Win10AndAbove, - DPIAwareness: DPIUnaware, - }}, - // language=manifest - want: ` - - Application Description - - - - - - - - - - false - unaware - true - true - true - true - SegmentHeap - - - - - - - - - - - - -`}, - { - name: "win8highest", - args: struct{ manifest AppManifest }{AppManifest{ - Identity: AssemblyIdentity{ - Name: "app.name", - // No version -> 0.0.0.0 - }, - Description: "Application Description", - UIAccess: true, - AutoElevate: true, - DisableTheming: true, - DisableWindowFiltering: true, - HighResolutionScrollingAware: true, - UltraHighResolutionScrollingAware: false, - LongPathAware: false, - PrinterDriverIsolation: false, - GDIScaling: false, - SegmentHeap: false, - ExecutionLevel: HighestAvailable, - Compatibility: Win8AndAbove, - DPIAwareness: DPIPerMonitor, - }}, - // language=manifest - want: ` - - - - Application Description - - - - - - - - - - - - true/pm - permonitor - true - true - true - true - - - - - - - - - - - - -`}, - { - name: "win81", - args: struct{ manifest AppManifest }{AppManifest{ - Identity: AssemblyIdentity{ - Name: "app.name", - Version: [4]uint16{0xFFFF, 65535, 0xFFFF, 65535}, - }, - Description: "Application™\nDescription", - UIAccess: false, - AutoElevate: false, - DisableTheming: false, - DisableWindowFiltering: false, - HighResolutionScrollingAware: false, - UltraHighResolutionScrollingAware: true, - LongPathAware: true, - PrinterDriverIsolation: true, - GDIScaling: true, - SegmentHeap: true, - ExecutionLevel: AsInvoker, - Compatibility: Win81AndAbove, - DPIAwareness: DPIPerMonitorV2, - }}, - // language=manifest - want: ` - - - - Application™ -Description - - - - - - - - - - - true - permonitorv2,system - true - true - true - true - SegmentHeap - - - - - - - - - - - - -`}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := makeManifest(tt.args.manifest); string(got) != tt.want { - t.Errorf("*** makeManifest():\n%v###\n*** want:\n%v###", string(got), tt.want) - } - }) - } -} - -func TestMakeManifest_Bug(t *testing.T) { - bak := manifestTemplate - defer func() { - recover() - manifestTemplate = bak - }() - - manifestTemplate = "{{.bobby}}" - makeManifest(AppManifest{}) - - t.Error("should have panicked") -} - -func TestAppManifestFromXML(t *testing.T) { - tests := []struct { - name string - xml string - want AppManifest - wantErr bool - }{ - { - name: "zero", xml: ` -`, - want: AppManifest{DPIAwareness: DPIUnaware}, - wantErr: false, - }, - { - name: "longVersion", xml: // language=manifest - ` - - -`, - want: AppManifest{ - Identity: AssemblyIdentity{ - Name: " a.name", - Version: [4]uint16{0, 2, 3, 4}, - }, - DPIAwareness: DPIUnaware, - }, - wantErr: false, - }, - { - name: "shortVersion", xml: // language=manifest - ` - - -`, - want: AppManifest{ - Identity: AssemblyIdentity{ - Name: "💨", - Version: [4]uint16{42}, - }, - DPIAwareness: DPIUnaware, - }, - wantErr: false, - }, - { - name: "dpiAware", xml: // language=manifest - ` - - - - TrUe - - -`, - want: AppManifest{}, - wantErr: false, - }, - { - name: "manifest1", xml: // language=manifest - ` - - - - something,system, - - true - tRue - true - - false - err - yes - true - segmentHeap - - - true - - - - - - - - - - - - - - This is a - description -`, - want: AppManifest{ - Description: "This is a \n description", - AutoElevate: true, - DisableTheming: true, - DisableWindowFiltering: true, - GDIScaling: true, - HighResolutionScrollingAware: true, - SegmentHeap: true, - }, - wantErr: false, - }, - { - name: "manifest2", xml: // language=manifest - ` - - - - - - - - - - - system - true - true - false - true - true - - - - - - - - - - - -`, - want: AppManifest{ - UIAccess: true, - PrinterDriverIsolation: true, - UltraHighResolutionScrollingAware: true, - LongPathAware: true, - GDIScaling: true, - UseCommonControlsV6: true, - }, - wantErr: false, - }, - { - name: "admin", xml: // language=manifest - ` - - - - - - - - - - - system - - -`, - want: AppManifest{ExecutionLevel: RequireAdministrator}, - wantErr: false, - }, - { - name: "highest", xml: // language=manifest - ` - - - - - - - - - - - - - - - - - true - - -`, - want: AppManifest{ - ExecutionLevel: HighestAvailable, - Compatibility: Win10AndAbove, - }, - wantErr: false, - }, - { - name: "parseError", xml: ``, - want: AppManifest{}, - wantErr: true, - }, - { - name: "os", xml: // language=manifest - ` - - - - - - - - - - - - - - - - - - - - true - - -`, - want: AppManifest{Compatibility: WinVistaAndAbove}, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := AppManifestFromXML([]byte(tt.xml)) - if (err != nil) != tt.wantErr { - t.Errorf("AppManifestFromXML() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("AppManifestFromXML() got = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_readDPIAwareness(t *testing.T) { - type args struct { - dpiAware string - dpiAwareness string - } - tests := []struct { - name string - args args - want DPIAwareness - }{ - {args: args{dpiAware: "", dpiAwareness: "true, systeM , permonitor"}, want: DPIAware}, - {args: args{dpiAware: "true", dpiAwareness: "perMonitorv2,system"}, want: DPIPerMonitorV2}, - {args: args{dpiAware: "true", dpiAwareness: ""}, want: DPIAware}, - {args: args{dpiAware: " true/PM ", dpiAwareness: ""}, want: DPIPerMonitor}, - {args: args{dpiAware: "false", dpiAwareness: "system"}, want: DPIAware}, - {args: args{dpiAware: "true / PM", dpiAwareness: "per monitor"}, want: DPIUnaware}, - {args: args{dpiAware: "true", dpiAwareness: "perMonitorv2,system"}, want: DPIPerMonitorV2}, - {args: args{dpiAware: "true", dpiAwareness: "PerMonitor"}, want: DPIPerMonitor}, - {args: args{dpiAware: "true", dpiAwareness: " unaWarE"}, want: DPIUnaware}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := readDPIAwareness(tt.args.dpiAware, tt.args.dpiAwareness); got != tt.want { - t.Errorf("readDPIAwareness() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_AppManifest_MarshalJSON(t *testing.T) { - tests := []struct { - name string - manifest AppManifest - want string - wantErr string - }{ - { - manifest: AppManifest{}, - want:// language=json - `{"identity":{},"description":"","minimum-os":"win7","execution-level":"","ui-access":false,"auto-elevate":false,"dpi-awareness":"system","disable-theming":false,"disable-window-filtering":false,"high-resolution-scrolling-aware":false,"ultra-high-resolution-scrolling-aware":false,"long-path-aware":false,"printer-driver-isolation":false,"gdi-scaling":false,"segment-heap":false,"use-common-controls-v6":false}`, - }, - { - manifest: AppManifest{ - Identity: AssemblyIdentity{ - Name: "app.name", - Version: [4]uint16{1, 2, 3, 65535}, - }, - Description: "app desc", - Compatibility: WinVistaAndAbove, - ExecutionLevel: RequireAdministrator, - UIAccess: true, - DPIAwareness: DPIPerMonitorV2, - DisableTheming: true, - HighResolutionScrollingAware: true, - LongPathAware: true, - GDIScaling: true, - UseCommonControlsV6: true, - }, - want:// language=json - `{"identity":{"name":"app.name","version":"1.2.3.65535"},"description":"app desc","minimum-os":"vista","execution-level":"administrator","ui-access":true,"auto-elevate":false,"dpi-awareness":"per monitor v2","disable-theming":true,"disable-window-filtering":false,"high-resolution-scrolling-aware":true,"ultra-high-resolution-scrolling-aware":false,"long-path-aware":true,"printer-driver-isolation":false,"gdi-scaling":true,"segment-heap":false,"use-common-controls-v6":true}`, - }, - { - manifest: AppManifest{ - Identity: AssemblyIdentity{ - Name: "", - Version: [4]uint16{1, 2, 3, 4}, - }, - Compatibility: Win8AndAbove, - ExecutionLevel: HighestAvailable, - AutoElevate: true, - DPIAwareness: DPIPerMonitor, - LongPathAware: true, - PrinterDriverIsolation: true, - GDIScaling: true, - SegmentHeap: true, - }, - want:// language=json - `{"identity":{},"description":"","minimum-os":"win8","execution-level":"highest","ui-access":false,"auto-elevate":true,"dpi-awareness":"per monitor","disable-theming":false,"disable-window-filtering":false,"high-resolution-scrolling-aware":false,"ultra-high-resolution-scrolling-aware":false,"long-path-aware":true,"printer-driver-isolation":true,"gdi-scaling":true,"segment-heap":true,"use-common-controls-v6":false}`, - }, - { - manifest: AppManifest{ - Identity: AssemblyIdentity{ - Name: "a", - }, - Compatibility: Win81AndAbove, - ExecutionLevel: AsInvoker, - AutoElevate: true, - DPIAwareness: DPIUnaware, - DisableTheming: true, - DisableWindowFiltering: true, - HighResolutionScrollingAware: true, - UltraHighResolutionScrollingAware: true, - }, - want:// language=json - `{"identity":{"name":"a","version":"0.0.0.0"},"description":"","minimum-os":"win8.1","execution-level":"","ui-access":false,"auto-elevate":true,"dpi-awareness":"unaware","disable-theming":true,"disable-window-filtering":true,"high-resolution-scrolling-aware":true,"ultra-high-resolution-scrolling-aware":true,"long-path-aware":false,"printer-driver-isolation":false,"gdi-scaling":false,"segment-heap":false,"use-common-controls-v6":false}`, - }, - { - manifest: AppManifest{ - Compatibility: Win10AndAbove, - UIAccess: true, - DPIAwareness: DPIAware, - DisableWindowFiltering: true, - UltraHighResolutionScrollingAware: true, - PrinterDriverIsolation: true, - SegmentHeap: true, - }, - want:// language=json - `{"identity":{},"description":"","minimum-os":"win10","execution-level":"","ui-access":true,"auto-elevate":false,"dpi-awareness":"system","disable-theming":false,"disable-window-filtering":true,"high-resolution-scrolling-aware":false,"ultra-high-resolution-scrolling-aware":true,"long-path-aware":false,"printer-driver-isolation":true,"gdi-scaling":false,"segment-heap":true,"use-common-controls-v6":false}`, - }, - { - manifest: AppManifest{Compatibility: 42}, - wantErr: errUnknownSupportedOS, - }, - { - manifest: AppManifest{DPIAwareness: 42}, - wantErr: errUnknownDPIAwareness, - }, - { - manifest: AppManifest{ExecutionLevel: 42}, - wantErr: errUnknownExecLevel, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := json.Marshal(tt.manifest) - if !isErr(err, tt.wantErr) { - t.Errorf("json.Marshal(AppManifest) error = %v, wantErr %v", err, tt.wantErr) - return - } - if err == nil && string(got) != tt.want { - t.Errorf("json.Marshal(AppManifest):\n%s\nwant:\n%s", string(got), tt.want) - } - }) - } -} - -func isErr(err error, msg string) bool { - if err == nil && msg == "" { - return true - } - if msg == "*" { - return err != nil - } - for err != nil { - if err.Error() == msg { - return true - } - err = errors.Unwrap(err) - } - return false -} - -func Test_AppManifest_UnmarshalJSON(t *testing.T) { - tests := []struct { - name string - json string - want AppManifest - wantErr string - }{ - { - json:// language=json - `{}`, - want: AppManifest{}, - }, - { - json:// language=json - `{"identity":{},"description":"","minimum-os":"win7","execution-level":"","ui-access":false,"auto-elevate":false,"dpi-awareness":"system","disable-theming":false,"disable-window-filtering":false,"high-resolution-scrolling-aware":false,"ultra-high-resolution-scrolling-aware":false,"long-path-aware":false,"printer-driver-isolation":false,"gdi-scaling":false,"segment-heap":false,"use-common-controls-v6":false}`, - want: AppManifest{}, - }, - { - json:// language=json - `{"identity":{},"description":"","minimum-os":"win10","execution-level":"","ui-access":true,"auto-elevate":false,"dpi-awareness":"system","disable-theming":false,"disable-window-filtering":true,"high-resolution-scrolling-aware":false,"ultra-high-resolution-scrolling-aware":true,"long-path-aware":false,"printer-driver-isolation":true,"gdi-scaling":false,"segment-heap":true,"use-common-controls-v6":false}`, - want: AppManifest{ - Compatibility: Win10AndAbove, - UIAccess: true, - DPIAwareness: DPIAware, - DisableWindowFiltering: true, - UltraHighResolutionScrollingAware: true, - PrinterDriverIsolation: true, - SegmentHeap: true, - }, - }, - { - json:// language=json - `{"identity":{"name":"app.name","version":" 65535.65535.65535.65535 "},"description":"app desc","minimum-os":" Vista","execution-level":"administrator","ui-access":true,"auto-elevate":false,"dpi-awareness":"per monitor v2","disable-theming":true,"disable-window-filtering":false,"high-resolution-scrolling-aware":true,"ultra-high-resolution-scrolling-aware":false,"long-path-aware":true,"printer-driver-isolation":false,"gdi-scaling":true,"segment-heap":false,"use-common-controls-v6":true}`, - want: AppManifest{ - Identity: AssemblyIdentity{ - Name: "app.name", - Version: [4]uint16{65535, 65535, 65535, 65535}, - }, - Description: "app desc", - Compatibility: WinVistaAndAbove, - ExecutionLevel: RequireAdministrator, - UIAccess: true, - DPIAwareness: DPIPerMonitorV2, - DisableTheming: true, - HighResolutionScrollingAware: true, - LongPathAware: true, - GDIScaling: true, - UseCommonControlsV6: true, - }, - }, - { - json:// language=json - `{"identity":{"name":"app.name","version":" 1.2"},"minimum-os":"win8","execution-level":"highest","auto-elevate":true,"dpi-awareness":"per monitor","disable-window-filtering":true,"high-resolution-scrolling-aware":false,"ultra-high-resolution-scrolling-aware":true,"long-path-aware":false,"printer-driver-isolation":true,"gdi-scaling":false,"segment-heap":true,"use-common-controls-v6":false}`, - want: AppManifest{ - Identity: AssemblyIdentity{ - Name: "app.name", - Version: [4]uint16{1, 2}, - }, - Compatibility: Win8AndAbove, - ExecutionLevel: HighestAvailable, - DPIAwareness: DPIPerMonitor, - AutoElevate: true, - DisableWindowFiltering: true, - UltraHighResolutionScrollingAware: true, - PrinterDriverIsolation: true, - SegmentHeap: true, - }, - }, - { - json:// language=json - `{"identity":{"version":"1"},"dpi-awareness":"unaware","minimum-os":"win8.1","execution-level":"as invoker","segment-heap":true,"use-common-controls-v6":true}`, - want: AppManifest{ - Identity: AssemblyIdentity{ - Version: [4]uint16{1}, - }, - Compatibility: Win81AndAbove, - ExecutionLevel: AsInvoker, - DPIAwareness: DPIUnaware, - SegmentHeap: true, - UseCommonControlsV6: true, - }, - }, - { - json:// language=json - `{"identity":{"version":"1.65536.1.1"}}`, - wantErr: errInvalidVersion, - }, - { - json:// language=json - `{"identity":{"version":"1.1.1.1.0"}}`, - wantErr: errInvalidVersion, - }, - { - json:// language=json - `{"identity":{"version":"1.1.1. 1"}}`, - wantErr: errInvalidVersion, - }, - { - json:// language=json - `{"identity":{"version":"-1.1.1.1"}}`, - wantErr: errInvalidVersion, - }, - { - json:// language=json - `{"minimum-os":"linux"}`, - wantErr: errUnknownSupportedOS, - }, - { - json:// language=json - `{"execution-level":"root"}`, - wantErr: errUnknownExecLevel, - }, - { - json:// language=json - `{"dpi-awareness":"mostly"}`, - wantErr: errUnknownDPIAwareness, - }, - { - json: `{"":""]`, - wantErr: "*", - }, - { - json:// language=json - `{"identity":["app.name"]}`, - wantErr: "*", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var got AppManifest - err := json.Unmarshal([]byte(tt.json), &got) - if (tt.wantErr != "") != (err != nil) || (err != nil && tt.wantErr != "*" && err.Error() != tt.wantErr) { - t.Errorf("json.Unmarshal(AppManifest) error = %v, wantErr %v", err, tt.wantErr) - return - } - if err == nil && !reflect.DeepEqual(got, tt.want) { - t.Errorf("json.Unmarshal(AppManifest) got = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/pkgs/winres/pesum.go b/pkgs/winres/pesum.go deleted file mode 100644 index 3aab3f4..0000000 --- a/pkgs/winres/pesum.go +++ /dev/null @@ -1,41 +0,0 @@ -package winres - -type peCheckSum struct { - size uint32 - sum uint32 - rem uint32 -} - -// Sum returns the PE checksum of the PE file written with Write -func (c *peCheckSum) Sum() uint32 { - if c.size&1 != 0 { - return add16c(c.sum, c.rem) + c.size - } - return c.sum + c.size -} - -// Write implements the Writer interface to compute the -// PE checksum of a PE file. -// -// That file must have its CheckSum field set to zero. -func (c *peCheckSum) Write(p []byte) (int, error) { - l := uint32(len(p)) - o := c.size & 1 - m := (l-o)&^1 + o - if o != 0 && l != 0 { - c.sum = add16c(c.sum, uint32(p[0])<<8|c.rem) - } - for i := o; i < m; i += 2 { - c.sum = add16c(c.sum, uint32(p[i+1])<<8|uint32(p[i])) - } - if m < l { - c.rem = uint32(p[m]) - } - c.size += l - return len(p), nil -} - -func add16c(a uint32, b uint32) uint32 { - a += b - return (a + (a >> 16)) & 0xFFFF -} diff --git a/pkgs/winres/pesum_test.go b/pkgs/winres/pesum_test.go deleted file mode 100644 index c7dced1..0000000 --- a/pkgs/winres/pesum_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package winres - -import "testing" - -func Test_peCheckSum_Write(t *testing.T) { - w := peCheckSum{} - w.Write(nil) - w.Write([]byte{0x42}) - w.Write([]byte{0xAF, 0xE7, 0x50}) - if w.sum != 42 { - t.FailNow() - } - w.Write([]byte{0x10, 0xFF, 0xFF, 0xCD, 0x00, 0x50, 0xF0}) - w.Write([]byte{0xFF, 0xFF, 0x01, 0x82, 0xBF, 0x51}) - if w.sum != 0xDEAD || w.rem != 0x51 || w.Sum() != 0xDF0F { - t.FailNow() - } - w.Write([]byte{0}) - if w.Sum() != 0xDF10 { - t.FailNow() - } - w.Write([]byte{0x10, 0x20, 0xF7}) - if w.Sum() != 0x1B { - t.FailNow() - } - b := make([]byte, 0x12345) - for i := 0; i < 0x100; i++ { - w.Write(b) - } - if w.Sum() != 0x123451B { - t.FailNow() - } -} diff --git a/pkgs/winres/reader.go b/pkgs/winres/reader.go deleted file mode 100644 index f39f31d..0000000 --- a/pkgs/winres/reader.go +++ /dev/null @@ -1,25 +0,0 @@ -package winres - -import ( - "encoding/binary" - "io" -) - -// readFull is like io.ReadFull, except it always returns io.ErrUnexpectedEOF instead of io.EOF. -func readFull(r io.Reader, data []byte) error { - _, err := io.ReadFull(r, data) - if err == io.EOF { - return io.ErrUnexpectedEOF - } - return err -} - -// binaryRead is like binary.Read, except it always returns io.ErrUnexpectedEOF instead of io.EOF. -// Furthermore, it always uses binary.LittleEndian. -func binaryRead(r io.Reader, v interface{}) error { - err := binary.Read(r, binary.LittleEndian, v) - if err == io.EOF { - return io.ErrUnexpectedEOF - } - return err -} diff --git a/pkgs/winres/rsrc.go b/pkgs/winres/rsrc.go deleted file mode 100644 index daf302f..0000000 --- a/pkgs/winres/rsrc.go +++ /dev/null @@ -1,550 +0,0 @@ -package winres - -import ( - "bytes" - "encoding/binary" - "errors" - "io" - "sort" - "unicode/utf16" -) - -const ( - // Visual C++ pads resource data to 8 bytes. - // 4 bytes is a minimum. - dataAlignment = 8 -) - -// state is a temporary state used during the execution of ResourceSet.write() -type state struct { - offset int // offset to write in the next directory entry - relocAddr []int // addresses to add to the relocation table (addresses of the data entries) - nameOffset map[Name]int // offset of every name in the names string table - namesData []uint16 // final data for the names string table - orderedKeys []Identifier // ordered keys for ResourceSet.types - namesCount int // number of keys of type Name in ResourceSet.types -} - -// write writes the whole rsrc section's content: the directory and the actual data. -// It returns a slice of addresses for the relocation table. -// https://docs.microsoft.com/en-us/previous-versions/ms809762(v=msdn.10)#pe-file-resources -func (rs *ResourceSet) write(w io.Writer) ([]int, error) { - s := rs.prepare() - if err := rs.writeTypeDir(w, s); err != nil { - return nil, err - } - if err := rs.writeResDirs(w, s); err != nil { - return nil, err - } - if err := rs.writeLangDirs(w, s); err != nil { - return nil, err - } - // names will be inserted between the last directory level (data index) and actual data - s.offset += len(s.namesData) * 2 - if err := rs.writeDataIndex(w, s); err != nil { - return nil, err - } - if err := binary.Write(w, binary.LittleEndian, s.namesData); err != nil { - return nil, err - } - if err := rs.writeData(w, s); err != nil { - return nil, err - } - return append([]int{}, s.relocAddr...), nil -} - -// order orders identifiers in the whole resource set. -func (rs *ResourceSet) order(s *state) { - s.orderedKeys = make([]Identifier, 0, len(rs.types)) - for ident, te := range rs.types { - s.orderedKeys = append(s.orderedKeys, ident) - te.order() - } - - // Names in case sensitive ascending order, then IDs in ascending order - sort.Slice(s.orderedKeys, func(i, j int) bool { - return s.orderedKeys[i].lessThan(s.orderedKeys[j]) - }) - - // Count Names by searching the first ID - s.namesCount = sort.Search(len(s.orderedKeys), func(i int) bool { - _, ok := s.orderedKeys[i].(ID) - return ok - }) -} - -// prepare calculates the names index offset and content, as this has to be known before writing the resource directory. -// It also freezes the order of identifiers in the directory, so that subsequent write functions can rely on it. -func (rs *ResourceSet) prepare() *state { - var ( - nameSet = make(map[Name]struct{}) - typeNameSet = make(map[Name]struct{}) - s = &state{ - nameOffset: make(map[Name]int), - } - ) - - rs.order(s) - - for ident, te := range rs.types { - if name, ok := ident.(Name); ok { - typeNameSet[name] = struct{}{} - nameSet[name] = struct{}{} - } - for _, name := range te.orderedKeys[:te.namesCount] { - nameSet[name.(Name)] = struct{}{} - } - } - - names := make([]string, 0, len(nameSet)) - for n := range nameSet { - names = append(names, string(n)) - } - sort.Strings(names) - - offset := rs.dirSize() - for _, n := range names { - s.nameOffset[Name(n)] = offset - u := utf16.Encode([]rune(n)) - s.namesData = append(s.namesData, uint16(len(u))) - s.namesData = append(s.namesData, u...) - offset += (len(u) + 1) * 2 - } - - // Names must be padded to align the resource data. - sz := len(s.namesData) * 2 - s.namesData = append(s.namesData, make([]uint16, (alignData(sz)-sz)/2)...) - return s -} - -// writeTypeDir is where we write the resource type directory (1st level: type) -func (rs *ResourceSet) writeTypeDir(w io.Writer, s *state) error { - if err := writeDirectoryTable(w, s.namesCount, len(s.orderedKeys)-s.namesCount); err != nil { - return err - } - s.offset += sizeOfDirTable + len(s.orderedKeys)*sizeOfDirEntry - for _, ident := range s.orderedKeys[:s.namesCount] { - if err := writeDirectoryEntry(w, s.nameOffset[ident.(Name)], s.offset, true, true); err != nil { - return err - } - s.offset += rs.types[ident].size() - } - for _, ident := range s.orderedKeys[s.namesCount:] { - if err := writeDirectoryEntry(w, int(ident.(ID)), s.offset, false, true); err != nil { - return err - } - s.offset += rs.types[ident].size() - } - return nil -} - -// writeResDirs is where we write the resource directory (2nd level: type->resource) -func (rs *ResourceSet) writeResDirs(w io.Writer, s *state) error { - for _, tid := range s.orderedKeys { - if err := rs.types[tid].write(w, s); err != nil { - return err - } - } - return nil -} - -// writeLangDirs is where we write the localized resource directory (3rd level: type->resource->locale) -func (rs *ResourceSet) writeLangDirs(w io.Writer, s *state) error { - for _, tid := range s.orderedKeys { - te := rs.types[tid] - for _, ident := range te.orderedKeys { - if err := te.resources[ident].write(w, s); err != nil { - return err - } - } - } - return nil -} - -// writeDataIndex is where we write the resource data index (4th level: type->resource->locale->data address) -func (rs *ResourceSet) writeDataIndex(w io.Writer, s *state) error { - for _, tid := range s.orderedKeys { - te := rs.types[tid] - for _, rid := range te.orderedKeys { - re := te.resources[rid] - for _, lcid := range re.orderedKeys { - if err := re.data[lcid].write(w, s); err != nil { - return err - } - } - } - } - return nil -} - -// writeData is where we write the actual resource data (6th part, 5th being names) -func (rs *ResourceSet) writeData(w io.Writer, s *state) error { - for _, tid := range s.orderedKeys { - te := rs.types[tid] - for _, rid := range te.orderedKeys { - re := te.resources[rid] - for _, lcid := range re.orderedKeys { - if err := re.data[lcid].writeData(w); err != nil { - return err - } - } - } - } - return nil -} - -// fullSize returns the full size of the rsrc section's content: the directory, the names, and the actual data. -func (rs *ResourceSet) fullSize() int { - s := rs.prepare() - sz := rs.dirSize() + len(s.namesData)*2 - for _, te := range rs.types { - for _, re := range te.resources { - for _, de := range re.data { - sz += de.paddedDataSize() - } - } - } - return sz -} - -// dirSize returns the size of the rsrc section's directory. -func (rs *ResourceSet) dirSize() int { - sz := sizeOfDirTable + len(rs.types)*sizeOfDirEntry - for _, te := range rs.types { - sz += te.size() - for _, re := range te.resources { - sz += re.size() + len(re.data)*sizeOfDataEntry - } - } - return sz -} - -// numDataEntries returns the number of resource data entries to be written into the rsrc section. -func (rs *ResourceSet) numDataEntries() int { - var n int - for _, te := range rs.types { - for _, re := range te.resources { - n += len(re.data) - } - } - return n -} - -type typeEntry struct { - resources map[Identifier]*resourceEntry - orderedKeys []Identifier - namesCount int -} - -// size returns the size of the entry's directory table -func (te *typeEntry) size() int { - return sizeOfDirTable + len(te.resources)*sizeOfDirEntry -} - -// order orders ids and names following the specification, and saves them in orderedKeys. -// It calls resourceEntry.order() too, for each resource entry it owns. -func (te *typeEntry) order() { - if te.orderedKeys != nil { - return - } - - te.orderedKeys = make([]Identifier, 0, len(te.resources)) - for ident, re := range te.resources { - te.orderedKeys = append(te.orderedKeys, ident) - - // Order LCIDs in every resource - re.order() - } - - // Names in case sensitive ascending order, then IDs in ascending order - sort.Slice(te.orderedKeys, func(i, j int) bool { - return te.orderedKeys[i].lessThan(te.orderedKeys[j]) - }) - - // Count Names by searching the first ID - te.namesCount = sort.Search(len(te.orderedKeys), func(i int) bool { - _, ok := te.orderedKeys[i].(ID) - return ok - }) -} - -// write writes the entry's directory table -func (te *typeEntry) write(w io.Writer, s *state) error { - if err := writeDirectoryTable(w, te.namesCount, len(te.orderedKeys)-te.namesCount); err != nil { - return err - } - for _, ident := range te.orderedKeys[:te.namesCount] { - if err := writeDirectoryEntry(w, s.nameOffset[ident.(Name)], s.offset, true, true); err != nil { - return err - } - s.offset += te.resources[ident].size() - } - for _, ident := range te.orderedKeys[te.namesCount:] { - if err := writeDirectoryEntry(w, int(ident.(ID)), s.offset, false, true); err != nil { - return err - } - s.offset += te.resources[ident].size() - } - return nil -} - -type resourceEntry struct { - data map[ID]*dataEntry - orderedKeys []ID -} - -// size returns the size of the entry's directory table -func (re *resourceEntry) size() int { - return sizeOfDirTable + len(re.data)*sizeOfDirEntry -} - -// order orders LCIDs and saves them in orderedKeys. -func (re *resourceEntry) order() { - if re.orderedKeys != nil { - return - } - - re.orderedKeys = make([]ID, 0, len(re.data)) - for id := range re.data { - re.orderedKeys = append(re.orderedKeys, id) - } - - // LCIDs in ascending order - sort.Slice(re.orderedKeys, func(i, j int) bool { - return re.orderedKeys[i].lessThan(re.orderedKeys[j]) - }) -} - -// write writes the entry's directory table -func (re *resourceEntry) write(w io.Writer, s *state) error { - if err := writeDirectoryTable(w, 0, len(re.data)); err != nil { - return err - } - for _, lcid := range re.orderedKeys { - s.relocAddr = append(s.relocAddr, s.offset) - if err := writeDirectoryEntry(w, int(lcid), s.offset, false, false); err != nil { - return err - } - s.offset += sizeOfDataEntry - } - return nil -} - -type dataEntry struct { - data []byte -} - -func alignData(offset int) int { - return (offset + dataAlignment - 1) &^ (dataAlignment - 1) -} - -// paddedDataSize returns the room taken by data, including some padding at the end. -func (de *dataEntry) paddedDataSize() int { - return alignData(len(de.data)) -} - -func (de *dataEntry) write(w io.Writer, s *state) error { - if err := writeDataEntry(w, s.offset, len(de.data)); err != nil { - return err - } - // Everything must be aligned, so we may skip a few byte when necessary after each resource data - s.offset += de.paddedDataSize() - return nil -} - -func (de *dataEntry) writeData(w io.Writer) error { - size, err := w.Write(de.data) - if err != nil { - return err - } - // Everything must be aligned, so we may skip a few byte when necessary after each resource data - b := [dataAlignment]byte{} - _, err = w.Write(b[:de.paddedDataSize()-size]) - return err -} - -// Binary format of the directory : - -// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#the-rsrc-section -// https://docs.microsoft.com/en-us/previous-versions/ms809762(v=msdn.10)#pe-file-resources - -type resourceDirectoryTable struct { - Characteristics uint32 - TimeDateStamp uint32 - MajorVersion uint16 - MinorVersion uint16 - NumberOfNameEntries uint16 - NumberOfIDEntries uint16 -} - -const sizeOfDirTable = 16 - -func writeDirectoryTable(w io.Writer, numNameEntries int, numIDEntries int) error { - return binary.Write(w, binary.LittleEndian, resourceDirectoryTable{ - NumberOfNameEntries: uint16(numNameEntries), - NumberOfIDEntries: uint16(numIDEntries), - }) -} - -type resourceDirectoryIDEntry struct { - ID uint32 - Offset uint32 -} - -const sizeOfDirEntry = 8 - -const subdirectoryOffset = 0x80000000 -const nameOffset = 0x80000000 - -func writeDirectoryEntry(w io.Writer, id int, offset int, isName bool, isSubDir bool) error { - e := resourceDirectoryIDEntry{ - ID: uint32(id), - Offset: uint32(offset), - } - if isSubDir { - e.Offset |= subdirectoryOffset - } - if isName { - e.ID |= nameOffset - } - return binary.Write(w, binary.LittleEndian, &e) -} - -type resourceDataEntry struct { - DataRVA uint32 - Size uint32 - Codepage uint32 - Reserved uint32 -} - -const sizeOfDataEntry = 16 - -func writeDataEntry(w io.Writer, offset int, dataSize int) error { - return binary.Write(w, binary.LittleEndian, resourceDataEntry{ - DataRVA: uint32(offset), - Size: uint32(dataSize), - Codepage: uint32(0), // String tables being encoded in utf-16, this field is useless. Visual studio sets it to zero. - }) -} - -// Reading functions: - -func (rs *ResourceSet) read(section []byte, baseAddress uint32, typeID Identifier) error { - r := bytes.NewReader(section) - return dirEntry{}.walk(r, func(typeEntry dirEntry) error { - if typeID != ID(0) && - typeEntry.ident != typeID && - (typeEntry.ident != RT_ICON || typeID != RT_GROUP_ICON) && - (typeEntry.ident != RT_CURSOR || typeID != RT_GROUP_CURSOR) { - return nil - } - return typeEntry.walk(r, func(resourceEntry dirEntry) error { - resourceEntry.leaf = true - return resourceEntry.walk(r, func(langEntry dirEntry) error { - data, err := langEntry.readData(r, baseAddress) - if err != nil { - return err - } - return rs.Set(typeEntry.ident, resourceEntry.ident, uint16(langEntry.ident.(ID)), data) - }) - }) - }) -} - -type dirEntry struct { - path string - ident Identifier - offset int64 - leaf bool -} - -func (de dirEntry) walk(section *bytes.Reader, fn func(dirEntry) error) error { - entries, err := de.readDirTable(section) - if err != nil { - return err - } - - for _, entry := range entries { - err = fn(entry) - if err != nil { - return err - } - } - return nil -} - -func (de dirEntry) readData(section *bytes.Reader, baseAddress uint32) ([]byte, error) { - section.Seek(de.offset, io.SeekStart) - - entry := resourceDataEntry{} - err := binaryRead(section, &entry) - if err != nil { - return nil, err - } - - offset := int64(entry.DataRVA) - int64(baseAddress) - if offset < 0 || offset+int64(entry.Size) > section.Size() { - return nil, errors.New(errDataEntryOutOfBounds) - } - - data := make([]byte, entry.Size) - section.Seek(offset, io.SeekStart) - section.Read(data) - - return data, nil -} - -func (de dirEntry) readDirTable(section *bytes.Reader) ([]dirEntry, error) { - section.Seek(de.offset, io.SeekStart) - - table := resourceDirectoryTable{} - err := binaryRead(section, &table) - if err != nil { - return nil, err - } - - dir := make([]resourceDirectoryIDEntry, table.NumberOfNameEntries+table.NumberOfIDEntries) - err = binaryRead(section, &dir) - if err != nil { - return nil, err - } - - entries := make([]dirEntry, len(dir)) - for i := range dir { - if de.leaf != (dir[i].Offset&subdirectoryOffset == 0) { - return nil, errors.New(errInvalidResDir) - } - if de.leaf && dir[i].ID&nameOffset != 0 { - return nil, errors.New(errInvalidResDir) - } - entries[i].offset = int64(dir[i].Offset &^ subdirectoryOffset) - if dir[i].ID&nameOffset == 0 { - entries[i].ident = ID(dir[i].ID) - } else { - entries[i].ident, err = readName(section, dir[i].ID&^nameOffset) - if err != nil { - return nil, err - } - } - } - - return entries, nil -} - -func readName(section *bytes.Reader, offset uint32) (Name, error) { - section.Seek(int64(offset), io.SeekStart) - - var length uint16 - err := binaryRead(section, &length) - if err != nil { - return "", err - } - - b := make([]uint16, length) - err = binaryRead(section, b) - if err != nil { - return "", err - } - - return Name(utf16.Decode(b)), nil -} diff --git a/pkgs/winres/rsrc_test.go b/pkgs/winres/rsrc_test.go deleted file mode 100644 index d26e850..0000000 --- a/pkgs/winres/rsrc_test.go +++ /dev/null @@ -1,304 +0,0 @@ -package winres - -import ( - "bytes" - "io" - "testing" -) - -func Test_ResourceSet_write_WriteErr(t *testing.T) { - rs := ResourceSet{} - rs.Set(Name("NAME"), Name("NAME"), 0, make([]byte, 6)) - const writeErrMsg = "expected write error, got" - - if _, err := rs.write(newBadWriter(15)); !isExpectedWriteErr(err) { - t.Fatal(writeErrMsg, err) - } - - if _, err := rs.write(newBadWriter(23)); !isExpectedWriteErr(err) { - t.Fatal(writeErrMsg, err) - } - - if _, err := rs.write(newBadWriter(39)); !isExpectedWriteErr(err) { - t.Fatal(writeErrMsg, err) - } - - if _, err := rs.write(newBadWriter(47)); !isExpectedWriteErr(err) { - t.Fatal(writeErrMsg, err) - } - - if _, err := rs.write(newBadWriter(63)); !isExpectedWriteErr(err) { - t.Fatal(writeErrMsg, err) - } - - if _, err := rs.write(newBadWriter(71)); !isExpectedWriteErr(err) { - t.Fatal(writeErrMsg, err) - } - - if _, err := rs.write(newBadWriter(87)); !isExpectedWriteErr(err) { - t.Fatal(writeErrMsg, err) - } - - if _, err := rs.write(newBadWriter(103)); !isExpectedWriteErr(err) { - t.Fatal(writeErrMsg, err) - } - - if _, err := rs.write(newBadWriter(111)); !isExpectedWriteErr(err) { - t.Fatal(writeErrMsg, err) - } - - rs.Set(ID(1), ID(1), 0, make([]byte, 6)) - - if _, err := rs.write(newBadWriter(31)); !isExpectedWriteErr(err) { - t.Fatal(writeErrMsg, err) - } - - if _, err := rs.write(newBadWriter(79)); !isExpectedWriteErr(err) { - t.Fatal(writeErrMsg, err) - } -} - -func Test_dataEntry_writeData(t *testing.T) { - expected := [][]byte{ - {}, - {1, 0, 0, 0, 0, 0, 0, 0}, - {1, 2, 0, 0, 0, 0, 0, 0}, - {1, 2, 3, 0, 0, 0, 0, 0}, - {1, 2, 3, 4, 0, 0, 0, 0}, - {1, 2, 3, 4, 5, 0, 0, 0}, - {1, 2, 3, 4, 5, 6, 0, 0}, - {1, 2, 3, 4, 5, 6, 7, 0}, - {1, 2, 3, 4, 5, 6, 7, 8}, - {1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0}, - {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 0, 0, 0, 0, 0}, - {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 0, 0, 0, 0}, - {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 0, 0, 0, 0}, - {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 0, 0, 0}, - {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 0, 0}, - {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0}, - {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, - } - - for i := 0; i <= 16; i++ { - de := dataEntry{data: make([]byte, i)} - for j := 0; j < i; j++ { - de.data[j] = byte(j + 1) - } - buf := &bytes.Buffer{} - - err := de.writeData(buf) - if err != nil || !bytes.Equal(buf.Bytes(), expected[i]) { - t.Fail() - } - err = de.writeData(newBadWriter(i - 1)) - if !isExpectedWriteErr(err) { - t.Fatal("expected write error, got", err) - } - } -} - -func TestResourceSet_read1(t *testing.T) { - rs := ResourceSet{} - err := rs.read(loadBinary(t, "rsrc1.bin"), 0xE6B000, ID(0)) - if err != nil || rs.lastIconID != 12 || rs.lastCursorID != 5 { - t.Fatal(err) - } - buf := &bytes.Buffer{} - rs.write(buf) - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_read_RT_ICON(t *testing.T) { - rs := ResourceSet{} - err := rs.read(loadBinary(t, "rsrc1.bin"), 0xE6B000, RT_ICON) - if err != nil || rs.lastIconID != 12 || rs.lastCursorID != 0 { - t.Fatal(err) - } - buf := &bytes.Buffer{} - rs.write(buf) - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_read_RT_GROUP_ICON(t *testing.T) { - rs := ResourceSet{} - err := rs.read(loadBinary(t, "rsrc1.bin"), 0xE6B000, RT_GROUP_ICON) - if err != nil || rs.lastIconID != 12 || rs.lastCursorID != 0 { - t.Fatal(err) - } - buf := &bytes.Buffer{} - rs.write(buf) - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_read_RT_CURSOR(t *testing.T) { - rs := ResourceSet{} - err := rs.read(loadBinary(t, "rsrc1.bin"), 0xE6B000, RT_CURSOR) - if err != nil || rs.lastIconID != 0 || rs.lastCursorID != 5 { - t.Fatal(err) - } - buf := &bytes.Buffer{} - rs.write(buf) - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_read_RT_GROUP_CURSOR(t *testing.T) { - rs := ResourceSet{} - err := rs.read(loadBinary(t, "rsrc1.bin"), 0xE6B000, RT_GROUP_CURSOR) - if err != nil || rs.lastIconID != 0 || rs.lastCursorID != 5 { - t.Fatal(err) - } - buf := &bytes.Buffer{} - rs.write(buf) - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_read_PNG(t *testing.T) { - rs := ResourceSet{} - err := rs.read(loadBinary(t, "rsrc1.bin"), 0xE6B000, Name("PNG")) - if err != nil || rs.lastIconID != 0 || rs.lastCursorID != 0 { - t.Fatal(err) - } - buf := &bytes.Buffer{} - rs.write(buf) - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_read_EOF1(t *testing.T) { - rs := ResourceSet{} - err := rs.read([]byte{}, 0x42, ID(0)) - if err != io.ErrUnexpectedEOF { - t.Fail() - } -} - -func TestResourceSet_read_EOF2(t *testing.T) { - rs := ResourceSet{} - err := rs.read([]byte{0xC: 1, 0x16: 0}, 0x42, ID(0)) - if err != io.ErrUnexpectedEOF { - t.Fail() - } -} - -func TestResourceSet_read_ErrNode(t *testing.T) { - rs := ResourceSet{} - err := rs.read([]byte{0xC: 1, 0x13: 0x80, 0x17: 0}, 0x42, ID(0)) - if err == nil || err.Error() != errInvalidResDir { - t.Fail() - } -} - -func TestResourceSet_read_ErrLeaf(t *testing.T) { - rs := ResourceSet{} - err := rs.read([]byte{0xC: 1, 0x17: 0x80}, 0x42, ID(0)) - if err == nil || err.Error() != errInvalidResDir { - t.Fail() - } -} - -func TestResourceSet_read_ErrLeafName(t *testing.T) { - rs := ResourceSet{} - err := rs.read([]byte{ - 0xE: 1, - 0x14: 0x18, - 0x17: 0x80, - 0x26: 1, - 0x2C: 0x30, - 0x2F: 0x80, - 0x3E: 1, - 0x43: 0x80, - 0x44: 0x48, - 0x47: 0, - }, 0x42, ID(0)) - if err == nil || err.Error() != errInvalidResDir { - t.Fail() - } -} - -func TestResourceSet_read_ErrEOF3(t *testing.T) { - rs := ResourceSet{} - err := rs.read([]byte{ - 0xC: 1, - 0x10: 0xFF, - 0x13: 0x80, - 0x14: 0x18, - 0x17: 0x80, - 0x26: 1, - 0x2C: 0x30, - 0x2F: 0x80, - 0x3E: 1, - 0x44: 0x48, - 0x47: 0, - }, 0x42, ID(0)) - if err != io.ErrUnexpectedEOF { - t.Fail() - } -} - -func TestResourceSet_read_ErrEOF4(t *testing.T) { - rs := ResourceSet{} - err := rs.read([]byte{ - 0xC: 1, - 0x10: 0x44, - 0x13: 0x80, - 0x14: 0x18, - 0x17: 0x80, - 0x26: 1, - 0x2C: 0x30, - 0x2F: 0x80, - 0x3E: 1, - 0x44: 0x48, - 0x47: 0, - }, 0x42, ID(0)) - if err != io.ErrUnexpectedEOF { - t.Fail() - } -} - -func TestResourceSet_read_ErrEOF5(t *testing.T) { - rs := ResourceSet{} - err := rs.read([]byte{ - 0xE: 1, - 0x10: 0x01, - 0x14: 0x18, - 0x17: 0x80, - 0x26: 1, - 0x2C: 0x30, - 0x2F: 0x80, - 0x3E: 1, - 0x44: 0x48, - 0x47: 0, - }, 0x42, ID(0)) - if err != io.ErrUnexpectedEOF { - t.Fail() - } -} - -func TestResourceSet_read_DataOutOfBounds1(t *testing.T) { - rs := ResourceSet{} - err := rs.read([]byte{ - 0xE: 1, - 0x10: 1, - 0x14: 0x18, - 0x17: 0x80, - 0x26: 1, - 0x2C: 0x30, - 0x2F: 0x80, - 0x3E: 1, - 0x44: 0x48, - 0x48: 0x58, - 0x49: 0x10, - 0x4C: 0x10, - 0x5D: 0, - }, 0x1000, ID(0)) - if err == nil || err.Error() != errDataEntryOutOfBounds { - t.Fail() - } -} - -func TestResourceSet_read_DataOutOfBounds2(t *testing.T) { - rs := ResourceSet{} - err := rs.read(loadBinary(t, "rsrc1.bin"), 0x42, ID(0)) - if err == nil || err.Error() != errDataEntryOutOfBounds { - t.Fail() - } -} diff --git a/pkgs/winres/version/binary.go b/pkgs/winres/version/binary.go deleted file mode 100644 index ebcb25d..0000000 --- a/pkgs/winres/version/binary.go +++ /dev/null @@ -1,413 +0,0 @@ -package version - -// In this file are functions to convert an Info structure to/from its binary representation. -// https://docs.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "io" - "sort" - "unicode/utf16" -) - -const codePageUTF16LE = 1200 - -type nodeHeader struct { - Length uint16 - ValueLength uint16 - Type uint16 -} - -const sizeOfNodeHeader = 6 - -// https://docs.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo -type _VS_FIXEDFILEINFO struct { - Signature uint32 - StrucVersion uint32 - FileVersionMS uint32 - FileVersionLS uint32 - ProductVersionMS uint32 - ProductVersionLS uint32 - FileFlagsMask uint32 - FileFlags uint32 - FileOS uint32 - FileType uint32 - FileSubtype uint32 - FileDateMS uint32 - FileDateLS uint32 -} - -const sizeOfFixedFileInfo = 52 - -const fixedFileInfoSignature = 0xFEEF04BD -const fixedFileInfoVersion = 0x10000 - -const ( - _VS_FF_DEBUG = 0x01 - _VS_FF_PRERELEASE = 0x02 - _VS_FF_PATCHED = 0x04 - _VS_FF_PRIVATEBUILD = 0x08 - _VS_FF_SPECIALBUILD = 0x20 - _VS_FF_MASK = 0x3F -) - -const ( - _VOS_NT_WINDOWS32 = 0x040004 -) - -const ( - _VFT_UNKNOWN = 0 - _VFT_APP = 1 - _VFT_DLL = 2 -) - -const ( - vsVersionInfo = "VS_VERSION_INFO" - stringFileInfo = "StringFileInfo" - varFileInfo = "VarFileInfo" - translation = "Translation" -) - -func (vi *Info) bytes() []byte { - buf := &bytes.Buffer{} - writeStructAligned(buf, vi.fixedFileInfo()) - sfi := stringFileInfoBytes(&vi.lt) - writeAligned(buf, sfi) - vfi := varFileInfoBytes(&vi.lt) - writeAligned(buf, vfi) - return nodeBytes(false, vsVersionInfo, buf.Bytes(), sizeOfFixedFileInfo) -} - -func (vi *Info) fixedFileInfo() _VS_FIXEDFILEINFO { - ffi := _VS_FIXEDFILEINFO{ - Signature: fixedFileInfoSignature, - StrucVersion: fixedFileInfoVersion, - FileVersionMS: uint32(vi.FileVersion[0])<<16 | uint32(vi.FileVersion[1]), - FileVersionLS: uint32(vi.FileVersion[2])<<16 | uint32(vi.FileVersion[3]), - ProductVersionMS: uint32(vi.ProductVersion[0])<<16 | uint32(vi.ProductVersion[1]), - ProductVersionLS: uint32(vi.ProductVersion[2])<<16 | uint32(vi.ProductVersion[3]), - FileFlagsMask: _VS_FF_MASK, - FileOS: _VOS_NT_WINDOWS32, - } - if vi.Flags.Debug { - ffi.FileFlags |= _VS_FF_DEBUG - } - if vi.Flags.Patched { - ffi.FileFlags |= _VS_FF_PATCHED - } - if vi.Flags.Prerelease { - ffi.FileFlags |= _VS_FF_PRERELEASE - } - if vi.Flags.PrivateBuild { - ffi.FileFlags |= _VS_FF_PRIVATEBUILD - } - if vi.Flags.SpecialBuild { - ffi.FileFlags |= _VS_FF_SPECIALBUILD - } - switch vi.Type { - case App: - ffi.FileType = _VFT_APP - case DLL: - ffi.FileType = _VFT_DLL - default: - ffi.FileType = _VFT_UNKNOWN - } - ffi.FileDateMS, ffi.FileDateLS = timeToTimestamp(vi.Timestamp) - return ffi -} - -func stringFileInfoBytes(lt *langTable) []byte { - buf := &bytes.Buffer{} - for _, langID := range lt.sortedKeys() { - b := stringTableBytes(langID, (*lt)[langID]) - writeAligned(buf, b) - } - return nodeBytes(true, stringFileInfo, buf.Bytes(), 0) -} - -func stringTableBytes(langID uint16, strings *stringTable) []byte { - buf := &bytes.Buffer{} - for _, k := range strings.sortedKeys() { - b := stringBytes(k, (*strings)[k]) - writeAligned(buf, b) - } - return nodeBytes(true, fmt.Sprintf("%08x", uint32(langID)<<16|codePageUTF16LE), buf.Bytes(), 0) -} - -func stringBytes(key string, value string) []byte { - wValue := utf16.Encode([]rune(value + "\x00")) - buf := &bytes.Buffer{} - binary.Write(buf, binary.LittleEndian, wValue) - return nodeBytes(true, key, buf.Bytes(), len(wValue)) -} - -func varFileInfoBytes(lt *langTable) []byte { - buf := &bytes.Buffer{} - var langs []uint32 - for _, langID := range lt.sortedKeys() { - langs = append(langs, codePageUTF16LE<<16|uint32(langID)) - } - b := varBytes(langs) - writeAligned(buf, b) - return nodeBytes(true, varFileInfo, buf.Bytes(), 0) -} - -func varBytes(langs []uint32) []byte { - buf := &bytes.Buffer{} - binary.Write(buf, binary.LittleEndian, langs) - return nodeBytes(false, translation, buf.Bytes(), buf.Len()) -} - -func nodeBytes(text bool, key string, value []byte, valueLength int) []byte { - wKey := utf16.Encode([]rune(key + "\x00")) - hdr := nodeHeader{} - hdr.Length = uint16(sizeOfNodeHeader + len(wKey)*2 + len(value)) - if len(wKey)&1 == 0 { - hdr.Length += 2 - } - hdr.ValueLength = uint16(valueLength) - if text { - hdr.Type = 1 - } - - buf := bytes.NewBuffer(make([]byte, 0, hdr.Length)) - binary.Write(buf, binary.LittleEndian, hdr) - binary.Write(buf, binary.LittleEndian, wKey) - writeAligned(buf, value) - return buf.Bytes() -} - -func align(s int) int { - return (s + 3) &^ 3 -} - -func writeAligned(buffer *bytes.Buffer, data []byte) { - var pad [4]byte - s := buffer.Len() - p := align(s) - s - buffer.Write(pad[:p]) - buffer.Write(data) -} - -func writeStructAligned(buffer *bytes.Buffer, data interface{}) { - var pad [4]byte - s := buffer.Len() - p := align(s) - s - buffer.Write(pad[:p]) - binary.Write(buffer, binary.LittleEndian, data) -} - -func (st *stringTable) sortedKeys() []string { - keys := make([]string, 0, len(*st)) - for k := range *st { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -func (lt *langTable) sortedKeys() []uint16 { - keys := make([]int, 0, len(*lt)) - for k := range *lt { - keys = append(keys, int(k)) - } - sort.Ints(keys) - keysU16 := make([]uint16, len(*lt)) - for i, v := range keys { - keysU16[i] = uint16(v) - } - return keysU16 -} - -func fromBytes(data []byte) (*Info, error) { - vi := &Info{} - _, err := vi.readNode(nil, data) - if err != nil { - return nil, err - } - return vi, nil -} - -// Returns the number of bytes read -func (vi *Info) readNode(parent interface{}, data []byte) (int, error) { - n, pos, err := readNodeHeader(data) - if err != nil { - return 0, err - } - if n.Length < sizeOfNodeHeader || int(n.Length) > len(data) { - return 0, errors.New(errInvalidLength) - } - data = data[:n.Length] - - key, err := readString(data, &pos) - if err != nil { - return 0, err - } - - if n.ValueLength > 0 && align(pos)+int(n.ValueLength) > len(data) { - return 0, io.ErrUnexpectedEOF - } - - switch true { - case parent == nil && key == vsVersionInfo: - if n.ValueLength >= sizeOfFixedFileInfo { - err := vi.readFixedStruct(data[align(pos):]) - if err != nil { - return 0, err - } - } - pos += int(n.ValueLength) - err := vi.readChildren(key, data, &pos) - if err != nil { - return 0, err - } - - case parent == vsVersionInfo && key == varFileInfo: - // skip these nodes - break - - case parent == vsVersionInfo && key == stringFileInfo: - err := vi.readChildren(key, data, &pos) - if err != nil { - return 0, err - } - - case parent == stringFileInfo: - var langID, codePage uint16 - _, err := fmt.Sscanf(key, "%04x%04x", &langID, &codePage) - if err != nil { - return 0, errors.New(errInvalidLangID) - } - if codePage != codePageUTF16LE { - return 0, errors.New(errUnhandledCodePage) - } - err = vi.readChildren(langID, data, &pos) - if err != nil { - return 0, err - } - - default: - // Under a language ID, this is a key/value pair - if id, ok := parent.(uint16); ok { - pos = align(pos) - value, err := readStringWithLength(data, &pos, int(n.ValueLength)) - if err != nil { - return 0, err - } - vi.Set(id, key, value) - } - } - - return len(data), nil -} - -func (vi *Info) readChildren(parent interface{}, data []byte, pos *int) error { - for align(*pos) < len(data) { - *pos = align(*pos) - offset, err := vi.readNode(parent, data[*pos:]) - if err != nil { - return err - } - *pos += offset - } - return nil -} - -func (vi *Info) readFixedStruct(data []byte) error { - fixed := _VS_FIXEDFILEINFO{} - // No error handling because the caller guaranteed "data" is long enough - _ = binaryRead(bytes.NewReader(data), &fixed) - if fixed.Signature != fixedFileInfoSignature { - return errors.New(errInvalidSignature) - } - switch fixed.FileType { - case _VFT_APP: - vi.Type = App - case _VFT_DLL: - vi.Type = DLL - default: - vi.Type = Unknown - } - flags := fixed.FileFlags & _VS_FF_MASK - vi.Flags.Debug = flags&_VS_FF_DEBUG != 0 - vi.Flags.Prerelease = flags&_VS_FF_PRERELEASE != 0 - vi.Flags.Patched = flags&_VS_FF_PATCHED != 0 - vi.Flags.PrivateBuild = flags&_VS_FF_PRIVATEBUILD != 0 - vi.Flags.SpecialBuild = flags&_VS_FF_SPECIALBUILD != 0 - vi.FileVersion[0] = uint16(fixed.FileVersionMS >> 16) - vi.FileVersion[1] = uint16(fixed.FileVersionMS) - vi.FileVersion[2] = uint16(fixed.FileVersionLS >> 16) - vi.FileVersion[3] = uint16(fixed.FileVersionLS) - vi.ProductVersion[0] = uint16(fixed.ProductVersionMS >> 16) - vi.ProductVersion[1] = uint16(fixed.ProductVersionMS) - vi.ProductVersion[2] = uint16(fixed.ProductVersionLS >> 16) - vi.ProductVersion[3] = uint16(fixed.ProductVersionLS) - vi.Timestamp = timestampToTime(fixed.FileDateMS, fixed.FileDateLS) - return nil -} - -func readString(data []byte, pos *int) (string, error) { - data = data[*pos:] - - var length int - - for i := 1; i < len(data); i += 2 { - if data[i-1] == 0 && data[i] == 0 { - length = (i - 1) / 2 - break - } - } - if length == 0 { - *pos = len(data) - return "", io.ErrUnexpectedEOF - } - - wKey := make([]uint16, length) - for i := 0; i < length; i++ { - wKey[i] = uint16(data[i*2+1])<<8 | uint16(data[i*2]) - } - *pos += (length + 1) * 2 - return string(utf16.Decode(wKey)), nil -} - -func readStringWithLength(data []byte, pos *int, length int) (string, error) { - data = data[*pos:] - - if length == 0 || length > len(data)/2 { - return "", errors.New(errInvalidStringLength) - } - - length-- - wString := make([]uint16, length) - for i := 0; i < length; i++ { - wString[i] = uint16(data[i*2+1])<<8 | uint16(data[i*2]) - } - *pos += (length + 1) * 2 - - return string(utf16.Decode(wString)), nil -} - -func readNodeHeader(data []byte) (nodeHeader, int, error) { - if len(data) < sizeOfNodeHeader { - return nodeHeader{}, 0, io.ErrUnexpectedEOF - } - n := nodeHeader{ - Length: uint16(data[1])<<8 | uint16(data[0]), - ValueLength: uint16(data[3])<<8 | uint16(data[2]), - Type: uint16(data[5])<<8 | uint16(data[4]), - } - return n, sizeOfNodeHeader, nil -} - -// binaryRead is like binary.Read, except it always returns io.ErrUnexpectedEOF instead of io.EOF. -// Furthermore, it always uses binary.LittleEndian. -func binaryRead(r io.Reader, v interface{}) error { - err := binary.Read(r, binary.LittleEndian, v) - if err == io.EOF { - return io.ErrUnexpectedEOF - } - return err -} diff --git a/pkgs/winres/version/binary_test.go b/pkgs/winres/version/binary_test.go deleted file mode 100644 index 2489215..0000000 --- a/pkgs/winres/version/binary_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package version - -import ( - "io" - "testing" -) - -type eofReader struct { - remainder int -} - -func (r *eofReader) Read(data []byte) (int, error) { - for i := range data { - if r.remainder <= 0 { - return i, io.EOF - } - data[i] = byte(i) - r.remainder-- - } - return len(data), nil -} - -func Test_binaryRead(t *testing.T) { - r := eofReader{3 * 4 * 5} - s := make([]uint32, 5) - for j := 0; j < 4; j++ { - err := binaryRead(&r, &s) - if (err == nil) != (j < 3) { - t.FailNow() - } - if err != nil && err != io.ErrUnexpectedEOF { - t.Fatal(err) - } - for i := range s { - if s[i] != uint32((i*4)|(i*4+1)<<8|(i*4+2)<<16|(i*4+3)<<24) { - t.FailNow() - } - } - } -} diff --git a/pkgs/winres/version/errors.go b/pkgs/winres/version/errors.go deleted file mode 100644 index 71bc14d..0000000 --- a/pkgs/winres/version/errors.go +++ /dev/null @@ -1,13 +0,0 @@ -package version - -const ( - errInvalidSignature = "invalid fixed file info signature" - errInvalidLength = "invalid length" - errInvalidLangID = "invalid language id" - errUnhandledCodePage = "unhandled code page" - errInvalidStringLength = "invalid string length" - - errEmptyKey = "empty key" - errKeyContainsNUL = "invalid key contains NUL character" - errValueContainsNUL = "invalid value contains NUL character" -) diff --git a/pkgs/winres/version/json.go b/pkgs/winres/version/json.go deleted file mode 100644 index 0b1202c..0000000 --- a/pkgs/winres/version/json.go +++ /dev/null @@ -1,112 +0,0 @@ -package version - -import ( - "encoding/json" - "fmt" - "strings" - "time" -) - -type jsonFixed struct { - FileVersion string `json:"file_version,omitempty"` - ProductVersion string `json:"product_version,omitempty"` - Flags string `json:"flags,omitempty"` - Type string `json:"type,omitempty"` - Timestamp *time.Time `json:"timestamp,omitempty"` -} - -type jsonVersionInfo struct { - Fixed *jsonFixed `json:"fixed,omitempty"` - Info map[string]*stringTable `json:"info,omitempty"` -} - -func (vi *Info) MarshalJSON() ([]byte, error) { - jvi := jsonVersionInfo{} - jf := jsonFixed{} - if vi.FileVersion != [4]uint16{} { - jf.FileVersion = fmt.Sprintf("%d.%d.%d.%d", vi.FileVersion[0], vi.FileVersion[1], vi.FileVersion[2], vi.FileVersion[3]) - } - if vi.ProductVersion != [4]uint16{} { - jf.ProductVersion = fmt.Sprintf("%d.%d.%d.%d", vi.ProductVersion[0], vi.ProductVersion[1], vi.ProductVersion[2], vi.ProductVersion[3]) - } - if vi.Flags.Debug { - jf.Flags += "Debug," - } - if vi.Flags.Prerelease { - jf.Flags += "Prerelease," - } - if vi.Flags.Patched { - jf.Flags += "Patched," - } - if vi.Flags.PrivateBuild { - jf.Flags += "PrivateBuild," - } - if vi.Flags.SpecialBuild { - jf.Flags += "SpecialBuild," - } - jf.Flags = strings.TrimRight(jf.Flags, ",") - switch vi.Type { - case App: - // This is the default, omit it - case DLL: - jf.Type = "DLL" - default: - jf.Type = "Unknown" - } - if !vi.Timestamp.IsZero() { - jf.Timestamp = &vi.Timestamp - } - if jf.FileVersion != "" || jf.ProductVersion != "" || jf.Flags != "" || jf.Type != "" || jf.Timestamp != nil { - jvi.Fixed = &jf - } - - jvi.Info = make(map[string]*stringTable) - for k, v := range vi.lt { - if v != nil { - jvi.Info[fmt.Sprintf("%04X", k)] = v - } - } - - return json.Marshal(jvi) -} - -func (vi *Info) UnmarshalJSON(b []byte) error { - jvi := &jsonVersionInfo{} - if err := json.Unmarshal(b, jvi); err != nil { - return err - } - *vi = Info{} - jf := jvi.Fixed - if jf != nil { - fmt.Sscanf(jf.FileVersion, "%d.%d.%d.%d", &vi.FileVersion[0], &vi.FileVersion[1], &vi.FileVersion[2], &vi.FileVersion[3]) - fmt.Sscanf(jf.ProductVersion, "%d.%d.%d.%d", &vi.ProductVersion[0], &vi.ProductVersion[1], &vi.ProductVersion[2], &vi.ProductVersion[3]) - f := strings.ToLower(jf.Flags) - vi.Flags.Debug = strings.Contains(f, "debug") - vi.Flags.Prerelease = strings.Contains(f, "prerelease") - vi.Flags.Patched = strings.Contains(f, "patched") - vi.Flags.PrivateBuild = strings.Contains(f, "privatebuild") - vi.Flags.SpecialBuild = strings.Contains(f, "specialbuild") - switch strings.ToLower(jf.Type) { - case "app", "": - vi.Type = App - case "dll": - vi.Type = DLL - default: - vi.Type = Unknown - } - if jf.Timestamp != nil { - vi.Timestamp = *jf.Timestamp - } - } - - vi.lt = make(langTable) - for h, v := range jvi.Info { - var k uint16 - _, err := fmt.Sscanf(h, "%X", &k) - if err == nil { - vi.lt[k] = v - } - } - - return nil -} diff --git a/pkgs/winres/version/json_test.go b/pkgs/winres/version/json_test.go deleted file mode 100644 index 4f048e3..0000000 --- a/pkgs/winres/version/json_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package version - -import ( - "encoding/json" - "testing" - "time" -) - -func TestInfo_MarshalJSON(t *testing.T) { - if marshal(t, nil) != `null` { - t.Fail() - } - - vi := &Info{} - checkMarshal(t, vi, `{}`) - - vi.FileVersion = [4]uint16{19, 79, 7, 3} - checkMarshal(t, vi, `{"fixed":{"file_version":"19.79.7.3"}}`) - - vi.ProductVersion = [4]uint16{0xFFFF, 1, 0x7FFF, 2} - vi.Type = DLL - vi.Timestamp = time.Date(1979, 7, 3, 00, 45, 20, 0, time.UTC) - checkMarshal(t, vi, `{"fixed":{"file_version":"19.79.7.3","product_version":"65535.1.32767.2","type":"DLL","timestamp":"1979-07-03T00:45:20Z"}}`) - - vi.Flags.SpecialBuild = true - vi.Flags.PrivateBuild = true - vi.Flags.Patched = true - vi.Flags.Prerelease = true - vi.Flags.Debug = true - checkMarshal(t, vi, `{"fixed":{"file_version":"19.79.7.3","product_version":"65535.1.32767.2","flags":"Debug,Prerelease,Patched,PrivateBuild,SpecialBuild","type":"DLL","timestamp":"1979-07-03T00:45:20Z"}}`) - - vi.FileVersion = [4]uint16{} - vi.ProductVersion = [4]uint16{} - vi.Type = 0 - vi.Timestamp = time.Time{} - vi.Flags.SpecialBuild = false - vi.Flags.Patched = false - vi.Flags.Debug = false - checkMarshal(t, vi, `{"fixed":{"flags":"Prerelease,PrivateBuild"}}`) - - vi.Set(0, CompanyName, "Company name") - vi.Set(0, ProductVersion, "Product version") - checkMarshal(t, vi, `{"fixed":{"flags":"Prerelease,PrivateBuild"},"info":{"0000":{"CompanyName":"Company name","ProductVersion":"Product version"}}}`) - - vi.Flags.PrivateBuild = false - vi.Flags.Prerelease = false - checkMarshal(t, vi, `{"info":{"0000":{"CompanyName":"Company name","ProductVersion":"Product version"}}}`) - - vi.Set(0x409, CompanyName, "Company name EN") - vi.Set(0x40C, CompanyName, "Company name FR") - checkMarshal(t, vi, `{"info":{"0000":{"CompanyName":"Company name","ProductVersion":"Product version"},"0409":{"CompanyName":"Company name EN"},"040C":{"CompanyName":"Company name FR"}}}`) - - vi.Type = Unknown - checkMarshal(t, vi, `{"fixed":{"type":"Unknown"},"info":{"0000":{"CompanyName":"Company name","ProductVersion":"Product version"},"0409":{"CompanyName":"Company name EN"},"040C":{"CompanyName":"Company name FR"}}}`) - - vi.Type = 42 - vi.lt = langTable{} - checkMarshal(t, vi, `{"fixed":{"type":"Unknown"}}`) - - vi.Type = App - vi.lt = make(langTable) - checkMarshal(t, vi, `{}`) -} - -func TestInfo_UnmarshalJSON(t *testing.T) { - var vi Info - if vi.UnmarshalJSON([]byte(` {,}`)) == nil { - t.Fail() - } -} - -func marshal(t *testing.T, vi *Info) string { - b, err := json.Marshal(vi) - if err != nil { - t.Fatal(err) - } - return string(b) -} - -func unmarshal(t *testing.T, b string) *Info { - vi := &Info{} - err := json.Unmarshal([]byte(b), vi) - if err != nil { - t.Fatal(err) - } - return vi -} - -func checkMarshal(t *testing.T, vi *Info, expected string) { - got := marshal(t, vi) - if got != expected { - t.Errorf("marshal:\nexpected:\n%s\ngot:\n%s", expected, got) - } - - got2 := marshal(t, unmarshal(t, got)) - if got2 != expected { - t.Errorf("unmarshal:\nexpected:\n%s\ngot:\n%s", expected, got2) - } -} diff --git a/pkgs/winres/version/timestamp.go b/pkgs/winres/version/timestamp.go deleted file mode 100644 index 9b34d00..0000000 --- a/pkgs/winres/version/timestamp.go +++ /dev/null @@ -1,26 +0,0 @@ -package version - -import "time" - -// Conversion of Windows Time Stamp - -// Difference Unix epoch in Windows timestamp format. -const unixEpoch = 0x019DB1DED53E8000 - -// Windows timestamp is in "hecto-nanoseconds" (100 ns) -const tsUnitToNano = 100 - -func timeToTimestamp(t time.Time) (uint32, uint32) { - if t.IsZero() { - return 0, 0 - } - ts := t.UnixNano()/tsUnitToNano + unixEpoch - return uint32(uint64(ts) >> 32), uint32(ts) -} - -func timestampToTime(ms uint32, ls uint32) time.Time { - if ms == 0 && ls == 0 { - return time.Time{} - } - return time.Unix(0, int64((uint64(ms)<<32|uint64(ls))-unixEpoch)*tsUnitToNano) -} diff --git a/pkgs/winres/version/version.go b/pkgs/winres/version/version.go deleted file mode 100644 index 03d7e63..0000000 --- a/pkgs/winres/version/version.go +++ /dev/null @@ -1,313 +0,0 @@ -// Package version provides functions to build a VERSIONINFO structure for Windows applications. -// -// This what Windows displays in the Details tab of file properties. -// -package version - -import ( - "errors" - "io" - "strings" - "time" -) - -const ( - Comments = "Comments" - CompanyName = "CompanyName" - FileDescription = "FileDescription" - FileVersion = "FileVersion" - InternalName = "InternalName" - LegalCopyright = "LegalCopyright" - LegalTrademarks = "LegalTrademarks" - OriginalFilename = "OriginalFilename" - PrivateBuild = "PrivateBuild" - ProductName = "ProductName" - ProductVersion = "ProductVersion" - SpecialBuild = "SpecialBuild" -) - -type fileType int - -const ( - App fileType = iota - DLL - Unknown -) - -const ( - // LangNeutral is the LCID for language agnostic data. - LangNeutral = 0 - // LangDefault is the LCID for en-US, and it is the default in many tools and APIs. - LangDefault = 0x409 -) - -// Info is the main struct of this package. -// Create one as Info{}, use Info.Set() to add key/value pairs, -// set other members, then add it to the resource set with Info.AddTo. -type Info struct { - FileVersion [4]uint16 - ProductVersion [4]uint16 - Flags versionFlags - Type fileType - Timestamp time.Time - lt langTable - - // temporary state - pos int - w io.Writer -} - -type versionFlags struct { - Debug bool - Prerelease bool - Patched bool - PrivateBuild bool - SpecialBuild bool -} - -type langTable map[uint16]*stringTable - -type stringTable map[string]string - -// Set sets a key/value pair in the Info structure for a specific locale. -// -// Standard keys are defined as constants in this package: version.ProductName, version.CompanyName, ... -// -// Strings must not contain NUL characters. -// -// Language Code Identifiers (LCID) are listed there: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/ -// -// langID may also be 0 for neutral. -func (vi *Info) Set(langID uint16, key string, value string) error { - if vi.lt == nil { - vi.lt = make(langTable) - } - if key == "" { - return errors.New(errEmptyKey) - } - if strings.ContainsRune(key, 0) { - return errors.New(errKeyContainsNUL) - } - if strings.ContainsRune(value, 0) { - return errors.New(errValueContainsNUL) - } - st, ok := vi.lt[langID] - if !ok { - st = &stringTable{} - vi.lt[langID] = st - } - (*st)[key] = value - return nil -} - -// Bytes returns the binary representation of the VS_VERSIONINFO struct. -func (vi *Info) Bytes() []byte { - if vi == nil { - return nil - } - return vi.bytes() -} - -// FromBytes loads an Info from the binary representation of a VS_VERSIONINFO struct. -func FromBytes(data []byte) (*Info, error) { - return fromBytes(data) -} - -// SplitTranslations splits the Info struct by language. -// It returns a map indexed by language ID. -// -// Windows Explorer doesn't seem to search for a proper translation inside the VERSIONINFO struct. -// So one has to embed a whole VERSIONINFO for each language as a resource translation. -func (vi *Info) SplitTranslations() map[uint16]*Info { - if vi == nil { - return nil - } - return vi.splitLangs() -} - -// SetProductVersion sets the product version, ensuring this is the only one in the structure. -// -// This should be called after json.Unmarshal to override the version. -func (vi *Info) SetProductVersion(productVersion string) { - vi.ProductVersion = versionStringToArray(productVersion) - if len(vi.lt) == 0 { - vi.Set(LangNeutral, ProductVersion, productVersion) - return - } - for langID := range vi.lt { - vi.Set(langID, ProductVersion, productVersion) - } -} - -// SetFileVersion sets the file version, ensuring this is the only one in the structure. -// -// This should be called after json.Unmarshal to override the version. -func (vi *Info) SetFileVersion(fileVersion string) { - vi.FileVersion = versionStringToArray(fileVersion) - if len(vi.lt) == 0 { - vi.Set(LangNeutral, FileVersion, fileVersion) - return - } - for langID := range vi.lt { - vi.Set(langID, FileVersion, fileVersion) - } -} - -// MergeTranslations merges several VERSIONINFO structs into one multilingual struct. -// -// The fixed part will be taken in priority from: -// 1. The neutral language (zero) -// 2. The default language (en-US) -// 3. The first language ID in ascending order -// -// Each struct corresponds to one translation, and its language ID will be the one it is mapped to. -// -// This means that each struct is supposed to contain exactly one translation, -// either neutral or of same language ID as it is mapped to. -// -// If a struct contains several translations, those that don't correspond to the map key will be ignored. -// -// If a struct contains one translation with a different language ID, -// it will be imported as if it had been the same value as the map key. -// -func MergeTranslations(translations map[uint16]*Info) *Info { - vi := &Info{} - - main := findMainTranslation(translations) - if main == nil { - return vi - } - vi.Flags = main.Flags - vi.Type = main.Type - vi.Timestamp = main.Timestamp - vi.ProductVersion = main.ProductVersion - vi.FileVersion = main.FileVersion - - for langID, trans := range translations { - st := trans.lt[langID] - if st == nil || len(*st) == 0 { - st = trans.lt[LangNeutral] - if st == nil || len(*st) == 0 { - st = trans.singleLang() - if st == nil { - continue - } - } - } - for k, v := range *st { - vi.Set(langID, k, v) - } - } - - return vi -} - -func (vi *Info) singleLang() *stringTable { - var ( - seen bool - lang *stringTable - ) - - for _, st := range vi.lt { - if st != nil && len(*st) != 0 { - if seen { - return nil - } - seen = true - lang = st - } - } - - return lang -} - -func findMainTranslation(translations map[uint16]*Info) *Info { - main := translations[LangNeutral] - if main != nil { - return main - } - - main = translations[LangDefault] - if main != nil { - return main - } - - var lang uint16 = 0xFFFF - for k := range translations { - if k < lang { - lang = k - } - } - return translations[lang] -} - -func versionStringToArray(v string) [4]uint16 { - var ( - part int - ver [4]uint16 - i int - ) - - for i = range v { - if '0' <= v[i] && v[i] <= '9' { - break - } - } - v = v[i:] - for i = range v { - if '0' <= v[i] && v[i] <= '9' { - ver[part] = ver[part]*10 + uint16(v[i]-'0') - } else if v[i] == '.' && part < 3 { - part++ - } else { - break - } - } - return ver -} - -func (vi *Info) splitLangs() map[uint16]*Info { - m := make(map[uint16]*Info) - - defaults := map[string]string{} - if st := vi.lt[LangNeutral]; st != nil { - for k, v := range *st { - if _, ok := defaults[k]; !ok { - defaults[k] = v - } - } - } - if st := vi.lt[LangDefault]; st != nil { - for k, v := range *st { - if _, ok := defaults[k]; !ok { - defaults[k] = v - } - } - } - for _, langID := range vi.lt.sortedKeys() { - for k, v := range *vi.lt[langID] { - if _, ok := defaults[k]; !ok { - defaults[k] = v - } - } - } - - for langID, st := range vi.lt { - trans := &Info{ - FileVersion: vi.FileVersion, - ProductVersion: vi.ProductVersion, - Flags: vi.Flags, - Type: vi.Type, - Timestamp: vi.Timestamp, - } - for k, v := range defaults { - trans.Set(langID, k, v) - } - for k, v := range *st { - trans.Set(langID, k, v) - } - m[langID] = trans - } - - return m -} diff --git a/pkgs/winres/version/version_test.go b/pkgs/winres/version/version_test.go deleted file mode 100644 index 517e933..0000000 --- a/pkgs/winres/version/version_test.go +++ /dev/null @@ -1,659 +0,0 @@ -package version - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - "reflect" - "testing" - "time" -) - -func ExampleInfo_Set() { - vi := Info{} - // 0x409 is en-US, and the default language - vi.Set(0x409, ProductName, "Good Product") - // 0x40C is fr-FR - vi.Set(0x40C, ProductName, "Bon Produit") - // 0 is neutral - vi.Set(0, "Smile", "😀") -} - -func ExampleInfo() { - vi := Info{} - // Set some info in the fixed structure - vi.ProductVersion = [4]uint16{1, 0, 0, 1} - // Set some info in the string table - vi.Set(0x409, ProductName, "Good Product") - vi.Set(0x40C, ProductName, "Bon Produit") - // Once it's complete, make a resource - // resourceData := vi.Bytes() - // ... -} - -func TestMergeTranslations(t *testing.T) { - trans := map[uint16]*Info{} - - var ( - v000, v401, v402, v409 Info - vMerged *Info - b []byte - ) - - vMerged = MergeTranslations(trans) - if vMerged == nil || !reflect.DeepEqual(vMerged, &Info{}) { - t.Fail() - } - - trans[0x401] = &v401 - trans[0x402] = &v402 - - v401.FileVersion = [4]uint16{0, 4, 0, 1} - v401.Set(0x000, "k1", "v1.401.000") - v401.Set(0x401, "k3", "v3.401") - - v402.FileVersion = [4]uint16{0, 4, 0, 2} - v402.Set(0x401, "k1", "v1.402.401") - v402.Set(0x000, "k2", "v2.402.000") - - vMerged = MergeTranslations(trans) - b, _ = json.MarshalIndent(vMerged, "", " ") - //language=JSON - if string(b) != `{ - "fixed": { - "file_version": "0.4.0.1" - }, - "info": { - "0401": { - "k3": "v3.401" - }, - "0402": { - "k2": "v2.402.000" - } - } -}` { - t.Fail() - } - - trans[0x409] = &v409 - v409.FileVersion = [4]uint16{0, 4, 0, 9} - v401.lt[0x401] = nil - v401.Set(0x402, "k3", "v3.401.402") - delete(*v402.lt[0x000], "k2") - v402.Set(0x401, "k2", "v2.402.401") - vMerged = MergeTranslations(trans) - b, _ = json.MarshalIndent(vMerged, "", " ") - //language=JSON - if string(b) != `{ - "fixed": { - "file_version": "0.4.0.9" - }, - "info": { - "0401": { - "k1": "v1.401.000" - }, - "0402": { - "k1": "v1.402.401", - "k2": "v2.402.401" - } - } -}` { - t.Fail() - } - - trans[0x000] = &v000 - v000.FileVersion = [4]uint16{} - v402.Set(0x403, "k2", "v2.402.403") - vMerged = MergeTranslations(trans) - b, _ = json.MarshalIndent(vMerged, "", " ") - //language=JSON - if string(b) != `{ - "info": { - "0401": { - "k1": "v1.401.000" - } - } -}` { - fmt.Println(string(b)) - t.Fail() - } -} - -func TestFromBytes1(t *testing.T) { - b := loadGolden(t) - vi, err := FromBytes(b) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(b, vi.Bytes()) { - t.Fail() - } -} - -func TestFromBytes2(t *testing.T) { - b := loadGolden(t) - vi, err := FromBytes(b) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(b, vi.Bytes()) { - t.Fail() - } -} - -func TestFromBytes_DLL(t *testing.T) { - b := loadGolden(t) - vi, err := FromBytes(b) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(b, vi.Bytes()) || vi.Type != DLL { - t.Fail() - } -} - -func TestFromBytes_Empty(t *testing.T) { - b := loadGolden(t) - vi, err := FromBytes(b) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(b, vi.Bytes()) { - t.Fail() - } -} - -func TestFromBytes_ErrCodePage(t *testing.T) { - b := loadGolden(t) - vi, err := FromBytes(b) - if err == nil || vi != nil { - t.Fail() - return - } - if err.Error() != errUnhandledCodePage { - t.Fail() - } -} - -func TestFromBytes_ErrEOF1(t *testing.T) { - b := loadGolden(t) - vi, err := FromBytes(b) - if err != io.ErrUnexpectedEOF || vi != nil { - t.Fail() - } -} - -func TestFromBytes_ErrEOF2(t *testing.T) { - b := loadGolden(t) - vi, err := FromBytes(b) - if err != io.ErrUnexpectedEOF || vi != nil { - t.Fail() - } -} - -func TestFromBytes_ErrEOF3(t *testing.T) { - b := loadGolden(t) - vi, err := FromBytes(b) - if err != io.ErrUnexpectedEOF || vi != nil { - t.Fail() - } -} - -func TestFromBytes_ErrInvalidLangID(t *testing.T) { - b := loadGolden(t) - vi, err := FromBytes(b) - if err == nil || vi != nil { - t.Fail() - return - } - if err.Error() != errInvalidLangID { - t.Fail() - } -} - -func TestFromBytes_ErrInvalidSignature(t *testing.T) { - b := loadGolden(t) - vi, err := FromBytes(b) - if err == nil || vi != nil { - t.Fail() - return - } - if err.Error() != errInvalidSignature { - t.Fail() - } -} - -func TestFromBytes_ErrLength1(t *testing.T) { - b := loadGolden(t) - vi, err := FromBytes(b) - if err == nil || vi != nil { - t.Fail() - return - } - if err.Error() != errInvalidLength { - t.Fail() - } -} - -func TestFromBytes_ErrLength2(t *testing.T) { - b := loadGolden(t) - vi, err := FromBytes(b) - if err == nil || vi != nil { - t.Fail() - return - } - if err.Error() != errInvalidLength { - t.Fail() - } -} - -func TestFromBytes_ErrStringLength(t *testing.T) { - b := loadGolden(t) - vi, err := FromBytes(b) - if err == nil || vi != nil { - t.Fail() - return - } - if err.Error() != errInvalidStringLength { - t.Fail() - } -} - -func TestFromBytes_ErrTruncFixed(t *testing.T) { - b := loadGolden(t) - vi, err := FromBytes(b) - if err != io.ErrUnexpectedEOF || vi != nil { - t.Fail() - } -} - -func TestFromBytes_OtherType(t *testing.T) { - b := loadGolden(t) - vi, err := FromBytes(b) - if err != nil { - t.Fatal(err) - } - - if vi.Type != Unknown { - t.Fail() - } -} - -func TestInfo_Bytes1(t *testing.T) { - vi := &Info{} - // 0x409 is en-US, and the default language - vi.Set(0x409, ProductName, "Good Product") - // 0x40C is fr-FR - vi.Set(0x40C, ProductName, "Bon Produit") - // 0 is neutral - vi.Set(0, "Smile", "😀") - - vi.Flags.Prerelease = true - vi.Flags.Patched = true - vi.Flags.PrivateBuild = true - vi.Flags.SpecialBuild = true - vi.Flags.Debug = true - - vi.Timestamp = time.Date(1979, 7, 3, 0, 15, 42, 100, time.UTC) - - vi.ProductVersion = [4]uint16{0xFFF5, 0xFFF6, 0xFFF7, 0xFFF8} - vi.FileVersion = [4]uint16{0xFFF1, 0xFFF2, 0xFFF3, 0xFFF4} - - checkBytes(t, vi) -} - -func TestInfo_Bytes2(t *testing.T) { - vi := &Info{} - vi.Set(LangDefault, "Custom Info", "Very important information") - vi.Set(LangDefault, Comments, "This is a test...\n\nLook, I even skipped a line!\n") - vi.Set(LangDefault, CompanyName, "Some Company") - vi.Set(LangDefault, FileDescription, "This great product does...\r\n\r\nNothing!\n\n") - vi.Set(LangDefault, FileVersion, "V. 421.422.423.424") - vi.Set(LangDefault, InternalName, "secret-product") - vi.Set(LangDefault, LegalCopyright, "© Some Company") - vi.Set(LangDefault, LegalTrademarks, "™ Some Trademarks") - vi.Set(LangDefault, OriginalFilename, "hey_there.dll") - vi.Set(LangDefault, PrivateBuild, "True") - vi.Set(LangDefault, ProductName, "Some Product") - vi.Set(LangDefault, ProductVersion, "v. 1.2.3.42 (private)") - vi.Set(LangDefault, SpecialBuild, "False") - - vi.Flags.Prerelease = true - vi.Flags.Patched = false - vi.Flags.PrivateBuild = true - vi.Flags.SpecialBuild = false - vi.Flags.Debug = true - - vi.ProductVersion = [4]uint16{8, 4, 2, 1} - - checkBytes(t, vi) -} - -func TestInfo_Bytes_DLL(t *testing.T) { - checkBytes(t, &Info{ - Type: DLL, - }) -} - -func TestInfo_Bytes_Empty(t *testing.T) { - var nilInfo *Info - - if len(nilInfo.Bytes()) > 0 { - t.Fail() - } - - checkBytes(t, &Info{}) -} - -func TestInfo_Bytes_OtherType(t *testing.T) { - checkBytes(t, &Info{ - Type: 42, - }) - - checkBytes(t, &Info{ - Type: Unknown, - }) -} - -func TestInfo_Set(t *testing.T) { - var ( - key string - value string - err error - vi Info - ) - - key, value = "", "Hi" - err = vi.Set(0x409, key, value) - if err == nil || err.Error() != errEmptyKey { - t.Fail() - } - - key, value = "Hey\x00", "Hi" - err = vi.Set(0x409, key, value) - if err == nil || err.Error() != errKeyContainsNUL { - t.Fail() - } - - key, value = "Hey", "Hi\x00" - err = vi.Set(0x409, key, value) - if err == nil || err.Error() != errValueContainsNUL { - t.Fail() - } - - key, value = "Hey", "\x00Hi" - err = vi.Set(0x409, key, value) - if err == nil || err.Error() != errValueContainsNUL { - t.Fail() - } - - if vi.Set(0, "😀", "") != nil { - t.Fail() - } - if vi.Set(0, "😀", "\n\n") != nil { - t.Fail() - } -} - -func TestInfo_SetFileVersion(t *testing.T) { - vi := &Info{} - // 0x409 is en-US, and the default language - vi.Set(0x409, ProductName, "Good Product") - // 0x40C is fr-FR - vi.Set(0x40C, ProductName, "Bon Produit") - // 0 is neutral - vi.Set(0, "Smile", "😀") - - vers := "v-1.65536.65537.65538beta" - vi.SetFileVersion(vers) - - if (*vi.lt[0x409])[FileVersion] != vers { - t.Fail() - } - if (*vi.lt[0x40C])[FileVersion] != vers { - t.Fail() - } - if (*vi.lt[0])[FileVersion] != vers { - t.Fail() - } - - checkBytes(t, vi) -} - -func TestInfo_SetFileVersion_Empty(t *testing.T) { - vi := &Info{} - vi.SetFileVersion("... 1.42.42.42 ...") - - if len(vi.lt) != 1 || (*vi.lt[0])[FileVersion] == "" { - t.Fail() - } - - checkBytes(t, vi) -} - -func TestInfo_SetProductVersion(t *testing.T) { - vi := &Info{} - // 0x409 is en-US, and the default language - vi.Set(0x409, ProductName, "Good Product") - // 0x40C is fr-FR - vi.Set(0x40C, ProductName, "Bon Produit") - // 0 is neutral - vi.Set(0, "Smile", "😀") - - vers := "v.65531.65532.65533.65534-alpha" - vi.SetProductVersion(vers) - - if (*vi.lt[0x409])[ProductVersion] != vers { - t.Fail() - } - if (*vi.lt[0x40C])[ProductVersion] != vers { - t.Fail() - } - if (*vi.lt[0])[ProductVersion] != vers { - t.Fail() - } - - checkBytes(t, vi) -} - -func TestInfo_SetProductVersion_Empty(t *testing.T) { - vi := &Info{} - vi.SetProductVersion("v 1.2.3.4 beta") - - if len(vi.lt) != 1 || (*vi.lt[0])[ProductVersion] == "" { - t.Fail() - } - - checkBytes(t, vi) -} - -func TestInfo_SplitTranslations(t *testing.T) { - var vi *Info - - if len(vi.SplitTranslations()) > 0 { - t.Fail() - } - - vi = &Info{ - FileVersion: [4]uint16{1, 2, 3, 4}, - ProductVersion: [4]uint16{1, 2, 3, 4}, - Flags: versionFlags{ - Debug: true, - Prerelease: true, - Patched: true, - PrivateBuild: true, - SpecialBuild: true, - }, - Type: DLL, - Timestamp: time.Date(2020, 1, 1, 12, 30, 42, 0, time.UTC), - } - vi.Set(0x000, "k1", "v1.000") - vi.Set(0x409, "k1", "v1.409") - - vi.Set(0x401, "k2", "v2.401") - vi.Set(0x409, "k2", "v2.409") - - vi.Set(0x401, "k3", "v3.401") - vi.Set(0x402, "k3", "v3.402") - - vi.Set(0x403, "k0", "v0.403") - delete(*vi.lt[0x403], "k0") - - trans := vi.SplitTranslations() - - for k, v := range trans { - switch k { - case 0x000: - b, _ := json.MarshalIndent(v, "", " ") - //language=JSON - if string(b) != `{ - "fixed": { - "file_version": "1.2.3.4", - "product_version": "1.2.3.4", - "flags": "Debug,Prerelease,Patched,PrivateBuild,SpecialBuild", - "type": "DLL", - "timestamp": "2020-01-01T12:30:42Z" - }, - "info": { - "0000": { - "k1": "v1.000", - "k2": "v2.409", - "k3": "v3.401" - } - } -}` { - t.Fail() - } - - case 0x401: - b, _ := json.MarshalIndent(v, "", " ") - //language=JSON - if string(b) != `{ - "fixed": { - "file_version": "1.2.3.4", - "product_version": "1.2.3.4", - "flags": "Debug,Prerelease,Patched,PrivateBuild,SpecialBuild", - "type": "DLL", - "timestamp": "2020-01-01T12:30:42Z" - }, - "info": { - "0401": { - "k1": "v1.000", - "k2": "v2.401", - "k3": "v3.401" - } - } -}` { - t.Fail() - } - - case 0x402: - b, _ := json.MarshalIndent(v, "", " ") - //language=JSON - if string(b) != `{ - "fixed": { - "file_version": "1.2.3.4", - "product_version": "1.2.3.4", - "flags": "Debug,Prerelease,Patched,PrivateBuild,SpecialBuild", - "type": "DLL", - "timestamp": "2020-01-01T12:30:42Z" - }, - "info": { - "0402": { - "k1": "v1.000", - "k2": "v2.409", - "k3": "v3.402" - } - } -}` { - t.Fail() - } - - case 0x409: - b, _ := json.MarshalIndent(v, "", " ") - //language=JSON - if string(b) != `{ - "fixed": { - "file_version": "1.2.3.4", - "product_version": "1.2.3.4", - "flags": "Debug,Prerelease,Patched,PrivateBuild,SpecialBuild", - "type": "DLL", - "timestamp": "2020-01-01T12:30:42Z" - }, - "info": { - "0409": { - "k1": "v1.409", - "k2": "v2.409", - "k3": "v3.401" - } - } -}` { - t.Fail() - } - - case 0x403: - b, _ := json.MarshalIndent(v, "", " ") - //language=JSON - if string(b) != `{ - "fixed": { - "file_version": "1.2.3.4", - "product_version": "1.2.3.4", - "flags": "Debug,Prerelease,Patched,PrivateBuild,SpecialBuild", - "type": "DLL", - "timestamp": "2020-01-01T12:30:42Z" - }, - "info": { - "0403": { - "k1": "v1.000", - "k2": "v2.409", - "k3": "v3.401" - } - } -}` { - t.Fail() - } - - default: - t.Fail() - } - } -} - -func checkBytes(t *testing.T, vi *Info) { - b := vi.Bytes() - - refFile := golden(t) - ref, _ := os.ReadFile(refFile) - - if !bytes.Equal(ref, b) { - t.Error(t.Name() + " output is different") - bugFile := refFile[:len(refFile)-7] + ".bug" - err := os.WriteFile(bugFile, b, 0666) - if err != nil { - t.Error(err) - return - } - t.Log("dumped output to", bugFile) - } -} - -func loadGolden(t *testing.T) []byte { - refFile := golden(t) - ref, err := os.ReadFile(refFile) - if err != nil { - t.Error(err) - return nil - } - return ref -} - -func golden(t *testing.T) string { - return filepath.Join("testdata", t.Name()+".golden") -} diff --git a/pkgs/winres/winres.go b/pkgs/winres/winres.go deleted file mode 100644 index 08264b0..0000000 --- a/pkgs/winres/winres.go +++ /dev/null @@ -1,357 +0,0 @@ -package winres - -import ( - "bytes" - "debug/pe" - "errors" - "io" - - "github.com/energye/energy/v2/pkgs/winres/version" -) - -// Standard type IDs from https://docs.microsoft.com/en-us/windows/win32/menurc/resource-types -const ( - RT_CURSOR ID = 1 - RT_BITMAP ID = 2 - RT_ICON ID = 3 - RT_MENU ID = 4 - RT_DIALOG ID = 5 - RT_STRING ID = 6 - RT_FONTDIR ID = 7 - RT_FONT ID = 8 - RT_ACCELERATOR ID = 9 - RT_RCDATA ID = 10 - RT_MESSAGETABLE ID = 11 - RT_GROUP_CURSOR ID = 12 - RT_GROUP_ICON ID = 14 - RT_VERSION ID = 16 - RT_PLUGPLAY ID = 19 - RT_VXD ID = 20 - RT_ANICURSOR ID = 21 - RT_ANIICON ID = 22 - RT_HTML ID = 23 - RT_MANIFEST ID = 24 -) - -const ( - LCIDNeutral = 0 - LCIDDefault = 0x409 // en-US is default -) - -// Arch defines the target architecture. -// Its value can be used as a target suffix too: -// "rsrc_windows_"+string(arch)+".syso" -type Arch string - -const ( - ArchI386 Arch = "386" - ArchAMD64 Arch = "amd64" - ArchARM Arch = "arm" - ArchARM64 Arch = "arm64" -) - -// ResourceSet is the main object in the package. -// -// Create an empty ResourceSet and call Set methods to add resources, then WriteObject to produce a COFF object file. -type ResourceSet struct { - types map[Identifier]*typeEntry - lastIconID uint16 - lastCursorID uint16 -} - -// Set adds or replaces a resource. -// -// typeID is the resource type's identifier. -// It can be either a standard type number (RT_ICON, RT_VERSION, ...) or any type name. -// -// resID is the resource's unique identifier for a given type. -// It can either be an ID starting from 1, or a Name. -// -// A resource ID can have different data depending on the user's locale. -// In this case Set can be called several times with the same resID but a different language ID. -// -// langID can be 0 (neutral), or one of those LCID: -// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid -// -// Warning: the ResourceSet takes ownership of the data parameter. -// The caller should not write into it anymore after calling this method. -// -func (rs *ResourceSet) Set(typeID, resID Identifier, langID uint16, data []byte) error { - if err := checkIdentifier(resID); err != nil { - return err - } - if err := checkIdentifier(typeID); err != nil { - return err - } - - rs.set(typeID, resID, langID, data) - - return nil -} - -// SetVersionInfo sets the VersionInfo structure. -// -// This what Windows displays in the Details tab of file properties. -func (rs *ResourceSet) SetVersionInfo(vi version.Info) { - for langID, res := range vi.SplitTranslations() { - rs.Set(RT_VERSION, ID(1), langID, res.Bytes()) - } -} - -// SetManifest is a simplified way to embed a typical application manifest, -// without writing xml directly. -func (rs *ResourceSet) SetManifest(manifest AppManifest) { - rs.Set(RT_MANIFEST, ID(1), LCIDDefault, makeManifest(manifest)) -} - -// WriteObject writes a full object file into w. -func (rs *ResourceSet) WriteObject(w io.Writer, arch Arch) error { - return writeObject(w, rs, arch) -} - -// Count returns the number of resources in the set. -func (rs *ResourceSet) Count() int { - return rs.numDataEntries() -} - -// Walk walks through the resources in same order as they will be written. -// -// It takes a callback function that takes same parameters as Set and returns a bool that should be true to continue, false to stop. -// -// If you modify the set during a call to Walk, behaviour is undefined. -func (rs *ResourceSet) Walk(f func(typeID, resID Identifier, langID uint16, data []byte) bool) { - s := &state{} - rs.order(s) - for _, tk := range s.orderedKeys { - te := rs.types[tk] - for _, rk := range te.orderedKeys { - re := te.resources[rk] - for _, dk := range re.orderedKeys { - if !f(tk, rk, uint16(dk), re.data[dk].data) { - return - } - } - } - } -} - -// WalkType walks through the resources or a certain type, in same order as they will be written. -// -// It takes a callback function that takes same parameters as Set and returns a bool that should be true to continue, false to stop. -// -// If you modify the set during a call to Walk, behaviour is undefined. -func (rs *ResourceSet) WalkType(typeID Identifier, f func(resID Identifier, langID uint16, data []byte) bool) { - te := rs.types[typeID] - if te == nil { - return - } - te.order() - for _, rk := range te.orderedKeys { - re := te.resources[rk] - for _, dk := range re.orderedKeys { - if !f(rk, uint16(dk), re.data[dk].data) { - return - } - } - } -} - -// Get returns resource data. -// -// Returns nil if the resource was not found. -func (rs *ResourceSet) Get(typeID, resID Identifier, langID uint16) []byte { - te := rs.types[typeID] - if te == nil { - return nil - } - - re := te.resources[resID] - if re == nil { - return nil - } - - de := re.data[ID(langID)] - if de == nil { - return nil - } - - return de.data -} - -// set is the only function that may create/modify entries in the ResourceSet -func (rs *ResourceSet) set(typeID Identifier, resID Identifier, langID uint16, data []byte) { - if rs.types == nil { - rs.types = make(map[Identifier]*typeEntry) - } - - if data == nil { - // Like UpdateResource, delete resources by passing nil - rs.delete(typeID, resID, langID) - return - } - - te := rs.types[typeID] - if te == nil { - te = &typeEntry{ - resources: make(map[Identifier]*resourceEntry), - } - rs.types[typeID] = te - } - - re := te.resources[resID] - if re == nil { - te.orderedKeys = nil - re = &resourceEntry{ - data: make(map[ID]*dataEntry), - } - te.resources[resID] = re - } - - if typeID == RT_ICON { - if id, ok := resID.(ID); ok && rs.lastIconID < uint16(id) { - rs.lastIconID = uint16(id) - } - } else if typeID == RT_CURSOR { - if id, ok := resID.(ID); ok && rs.lastCursorID < uint16(id) { - rs.lastCursorID = uint16(id) - } - } - - de := re.data[ID(langID)] - if de == nil { - re.orderedKeys = nil - de = &dataEntry{} - re.data[ID(langID)] = de - } - - de.data = data -} - -func (rs *ResourceSet) delete(typeID Identifier, resID Identifier, langID uint16) { - te := rs.types[typeID] - if te == nil { - return - } - - re := te.resources[resID] - if re == nil { - return - } - - delete(re.data, ID(langID)) - re.orderedKeys = nil - - if len(re.data) > 0 { - return - } - - delete(te.resources, resID) - te.orderedKeys = nil - - if len(te.resources) > 0 { - return - } - - delete(rs.types, typeID) -} - -// firstLang finds the first language ID of a resource. -// -// When an icon image has several languages, Windows takes the first one -// in the resource directory, even if it's not the same as the group's language, -// or the user's language. -// -// UpdateResource sorts resources just like winres does, so we may assume -// the first language in the file should be the lowest LCID. -func (rs *ResourceSet) firstLang(typeID, resID Identifier) uint16 { - te := rs.types[typeID] - if te == nil { - return 0 - } - - re := te.resources[resID] - if re == nil { - return 0 - } - - if len(re.data) == 0 { - return 0 - } - - re.order() - - return uint16(re.orderedKeys[0]) -} - -// LoadFromEXE loads the .rsrc section of the executable and returns a ResourceSet -func LoadFromEXE(exe io.ReadSeeker) (*ResourceSet, error) { - return loadFromEXE(exe, ID(0)) -} - -// LoadFromEXESingleType loads the .rsrc section of the executable and returns a ResourceSet -// containing only resources of one type. -func LoadFromEXESingleType(exe io.ReadSeeker, typeID Identifier) (*ResourceSet, error) { - if typeID == ID(0) { - return nil, errors.New(errZeroID) - } - return loadFromEXE(exe, typeID) -} - -func loadFromEXE(exe io.ReadSeeker, typeID Identifier) (*ResourceSet, error) { - rs := &ResourceSet{} - - section, baseAddress, err := extractRSRCSection(exe) - if err != nil { - if err == ErrNoResources { - return rs, err - } - return nil, err - } - - err = rs.read(section, baseAddress, typeID) - if err != nil { - return nil, err - } - - return rs, nil -} - -func (rs *ResourceSet) bytes() ([]byte, []int) { - buf := bytes.Buffer{} - // ResourceSet.write may only fail on io.Write() calls. - // bytes.Buffer.Write never returns an error. - reloc, _ := rs.write(&buf) - return buf.Bytes(), reloc -} - -// WriteToEXE patches an executable to replace its resources with this ResourceSet. -// -// It reads the original file from src and writes the new file to dst. -// -// src and dst should not point to a same file/buffer. -// -// Options: -// -// ForceCheckSum() // Forces updating the checksum even when it was not set in the original file -// WithAuthenticode() // Allows updating the .rsrc section despite the file being signed -// -func (rs *ResourceSet) WriteToEXE(dst io.Writer, src io.ReadSeeker, opt ...exeOption) error { - data, reloc := rs.bytes() - options := exeOptions{} - for _, o := range opt { - o(&options) - } - return replaceRSRCSection(dst, src, data, reloc, options) -} - -// IsSignedEXE helps knowing if an exe file is signed before encountering an error with WriteToEXE. -func IsSignedEXE(exe io.ReadSeeker) (bool, error) { - pos, _ := exe.Seek(0, io.SeekCurrent) - exe.Seek(0, io.SeekStart) - h, err := readPEHeaders(exe) - exe.Seek(pos, io.SeekStart) - if err != nil { - return false, err - } - return len(h.dirs) > pe.IMAGE_DIRECTORY_ENTRY_SECURITY && h.dirs[pe.IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress > 0, nil -} diff --git a/pkgs/winres/winres_test.go b/pkgs/winres/winres_test.go deleted file mode 100644 index 9c4d3ad..0000000 --- a/pkgs/winres/winres_test.go +++ /dev/null @@ -1,1062 +0,0 @@ -package winres - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "testing" - "time" - - "github.com/energye/energy/v2/pkgs/winres/version" -) - -func TestMain(m *testing.M) { - getTestData() - os.Exit(m.Run()) -} - -func getTestData() { - exec.Command("git", "clone", "https://github.com/tc-hib/winres-testdata.git", "testdata").Run() - - cmd := exec.Command("git", "fetch", "--all") - cmd.Dir = "./testdata" - cmd.Run() - - cmd = exec.Command("git", "reset", "--hard", "origin/main") - cmd.Dir = "./testdata" - cmd.Run() -} - -func TestErrors(t *testing.T) { - r := &ResourceSet{} - var err error - - err = r.Set(RT_RCDATA, ID(0), 0, []byte{}) - if err == nil || err.Error() != errZeroID { - t.Fail() - } - - err = r.Set(ID(0), ID(1), 0, []byte{}) - if err == nil || err.Error() != errZeroID { - t.Fail() - } - - if r.Set(RT_RCDATA, ID(0xFFFF), 0, []byte{}) != nil { - t.Fail() - } - - err = r.Set(RT_RCDATA, Name(""), 0, []byte{}) - if err == nil || err.Error() != errEmptyName { - t.Fail() - } - - err = r.Set(Name(""), ID(1), 0, []byte{}) - if err == nil || err.Error() != errEmptyName { - t.Fail() - } - - if r.Set(RT_RCDATA, Name("look, i'm not a nice resource name"), 0, []byte{}) != nil { - t.Fail() - } - - err = r.Set(RT_RCDATA, Name("IAMNICER\x00"), 0, []byte{}) - if err == nil || err.Error() != errNameContainsNUL { - t.Fail() - } - - if r.Set(Name("look, i'm not a nice type name"), ID(1), 0, []byte{}) != nil { - t.Fail() - } - - err = r.Set(Name("IAMNICER\x00"), ID(42), 0, []byte{}) - if err == nil || err.Error() != errNameContainsNUL { - t.Fail() - } - - err = r.WriteObject(io.Discard, "*") - if err == nil || err.Error() != errUnknownArch { - t.Fail() - } -} - -func TestEmpty(t *testing.T) { - rs := &ResourceSet{} - checkResourceSet(t, rs, ArchI386) -} - -func TestWinRes1(t *testing.T) { - r := &ResourceSet{} - - r.Set(RT_MANIFEST, ID(1), LCIDDefault, []byte(manifest1)) - r.Set(Name("CUSTOM TYPE"), Name("CUSTOM RESOURCE"), 1033, []byte("Hello World!")) - r.Set(Name("CUSTOM TYPE"), Name("CUSTOM RESOURCE"), 1036, []byte("Bonjour Monde !")) - r.Set(Name("CUSTOM TYPE"), ID(42), 1033, []byte("# Hello World!")) - r.Set(Name("CUSTOM TYPE"), ID(42), 1036, []byte("# Bonjour Monde !")) - r.Set(RT_RCDATA, ID(1), 1033, []byte("## Hello World!")) - r.Set(RT_RCDATA, ID(1), 1036, []byte("## Bonjour Monde !")) - r.Set(RT_RCDATA, ID(42), 1033, []byte("### Hello World!")) - r.Set(RT_RCDATA, ID(42), 1036, []byte("### Bonjour Monde !")) - icon1 := loadICOFile(t, "icon.ico") - cursor := loadCURFile(t, "cursor.cur") - icon2 := loadPNGFileAsIcon(t, "cur-64x128.png", nil) - icon3 := loadPNGFileAsIcon(t, "cur-32x64.png", []int{48, 16}) - icon4 := loadPNGFileAsIcon(t, "img.png", []int{128}) - r.SetIconTranslation(ID(1), 0, icon1) - r.SetIconTranslation(ID(1), 1033, icon2) - r.SetIconTranslation(ID(1), 1036, icon3) - r.SetIconTranslation(Name("SUPERB ICON"), 0, icon4) - r.SetIcon(ID(2), icon2) - r.SetCursor(ID(1), cursor) - v := version.Info{ - ProductVersion: [4]uint16{5, 6, 7, 8}, - } - v.Set(1036, "Custom Info", "Very important information") - v.Set(1036, version.ProductName, "A test for winres") - v.Set(1036, version.ProductVersion, "0.0.0.0-αlpha-") - v.Set(1036, version.CompanyName, "Test Corporation ltd") - v.Flags.SpecialBuild = true - v.FileVersion = [4]uint16{4, 42, 424, 4242} - v.Timestamp = time.Date(1979, 7, 3, 0, 15, 0, 0, time.UTC) - r.SetVersionInfo(v) - - checkResourceSet(t, r, ArchAMD64) -} - -func TestWinRes2(t *testing.T) { - r := &ResourceSet{} - - r.Set(RT_MANIFEST, ID(1), LCIDDefault, []byte(manifest1)) - - j, _ := os.ReadFile(filepath.Join(testDataDir, "vi.json")) - v := version.Info{} - json.Unmarshal(j, &v) - r.SetVersionInfo(v) - - r.SetIcon(ID(1), loadICOFile(t, "icon.ico")) - - checkResourceSet(t, r, ArchI386) -} - -func TestWinRes3(t *testing.T) { - rs := &ResourceSet{} - rs.SetCursor(ID(1), loadCURFile(t, "cursor.cur")) - checkResourceSet(t, rs, ArchARM) -} - -func TestWinRes4(t *testing.T) { - rs := &ResourceSet{} - rs.SetCursor(ID(1), loadPNGFileAsCursor(t, "cur-32x64.png", 10, 7)) - rs.SetIcon(ID(1), loadPNGFileAsIcon(t, "cur-32x64.png", []int{1, 7, 11, 15, 22, 255, 256})) - checkResourceSet(t, rs, ArchARM64) -} - -func TestResourceSet_Count(t *testing.T) { - rs := &ResourceSet{} - rs.SetManifest(AppManifest{}) - rs.SetManifest(AppManifest{Identity: AssemblyIdentity{Name: "Hello"}}) - rs.Set(RT_RCDATA, ID(42), 0x40C, make([]byte, 8)) - rs.Set(RT_RCDATA, ID(42), 0x40C, make([]byte, 9)) - rs.Set(RT_RCDATA, Name("Data"), 0x40C, make([]byte, 6)) - rs.Set(RT_RCDATA, ID(42), 0x409, make([]byte, 7)) - rs.Set(RT_VERSION, ID(1), 0x409, make([]byte, 9)) - rs.Set(RT_CURSOR, ID(42), 0x409, make([]byte, 5)) - rs.Set(Name("1"), ID(1), 0x409, make([]byte, 1)) - if rs.Count() != 7 { - t.Fail() - } -} - -func TestResourceSet_SetManifest(t *testing.T) { - rs := &ResourceSet{} - rs.SetManifest(AppManifest{}) - checkResourceSet(t, rs, ArchARM64) -} - -func TestResourceSet_SetVersionInfo(t *testing.T) { - rs := &ResourceSet{} - vi := version.Info{} - vi.FileVersion = [4]uint16{1, 2, 3, 4} - vi.ProductVersion = [4]uint16{1, 2, 3, 4} - vi.Set(0x0409, version.ProductName, "Good product") - vi.Set(0x040C, version.ProductName, "Bon produit") - rs.SetVersionInfo(vi) - checkResourceSet(t, rs, ArchAMD64) -} - -func TestResourceSet_Walk(t *testing.T) { - rs := ResourceSet{} - b := &bytes.Buffer{} - - walker := func(typeID, resID Identifier, langID uint16, data []byte) bool { - fmt.Fprintf(b, "%T(%v) -> %T(%v) -> 0x%04X -> [%d]byte\n", typeID, typeID, resID, resID, langID, len(data)) - return resID != Name("STOP") - } - - rs.Walk(walker) - if b.String() != "" { - t.Fail() - } - - rs.Set(RT_RCDATA, ID(42), 0x40C, make([]byte, 8)) - rs.Set(RT_RCDATA, Name("Data"), 0x40C, make([]byte, 6)) - rs.Set(RT_RCDATA, ID(42), 0x409, make([]byte, 7)) - rs.Set(RT_VERSION, ID(1), 0x409, make([]byte, 9)) - rs.Set(RT_CURSOR, ID(42), 0x409, make([]byte, 5)) - rs.Set(Name("1"), ID(1), 0x409, make([]byte, 1)) - rs.Set(Name("1"), ID(2), 0x409, make([]byte, 2)) - rs.Set(Name("Hi"), ID(2), 0x409, make([]byte, 3)) - rs.Set(Name("hey"), ID(2), 0x409, make([]byte, 4)) - rs.Set(ID(99), Name("STOP"), 0x409, make([]byte, 4)) - rs.Set(ID(99), Name("TOO FAR"), 0x409, make([]byte, 4)) - rs.Walk(walker) - expected := `winres.Name(1) -> winres.ID(1) -> 0x0409 -> [1]byte -winres.Name(1) -> winres.ID(2) -> 0x0409 -> [2]byte -winres.Name(Hi) -> winres.ID(2) -> 0x0409 -> [3]byte -winres.Name(hey) -> winres.ID(2) -> 0x0409 -> [4]byte -winres.ID(1) -> winres.ID(42) -> 0x0409 -> [5]byte -winres.ID(10) -> winres.Name(Data) -> 0x040C -> [6]byte -winres.ID(10) -> winres.ID(42) -> 0x0409 -> [7]byte -winres.ID(10) -> winres.ID(42) -> 0x040C -> [8]byte -winres.ID(16) -> winres.ID(1) -> 0x0409 -> [9]byte -winres.ID(99) -> winres.Name(STOP) -> 0x0409 -> [4]byte -` - if b.String() != expected { - t.Fail() - } -} - -func TestResourceSet_WalkType(t *testing.T) { - rs := ResourceSet{} - b := &bytes.Buffer{} - - walker := func(resID Identifier, langID uint16, data []byte) bool { - fmt.Fprintf(b, "%T(%v) -> 0x%04X -> [%d]byte\n", resID, resID, langID, len(data)) - return resID != ID(999) - } - - rs.WalkType(RT_RCDATA, walker) - if b.String() != "" { - t.Fail() - } - - rs.Set(RT_RCDATA, ID(42), 0x401, make([]byte, 8)) - rs.Set(RT_RCDATA, Name("Data"), 0x402, make([]byte, 6)) - rs.Set(RT_RCDATA, ID(42), 0x403, make([]byte, 7)) - rs.Set(RT_RCDATA, ID(999), 0x404, make([]byte, 4)) - rs.Set(RT_RCDATA, ID(1000), 0x405, make([]byte, 4)) - rs.Set(RT_VERSION, ID(1), 0x409, make([]byte, 9)) - rs.Set(RT_CURSOR, ID(42), 0x409, make([]byte, 5)) - rs.Set(Name("1"), ID(1), 0x409, make([]byte, 1)) - rs.Set(Name("1"), ID(2), 0x409, make([]byte, 2)) - rs.Set(Name("Hi"), ID(2), 0x409, make([]byte, 3)) - rs.Set(Name("hey"), ID(2), 0x409, make([]byte, 4)) - rs.WalkType(RT_RCDATA, walker) - expected := `winres.Name(Data) -> 0x0402 -> [6]byte -winres.ID(42) -> 0x0401 -> [8]byte -winres.ID(42) -> 0x0403 -> [7]byte -winres.ID(999) -> 0x0404 -> [4]byte -` - - if b.String() != expected { - t.Fail() - } -} - -// language=manifest -const manifest1 = ` - - - - - - - true/PM - - - - - - - - - - - - - - - - - - - - - - -` - -func Test_ResourceSet_set(t *testing.T) { - rs := ResourceSet{} - rs.set(ID(1), ID(2), 1, nil) - rs.set(ID(1), ID(2), 3, []byte{}) - rs.set(ID(1), ID(1), 4, nil) - rs.set(ID(1), ID(2), 4, nil) - if rs.Count() != 1 { - t.Fail() - } - rs.set(ID(1), ID(2), 3, nil) - if len(rs.types) != 0 { - t.Fail() - } - rs.set(Name("A"), Name("B"), 1, []byte{}) - rs.set(Name("A"), Name("B"), 2, []byte{}) - rs.set(Name("A"), Name("b"), 1, []byte{}) - if rs.Count() != 3 { - t.Fail() - } - rs.set(Name("A"), Name("B"), 1, nil) - if rs.Count() != 2 { - t.Fail() - } - rs.set(Name("A"), Name("B"), 2, nil) - if rs.Count() != 1 { - t.Fail() - } - if _, exists := rs.types[Name("A")].resources[Name("B")]; exists { - t.Fail() - } - rs.set(Name("A"), Name("b"), 1, nil) - if len(rs.types) != 0 { - t.Fail() - } - rs.set(RT_ICON, ID(4), 1, []byte{}) - rs.set(RT_ICON, ID(42), 2, []byte{}) - rs.set(RT_ICON, ID(1), 1, []byte{}) - rs.set(RT_ICON, Name("420"), 3, []byte{}) - if rs.lastIconID != 42 { - t.Fail() - } - rs.set(RT_CURSOR, ID(2), 1, []byte{}) - rs.set(RT_CURSOR, ID(24), 2, []byte{}) - rs.set(RT_CURSOR, ID(1), 1, []byte{}) - rs.set(RT_CURSOR, Name("420"), 3, []byte{}) - if rs.lastCursorID != 24 { - t.Fail() - } - rs.set(RT_ICON, ID(42), 2, nil) - rs.set(RT_CURSOR, ID(24), 2, nil) - if rs.lastIconID != 42 || rs.lastCursorID != 24 { - t.Fatal("delete is not supposed to rollback lastCursorID/lastIconID") - } - rs.set(RT_CURSOR, ID(2), 1, nil) - rs.set(RT_CURSOR, ID(1), 1, nil) - rs.set(RT_CURSOR, Name("420"), 3, nil) - if len(rs.types) != 1 { - t.Fail() - } -} - -func Test_ResourceSet_firstLang(t *testing.T) { - rs := ResourceSet{} - - rs.set(ID(1), ID(2), 3, []byte{1}) - if rs.types[ID(1)].resources[ID(2)].orderedKeys != nil { - t.Fail() - } - rs.order(&state{}) - if len(rs.types[ID(1)].resources[ID(2)].orderedKeys) != 1 { - t.Fail() - } - if rs.firstLang(ID(1), ID(2)) != 3 { - t.Fail() - } - rs.set(ID(1), ID(2), 2, []byte{2}) - if rs.types[ID(1)].resources[ID(2)].orderedKeys != nil { - t.Fail() - } - rs.set(ID(1), ID(2), 1, []byte{3}) - rs.set(ID(1), ID(2), 4, []byte{4}) - if rs.firstLang(ID(1), ID(2)) != 1 { - t.Fail() - } - rs.set(ID(1), ID(2), 0, []byte{5}) - if rs.firstLang(ID(1), ID(2)) != 0 { - t.Fail() - } - rs.set(ID(1), ID(2), 0, nil) - rs.set(ID(1), ID(2), 1, nil) - if rs.firstLang(ID(1), ID(2)) != 2 { - t.Fail() - } - rs.set(ID(1), ID(2), 2, nil) - if rs.firstLang(ID(1), ID(2)) != 3 { - t.Fail() - } - // Make up impossible case - delete(rs.types[ID(1)].resources[ID(2)].data, 3) - delete(rs.types[ID(1)].resources[ID(2)].data, 4) - if rs.firstLang(ID(1), ID(2)) != 0 { - t.Fail() - } -} - -func TestResourceSet_WriteToEXE_VS(t *testing.T) { - exe, _ := os.Open(filepath.Join(testDataDir, "vs.exe")) - defer exe.Close() - - rs, err := LoadFromEXE(exe) - if err != nil { - t.Fatal(err) - } - - rs.Set(Name("AAA"), Name("AAA"), 0x409, []byte{1}) - ico := loadPNGFileAsIcon(t, "img.png", nil) - rs.SetIcon(Name("aAA"), ico) - rs.SetIcon(Name("APPICON"), loadICOFile(t, "icon.ico")) - - buf := bytes.Buffer{} - err = rs.WriteToEXE(&buf, exe) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_WriteToEXE_VS0(t *testing.T) { - exe, _ := os.Open(filepath.Join(testDataDir, "vs0.exe")) - defer exe.Close() - - rs, err := LoadFromEXE(exe) - if rs == nil { - t.Fatal(err) - } - - rs.Set(Name("AAA"), Name("AAA"), 0x409, []byte{1}) - ico := loadPNGFileAsIcon(t, "img.png", nil) - rs.SetIcon(Name("aAA"), ico) - rs.SetIcon(Name("APPICON"), loadICOFile(t, "icon.ico")) - rs.SetManifest(AppManifest{ - Identity: AssemblyIdentity{ - Name: "Some app", - Version: [4]uint16{1, 2, 3, 4}, - }, - Description: "This is an application", - SegmentHeap: true, - UseCommonControlsV6: true, - Compatibility: Win81AndAbove, - DPIAwareness: DPIPerMonitorV2, - }) - - buf := bytes.Buffer{} - err = rs.WriteToEXE(&buf, exe) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_WriteToEXE_VS032(t *testing.T) { - exe, _ := os.Open(filepath.Join(testDataDir, "vs032.exe")) - defer exe.Close() - - rs, err := LoadFromEXE(exe) - if rs == nil { - t.Fatal(err) - } - - rs.SetManifest(AppManifest{}) - - buf := writeSeeker{} - err = rs.WriteToEXE(&buf, exe) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_WriteToEXE_VS32(t *testing.T) { - exe, _ := os.Open(filepath.Join(testDataDir, "vs32.exe")) - defer exe.Close() - - rs, err := LoadFromEXE(exe) - if err != nil { - t.Fatal(err) - } - - rs.SetIcon(Name("APPICON"), loadICOFile(t, "icon.ico")) - - buf := bytes.Buffer{} - err = rs.WriteToEXE(&buf, exe) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_WriteToEXEWithCheckSum_VS32(t *testing.T) { - exe, _ := os.Open(filepath.Join(testDataDir, "vs32.exe")) - defer exe.Close() - - rs, err := LoadFromEXE(exe) - if err != nil { - t.Fatal(err) - } - - rs.SetIcon(Name("APPICON"), loadICOFile(t, "icon.ico")) - - buf := bytes.Buffer{} - err = rs.WriteToEXE(&buf, onlyReadSeeker{exe}, ForceCheckSum()) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_WriteToEXEWithCheckSum_VS(t *testing.T) { - exe, _ := os.Open(filepath.Join(testDataDir, "vs.exe")) - defer exe.Close() - - rs, err := LoadFromEXE(exe) - if err != nil { - t.Fatal(err) - } - - rs.Set(Name("AAA"), Name("AAA"), 0x409, []byte{1}) - ico := loadPNGFileAsIcon(t, "img.png", nil) - rs.SetIcon(Name("aAA"), ico) - rs.SetIcon(Name("APPICON"), loadICOFile(t, "icon.ico")) - - buf := bytes.Buffer{} - err = rs.WriteToEXE(&buf, exe, ForceCheckSum()) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_WriteToEXE_End(t *testing.T) { - exe, _ := os.Open(filepath.Join(testDataDir, "end.exe")) - defer exe.Close() - - rs, err := LoadFromEXE(exe) - if err != nil { - t.Fatal(err) - } - - rs.SetIcon(Name("APPICON"), loadICOFile(t, "icon.ico")) - - buf := bytes.Buffer{} - err = rs.WriteToEXE(&buf, exe) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_WriteToEXE_NotEnd(t *testing.T) { - exe, _ := os.Open(filepath.Join(testDataDir, "notend.exe")) - defer exe.Close() - - rs, err := LoadFromEXE(exe) - if err != nil { - t.Fatal(err) - } - - rs.SetIcon(Name("APPICON"), loadICOFile(t, "icon.ico")) - - buf := bytes.Buffer{} - err = rs.WriteToEXE(&buf, exe) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_WriteToEXE_SignedErr(t *testing.T) { - exe, _ := os.Open(filepath.Join(testDataDir, "signed.exe")) - defer exe.Close() - - rs, err := LoadFromEXE(exe) - if err != nil { - t.Fatal(err) - } - - buf := bytes.Buffer{} - err = rs.WriteToEXE(&buf, exe) - if err != ErrSignedPE { - t.Fatal("expected error:\n", ErrSignedPE, "\ngot:\n", err) - } -} - -func TestResourceSet_WriteToEXE_IgnoreSignature(t *testing.T) { - exe, _ := os.Open(filepath.Join(testDataDir, "signed.exe")) - defer exe.Close() - - rs, err := LoadFromEXE(exe) - if err != nil { - t.Fatal(err) - } - - rs.SetIcon(Name("APPICON"), loadICOFile(t, "icon.ico")) - - buf := bytes.Buffer{} - err = rs.WriteToEXE(&buf, exe, WithAuthenticode(IgnoreSignature)) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_WriteToEXE_RemoveSignature(t *testing.T) { - exe, _ := os.Open(filepath.Join(testDataDir, "signed.exe")) - defer exe.Close() - - rs, err := LoadFromEXE(exe) - if err != nil { - t.Fatal(err) - } - - buf := bytes.Buffer{} - err = rs.WriteToEXE(&buf, exe, WithAuthenticode(RemoveSignature)) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, buf.Bytes()) -} - -func TestLoadFromEXESingleType(t *testing.T) { - exe, err := os.Open(filepath.Join(testDataDir, "rh.exe")) - if err != nil { - t.Fatal(err) - } - defer exe.Close() - - rs, err := LoadFromEXESingleType(exe, RT_GROUP_ICON) - if err != nil { - t.Fatal(err) - } - - if rs.Count() != 21 || rs.lastIconID != 18 { - t.Fail() - } - - buf := bytes.Buffer{} - rs.WriteObject(&buf, ArchAMD64) - checkBinary(t, buf.Bytes()) -} - -func TestLoadFromEXESingleType_Err(t *testing.T) { - exe, err := os.Open(filepath.Join(testDataDir, "rh.exe")) - if err != nil { - t.Fatal(err) - } - exe.Close() - - rs, err := LoadFromEXESingleType(exe, RT_GROUP_ICON) - if err == nil || rs != nil { - t.Fail() - } - rs, err = LoadFromEXESingleType(exe, ID(0)) - if err == nil || rs != nil || err.Error() != errZeroID { - t.Fail() - } - - exe, err = os.Open(filepath.Join(testDataDir, "invalid_rsrc.exe")) - if err != nil { - t.Fatal(err) - } - defer exe.Close() - - rs, err = LoadFromEXESingleType(exe, RT_GROUP_ICON) - if err != io.ErrUnexpectedEOF || rs != nil { - t.Fail() - } -} - -func TestResourceSet_LoadFromEXE_Err(t *testing.T) { - rs, err := LoadFromEXE(bytes.NewReader([]byte{'N', 'Z', 0x40: 0})) - if err == nil || rs != nil || err.Error() != errNotPEImage { - t.Error(err) - } - - rs, err = LoadFromEXE(bytes.NewReader([]byte{'M', 'Z'})) - if err != io.ErrUnexpectedEOF || rs != nil { - t.Error(err) - } - - rs, err = LoadFromEXE(bytes.NewReader([]byte{'M', 'Z'})) - if err != io.ErrUnexpectedEOF || rs != nil { - t.Error(err) - } - - b := loadBinary(t, "vs.exe") - b = b[:0x160] - rs, err = LoadFromEXE(bytes.NewReader(b)) - if err != io.ErrUnexpectedEOF || rs != nil { - t.Error(err) - } - - b = loadBinary(t, "vs.exe") - b[0x3C]++ // corrupt offset to PE signature - rs, err = LoadFromEXE(bytes.NewReader(b)) - if err == nil || rs != nil || err.Error() != errNotPEImage { - t.Error(err) - } - - b = loadBinary(t, "vs.exe") - b[0x191] = 0x80 // corrupt resource directory address - rs, err = LoadFromEXE(bytes.NewReader(b)) - if err == nil || rs != nil || err.Error() != errRSRCNotFound { - t.Error(err) - } - - b = loadBinary(t, "vs.exe") - b[0x2B1] = 0x06 // add 0x400 to the size of the resource section - rs, err = LoadFromEXE(bytes.NewReader(b)) - if err == nil || rs != nil || err.Error() != errSectionTooFar { - t.Error(err) - } - - b = loadBinary(t, "vs.exe") - b[0x110]++ // PE magic - rs, err = LoadFromEXE(bytes.NewReader(b)) - if err == nil || rs != nil || err.Error() != errUnknownPE { - t.Error(err) - } - b[0x110]-- - b[0x111]++ - rs, err = LoadFromEXE(bytes.NewReader(b)) - if err == nil || rs != nil || err.Error() != errUnknownPE { - t.Error(err) - } - - b = loadBinary(t, "vs32.exe") - b[0x10C] = 0x5F - rs, err = LoadFromEXE(bytes.NewReader(b)) - if err != io.ErrUnexpectedEOF || rs != nil { - t.Error(err) - } - - b = loadBinary(t, "vs32.exe") - b[0x16C] = 0x05 - rs, err = LoadFromEXE(bytes.NewReader(b)) - if err == nil || rs != nil || err.Error() != errUnknownPE { - t.Error(err) - } - - b = loadBinary(t, "vs32.exe") - b[0x10C] = 0xDF - rs, err = LoadFromEXE(bytes.NewReader(b)) - if err == nil || rs != nil || err.Error() != errNotPEImage { - t.Error(err) - } - - b = loadBinary(t, "vs32.exe") - br := badReader{ - br: bytes.NewReader(b), - errPos: 0x1EF, - } - rs, err = LoadFromEXE(&br) - if !isExpectedReadErr(err) || rs != nil { - t.Fatal("expected read error, got", err) - } - - b = loadBinary(t, "vs32.exe") - br = badReader{ - br: bytes.NewReader(b), - errPos: 0x201, - } - rs, err = LoadFromEXE(&br) - if !isExpectedReadErr(err) || rs != nil { - t.Fatal("expected read error, got", err) - } - - b = loadBinary(t, "vs0.exe") - rs, err = LoadFromEXE(bytes.NewReader(b)) - if err == nil || rs == nil || rs.types != nil || err.Error() != errNoRSRC { - t.Error(err) - } - - b = loadBinary(t, "vs.exe") - br = badReader{ - br: bytes.NewReader(b), - errPos: 0x2A60, - } - rs, err = LoadFromEXE(&br) - if !isExpectedReadErr(err) || rs != nil { - t.Fatal("expected read error, got", err) - } -} - -func TestResourceSet_WriteToEXE_Err(t *testing.T) { - data := loadBinary(t, "vs.exe") - data0 := loadBinary(t, "vs0.exe") - - tt := []struct { - w io.Writer - data []byte - errMsg string - poke []poke - badSeek int - }{ - {w: newBadWriter(252), data: data, errMsg: errWrite}, - {w: io.Discard, data: data, errMsg: errRSRCTwice, poke: []poke{{off: 0x25D, val: 0x60}}}, - {w: io.Discard, data: data, errMsg: errRelocTwice, poke: []poke{{off: 0x25D, val: 0x70}}}, - {w: &writeSeeker{bad: 0xA0}, data: data, errMsg: errWrite}, - {w: &writeSeeker{bad: 0xFE}, data: data, errMsg: errWrite}, - {w: &writeSeeker{bad: 0x140}, data: data, errMsg: errWrite}, - {w: &writeSeeker{bad: 0x180}, data: data, errMsg: errWrite}, - {w: &writeSeeker{bad: 0x200}, data: data, errMsg: errWrite}, - {w: &writeSeeker{bad: 0x300}, data: data, errMsg: errWrite}, - {w: newBadWriter(0x300), data: data, errMsg: errWrite}, - {w: &writeSeeker{bad: 0x27E0}, data: data, errMsg: errWrite}, - {w: &writeSeeker{bad: 0x2BF8}, data: data0[:0x2B00], errMsg: errWrite, poke: []poke{{off: 0x2A1, val: 0x01}}}, - {w: &writeSeeker{bad: 0x2A08}, data: data, errMsg: errWrite}, - {w: &writeSeeker{bad: 0x2A18}, data: data, errMsg: errWrite}, - {w: &writeSeeker{bad: 0x2C08}, data: data, errMsg: errWrite}, - {w: io.Discard, data: data, badSeek: 5, errMsg: errSeek}, - {w: io.Discard, data: data, badSeek: 6, errMsg: errSeek}, - {w: io.Discard, data: data, badSeek: 7, errMsg: errSeek}, - {w: io.Discard, data: data, badSeek: 8, errMsg: errSeek}, - { - w: io.Discard, - data: data0, - errMsg: errNoRoomForRSRC, - poke: []poke{ - {off: 0x205, val: 0x02}, - {off: 0x204, val: 0xB8}, - }, - }, - } - - for i := range tt { - rs := ResourceSet{} - - b := append([]byte{}, tt[i].data...) - for _, p := range tt[i].poke { - b[p.off] = p.val - } - - var r io.ReadSeeker = bytes.NewReader(b) - if tt[i].badSeek > 0 { - r = &badSeeker{ - br: bytes.NewReader(b), - errIter: tt[i].badSeek, - } - } - err := rs.WriteToEXE(tt[i].w, r) - - if err == nil || err.Error() != tt[i].errMsg { - t.Error(i, "got:", err, "\nexpected:", tt[i].errMsg) - } - } -} - -func TestResourceSet_WriteToEXE_Delete(t *testing.T) { - exe, _ := os.Open(filepath.Join(testDataDir, "notend.exe")) - defer exe.Close() - - rs, err := LoadFromEXE(exe) - if err != nil { - t.Fatal(err) - } - - for i := 1; i <= 12; i++ { - rs.Set(RT_ICON, ID(i), 0, nil) - rs.Set(RT_CURSOR, ID(i), 0, nil) - } - rs.Set(Name("PNG"), Name("CUR-16X8"), 0, nil) - rs.Set(RT_RCDATA, ID(1), 0x409, []byte{}) - - buf := bytes.Buffer{} - err = rs.WriteToEXE(&buf, exe) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_WriteToEXE_SFX(t *testing.T) { - // Self extracting archives have data after the last section of the executable - exe, _ := os.Open(filepath.Join(testDataDir, "sfx.exe")) - defer exe.Close() - - rs, err := LoadFromEXE(exe) - if err != nil { - t.Fatal(err) - } - - // Replace file properties - vi, err := version.FromBytes(rs.Get(RT_VERSION, ID(1), LCIDDefault)) - if err != nil { - t.Fatal(err) - } - vi.SetProductVersion("1.0.0.42") - vi.Set(LCIDDefault, version.ProductName, "My Archive") - vi.Set(LCIDDefault, version.CompanyName, "My Company") - vi.Set(LCIDDefault, version.LegalCopyright, "My copyright (but thanks to 7z author)") - rs.SetVersionInfo(*vi) - // Replace the icon - rs.Set(RT_ICON, ID(1), LCIDDefault, nil) - rs.Set(RT_ICON, ID(2), LCIDDefault, nil) - rs.SetIconTranslation(ID(1), LCIDDefault, loadICOFile(t, "en.ico")) - rs.SetIconTranslation(ID(1), 0x40C, loadICOFile(t, "fr.ico")) - // Add a manifest for a better GUI on high DPI - rs.SetManifest(AppManifest{ - DPIAwareness: DPIPerMonitorV2, - UseCommonControlsV6: true, - }) - - buf := bytes.Buffer{} - err = rs.WriteToEXE(&buf, exe) - if err != nil { - t.Fatal(err) - } - - checkBinary(t, buf.Bytes()) -} - -func TestResourceSet_WriteToEXE_EOF(t *testing.T) { - data := loadBinary(t, "vs.exe") - - tt := []struct { - data []byte - }{ - {data: []byte{'M', 'Z', 0x3C: 0x40, 0x40: 'P', 'E'}}, - {data: []byte{'M', 'Z', 0x3C: 0x40, 0x40: 'P', 'E', 0x44: 0}}, - {data: data[:len(data)-0x1FF]}, - } - - for i := range tt { - rs := ResourceSet{} - - err := rs.WriteToEXE(io.Discard, bytes.NewReader(tt[i].data)) - - if err != io.ErrUnexpectedEOF { - t.Error(i, err) - } - } -} - -func TestIsSignedEXE_False(t *testing.T) { - f, err := os.Open(filepath.Join(testDataDir, "sfx.exe")) - if err != nil { - t.Fatal(err) - } - defer f.Close() - - s, err := IsSignedEXE(f) - if err != nil { - t.Fatal(err) - } - if s { - t.Fatal("expected IsSignedEXE to return false, got true") - } -} - -func TestIsSignedEXE_True(t *testing.T) { - f, err := os.Open(filepath.Join(testDataDir, "signed.exe")) - if err != nil { - t.Fatal(err) - } - defer f.Close() - - f.Seek(42, io.SeekStart) - s, err := IsSignedEXE(f) - if err != nil { - t.Fatal(err) - } - if !s { - t.Fatal("expected IsSignedEXE to return true, got false") - } - p, _ := f.Seek(0, io.SeekCurrent) - if p != 42 { - t.Fatal("expected IsSignedEXE to restore reader's position, but it didn't") - } -} - -func TestIsSignedEXE_Error(t *testing.T) { - r := bytes.NewReader([]byte{'N', 'Z', 0x40: 0}) - r.Seek(2, io.SeekStart) - _, err := IsSignedEXE(r) - if err == nil { - t.Fatal("expected an error, didn't get one") - } - p, _ := r.Seek(0, io.SeekCurrent) - if p != 2 { - t.Fatal("expected IsSignedEXE to restore reader's position, but it didn't") - } -} - -type onlyReadSeeker struct { - io.ReadSeeker -} - -type poke struct { - off int - val byte -} - -type writeSeeker struct { - buf bytes.Buffer - pos int64 - end int64 - bad int64 -} - -func (ws *writeSeeker) Write(data []byte) (int, error) { - if ws.bad > 0 && ws.end <= ws.bad && ws.bad < ws.pos+int64(len(data)) { - return 0, errors.New(errWrite) - } - if ws.pos < int64(ws.buf.Len()) { - ws.buf.Truncate(int(ws.pos)) - } else if ws.pos > int64(ws.buf.Len()) { - ws.buf.Write(make([]byte, ws.pos-int64(ws.buf.Len()))) - } - n, err := ws.buf.Write(data) - ws.pos += int64(n) - if ws.end < ws.pos { - ws.end = ws.pos - } - return n, err -} - -func (ws *writeSeeker) Seek(off int64, whence int) (int64, error) { - switch whence { - case io.SeekStart: - ws.pos = off - case io.SeekCurrent: - ws.pos = int64(ws.buf.Len()) + off - case io.SeekEnd: - ws.pos = ws.end + off - } - return ws.pos, nil -} - -func (ws writeSeeker) Bytes() []byte { - return ws.buf.Bytes() -} -- GitLab