Files
mapsize/internal/platform/ops_linux_test.go
T
Andy Dixon 8d9344cfef mapsize 1.2.0: permanent delete and in-map reveal when headless
On a headless Linux or BSD system (no DISPLAY or WAYLAND_DISPLAY) nothing
empties the freedesktop trash, so trashing never freed space. There, d now
deletes permanently after an explicit confirmation, checking owner and type
against the scan and removing through an os.Root on the parent so a swapped
symlink cannot redirect it. o shows the folder in the map instead of
launching a file manager. Desktop sessions, macOS and Windows are unchanged.
2026-10-03 12:50:07 +01:00

156 lines
4.5 KiB
Go

package platform
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestTrashFreedesktop(t *testing.T) {
data := t.TempDir()
t.Setenv("XDG_DATA_HOME", data)
dir := filepath.Join(data, "work")
os.Mkdir(dir, 0o755)
victim := filepath.Join(dir, "weird name ;rm -rf ~;")
if err := os.WriteFile(victim, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := Trash(victim, want(t, victim)); err != nil {
t.Fatal(err)
}
if _, err := os.Lstat(victim); !os.IsNotExist(err) {
t.Fatal("file still present")
}
trashed := filepath.Join(data, "Trash", "files", "weird name ;rm -rf ~;")
if _, err := os.Lstat(trashed); err != nil {
t.Fatalf("not in trash: %v", err)
}
info, err := os.ReadFile(filepath.Join(data, "Trash", "info", "weird name ;rm -rf ~;.trashinfo"))
if err != nil || !strings.Contains(string(info), "Path="+strings.ReplaceAll(dir, " ", "%20")) {
t.Fatalf("bad trashinfo: %q %v", info, err)
}
// A second item with the same name gets a distinct trash name.
os.WriteFile(victim, []byte("y"), 0o644)
if err := Trash(victim, want(t, victim)); err != nil {
t.Fatal(err)
}
if _, err := os.Lstat(trashed + ".2"); err != nil {
t.Fatal("collision not handled")
}
if err := Trash("relative/path", Expect{}); err == nil {
t.Fatal("relative paths must be refused")
}
}
func TestEnsurePrivateDirRejectsSymlink(t *testing.T) {
base := t.TempDir()
target := filepath.Join(base, "attacker")
os.Mkdir(target, 0o700)
link := filepath.Join(base, ".Trash-1000")
os.Symlink(target, link)
if err := ensurePrivateDir(link, false); err == nil {
t.Fatal("symlinked trash dir accepted")
}
open := filepath.Join(base, "open")
os.Mkdir(open, 0o777)
os.Chmod(open, 0o777)
if err := ensurePrivateDir(open, true); err == nil {
t.Fatal("world-accessible trash dir accepted")
}
if err := ensurePrivateDir(filepath.Join(base, "fresh"), true); err != nil {
t.Fatal(err)
}
}
func want(t *testing.T, path string) Expect {
t.Helper()
m, err := Lstat(path)
if err != nil {
t.Fatal(err)
}
return Expect{UID: m.UID, Mode: m.Mode}
}
// A parent swapped for a symlink after the scan must not redirect the move
// to whatever the symlink points at.
func TestTrashRefusesSwappedItem(t *testing.T) {
data := t.TempDir()
t.Setenv("XDG_DATA_HOME", data)
dir := filepath.Join(data, "work")
os.Mkdir(dir, 0o755)
victim := filepath.Join(dir, "big.iso")
os.WriteFile(victim, []byte("x"), 0o644)
scanned := want(t, victim)
// Item replaced by a symlink to a file elsewhere: type differs.
other := filepath.Join(data, "precious")
os.WriteFile(other, []byte("keep"), 0o644)
os.Remove(victim)
os.Symlink(other, victim)
scannedDir := Expect{UID: scanned.UID, Mode: os.ModeDir}
if err := Trash(victim, scanned); err == nil {
t.Fatal("symlink accepted where a regular file was scanned")
}
if err := Trash(victim, scannedDir); err == nil {
t.Fatal("type mismatch accepted")
}
if _, err := os.Lstat(victim); err != nil {
t.Fatal("refused item was moved")
}
// Owner differs from the scan.
os.Remove(victim)
os.WriteFile(victim, []byte("x"), 0o644)
if err := Trash(victim, Expect{UID: scanned.UID + 1, Mode: scanned.Mode}); err == nil {
t.Fatal("owner mismatch accepted")
}
entries, _ := os.ReadDir(filepath.Join(data, "Trash", "info"))
if len(entries) != 0 {
t.Fatalf("refused trash left %d .trashinfo files", len(entries))
}
if err := Trash(victim, scanned); err != nil {
t.Fatal(err)
}
}
func TestDeletePermanently(t *testing.T) {
base := t.TempDir()
dir := filepath.Join(base, "S01")
os.MkdirAll(filepath.Join(dir, "extras"), 0o755)
os.WriteFile(filepath.Join(dir, "extras", "E01.mkv"), []byte("x"), 0o644)
// A symlink inside must be removed, not followed.
keep := filepath.Join(base, "keep")
os.WriteFile(keep, []byte("k"), 0o644)
os.Symlink(keep, filepath.Join(dir, "link"))
w := want(t, dir)
if err := Delete(dir, Expect{UID: w.UID, Mode: 0}); err == nil {
t.Fatal("type mismatch accepted")
}
if err := Delete(dir, w); err != nil {
t.Fatal(err)
}
if _, err := os.Lstat(dir); !os.IsNotExist(err) {
t.Fatal("directory still present")
}
if _, err := os.Lstat(keep); err != nil {
t.Fatal("symlink target was deleted")
}
if err := Delete("relative/path", Expect{}); err == nil {
t.Fatal("relative paths must be refused")
}
}
func TestHeadless(t *testing.T) {
t.Setenv("DISPLAY", "")
t.Setenv("WAYLAND_DISPLAY", "")
if !Headless() {
t.Fatal("no display should be headless")
}
t.Setenv("WAYLAND_DISPLAY", "wayland-0")
if Headless() {
t.Fatal("wayland session reported headless")
}
}