remote_file kitten: correct pythonSplitExt to match os.path.splitext semantics

This commit is contained in:
OBrutus 2026-08-12 17:50:13 +05:30
parent f9e242e308
commit 062ff5fed1
2 changed files with 65 additions and 13 deletions

View file

@ -99,23 +99,43 @@ func hostname_matches(from_hyperlink, actual string) bool {
return fl == al
}
// pythonSplitExt mirrors Python's os.path.splitext behavior for dotfiles.
// For a file like "/x/.bashrc" (basename ".bashrc"), Python returns ("/x/.bashrc", "").
// Go's filepath.Ext would return (".bashrc", "/x/"), losing the filename.
// Rule: if basename starts with '.' and has no other dots, treat extension as empty.
// pythonSplitExt mirrors Python's os.path.splitext behavior exactly.
// Algorithm: skip all leading dots in basename; find last dot in remainder;
// if found, split at that dot; otherwise no extension.
// Examples:
// "/x/.bashrc" → ("/x/.bashrc", "") [1 leading dot, no dot in remainder]
// "/x/.bashrc.bak" → ("/x/.bashrc", ".bak") [1 leading dot, 1 dot in remainder]
// "/x/a.b.c" → ("/x/a.b", ".c") [0 leading dots, last dot at position 3]
// "....txt" → ("....txt", "") [4 leading dots, no dot in remainder]
func pythonSplitExt(path string) (string, string) {
ext := filepath.Ext(path)
if ext == "" {
return path, ""
}
basename := filepath.Base(path)
// If basename starts with '.' and has exactly one dot (the leading dot),
// then this is a dotfile with no extension.
if strings.HasPrefix(basename, ".") && strings.Count(basename, ".") == 1 {
dir := filepath.Dir(path)
// Skip all leading dots in basename
i := 0
for i < len(basename) && basename[i] == '.' {
i++
}
// Find the last dot in the remainder (after leading dots)
remainder := basename[i:]
idx := strings.LastIndex(remainder, ".")
if idx == -1 {
// No extension found
return path, ""
}
base := strings.TrimSuffix(path, ext)
return base, ext
// Extension starts at position i + idx in the original basename
extStart := i + idx
baseName := basename[:extStart]
ext := basename[extStart:]
// Reconstruct the full path
if dir == "." {
return baseName, ext
}
return filepath.Join(dir, baseName), ext
}
func auto_rename_dest(dest string, exists func(string) bool) string {

View file

@ -95,3 +95,35 @@ func TestParseConnDataSSHKittenEdge(t *testing.T) {
t.Fatalf("ssh kitten edge case (single item): %+v, SSHKittenCmdline=%v", c, c.SSHKittenCmdline)
}
}
func TestPythonSplitExt(t *testing.T) {
// Table-driven test for pythonSplitExt matching os.path.splitext semantics.
// Expected values from Python 3: python3 -c "import os; print(os.path.splitext(f))" for each.
// Reference run: 2026-08-12
for _, tc := range []struct {
input string
wantBase string
wantExt string
}{
// Dotfiles with no extension in remainder (leading dots only)
{".bashrc", ".bashrc", ""},
{"..bashrc", "..bashrc", ""},
{"...", "...", ""},
{"..", "..", ""},
{"....txt", "....txt", ""}, // 4 leading dots, then "txt" with no dot
// Dotfiles with extension in remainder
{".bashrc.bak", ".bashrc", ".bak"},
// Normal files
{"a.b.c", "a.b", ".c"},
{"a.txt", "a", ".txt"},
{"plain", "plain", ""},
} {
base, ext := pythonSplitExt(tc.input)
if base != tc.wantBase || ext != tc.wantExt {
t.Fatalf("pythonSplitExt(%q):\n got (%q, %q)\n want (%q, %q)",
tc.input, base, ext, tc.wantBase, tc.wantExt)
}
}
}