Skip to content

Commit

Permalink
wrappers: update desktop helpers to support desktop IDs (canonical#14296
Browse files Browse the repository at this point in the history
)

{Ensure,Remove}SnapDesktopFiles are updated to support desktop
file IDs.

Snaps are allowed to have their desktop files copied without
name mangling (with snap name prefix) if the file name is
listed under desktop-file-ids attribute in their desktop
interface declartion.

This is needed to support desktop IDs detailed in SD170 spec.

Signed-off-by: Zeyad Gouda <[email protected]>
  • Loading branch information
ZeyadYasser authored Sep 2, 2024
1 parent 01d54ca commit 4159f95
Show file tree
Hide file tree
Showing 3 changed files with 379 additions and 34 deletions.
8 changes: 6 additions & 2 deletions overlord/snapstate/snapstate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9125,11 +9125,15 @@ Name=test
Exec=test-snap
`[1:]), 0o644), IsNil)

var mockOldDesktopFile = []byte(`
[Desktop Entry]
Name=old-content
X-SnapInstanceName=test-snap`)
desktopFile := filepath.Join(dirs.SnapDesktopFilesDir, "test-snap_test-snap.desktop")
otherDesktopFile := filepath.Join(dirs.SnapDesktopFilesDir, "test-snap_other.desktop")
c.Assert(os.MkdirAll(dirs.SnapDesktopFilesDir, 0o755), IsNil)
c.Assert(os.WriteFile(desktopFile, []byte("old content"), 0o644), IsNil)
c.Assert(os.WriteFile(otherDesktopFile, []byte("other old content"), 0o644), IsNil)
c.Assert(os.WriteFile(desktopFile, mockOldDesktopFile, 0o644), IsNil)
c.Assert(os.WriteFile(otherDesktopFile, mockOldDesktopFile, 0o644), IsNil)

err := s.snapmgr.Ensure()
c.Assert(err, IsNil)
Expand Down
163 changes: 150 additions & 13 deletions wrappers/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package wrappers
import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
"os/exec"
Expand Down Expand Up @@ -247,7 +248,45 @@ func findDesktopFiles(rootDir string) ([]string, error) {
return desktopFiles, nil
}

func deriveDesktopFilesContent(s *snap.Info) (map[string]osutil.FileState, error) {
// Note: explicitSnapDesktopFileIDs doesn't check if the desktop plug
// is connected because the desktop-file-ids attribute is controlled
// by an allow-installation rule.
func explicitSnapDesktopFileIDs(s *snap.Info) (map[string]bool, error) {
var desktopPlug *snap.PlugInfo
for _, plug := range s.Plugs {
if plug.Interface == "desktop" {
desktopPlug = plug
break
}
}
if desktopPlug == nil {
return nil, nil
}

attrVal, exists := desktopPlug.Lookup("desktop-file-ids")
if !exists {
// desktop-file-ids attribute is optional
return nil, nil
}

// desktop-file-ids must be a list of strings
desktopFileIDs, ok := attrVal.([]interface{})
if !ok {
return nil, errors.New(`internal error: "desktop-file-ids" must be a list of strings`)
}

desktopFileIDsMap := make(map[string]bool, len(desktopFileIDs))
for _, val := range desktopFileIDs {
desktopFileID, ok := val.(string)
if !ok {
return nil, errors.New(`internal error: "desktop-file-ids" must be a list of strings`)
}
desktopFileIDsMap[desktopFileID] = true
}
return desktopFileIDsMap, nil
}

func deriveDesktopFilesContent(s *snap.Info, desktopFileIDs map[string]bool) (map[string]osutil.FileState, error) {
rootDir := filepath.Join(s.MountDir(), "meta", "gui")
desktopFiles, err := findDesktopFiles(rootDir)
if err != nil {
Expand All @@ -261,11 +300,16 @@ func deriveDesktopFilesContent(s *snap.Info) (map[string]osutil.FileState, error
if err != nil {
return nil, err
}
// FIXME: don't blindly use the snap desktop filename, mangle it
// but we can't just use the app name because a desktop file
// may call the same app with multiple parameters, e.g.
// --create-new, --open-existing etc
base = fmt.Sprintf("%s_%s", s.DesktopPrefix(), base)
// Don't mangle desktop files if listed under desktop-file-ids attribute
// XXX: Do we want to fail if a desktop-file-ids entry doesn't
// have a corresponding file?
if !desktopFileIDs[strings.TrimSuffix(base, ".desktop")] {
// FIXME: don't blindly use the snap desktop filename, mangle it
// but we can't just use the app name because a desktop file
// may call the same app with multiple parameters, e.g.
// --create-new, --open-existing etc
base = fmt.Sprintf("%s_%s", s.DesktopPrefix(), base)
}
installedDesktopFileName := filepath.Join(dirs.SnapDesktopFilesDir, base)
fileContent = sanitizeDesktopFile(s, installedDesktopFileName, fileContent)
content[base] = &osutil.MemoryFileState{
Expand All @@ -276,6 +320,60 @@ func deriveDesktopFilesContent(s *snap.Info) (map[string]osutil.FileState, error
return content, nil
}

// TODO: Merge desktop file helpers into desktop/desktopentry package
func snapInstanceNameFromDesktopFile(desktopFile string) (string, error) {
file, err := os.Open(desktopFile)
if err != nil {
return "", err
}
defer file.Close()

scanner := bufio.NewScanner(file)
for i := 0; scanner.Scan(); i++ {
bline := scanner.Text()
if !strings.HasPrefix(bline, "X-SnapInstanceName=") {
continue
}
return strings.TrimPrefix(bline, "X-SnapInstanceName="), nil
}

return "", fmt.Errorf("cannot find X-SnapInstanceName entry in %q", desktopFile)
}

// forAllDesktopFiles loops over all installed desktop files under
// dirs.SnapDesktopFilesDir.
//
// Only the desktop file base and parsed instance name is passed the callback
// function.
func forAllDesktopFiles(cb func(base, instanceName string) error) error {
installedDesktopFiles, err := findDesktopFiles(dirs.SnapDesktopFilesDir)
if err != nil {
return err
}

for _, desktopFile := range installedDesktopFiles {
instanceName, err := snapInstanceNameFromDesktopFile(desktopFile)
if err != nil {
// cannot read instance name from desktop file, ignore
logger.Noticef("cannot read instance name: %s", err)
continue
}

base := filepath.Base(desktopFile)
if err := cb(base, instanceName); err != nil {
return err
}
}

return nil
}

func hasDesktopPrefix(s *snap.Info, desktopFile string) bool {
base := filepath.Base(desktopFile)
prefix := s.DesktopPrefix() + "_"
return strings.HasPrefix(base, prefix) && strings.HasSuffix(base, ".desktop")
}

// EnsureSnapDesktopFiles puts in place the desktop files for the applications from the snap.
//
// It also removes desktop files from the applications of the old snap revision to ensure
Expand All @@ -286,17 +384,42 @@ func EnsureSnapDesktopFiles(snaps []*snap.Info) error {
}

var updated []string
for _, s := range snaps {
if s == nil {
for _, info := range snaps {
if info == nil {
return fmt.Errorf("internal error: snap info cannot be nil")
}
content, err := deriveDesktopFilesContent(s)

desktopFileIDs, err := explicitSnapDesktopFileIDs(info)
if err != nil {
return err
}
desktopFilesGlobs := []string{fmt.Sprintf("%s_*.desktop", info.DesktopPrefix())}
for desktopFileID := range desktopFileIDs {
desktopFilesGlobs = append(desktopFilesGlobs, desktopFileID+".desktop")
}
content, err := deriveDesktopFilesContent(info, desktopFileIDs)
if err != nil {
return err
}

addGlobPatternAndConflictCheck := func(base, instanceName string) error {
// Check if a target desktop file belongs to another snap
_, hasTarget := content[base]
if hasTarget && instanceName != info.InstanceName() {
return fmt.Errorf("cannot install %q: %q already exists for another snap", base, filepath.Join(dirs.SnapDesktopFilesDir, base))
}
if instanceName == info.InstanceName() && !hasTarget && !hasDesktopPrefix(info, base) {
// An unmangled desktop file exists for the snap, add to glob
// patterns for removal
desktopFilesGlobs = append(desktopFilesGlobs, base)
}
return nil
}
if err := forAllDesktopFiles(addGlobPatternAndConflictCheck); err != nil {
return err
}

desktopFilesGlob := fmt.Sprintf("%s_*.desktop", s.DesktopPrefix())
changed, removed, err := osutil.EnsureDirState(dirs.SnapDesktopFilesDir, desktopFilesGlob, content)
changed, removed, err := osutil.EnsureDirStateGlobs(dirs.SnapDesktopFilesDir, desktopFilesGlobs, content)
if err != nil {
return err
}
Expand All @@ -318,8 +441,22 @@ func RemoveSnapDesktopFiles(s *snap.Info) error {
return nil
}

desktopFilesGlob := fmt.Sprintf("%s_*.desktop", s.DesktopPrefix())
_, removed, err := osutil.EnsureDirState(dirs.SnapDesktopFilesDir, desktopFilesGlob, nil)
desktopFilesGlobs := []string{fmt.Sprintf("%s_*.desktop", s.DesktopPrefix())}

addGlobPattern := func(base, instanceName string) error {
if instanceName == s.InstanceName() && !hasDesktopPrefix(s, base) {
// An unmangled desktop file exists for the snap, add to glob
// patterns for removal
desktopFilesGlobs = append(desktopFilesGlobs, base)
}

return nil
}
if err := forAllDesktopFiles(addGlobPattern); err != nil {
return err
}

_, removed, err := osutil.EnsureDirStateGlobs(dirs.SnapDesktopFilesDir, desktopFilesGlobs, nil)
if err != nil {
return err
}
Expand Down
Loading

0 comments on commit 4159f95

Please sign in to comment.