From 78d2c617edad58da6ba537c9b65a80ac0d1b3574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Mon, 14 Jul 2025 22:38:41 +0200 Subject: [PATCH 1/4] util: Hash the path in `DetermineEscapePath()` ...in case the escaped path exceeds the file name length limit --- internal/util/util.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/internal/util/util.go b/internal/util/util.go index d0f20022..86351853 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -3,6 +3,7 @@ package util import ( "archive/zip" "bytes" + "crypto/md5" "errors" "fmt" "io" @@ -450,6 +451,10 @@ func AppendBackupSuffix(path string) string { return path + ".micro-backup" } +func HashStringMd5(str string) string { + return fmt.Sprintf("%x", md5.Sum([]byte(str))) +} + // EscapePathUrl encodes the path in URL query form func EscapePathUrl(path string) string { return url.QueryEscape(filepath.ToSlash(path)) @@ -469,6 +474,8 @@ func EscapePathLegacy(path string) string { // using URL encoding (preferred, since it encodes unambiguously) or // legacy encoding with '%' (for backward compatibility, if the legacy-escaped // path exists in the given directory). +// In case the length of the escaped path (plus the backup extension) exceeds +// the filename length limit, a hash of the path is returned instead. func DetermineEscapePath(dir string, path string) string { url := filepath.Join(dir, EscapePathUrl(path)) if _, err := os.Stat(url); err == nil { @@ -480,6 +487,10 @@ func DetermineEscapePath(dir string, path string) string { return legacy } + if len(url)+len(".micro-backup") > 255 { + return filepath.Join(dir, HashStringMd5(path)) + } + return url } From 1ce2202d9a50a1c59a8809b121a67ca4ae021772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Mon, 13 Oct 2025 20:50:35 +0200 Subject: [PATCH 2/4] util: Convert suffix added with `AppendBackupSuffix()` to simple constant --- cmd/micro/micro.go | 2 +- internal/buffer/backup.go | 2 +- internal/util/util.go | 10 ++++------ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index cf85df20..f3f3be27 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -250,7 +250,7 @@ func LoadInput(args []string) []*buffer.Buffer { func checkBackup(name string) error { target := filepath.Join(config.ConfigDir, name) - backup := util.AppendBackupSuffix(target) + backup := target + util.BackupSuffix if info, err := os.Stat(backup); err == nil { input, err := os.ReadFile(backup) if err == nil { diff --git a/internal/buffer/backup.go b/internal/buffer/backup.go index 9920897d..838c1f6c 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -104,7 +104,7 @@ func (b *SharedBuffer) writeBackup(path string) (string, error) { } name := util.DetermineEscapePath(backupdir, path) - tmp := util.AppendBackupSuffix(name) + tmp := name + util.BackupSuffix _, err := b.overwriteFile(tmp) if err != nil { diff --git a/internal/util/util.go b/internal/util/util.go index 86351853..81e95314 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -54,6 +54,8 @@ var ( // To be used for file writes before umask is applied const FileMode os.FileMode = 0666 +const BackupSuffix = ".micro-backup" + const OverwriteFailMsg = `An error occurred while writing to the file: %s @@ -447,10 +449,6 @@ func GetModTime(path string) (time.Time, error) { return info.ModTime(), nil } -func AppendBackupSuffix(path string) string { - return path + ".micro-backup" -} - func HashStringMd5(str string) string { return fmt.Sprintf("%x", md5.Sum([]byte(str))) } @@ -487,7 +485,7 @@ func DetermineEscapePath(dir string, path string) string { return legacy } - if len(url)+len(".micro-backup") > 255 { + if len(url)+len(BackupSuffix) > 255 { return filepath.Join(dir, HashStringMd5(path)) } @@ -708,7 +706,7 @@ func SafeWrite(path string, bytes []byte, rename bool) error { defer file.Close() } - tmp := AppendBackupSuffix(path) + tmp := path + BackupSuffix err = os.WriteFile(tmp, bytes, FileMode) if err != nil { os.Remove(tmp) From 02611f4ad2cad01e8aee0b0b549c7ce9c52b1f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Fri, 19 Sep 2025 19:56:02 +0200 Subject: [PATCH 3/4] backup: Keep path of stored & hashed backup files in a own $hash.path file Since full escaped backup paths can become longer than the maximum filename size and hashed filenames cannot be restored it is helpful to have a lookup file for the user to resolve the hashed path. --- internal/buffer/backup.go | 38 ++++++++++++++++++++++++------------ internal/buffer/save.go | 4 ++-- internal/buffer/serialize.go | 19 +++++++++++++++--- internal/util/util.go | 16 +++++++++------ 4 files changed, 54 insertions(+), 23 deletions(-) diff --git a/internal/buffer/backup.go b/internal/buffer/backup.go index 838c1f6c..43daa286 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -92,32 +92,46 @@ func (b *SharedBuffer) keepBackup() bool { return b.forceKeepBackup || b.Settings["permbackup"].(bool) } -func (b *SharedBuffer) writeBackup(path string) (string, error) { +func (b *SharedBuffer) writeBackup(path string) (string, string, error) { backupdir := b.backupDir() if _, err := os.Stat(backupdir); err != nil { if !errors.Is(err, fs.ErrNotExist) { - return "", err + return "", "", err } if err = os.Mkdir(backupdir, os.ModePerm); err != nil { - return "", err + return "", "", err } } - name := util.DetermineEscapePath(backupdir, path) + name, resolveName := util.DetermineEscapePath(backupdir, path) tmp := name + util.BackupSuffix _, err := b.overwriteFile(tmp) if err != nil { os.Remove(tmp) - return name, err + return name, resolveName, err } err = os.Rename(tmp, name) if err != nil { os.Remove(tmp) - return name, err + return name, resolveName, err } - return name, nil + if resolveName != "" { + err = util.SafeWrite(resolveName, []byte(path), true) + if err != nil { + return name, resolveName, err + } + } + + return name, resolveName, nil +} + +func (b *SharedBuffer) removeBackup(path string, resolveName string) { + os.Remove(path) + if resolveName != "" { + os.Remove(resolveName) + } } // Backup saves the buffer to the backups directory @@ -126,7 +140,7 @@ func (b *SharedBuffer) Backup() error { return nil } - _, err := b.writeBackup(b.AbsPath) + _, _, err := b.writeBackup(b.AbsPath) return err } @@ -135,15 +149,15 @@ func (b *SharedBuffer) RemoveBackup() { if b.keepBackup() || b.Path == "" || b.Type != BTDefault { return } - f := util.DetermineEscapePath(b.backupDir(), b.AbsPath) - os.Remove(f) + f, resolveName := util.DetermineEscapePath(b.backupDir(), b.AbsPath) + b.removeBackup(f, resolveName) } // ApplyBackup applies the corresponding backup file to this buffer (if one exists) // Returns true if a backup was applied func (b *SharedBuffer) ApplyBackup(fsize int64) (bool, bool) { if b.Settings["backup"].(bool) && !b.Settings["permbackup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault { - backupfile := util.DetermineEscapePath(b.backupDir(), b.AbsPath) + backupfile, resolveName := util.DetermineEscapePath(b.backupDir(), b.AbsPath) if info, err := os.Stat(backupfile); err == nil { backup, err := os.Open(backupfile) if err == nil { @@ -159,7 +173,7 @@ func (b *SharedBuffer) ApplyBackup(fsize int64) (bool, bool) { return true, true } else if choice%3 == 1 { // delete - os.Remove(backupfile) + b.removeBackup(backupfile, resolveName) } else if choice%3 == 2 { return false, false } diff --git a/internal/buffer/save.go b/internal/buffer/save.go index 85fa5ed5..96db874d 100644 --- a/internal/buffer/save.go +++ b/internal/buffer/save.go @@ -362,7 +362,7 @@ func (b *SharedBuffer) safeWrite(path string, withSudo bool, newFile bool) (int, }() // Try to backup first before writing - backupName, err := b.writeBackup(path) + backupName, resolveName, err := b.writeBackup(path) if err != nil { file.Close() return 0, err @@ -389,7 +389,7 @@ func (b *SharedBuffer) safeWrite(path string, withSudo bool, newFile bool) (int, b.forceKeepBackup = false if !b.keepBackup() { - os.Remove(backupName) + b.removeBackup(backupName, resolveName) } return size, err diff --git a/internal/buffer/serialize.go b/internal/buffer/serialize.go index bedac2ac..c765ca93 100644 --- a/internal/buffer/serialize.go +++ b/internal/buffer/serialize.go @@ -39,8 +39,20 @@ func (b *Buffer) Serialize() error { return err } - name := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath) - return util.SafeWrite(name, buf.Bytes(), true) + name, resolveName := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath) + err = util.SafeWrite(name, buf.Bytes(), true) + if err != nil { + return err + } + + if resolveName != "" { + err = util.SafeWrite(resolveName, []byte(b.AbsPath), true) + if err != nil { + return err + } + } + + return nil } // Unserialize loads the buffer info from config.ConfigDir/buffers @@ -50,7 +62,8 @@ func (b *Buffer) Unserialize() error { if b.Path == "" { return nil } - file, err := os.Open(util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath)) + name, _ := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath) + file, err := os.Open(name) if err == nil { defer file.Close() var buffer SerializedBuffer diff --git a/internal/util/util.go b/internal/util/util.go index 81e95314..e0ae62f2 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -473,23 +473,27 @@ func EscapePathLegacy(path string) string { // legacy encoding with '%' (for backward compatibility, if the legacy-escaped // path exists in the given directory). // In case the length of the escaped path (plus the backup extension) exceeds -// the filename length limit, a hash of the path is returned instead. -func DetermineEscapePath(dir string, path string) string { +// the filename length limit, a hash of the path is returned instead. In such +// case the second return value is the name of a file the original path should +// be saved to (since the original path cannot be derived from its hash). +// Otherwise the second return value is an empty string. +func DetermineEscapePath(dir string, path string) (string, string) { url := filepath.Join(dir, EscapePathUrl(path)) if _, err := os.Stat(url); err == nil { - return url + return url, "" } legacy := filepath.Join(dir, EscapePathLegacy(path)) if _, err := os.Stat(legacy); err == nil { - return legacy + return legacy, "" } if len(url)+len(BackupSuffix) > 255 { - return filepath.Join(dir, HashStringMd5(path)) + hash := HashStringMd5(path) + return filepath.Join(dir, hash), filepath.Join(dir, hash+".path") } - return url + return url, "" } // GetLeadingWhitespace returns the leading whitespace of the given byte array From bab39079b35652e9810b676320e7b445d1e5f242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Sun, 12 Oct 2025 18:42:22 +0200 Subject: [PATCH 4/4] save: Remove a possible written backup in case the path has changed --- internal/buffer/save.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/buffer/save.go b/internal/buffer/save.go index 96db874d..16e6ed31 100644 --- a/internal/buffer/save.go +++ b/internal/buffer/save.go @@ -331,6 +331,10 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error } newPath := b.Path != filename + if newPath { + b.RemoveBackup() + } + b.Path = filename b.AbsPath = absFilename b.isModified = false