Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix and improve profile import/export and add MVP linux icon support #1385

Merged
merged 6 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions process/tags/svchost_windows.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package tags

import (
"context"
"fmt"
"strings"

"github.com/safing/portbase/log"
"github.com/safing/portbase/utils/osdetail"
"github.com/safing/portmaster/process"
"github.com/safing/portmaster/profile"
"github.com/safing/portmaster/profile/icons"
)

func init() {
Expand Down Expand Up @@ -81,11 +83,10 @@ func (h *SVCHostTagHandler) AddTags(p *process.Process) {
// Returns nil to skip.
func (h *SVCHostTagHandler) CreateProfile(p *process.Process) *profile.Profile {
if tag, ok := p.GetTag(svchostTagKey); ok {
return profile.New(&profile.Profile{
// Create new profile based on tag.
newProfile := profile.New(&profile.Profile{
Source: profile.SourceLocal,
Name: "Windows Service: " + osdetail.GenerateBinaryNameFromPath(tag.Value),
Icon: `C:\Windows\System32\@WLOGO_48x48.png`,
IconType: profile.IconTypeFile,
UsePresentationPath: false,
Fingerprints: []profile.Fingerprint{
profile.Fingerprint{
Expand All @@ -96,6 +97,14 @@ func (h *SVCHostTagHandler) CreateProfile(p *process.Process) *profile.Profile {
},
},
})

// Load default icon for windows service.
icon, err := icons.LoadAndSaveIcon(context.TODO(), `C:\Windows\System32\@WLOGO_48x48.png`)
if err == nil {
newProfile.Icons = []icons.Icon{*icon}
}

return newProfile
}

return nil
Expand Down
5 changes: 3 additions & 2 deletions profile/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/safing/portbase/api"
"github.com/safing/portbase/formats/dsd"
"github.com/safing/portbase/utils"
"github.com/safing/portmaster/profile/icons"
)

func registerAPIEndpoints() error {
Expand Down Expand Up @@ -98,7 +99,7 @@ func handleGetProfileIcon(ar *api.Request) (data []byte, err error) {
ext := filepath.Ext(name)

// Get profile icon.
data, err = GetProfileIcon(name)
data, err = icons.GetProfileIcon(name)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -152,7 +153,7 @@ func handleUpdateProfileIcon(ar *api.Request) (any, error) {
}

// Update profile icon.
filename, err := UpdateProfileIcon(ar.InputData, ext)
filename, err := icons.UpdateProfileIcon(ar.InputData, ext)
if err != nil {
return nil, err
}
Expand Down
5 changes: 4 additions & 1 deletion profile/get.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package profile

import (
"context"
"errors"
"fmt"
"path"
Expand Down Expand Up @@ -146,7 +147,9 @@ func GetLocalProfile(id string, md MatchingData, createProfileCallback func() *P

// Trigger further metadata fetching from system if profile was created.
if created && profile.UsePresentationPath && !special {
module.StartWorker("get profile metadata", profile.updateMetadataFromSystem)
module.StartWorker("get profile metadata", func(ctx context.Context) error {
return profile.updateMetadataFromSystem(ctx, md)
})
}

// Prepare profile for first use.
Expand Down
10 changes: 10 additions & 0 deletions profile/icons/find_default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//go:build !linux

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
}
100 changes: 100 additions & 0 deletions profile/icons/find_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package icons

import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"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) {
// Search for icon.
iconPath, err := search(binName, homeDir)
if iconPath == "" {
if err != nil {
return nil, fmt.Errorf("failed to find icon for %s: %w", binName, err)
}
return nil, nil
}

return LoadAndSaveIcon(ctx, iconPath)
}

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

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

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

if iconPath != "" {
return
}
}
return
}

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

func searchDirectory(directory string, binName string) (iconPath string, err error) {
entries, err := os.ReadDir(directory)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return "", nil
}
return "", fmt.Errorf("failed to read directory %s: %w", directory, err)
}
fmt.Println(directory)

var (
bestMatch string
bestMatchExcessChars int
)
for _, entry := range entries {
// Skip dirs.
if entry.IsDir() {
continue
}

iconName := strings.ToLower(entry.Name())
iconName = strings.TrimSuffix(iconName, filepath.Ext(iconName))
switch {
case len(iconName) < len(binName):
// Continue to next.
case iconName == binName:
// Exact match, return immediately.
return filepath.Join(directory, entry.Name()), nil
case strings.HasPrefix(iconName, binName):
excessChars := len(iconName) - len(binName)
if bestMatch == "" || excessChars < bestMatchExcessChars {
bestMatch = entry.Name()
bestMatchExcessChars = excessChars
}
}
}

return bestMatch, nil
}
32 changes: 32 additions & 0 deletions profile/icons/find_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package icons

import (
"os"
"testing"
)

func TestFindIcon(t *testing.T) {
if testing.Short() {
t.Skip("test depends on linux desktop environment")
}
t.Parallel()

home := os.Getenv("HOME")
testFindIcon(t, "evolution", home)
testFindIcon(t, "nextcloud", home)
}

func testFindIcon(t *testing.T, binName string, homeDir string) {
t.Helper()

iconPath, err := search(binName, homeDir)
if err != nil {
t.Error(err)
return
}
if iconPath == "" {
t.Errorf("no icon found for %s", binName)
return
}
t.Logf("icon for %s found: %s", binName, iconPath)
}
5 changes: 3 additions & 2 deletions profile/icon.go → profile/icons/icon.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package profile
package icons

import (
"errors"
Expand Down Expand Up @@ -42,7 +42,8 @@ func (t IconType) sortOrder() int {
}
}

func sortAndCompactIcons(icons []Icon) []Icon {
// SortAndCompact sorts and compacts a list of icons.
func SortAndCompact(icons []Icon) []Icon {
// Sort.
slices.SortFunc[[]Icon, Icon](icons, func(a, b Icon) int {
aOrder := a.Type.sortOrder()
Expand Down
34 changes: 28 additions & 6 deletions profile/icons.go → profile/icons/icons.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package profile
package icons

import (
"context"
"crypto"
"encoding/hex"
"errors"
Expand All @@ -13,18 +14,21 @@ import (
"github.com/safing/portbase/api"
)

var profileIconStoragePath = ""
// ProfileIconStoragePath defines the location where profile icons are stored.
// Must be set before anything else from this package is called.
// Must not be changed once set.
var ProfileIconStoragePath = ""

// GetProfileIcon returns the profile icon with the given ID and extension.
func GetProfileIcon(name string) (data []byte, err error) {
// Check if enabled.
if profileIconStoragePath == "" {
if ProfileIconStoragePath == "" {
return nil, errors.New("api icon storage not configured")
}

// Build storage path.
iconPath := filepath.Clean(
filepath.Join(profileIconStoragePath, name),
filepath.Join(ProfileIconStoragePath, name),
)

iconPath, err = filepath.Abs(iconPath)
Expand All @@ -34,7 +38,7 @@ func GetProfileIcon(name string) (data []byte, err error) {

// Do a quick check if we are still within the right directory.
// This check is not entirely correct, but is sufficient for this use case.
if filepath.Dir(iconPath) != profileIconStoragePath {
if filepath.Dir(iconPath) != ProfileIconStoragePath {
return nil, api.ErrorWithStatus(errors.New("invalid icon"), http.StatusBadRequest)
}

Expand Down Expand Up @@ -72,7 +76,25 @@ func UpdateProfileIcon(data []byte, ext string) (filename string, err error) {

// Save to disk.
filename = sum + "." + ext
return filename, os.WriteFile(filepath.Join(profileIconStoragePath, filename), data, 0o0644) //nolint:gosec
return filename, os.WriteFile(filepath.Join(ProfileIconStoragePath, filename), data, 0o0644) //nolint:gosec
}

// LoadAndSaveIcon loads an icon from disk, updates it in the icon database
// and returns the icon object.
func LoadAndSaveIcon(ctx context.Context, iconPath string) (*Icon, error) {
// Load icon and save it.
data, err := os.ReadFile(iconPath)
if err != nil {
return nil, fmt.Errorf("failed to read icon %s: %w", iconPath, err)
}
filename, err := UpdateProfileIcon(data, filepath.Ext(iconPath))
if err != nil {
return nil, fmt.Errorf("failed to import icon %s: %w", iconPath, err)
}
return &Icon{
Type: IconTypeAPI,
Value: filename,
}, nil
}

// TODO: Clean up icons regularly.
68 changes: 68 additions & 0 deletions profile/icons/locations_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package icons

import (
"fmt"
)

// IconLocation describes an icon location.
type IconLocation struct {
Directory string
Type IconLocationType
PathArg PathArg
}

// IconLocationType describes an icon location type.
type IconLocationType uint8

// Icon Location Types.
const (
FlatDir IconLocationType = iota
XDGIcons
)

// PathArg describes an icon location path argument.
type PathArg uint8

// Path Args.
const (
NoPathArg PathArg = iota
Home
BinName
)

var (
iconLocations = []IconLocation{
{Directory: "/usr/share/pixmaps", Type: FlatDir},
{Directory: "/usr/share", Type: XDGIcons},
{Directory: "%s/.local/share", Type: XDGIcons, PathArg: Home},
{Directory: "%s/.local/share/flatpak/exports/share", Type: XDGIcons, PathArg: Home},
{Directory: "/usr/share/%s", Type: XDGIcons, PathArg: BinName},
}

xdgIconPaths = []string{
// UI currently uses 48x48, so 256x256 should suffice for the future, even at 2x. (12.2023)
"icons/hicolor/256x256/apps",
"icons/hicolor/192x192/apps",
"icons/hicolor/128x128/apps",
"icons/hicolor/96x96/apps",
"icons/hicolor/72x72/apps",
"icons/hicolor/64x64/apps",
"icons/hicolor/48x48/apps",
"icons/hicolor/512x512/apps",
}
)

// GetPath returns the path of an icon.
func (il IconLocation) GetPath(binName string, homeDir string) string {
switch il.PathArg {
case NoPathArg:
return il.Directory
case Home:
if homeDir != "" {
return fmt.Sprintf(il.Directory, homeDir)
}
case BinName:
return fmt.Sprintf(il.Directory, binName)
}
return ""
}
Loading
Loading