Skip to content
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
1 change: 1 addition & 0 deletions v3/UNRELEASED_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ After processing, the content will be moved to the main changelog and this file
**Fixed:**
- Fix memory leak in event system during window close operations (#5678)
- Fix crash when using context menus on Linux with Wayland
- Fix on Linux, trying to open a file with the `fileexplorer` will open parent directory (#4521)

**Security:**
- Update dependencies to address CVE-2024-12345 in third-party library
204 changes: 107 additions & 97 deletions v3/internal/fileexplorer/fileexplorer_linux.go
Original file line number Diff line number Diff line change
@@ -1,97 +1,107 @@
//go:build linux

package fileexplorer

import (
"bytes"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"

ini "gopkg.in/ini.v1"
)

func explorerBinArgs(path string, selectFile bool) (string, []string, error) {
// Map of field codes to their replacements
var fieldCodes = map[string]string{
"%d": "",
"%D": "",
"%n": "",
"%N": "",
"%v": "",
"%m": "",
"%f": path,
"%F": path,
"%u": pathToURI(path),
"%U": pathToURI(path),
}
fileManagerQuery := exec.Command("xdg-mime", "query", "default", "inode/directory")
buf := new(bytes.Buffer)
fileManagerQuery.Stdout = buf
fileManagerQuery.Stderr = nil

if err := fileManagerQuery.Run(); err != nil {
return fallbackExplorerBinArgs(path, selectFile)
}

desktopFile, err := findDesktopFile(strings.TrimSpace((buf.String())))
if err != nil {
return fallbackExplorerBinArgs(path, selectFile)
}

cfg, err := ini.Load(desktopFile)
if err != nil {
// Opting to fallback rather than fail
return fallbackExplorerBinArgs(path, selectFile)
}

exec := cfg.Section("Desktop Entry").Key("Exec").String()
for fieldCode, replacement := range fieldCodes {
exec = strings.ReplaceAll(exec, fieldCode, replacement)
}
args := strings.Fields(exec)
if !strings.Contains(strings.Join(args, " "), path) {
args = append(args, path)
}

return args[0], args[1:], nil
}

func sysProcAttr(path string, selectFile bool) *syscall.SysProcAttr {
return &syscall.SysProcAttr{}
}

func fallbackExplorerBinArgs(path string, selectFile bool) (string, []string, error) {
// NOTE: The linux fallback explorer opening is not supporting file selection
path = filepath.Dir(path)
return "xdg-open", []string{path}, nil
}

func pathToURI(path string) string {
absPath, err := filepath.Abs(path)
if err != nil {
return path
}
return "file://" + url.PathEscape(absPath)
}

func findDesktopFile(xdgFileName string) (string, error) {
paths := []string{
filepath.Join(os.Getenv("XDG_DATA_HOME"), "applications"),
filepath.Join(os.Getenv("HOME"), ".local", "share", "applications"),
"/usr/share/applications",
}

for _, path := range paths {
desktopFile := filepath.Join(path, xdgFileName)
if _, err := os.Stat(desktopFile); err == nil {
return desktopFile, nil
}
}
err := fmt.Errorf("desktop file not found: %s", xdgFileName)
return "", err
}
//go:build linux

package fileexplorer

import (
"bytes"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"

ini "gopkg.in/ini.v1"
)

func explorerBinArgs(path string, selectFile bool) (string, []string, error) {
// Map of field codes to their replacements
var fieldCodes = map[string]string{
"%d": "",
"%D": "",
"%n": "",
"%N": "",
"%v": "",
"%m": "",
"%f": path,
"%F": path,
"%u": pathToURI(path),
"%U": pathToURI(path),
}
fileManagerQuery := exec.Command("xdg-mime", "query", "default", "inode/directory")
buf := new(bytes.Buffer)
fileManagerQuery.Stdout = buf
fileManagerQuery.Stderr = nil

if err := fileManagerQuery.Run(); err != nil {
return fallbackExplorerBinArgs(path, selectFile)
}

desktopFile, err := findDesktopFile(strings.TrimSpace((buf.String())))
if err != nil {
return fallbackExplorerBinArgs(path, selectFile)
}

cfg, err := ini.Load(desktopFile)
if err != nil {
// Opting to fallback rather than fail
return fallbackExplorerBinArgs(path, selectFile)
}

exec := cfg.Section("Desktop Entry").Key("Exec").String()
for fieldCode, replacement := range fieldCodes {
exec = strings.ReplaceAll(exec, fieldCode, replacement)
}
args := strings.Fields(exec)
Comment on lines +52 to +56

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Avoid shadowing the exec package import.

The variable name exec on line 52 shadows the exec package imported at line 10. This reduces code clarity and could lead to confusion.

Apply this diff to rename the variable:

-	exec := cfg.Section("Desktop Entry").Key("Exec").String()
+	execCmd := cfg.Section("Desktop Entry").Key("Exec").String()
 	for fieldCode, replacement := range fieldCodes {
-		exec = strings.ReplaceAll(exec, fieldCode, replacement)
+		execCmd = strings.ReplaceAll(execCmd, fieldCode, replacement)
 	}
-	args := strings.Fields(exec)
+	args := strings.Fields(execCmd)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exec := cfg.Section("Desktop Entry").Key("Exec").String()
for fieldCode, replacement := range fieldCodes {
exec = strings.ReplaceAll(exec, fieldCode, replacement)
}
args := strings.Fields(exec)
execCmd := cfg.Section("Desktop Entry").Key("Exec").String()
for fieldCode, replacement := range fieldCodes {
execCmd = strings.ReplaceAll(execCmd, fieldCode, replacement)
}
args := strings.Fields(execCmd)
🤖 Prompt for AI Agents
In v3/internal/fileexplorer/fileexplorer_linux.go around lines 52 to 56, the
local variable named `exec` shadows the imported `exec` package; rename the
variable (e.g., `execStr`, `execCmd` or `execLine`) and update its subsequent
uses in this block so the package import is no longer shadowed, preserving the
same logic (replace field codes, then split into args with strings.Fields).

if !strings.Contains(strings.Join(args, " "), path) {
args = append(args, path)
}

return args[0], args[1:], nil
}

func sysProcAttr(path string, selectFile bool) *syscall.SysProcAttr {
return &syscall.SysProcAttr{}
}

func fallbackExplorerBinArgs(path string, selectFile bool) (string, []string, error) {
// NOTE: The linux fallback explorer opening does not support file selection

stat, err := os.Stat(path)
if err != nil {
return "", []string{}, fmt.Errorf("stat path: %w", err)
}

// If the path is a file, we want to open the directory containing the file
if !stat.IsDir() {
path = filepath.Dir(path)
}

return "xdg-open", []string{path}, nil
}

func pathToURI(path string) string {
absPath, err := filepath.Abs(path)
if err != nil {
return path
}
return "file://" + url.PathEscape(absPath)
}
Comment on lines +84 to +90

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix URI encoding to preserve path separators.

url.PathEscape escapes all special characters including forward slashes, which breaks the file URI. For example, /tmp/file.txt becomes %2Ftmp%2Ffile.txt, which is invalid.

Use url.URL to properly construct the URI:

 func pathToURI(path string) string {
 	absPath, err := filepath.Abs(path)
 	if err != nil {
 		return path
 	}
-	return "file://" + url.PathEscape(absPath)
+	u := url.URL{
+		Scheme: "file",
+		Path:   absPath,
+	}
+	return u.String()
 }
🤖 Prompt for AI Agents
In v3/internal/fileexplorer/fileexplorer_linux.go around lines 84 to 90, the
current pathToURI uses url.PathEscape which encodes slashes and breaks file
URIs; replace that logic by constructing a url.URL with Scheme "file" and Path
set to the absolute path converted to forward-slash form (use
filepath.ToSlash(absPath)) and return url.String(); this preserves separators
while still percent-encoding characters like spaces.


func findDesktopFile(xdgFileName string) (string, error) {
paths := []string{
filepath.Join(os.Getenv("XDG_DATA_HOME"), "applications"),
filepath.Join(os.Getenv("HOME"), ".local", "share", "applications"),
"/usr/share/applications",
}

for _, path := range paths {
desktopFile := filepath.Join(path, xdgFileName)
if _, err := os.Stat(desktopFile); err == nil {
return desktopFile, nil
}
}
err := fmt.Errorf("desktop file not found: %s", xdgFileName)
return "", err
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading