Skip to content

Commit

Permalink
Add support for getting binary icon and name from exe on Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
dhaavi committed Dec 15, 2023
1 parent 19ad181 commit 2a04bf3
Show file tree
Hide file tree
Showing 10 changed files with 393 additions and 57 deletions.
6 changes: 6 additions & 0 deletions go.mod
Expand Up @@ -4,20 +4,25 @@ go 1.21.1

toolchain go1.21.2

// TODO: Remove when https://github.com/tc-hib/winres/pull/4 is merged or changes are otherwise integrated.
replace github.com/tc-hib/winres => github.com/dhaavi/winres v0.2.2

require (
github.com/Xuanwo/go-locale v1.1.0
github.com/agext/levenshtein v1.2.3
github.com/cilium/ebpf v0.12.3
github.com/coreos/go-iptables v0.7.0
github.com/florianl/go-conntrack v0.4.0
github.com/florianl/go-nfqueue v1.3.1
github.com/fogleman/gg v1.3.0
github.com/ghodss/yaml v1.0.0
github.com/godbus/dbus/v5 v5.1.0
github.com/google/gopacket v1.1.19
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.6.0
github.com/jackc/puddle/v2 v2.2.1
github.com/mat/besticon v3.12.0+incompatible
github.com/miekg/dns v1.1.57
github.com/mitchellh/go-server-timing v1.0.1
github.com/oschwald/maxminddb-golang v1.12.0
Expand All @@ -30,6 +35,7 @@ require (
github.com/spkg/zipfs v0.7.1
github.com/stretchr/testify v1.8.4
github.com/tannerryan/ring v1.1.2
github.com/tc-hib/winres v0.2.1
github.com/tevino/abool v1.2.0
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26
github.com/vincent-petithory/dataurl v1.0.0
Expand Down
32 changes: 32 additions & 0 deletions profile/icons/convert.go
@@ -0,0 +1,32 @@
package icons

import (
"bytes"
"fmt"
"image"
_ "image/png" // Register png support for image package

"github.com/fogleman/gg"
_ "github.com/mat/besticon/ico" // Register ico support for image package
)

// ConvertICOtoPNG converts a an .ico to a .png image.
func ConvertICOtoPNG(ico []byte) (png []byte, err error) {
// Decode the ICO.
icon, _, err := image.Decode(bytes.NewReader(ico))
if err != nil {
return nil, fmt.Errorf("failed to decode ICO: %w", err)
}

// Convert to raw image.
img := gg.NewContextForImage(icon)

// Convert to PNG.
imgBuf := &bytes.Buffer{}
err = img.EncodePNG(imgBuf)
if err != nil {
return nil, fmt.Errorf("failed to encode PNG: %w", err)
}

return imgBuf.Bytes(), nil
}
8 changes: 4 additions & 4 deletions profile/icons/find_default.go
@@ -1,10 +1,10 @@
//go:build !linux
//go:build !linux && !windows

package icons

import "context"

// FindIcon returns nil, nil for unsupported platforms.
func FindIcon(ctx context.Context, binName string, homeDir string) (*Icon, error) {
return nil, nil
// GetIconAndName returns zero values for unsupported platforms.
func GetIconAndName(ctx context.Context, binPath string, homeDir string) (icon *Icon, name string, err error) {
return nil, "", nil
}
48 changes: 29 additions & 19 deletions profile/icons/find_linux.go
Expand Up @@ -9,36 +9,46 @@ import (
"strings"
)

// FindIcon finds an icon for the given binary name.
// Providing the home directory of the user running the process of that binary can help find an icon.
func FindIcon(ctx context.Context, binName string, homeDir string) (*Icon, error) {
// GetIconAndName returns an icon and name of the given binary path.
// Providing the home directory of the user running the process of that binary can improve results.
// Even if an error is returned, the other return values are valid, if set.
func GetIconAndName(ctx context.Context, binPath string, homeDir string) (icon *Icon, name string, err error) {
// Derive name from binary.
name = GenerateBinaryNameFromPath(binPath)

// Search for icon.
iconPath, err := search(binName, homeDir)
iconPath, err := searchForIcon(binPath, homeDir)
if iconPath == "" {
if err != nil {
return nil, fmt.Errorf("failed to find icon for %s: %w", binName, err)
return nil, name, fmt.Errorf("failed to find icon for %s: %w", binPath, err)
}
return nil, nil
return nil, name, nil
}

// Save icon to internal storage.
icon, err = LoadAndSaveIcon(ctx, iconPath)
if err != nil {
return nil, name, fmt.Errorf("failed to store icon for %s: %w", binPath, err)
}

return LoadAndSaveIcon(ctx, iconPath)
return icon, name, nil
}

func search(binName string, homeDir string) (iconPath string, err error) {
binName = strings.ToLower(binName)
func searchForIcon(binPath string, homeDir string) (iconPath string, err error) {
binPath = strings.ToLower(binPath)

// Search for icon path.
for _, iconLoc := range iconLocations {
basePath := iconLoc.GetPath(binName, homeDir)
basePath := iconLoc.GetPath(binPath, homeDir)
if basePath == "" {
continue
}

switch iconLoc.Type {
case FlatDir:
iconPath, err = searchDirectory(basePath, binName)
iconPath, err = searchDirectory(basePath, binPath)
case XDGIcons:
iconPath, err = searchXDGIconStructure(basePath, binName)
iconPath, err = searchXDGIconStructure(basePath, binPath)
}

if iconPath != "" {
Expand All @@ -48,18 +58,18 @@ func search(binName string, homeDir string) (iconPath string, err error) {
return
}

func searchXDGIconStructure(baseDirectory string, binName string) (iconPath string, err error) {
func searchXDGIconStructure(baseDirectory string, binPath string) (iconPath string, err error) {
for _, xdgIconDir := range xdgIconPaths {
directory := filepath.Join(baseDirectory, xdgIconDir)
iconPath, err = searchDirectory(directory, binName)
iconPath, err = searchDirectory(directory, binPath)
if iconPath != "" {
return
}
}
return
}

func searchDirectory(directory string, binName string) (iconPath string, err error) {
func searchDirectory(directory string, binPath string) (iconPath string, err error) {
entries, err := os.ReadDir(directory)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
Expand All @@ -82,13 +92,13 @@ func searchDirectory(directory string, binName string) (iconPath string, err err
iconName := strings.ToLower(entry.Name())
iconName = strings.TrimSuffix(iconName, filepath.Ext(iconName))
switch {
case len(iconName) < len(binName):
case len(iconName) < len(binPath):
// Continue to next.
case iconName == binName:
case iconName == binPath:
// Exact match, return immediately.
return filepath.Join(directory, entry.Name()), nil
case strings.HasPrefix(iconName, binName):
excessChars := len(iconName) - len(binName)
case strings.HasPrefix(iconName, binPath):
excessChars := len(iconName) - len(binPath)
if bestMatch == "" || excessChars < bestMatchExcessChars {
bestMatch = entry.Name()
bestMatchExcessChars = excessChars
Expand Down
2 changes: 1 addition & 1 deletion profile/icons/find_linux_test.go
Expand Up @@ -19,7 +19,7 @@ func TestFindIcon(t *testing.T) {
func testFindIcon(t *testing.T, binName string, homeDir string) {
t.Helper()

iconPath, err := search(binName, homeDir)
iconPath, err := searchForIcon(binName, homeDir)
if err != nil {
t.Error(err)
return
Expand Down
115 changes: 115 additions & 0 deletions profile/icons/find_windows.go
@@ -0,0 +1,115 @@
package icons

import (
"bytes"
"context"
"errors"
"fmt"
"os"

"github.com/tc-hib/winres"
"github.com/tc-hib/winres/version"
)

// GetIconAndName returns an icon and name of the given binary path.
// Providing the home directory of the user running the process of that binary can improve results.
// Even if an error is returned, the other return values are valid, if set.
func GetIconAndName(ctx context.Context, binPath string, homeDir string) (icon *Icon, name string, err error) {
// Get name and png from exe.
png, name, err := getIconAndNamefromRSS(ctx, binPath)

// Fall back to name generation if name is not set.
if name == "" {
name = GenerateBinaryNameFromPath(binPath)
}

// Handle previous error.
if err != nil {
return nil, name, err
}

// Update profile icon and return icon object.
filename, err := UpdateProfileIcon(png, "png")
if err != nil {
return nil, name, fmt.Errorf("failed to store icon: %w", err)
}

return &Icon{
Type: IconTypeAPI,
Value: filename,
}, name, nil
}

func getIconAndNamefromRSS(ctx context.Context, binPath string) (png []byte, name string, err error) {
// Open .exe file.
exeFile, err := os.Open(binPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, "", nil
}
return nil, "", fmt.Errorf("failed to open exe %s to get icon: %w", binPath, err)
}
defer exeFile.Close() //nolint:errcheck

// Load .exe resources.
rss, err := winres.LoadFromEXE(exeFile)
if err != nil {
return nil, "", fmt.Errorf("failed to get rss: %w", err)
}

// DEBUG: Print all available resources:
// rss.Walk(func(typeID, resID winres.Identifier, langID uint16, data []byte) bool {
// fmt.Printf("typeID=%d resID=%d langID=%d\n", typeID, resID, langID)
// return true
// })

// Get first icon.
var (
icon *winres.Icon
iconErr error
)
rss.WalkType(winres.RT_GROUP_ICON, func(resID winres.Identifier, langID uint16, _ []byte) bool {
icon, iconErr = rss.GetIconTranslation(resID, langID)
return iconErr != nil
})
if iconErr != nil {
return nil, "", fmt.Errorf("failed to get icon: %w", err)
}
// Convert icon.
icoBuf := &bytes.Buffer{}
err = icon.SaveICO(icoBuf)
if err != nil {
return nil, "", fmt.Errorf("failed to save ico: %w", err)
}
png, err = ConvertICOtoPNG(icoBuf.Bytes())
if err != nil {
return nil, "", fmt.Errorf("failed to convert ico to png: %w", err)
}

// Get name from version record.
var (
versionInfo *version.Info
versionInfoErr error
)
rss.WalkType(winres.RT_VERSION, func(resID winres.Identifier, langID uint16, data []byte) bool {
versionInfo, versionInfoErr = version.FromBytes(data)
switch {
case versionInfoErr != nil:
return true
case versionInfo == nil:
return true
}

// Get metadata table and main language.
table := versionInfo.Table().GetMainTranslation()
if table == nil {
return true
}

name = table[version.ProductName]
return name == ""
})
name = cleanFileDescription(name)

return png, name, nil
}
27 changes: 27 additions & 0 deletions profile/icons/find_windows_test.go
@@ -0,0 +1,27 @@
package icons

import (
"context"
"os"
"testing"
)

func TestFindIcon(t *testing.T) {
if testing.Short() {
t.Skip("test meant for compiling and running on desktop")
}
t.Parallel()

binName := os.Args[len(os.Args)-1]
t.Logf("getting name and icon for %s", binName)
png, name, err := getIconAndNamefromRSS(context.Background(), binName)
if err != nil {
t.Fatal(err)
}

t.Logf("name: %s", name)
err = os.WriteFile("icon.png", png, 0o0600)
if err != nil {
t.Fatal(err)
}
}

0 comments on commit 2a04bf3

Please sign in to comment.