diff --git a/langs.go b/langs.go new file mode 100644 index 0000000..718d97f --- /dev/null +++ b/langs.go @@ -0,0 +1,18 @@ +package pelican + +// see +// https://msdn.microsoft.com/en-us/library/windows/desktop/dd318693(v=vs.85).aspx +func isLanguageWhitelisted(key string) bool { + localeID := key[:4] + primaryLangID := localeID[2:] + + switch primaryLangID { + // neutral + case "00": + return true + // english + case "09": + return true + } + return false +} diff --git a/manifest.go b/manifest.go new file mode 100644 index 0000000..3524d99 --- /dev/null +++ b/manifest.go @@ -0,0 +1,90 @@ +package pelican + +import ( + "encoding/json" + + "github.com/go-errors/errors" +) + +type node = map[string]interface{} + +func visit(n node, key string, f func(c node)) { + if c, ok := n[key].(node); ok { + f(c) + } +} + +func visitMany(n node, key string, f func(c node)) { + if cs, ok := n[key].([]node); ok { + for _, c := range cs { + f(c) + } + } + if c, ok := n[key].(node); ok { + f(c) + } +} + +func getString(n node, key string, f func(s string)) { + if s, ok := n[key].(string); ok { + f(s) + } +} + +func interpretManifest(info *PeInfo, manifest []byte) error { + intermediate := make(node) + err := json.Unmarshal([]byte(manifest), &intermediate) + if err != nil { + return errors.Wrap(err, 0) + } + + assInfo := &AssemblyInfo{} + + interpretIdentity := func(id node, f func(id *AssemblyIdentity)) { + ai := &AssemblyIdentity{} + getString(id, "-name", func(s string) { ai.Name = s }) + getString(id, "-version", func(s string) { ai.Version = s }) + getString(id, "-type", func(s string) { ai.Type = s }) + + getString(id, "-processorArchitecture", func(s string) { ai.ProcessorArchitecture = s }) + getString(id, "-publicKeyToken", func(s string) { ai.PublicKeyToken = s }) + getString(id, "-language", func(s string) { ai.Language = s }) + f(ai) + } + + visit(intermediate, "assembly", func(assembly node) { + visit(assembly, "assemblyIdentity", func(id node) { + interpretIdentity(id, func(ai *AssemblyIdentity) { + assInfo.Identity = ai + }) + }) + + getString(assembly, "description", func(s string) { assInfo.Description = s }) + + visit(assembly, "trustInfo", func(ti node) { + visit(ti, "security", func(sec node) { + visit(sec, "requestedPrivileges", func(rp node) { + visit(rp, "requestedExecutionLevel", func(rel node) { + getString(rel, "-level", func(s string) { + assInfo.RequestedExecutionLevel = s + }) + }) + }) + }) + }) + + visit(assembly, "dependency", func(dep node) { + visitMany(dep, "dependentAssembly", func(da node) { + visit(da, "assemblyIdentity", func(id node) { + interpretIdentity(id, func(ai *AssemblyIdentity) { + info.DependentAssemblies = append(info.DependentAssemblies, ai) + }) + }) + }) + }) + }) + + info.AssemblyInfo = assInfo + + return nil +} diff --git a/probe.go b/probe.go index c1855d0..36ca7d6 100644 --- a/probe.go +++ b/probe.go @@ -5,37 +5,40 @@ import ( "github.com/go-errors/errors" "github.com/itchio/wharf/eos" + "github.com/itchio/wharf/state" ) -type Arch string - -const ( - Arch386 = "386" - ArchAmd64 = "amd64" -) - -type PeInfo struct { - Arch Arch -} - type ProbeParams struct { - // nothing yet + Consumer *state.Consumer } // Probe retrieves information about an PE file func Probe(file eos.File, params *ProbeParams) (*PeInfo, error) { + if params == nil { + return nil, errors.New("params must be set") + } + consumer := params.Consumer + pf, err := pe.NewFile(file) if err != nil { return nil, errors.Wrap(err, 0) } - res := &PeInfo{} + info := &PeInfo{ + VersionProperties: make(map[string]string), + } switch pf.Machine { case pe.IMAGE_FILE_MACHINE_I386: - res.Arch = Arch386 + info.Arch = "386" case pe.IMAGE_FILE_MACHINE_AMD64: - res.Arch = ArchAmd64 + info.Arch = "amd64" } - return res, nil + + sect := pf.Section(".rsrc") + if sect != nil { + parseResources(consumer, info, sect) + } + + return info, nil } diff --git a/probe_test.go b/probe_test.go index 03aed09..a7789b6 100644 --- a/probe_test.go +++ b/probe_test.go @@ -5,54 +5,104 @@ import ( "github.com/itchio/pelican" "github.com/itchio/wharf/eos" + "github.com/itchio/wharf/state" "github.com/stretchr/testify/assert" ) +func testProbeParams(t *testing.T) *pelican.ProbeParams { + return &pelican.ProbeParams{ + Consumer: &state.Consumer{ + OnMessage: func(level string, message string) { + t.Logf("[%s] %s", level, message) + }, + }, + } +} + func Test_NotPeFile(t *testing.T) { - f, err := eos.Open("./testdata/hello.c") + f, err := eos.Open("./testdata/hello/hello.c") assert.NoError(t, err) defer f.Close() - _, err = pelican.Probe(f, nil) + _, err = pelican.Probe(f, testProbeParams(t)) assert.Error(t, err) } func Test_Hello32Mingw(t *testing.T) { - f, err := eos.Open("./testdata/hello32-mingw.exe") + f, err := eos.Open("./testdata/hello/hello32-mingw.exe") assert.NoError(t, err) defer f.Close() - res, err := pelican.Probe(f, nil) + info, err := pelican.Probe(f, testProbeParams(t)) assert.NoError(t, err) - assert.EqualValues(t, pelican.Arch386, res.Arch) + assert.EqualValues(t, pelican.Arch386, info.Arch) } func Test_Hello32Msvc(t *testing.T) { - f, err := eos.Open("./testdata/hello32-msvc.exe") + f, err := eos.Open("./testdata/hello/hello32-msvc.exe") assert.NoError(t, err) defer f.Close() - res, err := pelican.Probe(f, nil) + info, err := pelican.Probe(f, testProbeParams(t)) assert.NoError(t, err) - assert.EqualValues(t, pelican.Arch386, res.Arch) + assert.EqualValues(t, pelican.Arch386, info.Arch) } func Test_Hello64Mingw(t *testing.T) { - f, err := eos.Open("./testdata/hello64-mingw.exe") + f, err := eos.Open("./testdata/hello/hello64-mingw.exe") assert.NoError(t, err) defer f.Close() - res, err := pelican.Probe(f, nil) + info, err := pelican.Probe(f, testProbeParams(t)) assert.NoError(t, err) - assert.EqualValues(t, pelican.ArchAmd64, res.Arch) + assert.EqualValues(t, pelican.ArchAmd64, info.Arch) } func Test_Hello64Msvc(t *testing.T) { - f, err := eos.Open("./testdata/hello64-msvc.exe") + f, err := eos.Open("./testdata/hello/hello64-msvc.exe") + assert.NoError(t, err) + defer f.Close() + + info, err := pelican.Probe(f, testProbeParams(t)) + assert.NoError(t, err) + assert.EqualValues(t, pelican.ArchAmd64, info.Arch) +} + +func assertResources(t *testing.T, info *pelican.PeInfo) { + vp := info.VersionProperties + assert.EqualValues(t, "itch corp.", vp["CompanyName"]) + assert.EqualValues(t, "Test PE file for pelican", vp["FileDescription"]) + assert.EqualValues(t, "3.14", vp["FileVersion"]) + assert.EqualValues(t, "resourceful", vp["InternalName"]) + assert.EqualValues(t, "(c) 2018 itch corp.", vp["LegalCopyright"]) + + // totally a mistake, but leaving this as a reminder that + // not everything is worth fixing + assert.EqualValues(t, "butler", vp["ProductName"]) + + assert.EqualValues(t, "6.28", vp["ProductVersion"]) +} + +func Test_Resourceful32Mingw(t *testing.T) { + f, err := eos.Open("./testdata/resourceful/resourceful32-mingw.exe") + assert.NoError(t, err) + defer f.Close() + + info, err := pelican.Probe(f, testProbeParams(t)) + assert.NoError(t, err) + assert.EqualValues(t, pelican.Arch386, info.Arch) + + assertResources(t, info) +} + +func Test_Resourceful64Mingw(t *testing.T) { + f, err := eos.Open("./testdata/resourceful/resourceful64-mingw.exe") assert.NoError(t, err) defer f.Close() - res, err := pelican.Probe(f, nil) + info, err := pelican.Probe(f, testProbeParams(t)) assert.NoError(t, err) - assert.EqualValues(t, pelican.ArchAmd64, res.Arch) + assert.EqualValues(t, pelican.ArchAmd64, info.Arch) + + assertResources(t, info) } diff --git a/resources.go b/resources.go new file mode 100644 index 0000000..7fd619b --- /dev/null +++ b/resources.go @@ -0,0 +1,202 @@ +package pelican + +import ( + "debug/pe" + "encoding/binary" + "fmt" + "io" + "io/ioutil" + "strings" + + xj "github.com/basgys/goxml2json" + humanize "github.com/dustin/go-humanize" + "github.com/go-errors/errors" + "github.com/itchio/wharf/state" +) + +type imageResourceDirectory struct { + Characteristics uint32 + TimeDateStamp uint32 + MajorVersion uint16 + MinorVersion uint16 + NumberOfNamedEntries uint16 + NumberOfIdEntries uint16 +} + +type imageResourceDirectoryEntry struct { + NameId uint32 + Data uint32 +} + +type imageResourceDataEntry struct { + Data uint32 + Size uint32 + CodePage uint32 + Reserved uint32 +} + +type ResourceType uint32 + +// https://msdn.microsoft.com/fr-fr/library/windows/desktop/ms648009(v=vs.85).aspx +const ( + ResourceTypeNone ResourceType = 0 + + ResourceTypeCursor ResourceType = 1 + ResourceTypeBitmap ResourceType = 2 + ResourceTypeIcon ResourceType = 3 + ResourceTypeMenu ResourceType = 4 + ResourceTypeDialog ResourceType = 5 + ResourceTypeString ResourceType = 6 + ResourceTypeFontDir ResourceType = 7 + ResourceTypeFont ResourceType = 8 + ResourceTypeAccelerator ResourceType = 9 + ResourceTypeRcData ResourceType = 10 + ResourceTypeMessageTable ResourceType = 11 + + ResourceTypeGroupCursor ResourceType = ResourceTypeCursor + 11 // 12 + ResourceTypeGroupIcon ResourceType = ResourceTypeIcon + 11 // 14 + + ResourceTypeVersion ResourceType = 16 + ResourceTypeDlgInclude ResourceType = 17 + ResourceTypePlugPlay ResourceType = 19 + ResourceTypeVXD ResourceType = 20 // vxd = virtual device + ResourceTypeAniCursor ResourceType = 21 + ResourceTypeAniIcon ResourceType = 22 + ResourceTypeHTML ResourceType = 23 + ResourceTypeManifest ResourceType = 24 +) + +var ResourceTypeNames = map[ResourceType]string{ + ResourceTypeCursor: "Cursor", + ResourceTypeBitmap: "Bitmap", + ResourceTypeIcon: "Icon", + ResourceTypeMenu: "Menu", + ResourceTypeDialog: "Dialog", + ResourceTypeString: "String", + ResourceTypeFontDir: "FontDir", + ResourceTypeFont: "Font", + ResourceTypeAccelerator: "Accelerator", + ResourceTypeRcData: "RcData", + ResourceTypeMessageTable: "MessageTable", + ResourceTypeGroupCursor: "GroupCursor", + ResourceTypeGroupIcon: "GroupIcon", + ResourceTypeVersion: "Version", + ResourceTypeDlgInclude: "DlgInclude", + ResourceTypePlugPlay: "PlugPlay", + ResourceTypeVXD: "VXD", + ResourceTypeAniCursor: "AniCursor", + ResourceTypeAniIcon: "AniIcon", + ResourceTypeHTML: "HTML", + ResourceTypeManifest: "Manifest", +} + +func parseResources(consumer *state.Consumer, info *PeInfo, sect *pe.Section) error { + consumer.Debugf("Found resource section (%s)", humanize.IBytes(uint64(sect.Size))) + + var readDirectory func(offset uint32, level int, resourceType ResourceType) error + readDirectory = func(offset uint32, level int, resourceType ResourceType) error { + prefix := strings.Repeat(" ", level) + log := func(msg string, args ...interface{}) { + consumer.Debugf("%s%s", prefix, fmt.Sprintf(msg, args...)) + } + + br := io.NewSectionReader(sect, int64(offset), int64(sect.Size)-int64(offset)) + ird := new(imageResourceDirectory) + err := binary.Read(br, binary.LittleEndian, ird) + if err != nil { + return errors.Wrap(err, 0) + } + + for i := uint16(0); i < ird.NumberOfNamedEntries+ird.NumberOfIdEntries; i++ { + irde := new(imageResourceDirectoryEntry) + err = binary.Read(br, binary.LittleEndian, irde) + if err != nil { + return errors.Wrap(err, 0) + } + + if irde.NameId&0x80000000 > 0 { + continue + } + + id := irde.NameId & 0xffff + if level == 0 { + typ := ResourceType(id) + if name, ok := ResourceTypeNames[typ]; ok { + log("=> %s", name) + } else { + log("=> type #%d (unknown)", id) + } + } else { + log("=> %d", id) + } + + if irde.Data&0x80000000 > 0 { + offset := irde.Data & 0x7fffffff + recResourceType := resourceType + if level == 0 { + recResourceType = ResourceType(id) + } + + err := readDirectory(offset, level+1, recResourceType) + if err != nil { + return errors.Wrap(err, 0) + } + continue + } + + dbr := io.NewSectionReader(sect, int64(irde.Data), int64(sect.Size)-int64(irde.Data)) + + irda := new(imageResourceDataEntry) + err = binary.Read(dbr, binary.LittleEndian, irda) + if err != nil { + return errors.Wrap(err, 0) + } + + if resourceType == ResourceTypeManifest || resourceType == ResourceTypeVersion { + log("@ %x (%s, %d bytes)", irda.Data, humanize.IBytes(uint64(irda.Size)), irda.Size) + + sr := io.NewSectionReader(sect, int64(irda.Data-sect.VirtualAddress), int64(irda.Size)) + rawData, err := ioutil.ReadAll(sr) + if err != nil { + return errors.Wrap(err, 0) + } + + switch resourceType { + case ResourceTypeManifest: + // actually not utf-16, + // but TODO: figure out + // codepage + stringData := string(rawData) + log("=========================") + for _, l := range strings.Split(stringData, "\n") { + log("%s", l) + } + log("=========================") + + js, err := xj.Convert(strings.NewReader(stringData)) + if err != nil { + log("could not convert xml to json: %s", err.Error()) + } else { + err := interpretManifest(info, js.Bytes()) + if err != nil { + log("could not interpret manifest: %s", err.Error()) + } + } + case ResourceTypeVersion: + err := parseVersion(info, consumer, rawData) + if err != nil { + return errors.Wrap(err, 0) + } + } + } + } + return nil + } + + err := readDirectory(0, 0, 0) + if err != nil { + return errors.Wrap(err, 0) + } + + return nil +} diff --git a/testdata/hello.c b/testdata/hello/hello.c similarity index 100% rename from testdata/hello.c rename to testdata/hello/hello.c diff --git a/testdata/hello.obj b/testdata/hello/hello.obj similarity index 100% rename from testdata/hello.obj rename to testdata/hello/hello.obj diff --git a/testdata/hello32-mingw.exe b/testdata/hello/hello32-mingw.exe similarity index 100% rename from testdata/hello32-mingw.exe rename to testdata/hello/hello32-mingw.exe diff --git a/testdata/hello32-msvc.exe b/testdata/hello/hello32-msvc.exe similarity index 100% rename from testdata/hello32-msvc.exe rename to testdata/hello/hello32-msvc.exe diff --git a/testdata/hello64-mingw.exe b/testdata/hello/hello64-mingw.exe similarity index 100% rename from testdata/hello64-mingw.exe rename to testdata/hello/hello64-mingw.exe diff --git a/testdata/hello64-msvc.exe b/testdata/hello/hello64-msvc.exe similarity index 100% rename from testdata/hello64-msvc.exe rename to testdata/hello/hello64-msvc.exe diff --git a/testdata/resourceful/pelican.ico b/testdata/resourceful/pelican.ico new file mode 100644 index 0000000..0609192 Binary files /dev/null and b/testdata/resourceful/pelican.ico differ diff --git a/testdata/resourceful/resourceful.c b/testdata/resourceful/resourceful.c new file mode 100644 index 0000000..760e9b7 --- /dev/null +++ b/testdata/resourceful/resourceful.c @@ -0,0 +1,6 @@ +#include + +int main(int argc, char **argv) { + printf("%s\n", "IAMA PE file with a manifest, AMA"); + return 0; +} diff --git a/testdata/resourceful/resourceful.rc b/testdata/resourceful/resourceful.rc new file mode 100644 index 0000000..163f1f2 --- /dev/null +++ b/testdata/resourceful/resourceful.rc @@ -0,0 +1,30 @@ +#include "winver.h" +#define IDI_ICON_S 101 + +1 VERSIONINFO +FILEVERSION 1,0,0,0 +PRODUCTVERSION 1,0,0,0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "080904E4" + BEGIN + VALUE "CompanyName", "itch corp." + VALUE "FileDescription", "Test PE file for pelican" + VALUE "FileVersion", "3.14" + VALUE "InternalName", "resourceful" + VALUE "LegalCopyright", "(c) 2018 itch corp." + VALUE "OriginalFilename", "resourceful.exe" + VALUE "ProductName", "butler" + VALUE "ProductVersion", "6.28" + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x809, 1252 + END +END + +IDI_ICON_S ICON "pelican.ico" + diff --git a/testdata/resourceful/resourceful32-mingw.exe b/testdata/resourceful/resourceful32-mingw.exe new file mode 100755 index 0000000..9193e55 Binary files /dev/null and b/testdata/resourceful/resourceful32-mingw.exe differ diff --git a/testdata/resourceful/resourceful64-mingw.exe b/testdata/resourceful/resourceful64-mingw.exe new file mode 100755 index 0000000..3aee783 Binary files /dev/null and b/testdata/resourceful/resourceful64-mingw.exe differ diff --git a/types.go b/types.go new file mode 100644 index 0000000..62f740f --- /dev/null +++ b/types.go @@ -0,0 +1,35 @@ +package pelican + +type Arch string + +const ( + Arch386 = "386" + ArchAmd64 = "amd64" +) + +// PeInfo contains the architecture of a binary file +// +// For command `PeInfo` +type PeInfo struct { + Arch Arch `json:"arch"` + VersionProperties map[string]string `json:"versionProperties"` + AssemblyInfo *AssemblyInfo `json:"assemblyInfo"` + DependentAssemblies []*AssemblyIdentity `json:"dependentAssemblies"` +} + +type AssemblyInfo struct { + Identity *AssemblyIdentity `json:"identity"` + Description string `json:"description"` + + RequestedExecutionLevel string `json:"requestedExecutionLevel,omitempty"` +} + +type AssemblyIdentity struct { + Name string `json:"name"` + Version string `json:"version"` + Type string `json:"type"` + + ProcessorArchitecture string `json:"processorArchitecture,omitempty"` + Language string `json:"language,omitempty"` + PublicKeyToken string `json:"publicKeyToken,omitempty"` +} diff --git a/utf16.go b/utf16.go new file mode 100644 index 0000000..b5fa7ae --- /dev/null +++ b/utf16.go @@ -0,0 +1,15 @@ +package pelican + +import ( + "encoding/binary" + "unicode/utf16" +) + +// Convert a UTF-16 string (as a byte slice) to unicode +func DecodeUTF16(bs []byte) string { + ints := make([]uint16, len(bs)/2) + for i := 0; i < len(ints); i++ { + ints[i] = binary.LittleEndian.Uint16(bs[i*2 : (i+1)*2]) + } + return string(utf16.Decode(ints)) +} diff --git a/vs_block.go b/vs_block.go new file mode 100644 index 0000000..15c9033 --- /dev/null +++ b/vs_block.go @@ -0,0 +1,237 @@ +package pelican + +import ( + "bytes" + "encoding/binary" + "io" + "strings" + + "github.com/go-errors/errors" + "github.com/itchio/wharf/state" +) + +// PE version block utilities + +type ReadSeekerAt interface { + io.ReadSeeker + io.ReaderAt +} + +type VsBlock struct { + Length uint16 + ValueLength uint16 + Type uint16 + Key []byte + EndOffset int64 + + ReadSeekerAt +} + +func (vb *VsBlock) KeyString() string { + return DecodeUTF16(vb.Key) +} + +type VsFixedFileInfo struct { + DwSignature uint32 + DwStrucVersion uint32 + DwFileVersionMS uint32 + DwFileVersionLS uint32 + DwProductVersionMS uint32 + DwProductVersionLS uint32 + DwFileFlagsMask uint32 + DwFileFlags uint32 + DwFileOS uint32 + DwFileType uint32 + DwFileSubtype uint32 + DwFileDateMS uint32 + DwFileDateLS uint32 +} + +func parseVersion(info *PeInfo, consumer *state.Consumer, rawData []byte) error { + br := bytes.NewReader(rawData) + buf := make([]byte, 2) + + skipPadding := func(r ReadSeekerAt) error { + for { + _, err := r.Read(buf) + if err != nil { + if err == io.EOF { + // alles gut + return nil + } + return errors.Wrap(err, 0) + } + + if buf[0] != 0 || buf[1] != 0 { + _, err = r.Seek(-2, io.SeekCurrent) + if err != nil { + return errors.Wrap(err, 0) + } + break + } + } + return nil + } + + parseNullTerminatedString := func(r ReadSeekerAt) ([]byte, error) { + var res []byte + + for { + _, err := r.Read(buf) + if err != nil { + if errors.Is(err, io.EOF) { + return res, nil + } + return nil, errors.Wrap(err, 0) + } + + if buf[0] == 0 && buf[1] == 0 { + break + } else { + res = append(res, buf...) + } + } + return res, nil + } + + parseVSBlock := func(r ReadSeekerAt) (*VsBlock, error) { + startOffset, err := r.Seek(0, io.SeekCurrent) + if err != nil { + return nil, errors.Wrap(err, 0) + } + + var wLength uint16 + err = binary.Read(r, binary.LittleEndian, &wLength) + if err != nil { + return nil, errors.Wrap(err, 0) + } + + endOffset := startOffset + int64(wLength) + sr := io.NewSectionReader(r, startOffset+2, int64(wLength)-2 /* we already read the wLength uint16 */) + + var wValueLength uint16 + err = binary.Read(sr, binary.LittleEndian, &wValueLength) + if err != nil { + return nil, errors.Wrap(err, 0) + } + + var wType uint16 + err = binary.Read(sr, binary.LittleEndian, &wType) + if err != nil { + return nil, errors.Wrap(err, 0) + } + + szKey, err := parseNullTerminatedString(sr) + if err != nil { + return nil, errors.Wrap(err, 0) + } + + err = skipPadding(sr) + if err != nil { + return nil, errors.Wrap(err, 0) + } + + return &VsBlock{ + Length: wLength, + ValueLength: wValueLength, + Type: wType, + Key: szKey, + EndOffset: endOffset, + ReadSeekerAt: sr, + }, nil + } + + vsVersionInfo, err := parseVSBlock(br) + if err != nil { + return errors.Wrap(err, 0) + } + + if vsVersionInfo.ValueLength == 0 { + return nil + } + + ffi := new(VsFixedFileInfo) + err = binary.Read(vsVersionInfo, binary.LittleEndian, ffi) + if err != nil { + return errors.Wrap(err, 0) + } + + if ffi.DwSignature != 0xFEEF04BD { + consumer.Debugf("invalid signature, either the version block is invalid or we messed up") + return nil + } + + err = skipPadding(vsVersionInfo) + if err != nil { + return errors.Wrap(err, 0) + } + + for { + fileInfo, err := parseVSBlock(vsVersionInfo) + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return errors.Wrap(err, 0) + } + + switch fileInfo.KeyString() { + case "StringFileInfo": + for { + stable, err := parseVSBlock(fileInfo) + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return errors.Wrap(err, 0) + } + + if isLanguageWhitelisted(stable.KeyString()) { + for { + str, err := parseVSBlock(stable) + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return errors.Wrap(err, 0) + } + + keyString := str.KeyString() + + val, err := parseNullTerminatedString(str) + if err != nil { + return errors.Wrap(err, 0) + } + valString := strings.TrimSpace(DecodeUTF16(val)) + + consumer.Debugf("%s: %s", keyString, valString) + info.VersionProperties[keyString] = valString + _, err = stable.Seek(str.EndOffset, io.SeekStart) + if err != nil { + return errors.Wrap(err, 0) + } + + err = skipPadding(stable) + if err != nil { + return errors.Wrap(err, 0) + } + } + } + + _, err = fileInfo.Seek(stable.EndOffset, io.SeekStart) + if err != nil { + return errors.Wrap(err, 0) + } + } + case "VarFileInfo": + // skip + } + + _, err = vsVersionInfo.Seek(fileInfo.EndOffset, io.SeekStart) + if err != nil { + return errors.Wrap(err, 0) + } + } + + return nil +}