Fix ext install with relative path source

This commit is contained in:
patriceckhart 2026-06-07 10:18:41 +02:00
parent 84fd98ea74
commit 10fde8fd0e
2 changed files with 85 additions and 2 deletions

View file

@ -269,12 +269,24 @@ func extInstall(args []string) error {
if _, err := os.Stat(filepath.Join(src, "extension.json")); err != nil {
return fmt.Errorf("source lacks extension.json")
}
name := filepath.Base(src)
// Resolve to an absolute, cleaned path before deriving the install
// name. Otherwise relative sources like "." or "./" collapse to a
// basename of ".", and the destination wrongly resolves to the
// extensions/ parent directory (which zot creates on first run),
// triggering a false "already exists" failure.
absSrc, err := filepath.Abs(src)
if err != nil {
return err
}
name := filepath.Base(absSrc)
if name == "." || name == ".." || name == string(filepath.Separator) || name == "" {
return fmt.Errorf("cannot derive extension name from %q", src)
}
out := filepath.Join(dest, name)
if _, err := os.Stat(out); err == nil {
return fmt.Errorf("destination %s already exists; remove it first", out)
}
if err := copyDir(src, out); err != nil {
if err := copyDir(absSrc, out); err != nil {
return err
}
fmt.Fprintf(os.Stderr, "installed %s\n", out)

View file

@ -0,0 +1,71 @@
package agent
import (
"os"
"path/filepath"
"testing"
)
// TestExtInstallDotSource verifies that `zot ext install .` derives the
// extension name from the resolved directory name rather than collapsing
// to the extensions/ parent directory (the false "already exists" bug).
func TestExtInstallDotSource(t *testing.T) {
home := t.TempDir()
t.Setenv("ZOT_HOME", home)
// Pre-create extensions/ to mimic a normal first run.
if err := os.MkdirAll(filepath.Join(home, "extensions"), 0o755); err != nil {
t.Fatal(err)
}
srcParent := t.TempDir()
src := filepath.Join(srcParent, "kagi")
if err := os.MkdirAll(src, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(src, "extension.json"), []byte(`{"name":"kagi"}`), 0o644); err != nil {
t.Fatal(err)
}
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
defer os.Chdir(cwd)
if err := os.Chdir(src); err != nil {
t.Fatal(err)
}
if err := extInstall([]string{"."}); err != nil {
t.Fatalf("install with '.' failed: %v", err)
}
out := filepath.Join(home, "extensions", "kagi")
if _, err := os.Stat(filepath.Join(out, "extension.json")); err != nil {
t.Fatalf("expected installed extension at %s: %v", out, err)
}
}
// TestExtInstallRejectsParentName guards against deriving a name of ".."
// from a source that resolves to a filesystem root edge case. A normal
// directory always yields a real basename, so this just ensures the
// guard logic does not crash for well-formed input.
func TestExtInstallNamedDir(t *testing.T) {
home := t.TempDir()
t.Setenv("ZOT_HOME", home)
src := filepath.Join(t.TempDir(), "myext")
if err := os.MkdirAll(src, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(src, "extension.json"), []byte(`{"name":"myext"}`), 0o644); err != nil {
t.Fatal(err)
}
if err := extInstall([]string{src}); err != nil {
t.Fatalf("install failed: %v", err)
}
if _, err := os.Stat(filepath.Join(home, "extensions", "myext", "extension.json")); err != nil {
t.Fatalf("expected installed extension: %v", err)
}
}