diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-14 21:28:11 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-14 21:28:11 +0200 |
| commit | 7e0d8494074398854f30feb75211c52ea5cc2635 (patch) | |
| tree | f8ab4b7ce5dac8e75aded9ef4d4cd488d948d339 /internal/trash/trash.go | |
| parent | aca389f0e63713b890214cad950ac3aee7e6aaf4 (diff) | |
| download | krino-7e0d8494074398854f30feb75211c52ea5cc2635.tar.gz krino-7e0d8494074398854f30feb75211c52ea5cc2635.zip | |
plan 9: trash skips orphaned names, fits long names, exposes InfoPath
Diffstat (limited to 'internal/trash/trash.go')
| -rw-r--r-- | internal/trash/trash.go | 65 |
1 files changed, 52 insertions, 13 deletions
diff --git a/internal/trash/trash.go b/internal/trash/trash.go index b96c3aa..fdd21f8 100644 --- a/internal/trash/trash.go +++ b/internal/trash/trash.go @@ -15,6 +15,7 @@ import ( "strings" "syscall" "time" + "unicode/utf8" "krino/internal/xdg" ) @@ -59,7 +60,7 @@ func Put(path string) (entry string, err error) { return "", fmt.Errorf("trash: %w", err) } - entry, infoPath, f, err := claimName(filepath.Base(abs)) + entry, infoPath, f, err := claimName(fitName(filepath.Base(abs), maxEntryName)) if err != nil { return "", fmt.Errorf("trash: %w", err) } @@ -101,6 +102,11 @@ func claimName(base string) (entry, infoPath string, f *os.File, err error) { if n > 0 { candidate = stem + "_" + strconv.Itoa(n) + ext } + // A name already used in files/ - a file left there without its + // trashinfo - is taken too: renaming onto it would destroy it. + if _, err := os.Lstat(filepath.Join(filesDir(), candidate)); err == nil { + continue + } path := filepath.Join(infoDir(), candidate+".trashinfo") f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) if err == nil { @@ -113,6 +119,30 @@ func claimName(base string) (entry, infoPath string, f *os.File, err error) { return "", "", nil, errors.New("too many conflicting names") } +// maxEntryName is the longest entry name Put uses: 255 bytes, the usual +// limit of a file name, less ".trashinfo" and the longest suffix claimName +// can add ("_10000"). +const maxEntryName = 255 - len(".trashinfo") - len("_10000") + +// fitName shortens name to at most max bytes for use as a trash entry: the +// stem is cut at a character boundary and an extension shorter than 16 bytes +// is kept. The trashinfo still records the full original path, so Restore +// puts the file back under its own name. +func fitName(name string, max int) string { + if len(name) <= max { + return name + } + stem, ext := splitExt(name) + if len(ext) >= 16 || len(ext) >= max { + stem, ext = name, "" + } + cut := max - len(ext) + for cut > 0 && !utf8.RuneStart(stem[cut]) { + cut-- + } + return stem[:cut] + ext +} + // splitExt splits name on its last dot, which does not count when it is the // first character: "a.tar.gz" -> "a.tar", ".gz"; ".bashrc" -> ".bashrc", "". func splitExt(name string) (stem, ext string) { @@ -185,20 +215,11 @@ func percentDecode(s string) string { // as an error — Restore returns (path, nil) regardless — and the // .trashinfo may survive as a stale, otherwise-harmless record. func Restore(entry string) (restored string, err error) { - // An entry comes from the log; it must name something inside the Trash - // (spec §15.1), never a path that climbs out of it. - if entry == "" || entry == "." || entry == ".." || strings.ContainsRune(entry, '/') { - return "", fmt.Errorf("trash: bad entry name %q", entry) - } - infoPath := filepath.Join(infoDir(), entry+".trashinfo") - b, err := os.ReadFile(infoPath) + path, err := InfoPath(entry) if err != nil { - return "", fmt.Errorf("trash: %w", err) - } - path, err := parsePath(string(b)) - if err != nil { - return "", fmt.Errorf("trash: %s: %w", entry, err) + return "", err } + infoPath := filepath.Join(infoDir(), entry+".trashinfo") if _, err := os.Lstat(path); err == nil { return "", fmt.Errorf("trash: %s: already exists", path) } else if !os.IsNotExist(err) { @@ -218,6 +239,24 @@ func Restore(entry string) (restored string, err error) { return path, nil } +// InfoPath returns the absolute original path the trash entry's trashinfo +// records. An entry comes from the log; it must name something inside the +// Trash (spec §15.1), never a path that climbs out of it. +func InfoPath(entry string) (string, error) { + if entry == "" || entry == "." || entry == ".." || strings.ContainsRune(entry, '/') { + return "", fmt.Errorf("trash: bad entry name %q", entry) + } + b, err := os.ReadFile(filepath.Join(infoDir(), entry+".trashinfo")) + if err != nil { + return "", fmt.Errorf("trash: %w", err) + } + path, err := parsePath(string(b)) + if err != nil { + return "", fmt.Errorf("trash: %s: %w", entry, err) + } + return path, nil +} + // parsePath extracts and decodes the Path= line of a .trashinfo file. The // path must be absolute: Restore must never resolve one against the // working directory. |
