Permalink
Fetching contributors…
Cannot retrieve contributors at this time
235 lines (203 sloc) 6.03 KB
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package wrappers
import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
)
// valid simple prefixes
var validDesktopFilePrefixes = []string{
// headers
"[Desktop Entry]",
"[Desktop Action ",
// https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s05.html
"Type=",
"Version=",
"Name=",
"GenericName=",
"NoDisplay=",
"Comment=",
"Icon=",
"Hidden=",
"OnlyShowIn=",
"NotShowIn=",
"Exec=",
// Note that we do not support TryExec, it does not make sense
// in the snap context
"Terminal=",
"Actions=",
"MimeType=",
"Categories=",
"Keywords=",
"StartupNotify=",
"StartupWMClass=",
}
// name desktop file keys are localized as key[LOCALE]=:
// lang_COUNTRY@MODIFIER
// lang_COUNTRY
// lang@MODIFIER
// lang
var validLocalizedDesktopFilePrefixes = []string{
"Name",
"GenericName",
"Comment",
"Keywords",
}
func isValidDesktopFilePrefix(line string) bool {
for _, prefix := range validDesktopFilePrefixes {
if strings.HasPrefix(line, prefix) {
return true
}
}
return false
}
func trimLang(s string) string {
const langChars = "@_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
if s == "" || s[0] != '[' {
return s
}
t := strings.TrimLeft(s[1:], langChars)
if t != "" && t[0] == ']' {
return t[1:]
}
return s
}
func isValidLocalizedDesktopFilePrefix(line string) bool {
for _, prefix := range validLocalizedDesktopFilePrefixes {
s := strings.TrimPrefix(line, prefix)
if s == line {
continue
}
if strings.HasPrefix(trimLang(s), "=") {
return true
}
}
return false
}
// rewriteExecLine rewrites a "Exec=" line to use the wrapper path for snap application.
func rewriteExecLine(s *snap.Info, desktopFile, line string) (string, error) {
cmd := strings.SplitN(line, "=", 2)[1]
for _, app := range s.Apps {
env := fmt.Sprintf("env BAMF_DESKTOP_FILE_HINT=%s ", desktopFile)
wrapper := app.WrapperPath()
validCmd := filepath.Base(wrapper)
// check the prefix to allow %flag style args
// this is ok because desktop files are not run through sh
// so we don't have to worry about the arguments too much
if cmd == validCmd {
return "Exec=" + env + wrapper, nil
} else if strings.HasPrefix(cmd, validCmd+" ") {
return fmt.Sprintf("Exec=%s%s%s", env, wrapper, line[len("Exec=")+len(validCmd):]), nil
}
}
return "", fmt.Errorf("invalid exec command: %q", cmd)
}
func sanitizeDesktopFile(s *snap.Info, desktopFile string, rawcontent []byte) []byte {
newContent := []string{}
scanner := bufio.NewScanner(bytes.NewReader(rawcontent))
for scanner.Scan() {
line := scanner.Text()
// whitespace/comments are just copied
if strings.TrimSpace(line) == "" || strings.HasPrefix(strings.TrimSpace(line), "#") {
newContent = append(newContent, line)
continue
}
// ignore everything we have not whitelisted
if !isValidDesktopFilePrefix(line) && !isValidLocalizedDesktopFilePrefix(line) {
continue
}
// rewrite exec lines to an absolute path for the binary
if strings.HasPrefix(line, "Exec=") {
var err error
line, err = rewriteExecLine(s, desktopFile, line)
if err != nil {
// something went wrong, ignore the line
continue
}
}
// do variable substitution
line = strings.Replace(line, "${SNAP}", s.MountDir(), -1)
newContent = append(newContent, line)
}
return []byte(strings.Join(newContent, "\n"))
}
func updateDesktopDatabase(desktopFiles []string) error {
if len(desktopFiles) == 0 {
return nil
}
if _, err := exec.LookPath("update-desktop-database"); err == nil {
if output, err := exec.Command("update-desktop-database", dirs.SnapDesktopFilesDir).CombinedOutput(); err != nil {
return fmt.Errorf("cannot update-desktop-database %q: %s", output, err)
}
logger.Debugf("update-desktop-database successful")
}
return nil
}
// AddSnapDesktopFiles puts in place the desktop files for the applications from the snap.
func AddSnapDesktopFiles(s *snap.Info) error {
if err := os.MkdirAll(dirs.SnapDesktopFilesDir, 0755); err != nil {
return err
}
baseDir := s.MountDir()
desktopFiles, err := filepath.Glob(filepath.Join(baseDir, "meta", "gui", "*.desktop"))
if err != nil {
return fmt.Errorf("cannot get desktop files for %v: %s", baseDir, err)
}
for _, df := range desktopFiles {
content, err := ioutil.ReadFile(df)
if err != nil {
return err
}
installedDesktopFileName := filepath.Join(dirs.SnapDesktopFilesDir, fmt.Sprintf("%s_%s", s.Name(), filepath.Base(df)))
content = sanitizeDesktopFile(s, installedDesktopFileName, content)
if err := osutil.AtomicWriteFile(installedDesktopFileName, []byte(content), 0755, 0); err != nil {
return err
}
}
// updates mime info etc
if err := updateDesktopDatabase(desktopFiles); err != nil {
return err
}
return nil
}
// RemoveSnapDesktopFiles removes the added desktop files for the applications in the snap.
func RemoveSnapDesktopFiles(s *snap.Info) error {
glob := filepath.Join(dirs.SnapDesktopFilesDir, s.Name()+"_*.desktop")
activeDesktopFiles, err := filepath.Glob(glob)
if err != nil {
return fmt.Errorf("cannot get desktop files for %v: %s", glob, err)
}
for _, f := range activeDesktopFiles {
os.Remove(f)
}
// updates mime info etc
if err := updateDesktopDatabase(activeDesktopFiles); err != nil {
return err
}
return nil
}