Repository: open-policy-agent/conftest

Issue:
Plugin insstall is broken on macOS
- Arch: arm64
- OS: macOS 26.0

```sh
$ make build
$ ./conftest plugin install ../../contrib/plugins/kubectl
Error: install: make plugin dir: mkdir /usr/local/share: permission denied
```

Note that this also breaks `make test-acceptance` for the `tests/plugin` case. 

Discussion:

Solution patch:
diff --git a/Makefile b/Makefile
index af01143..ac9ed7f 100644
--- a/Makefile
+++ b/Makefile
@@ -45,7 +45,7 @@ test-acceptance: build install-test-deps ## Runs the tests in the test folder.
 
 .PHONY: install-test-deps
 install-test-deps: ## Installs dependencies required for testing.
-	@command -v pre-commit >/dev/null 2>&1 || python -m pip install -r requirements-dev.txt
+	@command -v pre-commit >/dev/null 2>&1 || python3 -m pip install -r requirements-dev.txt
 
 .PHONY: test-oci
 test-oci: ## Runs the OCI integration test for push and pull.
diff --git a/plugin/plugin.go b/plugin/plugin.go
index d871221..f3f69cf 100644
--- a/plugin/plugin.go
+++ b/plugin/plugin.go
@@ -142,8 +142,13 @@ func (p *Plugin) Directory() string {
 // CacheDirectory returns the full path to the
 // cache directory where all of the plugins are stored.
 func CacheDirectory() string {
-	dir, _ := cacheDirectory.Find("plugins")
-	return dir
+	// Use pre-existing cache directory if it exists, otherwise
+	// pick the preferred directory based on XDG environment vars.
+	dir, err := cacheDirectory.Find("plugins")
+	if err != nil {
+		return dir
+	}
+	return cacheDirectory.Preferred("plugins")
 }
 
 // FromDirectory returns a plugin from a specific directory.
diff --git a/plugin/xdg.go b/plugin/xdg.go
index 4b9a4b5..2edd699 100644
--- a/plugin/xdg.go
+++ b/plugin/xdg.go
@@ -3,8 +3,11 @@ package plugin
 import (
 	"fmt"
 	"os"
+	"os/user"
 	"path/filepath"
+	"strconv"
 	"strings"
+	"syscall"
 )
 
 const (
@@ -19,48 +22,91 @@ type xdgPath string
 
 // Preferred returns the preferred path according to the XDG specification
 func (p xdgPath) Preferred(path string) string {
-	dataHome := os.Getenv(XDGDataHome)
-	if dataHome != "" {
-		return filepath.ToSlash(filepath.Join(dataHome, string(p), path))
+	return p.preferred(path, os.Getenv(XDGDataHome), os.Getenv(XDGDataDirs))
+}
+
+func (p xdgPath) preferred(path, xdgDataHome, xdgDataDirs string) string {
+	user, err := user.Current()
+	if err != nil {
+		return p.homeDir(path)
+	}
+
+	if xdgDataHome != "" && p.writable(xdgDataHome, user) {
+		return filepath.ToSlash(filepath.Join(xdgDataHome, string(p), path))
+	}
+
+	if xdgDataDirs != "" {
+		// Pick the first dir that is writable.
+		for dir := range strings.SplitSeq(xdgDataDirs, ":") {
+			if p.writable(dir, user) {
+				return filepath.ToSlash(filepath.Join(dir, string(p), path))
+			}
+		}
+	}
+
+	return p.homeDir(path)
+}
+
+func (p xdgPath) homeDir(path string) string {
+	dir, _ := os.UserHomeDir()
+	return filepath.ToSlash(filepath.Join(dir, string(p), path))
+}
+
+func (p xdgPath) writable(path string, user *user.User) bool {
+	fd, err := os.Stat(path)
+	if err != nil {
+		return false
 	}
+	perms := fd.Mode().Perm()
 
-	dataDirs := os.Getenv(XDGDataDirs)
-	if dataDirs != "" {
-		dirs := strings.Split(dataDirs, ":")
-		return filepath.ToSlash(filepath.Join(dirs[0], string(p), path))
+	// First check if the directory is world-writable.
+	if perms&os.FileMode(0002) != 0 {
+		return true
 	}
 
-	homeDir, _ := os.UserHomeDir()
-	return filepath.ToSlash(filepath.Join(homeDir, string(p), path))
+	// Then check user and group permissions.
+	if fd.Sys() == nil {
+		return false
+	}
+	stat, ok := fd.Sys().(*syscall.Stat_t)
+	if !ok {
+		return false
+	}
+	if user.Uid == strconv.Itoa(int(stat.Uid)) && perms&os.FileMode(0200) != 0 {
+		return true
+	}
+	if user.Gid == strconv.Itoa(int(stat.Gid)) && perms&os.FileMode(0020) != 0 {
+		return true
+	}
+	return false
 }
 
 // Find verifies whether the file exists somewhere in the expected XDG
 // preference order. If no error is returned, the given string indicates
 // where the file was found.
 func (p xdgPath) Find(path string) (string, error) {
-	dataHome := os.Getenv(XDGDataHome)
-	if dataHome != "" {
-		dir := filepath.ToSlash(filepath.Join(dataHome, string(p), path))
+	return p.find(path, os.Getenv(XDGDataHome), os.Getenv(XDGDataDirs))
+}
+
+func (p xdgPath) find(path, xdgDataHome, xdgDataDirs string) (string, error) {
+	if xdgDataHome != "" {
+		dir := filepath.ToSlash(filepath.Join(xdgDataHome, string(p), path))
 		_, err := os.Stat(dir)
 		if err != nil && !os.IsNotExist(err) {
 			return "", fmt.Errorf("get data home directory: %w", err)
 		}
-
 		if err == nil {
 			return dir, nil
 		}
 	}
 
-	dataDirs := os.Getenv(XDGDataDirs)
-	if dataDirs != "" {
-		dirs := strings.Split(dataDirs, ":")
-		for _, dataDir := range dirs {
+	if xdgDataDirs != "" {
+		for dataDir := range strings.SplitSeq(xdgDataDirs, ":") {
 			dir := filepath.ToSlash(filepath.Join(dataDir, string(p), path))
 			_, err := os.Stat(dir)
 			if err != nil && !os.IsNotExist(err) {
 				return "", fmt.Errorf("get data dirs directory: %w", err)
 			}
-
 			if err == nil {
 				return dir, nil
 			}



Test patch:
diff --git a/plugin/install_test.go b/plugin/install_test.go
index 436f87a..e644139 100644
--- a/plugin/install_test.go
+++ b/plugin/install_test.go
@@ -4,7 +4,6 @@ import (
 	"archive/tar"
 	"bytes"
 	"compress/gzip"
-	"context"
 	"fmt"
 	"net/http"
 	"net/http/httptest"
@@ -12,9 +11,13 @@ import (
 	"path/filepath"
 	"strings"
 	"testing"
+
+	"sigs.k8s.io/yaml"
 )
 
 func TestIsDirectory(t *testing.T) {
+	t.Parallel()
+
 	testCases := []struct {
 		input    string
 		expected bool
@@ -26,23 +29,27 @@ func TestIsDirectory(t *testing.T) {
 		{input: "C:\\some\\path", expected: true},
 		{input: "unknown", expected: true},
 		{input: "unknown.com", expected: true},
-
 		{input: "github.com/username/repo", expected: false},
 	}
 
 	for _, testCase := range testCases {
-		actual, err := isDirectory(testCase.input)
-		if err != nil {
-			t.Fatal("is directory:", err)
-		}
-
-		if actual != testCase.expected {
-			t.Errorf("Directory check failed. expected %v, actual %v", testCase.expected, actual)
-		}
+		t.Run(testCase.input, func(t *testing.T) {
+			t.Parallel()
+
+			actual, err := isDirectory(testCase.input)
+			if err != nil {
+				t.Fatal("is directory:", err)
+			}
+			if actual != testCase.expected {
+				t.Errorf("Directory check failed. expected %v, actual %v", testCase.expected, actual)
+			}
+		})
 	}
 }
 
 func TestInstall(t *testing.T) {
+	tmpDir := t.TempDir()
+
 	tests := []struct {
 		name             string
 		setup            func(t *testing.T) (source string, wantPluginName string)
@@ -52,7 +59,7 @@ func TestInstall(t *testing.T) {
 		{
 			name: "install from directory",
 			setup: func(t *testing.T) (string, string) {
-				pluginDir := createInstallTestPlugin(t, "test-plugin")
+				pluginDir := createInstallTestPlugin(t, tmpDir, "test-plugin")
 				return pluginDir, "test-plugin"
 			},
 			wantErr: false,
@@ -85,11 +92,10 @@ func TestInstall(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			tmpDir := t.TempDir()
-			t.Setenv("XDG_DATA_HOME", tmpDir)
+			t.Setenv(XDGDataHome, tmpDir)
 
 			source, wantPluginName := tt.setup(t)
-			err := Install(context.Background(), source)
+			err := Install(t.Context(), source)
 
 			assertError(t, tt.wantErr, tt.wantErrSubstring, err)
 			assertInstallationResult(t, tmpDir, wantPluginName, tt.wantErr)
@@ -98,19 +104,18 @@ func TestInstall(t *testing.T) {
 }
 
 // Helper functions
-func createInstallTestPlugin(t *testing.T, name string) string {
+func createInstallTestPlugin(t *testing.T, tmpDir, name string) string {
 	t.Helper()
 
-	pluginDir := filepath.Join(t.TempDir(), name)
+	pluginDir := filepath.Join(tmpDir, name)
 	if err := os.MkdirAll(pluginDir, 0755); err != nil {
 		t.Fatal(err)
 	}
 
-	configContent := []byte(fmt.Sprintf(`name: %s
-version: 1.0.0
-command: test-command`, name))
-	if err := os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), configContent, 0o600); err != nil {
-		t.Fatal(err)
+	pluginPath := filepath.Join(pluginDir, "plugin.yaml")
+	configContent := marshalTestPlugin(t, name)
+	if err := os.WriteFile(pluginPath, configContent, 0o600); err != nil {
+		t.Fatalf("Write test plugin: %v", err)
 	}
 
 	return pluginDir
@@ -123,9 +128,7 @@ func createTestArchiveServer(t *testing.T, pluginName string) *httptest.Server {
 	gw := gzip.NewWriter(&buf)
 	tw := tar.NewWriter(gw)
 
-	configContent := []byte(fmt.Sprintf(`name: %s
-version: 1.0.0
-command: test-command`, pluginName))
+	configContent := marshalTestPlugin(t, pluginName)
 
 	header := &tar.Header{
 		Name: "plugin.yaml",
@@ -152,6 +155,21 @@ command: test-command`, pluginName))
 	}))
 }
 
+func marshalTestPlugin(t *testing.T, name string) []byte {
+	t.Helper()
+
+	plugin := Plugin{
+		Name:    name,
+		Version: "1.0.0",
+		Command: "test-command",
+	}
+	by, err := yaml.Marshal(plugin)
+	if err != nil {
+		t.Fatalf("Marshal test plugin: %v", err)
+	}
+	return by
+}
+
 func createInvalidArchiveServer(t *testing.T) *httptest.Server {
 	t.Helper()
 
@@ -205,12 +223,11 @@ func assertInstallationResult(t *testing.T, tmpDir, wantPluginName string, wantE
 
 	pluginDir := filepath.Join(tmpDir, ".conftest", "plugins", wantPluginName)
 	if _, err := os.Stat(pluginDir); os.IsNotExist(err) {
-		t.Errorf("plugin directory %q not found", pluginDir)
+		t.Fatalf("plugin directory %q not found", pluginDir)
 	}
-
 	configPath := filepath.Join(pluginDir, "plugin.yaml")
 	if _, err := os.Stat(configPath); os.IsNotExist(err) {
-		t.Error("plugin.yaml not found in plugin directory")
+		t.Errorf("plugin.yaml not found in %s", pluginDir)
 	}
 }
 
diff --git a/plugin/xdg_test.go b/plugin/xdg_test.go
index 1a24450..4395567 100644
--- a/plugin/xdg_test.go
+++ b/plugin/xdg_test.go
@@ -7,62 +7,58 @@ import (
 )
 
 func TestPreferred(t *testing.T) {
+	t.Parallel()
+
 	userHome, err := os.UserHomeDir()
 	if err != nil {
 		t.Fatalf("unexpected error %v", err)
 	}
+	tempDir := t.TempDir()
 
 	const (
-		conftestDir = ".conftest"
+		conftestDir = ".conftestcache-test"
 		pluginsDir  = "plugins"
+		nonExistant = "/doesnotexist-349857042375"
 	)
 
 	tests := []struct {
-		name  string
-		xdg   xdgPath
-		path  string
-		setup func()
-		want  string
+		name        string
+		path        string
+		xdgDataHome string
+		xdgDataDirs string
+		want        string
 	}{
 		{
-			"should return homeDir if no XDG path is set.",
-			xdgPath(conftestDir),
-			pluginsDir,
-			func() {
-				os.Unsetenv(XDGDataHome)
-				os.Unsetenv(XDGDataDirs)
-			},
-			filepath.Join(userHome, conftestDir, pluginsDir),
+			name: "should return homeDir if no XDG path is set.",
+			path: pluginsDir,
+			want: filepath.Join(userHome, conftestDir, pluginsDir),
+		},
+		{
+			name:        "unwritble XDG_DATA_HOME also returns homeDir",
+			path:        pluginsDir,
+			xdgDataHome: nonExistant,
+			want:        filepath.Join(userHome, conftestDir, pluginsDir),
 		},
 		{
-			"should return XDG_DATA_HOME if both XDG_DATA_HOME and XDG_DATA_DIRS is set",
-			xdgPath(conftestDir),
-			pluginsDir,
-			func() {
-				os.Unsetenv(XDGDataHome)
-				os.Unsetenv(XDGDataDirs)
-				os.Setenv(XDGDataHome, "/tmp")
-				os.Setenv(XDGDataDirs, "/tmp2:/tmp3")
-			},
-			filepath.Join("/tmp", conftestDir, pluginsDir),
+			name:        "should return XDG_DATA_HOME if both XDG_DATA_HOME and XDG_DATA_DIRS is set",
+			path:        pluginsDir,
+			xdgDataHome: tempDir,
+			xdgDataDirs: "/tmp2:/tmp3",
+			want:        filepath.Join(tempDir, conftestDir, pluginsDir),
 		},
 		{
-			"should return first XDG_DATA_DIRS if only XDG_DATA_DIRS is set",
-			xdgPath(conftestDir),
-			pluginsDir,
-			func() {
-				os.Unsetenv(XDGDataHome)
-				os.Unsetenv(XDGDataDirs)
-				os.Setenv(XDGDataDirs, "/tmp2:/tmp3")
-			},
-			filepath.Join("/tmp2", conftestDir, pluginsDir),
+			name:        "should return first XDG_DATA_DIRS that exists if only XDG_DATA_DIRS is set",
+			path:        pluginsDir,
+			xdgDataDirs: nonExistant + ":" + tempDir,
+			want:        filepath.Join(tempDir, conftestDir, pluginsDir),
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			tt.setup()
+			t.Parallel()
 
-			if got := tt.xdg.Preferred(tt.path); got != tt.want {
+			xdg := xdgPath(conftestDir)
+			if got := xdg.preferred(tt.path, tt.xdgDataHome, tt.xdgDataDirs); got != tt.want {
 				t.Errorf("xdgPath.Preferred() = %v, want %v", got, tt.want)
 			}
 		})
@@ -70,184 +66,78 @@ func TestPreferred(t *testing.T) {
 }
 
 func TestFind(t *testing.T) {
+	t.Parallel()
+
+	const (
+		cacheDir    = ".conftestcache-test"
+		pluginsDir  = "plugins"
+		nonExistant = "/doesnotexist-349857042375"
+	)
+
 	userHome, err := os.UserHomeDir()
 	if err != nil {
 		t.Fatalf("unexpected error %v", err)
 	}
+	userTempDir := filepath.Join(userHome, cacheDir, pluginsDir)
+	if err := os.MkdirAll(userTempDir, os.ModePerm); err != nil {
+		t.Fatalf("create cache dir under homeDir: %v", err)
+	}
+	t.Cleanup(func() { os.RemoveAll(userTempDir) })
 
-	const (
-		cacheDir      = ".conftestcache"
-		pluginsDir    = "plugins"
-		xdgDataHome   = "xdgDataHome"
-		xdgDataDirOne = "xdgDataDirOne"
-		xdgDataDirTwo = "xdgDataDirTwo"
-	)
+	tempDir := t.TempDir()
+	if err := os.MkdirAll(filepath.Join(tempDir, cacheDir, pluginsDir), os.ModePerm); err != nil {
+		t.Fatalf("create cache dir under temp dir: %v", err)
+	}
 
 	tests := []struct {
-		name     string
-		path     string
-		setup    func() (string, error)
-		teardown func(string) error
-		want     func(string) string
-		wantErr  bool
+		name        string
+		path        string
+		xdgDataHome string
+		xdgDataDirs string
+		want        string
 	}{
 		{
-			"should return error if dir does not exist.",
-			pluginsDir,
-			func() (string, error) {
-				os.Unsetenv(XDGDataHome)
-				os.Unsetenv(XDGDataDirs)
-				return "/does/not/exist", nil
-			},
-			func(_ string) error {
-				return nil
-			},
-			func(_ string) string {
-				return ""
-			},
-			true,
+			name: "should return error if dir does not exist",
+			path: nonExistant,
 		},
 		{
-			"should return homeDir if no XDG path is set.",
-			pluginsDir,
-			func() (string, error) {
-				os.Unsetenv(XDGDataHome)
-				os.Unsetenv(XDGDataDirs)
-
-				homeDir, err := os.UserHomeDir()
-				if err != nil {
-					return "", err
-				}
-
-				dir := filepath.ToSlash(filepath.Join(homeDir))
-				cache := filepath.Join(dir, cacheDir)
-
-				err = os.MkdirAll(filepath.Join(cache, pluginsDir), os.ModePerm)
-				if err != nil {
-					return "", err
-				}
-
-				return cache, nil
-			},
-			func(path string) error {
-				return os.RemoveAll(path)
-			},
-			func(_ string) string {
-				return filepath.Join(userHome, cacheDir, pluginsDir)
-			},
-			false,
+			name: "should use homeDir if no XDG path is set.",
+			path: pluginsDir,
+			want: filepath.ToSlash(filepath.Join(userHome, cacheDir, pluginsDir)),
 		},
 		{
-			"should return XDG_DATA_HOME if XDG_DATA_HOME is set.",
-			pluginsDir,
-			func() (string, error) {
-				os.Unsetenv(XDGDataHome)
-				os.Unsetenv(XDGDataDirs)
-
-				homeDir, err := os.UserHomeDir()
-				if err != nil {
-					return "", err
-				}
-
-				dir := filepath.ToSlash(filepath.Join(homeDir))
-				tmp, err := os.MkdirTemp(dir, "")
-				if err != nil {
-					return "", err
-				}
-
-				tmpXdg := filepath.Join(tmp, xdgDataHome)
-				err = os.Mkdir(tmpXdg, os.ModePerm)
-				if err != nil {
-					return "", err
-				}
-				os.Setenv(XDGDataHome, tmpXdg)
-
-				err = os.MkdirAll(filepath.Join(tmpXdg, cacheDir, pluginsDir), os.ModePerm)
-				if err != nil {
-					return "", err
-				}
-
-				return tmp, nil
-			},
-			func(path string) error {
-				return os.RemoveAll(path)
-			},
-			func(path string) string {
-				return filepath.Join(path, xdgDataHome, cacheDir, pluginsDir)
-			},
-			false,
+			name:        "should use XDG_DATA_HOME if set",
+			path:        pluginsDir,
+			xdgDataHome: tempDir,
+			want:        filepath.ToSlash(filepath.Join(tempDir, cacheDir, pluginsDir)),
 		},
 		{
-			"should return Data Dir with cache if XDG_DATA_DIRS is set.",
-			pluginsDir,
-			func() (string, error) {
-				os.Unsetenv(XDGDataHome)
-				os.Unsetenv(XDGDataDirs)
-
-				homeDir, err := os.UserHomeDir()
-				if err != nil {
-					return "", err
-				}
-
-				dir := filepath.ToSlash(filepath.Join(homeDir))
-				tmp, err := os.MkdirTemp(dir, "")
-				if err != nil {
-					return "", err
-				}
-
-				tmpXdg1 := filepath.Join(tmp, xdgDataDirOne)
-				err = os.Mkdir(tmpXdg1, os.ModePerm)
-				if err != nil {
-					return "", err
-				}
-
-				tmpXdg2 := filepath.Join(tmp, xdgDataDirTwo)
-				err = os.Mkdir(tmpXdg2, os.ModePerm)
-				if err != nil {
-					return "", err
-				}
-				os.Setenv(XDGDataDirs, tmpXdg1+":"+tmpXdg2)
-
-				err = os.MkdirAll(filepath.Join(tmpXdg2, cacheDir, pluginsDir), os.ModePerm)
-				if err != nil {
-					return "", err
-				}
-
-				return tmp, nil
-			},
-			func(path string) error {
-				return os.RemoveAll(path)
-			},
-			func(path string) string {
-				return filepath.Join(path, xdgDataDirTwo, cacheDir, pluginsDir)
-			},
-			false,
+			name:        "should use first existing XDG_DATA_DIRS if set",
+			path:        pluginsDir,
+			xdgDataDirs: "/does/not/exist:" + tempDir,
+			want:        filepath.ToSlash(filepath.Join(tempDir, cacheDir, pluginsDir)),
+		},
+		{
+			name:        "fall back to homeDir if XDG dirs point at non-existent paths",
+			path:        pluginsDir,
+			xdgDataHome: nonExistant,
+			xdgDataDirs: nonExistant,
+			want:        filepath.ToSlash(filepath.Join(userHome, cacheDir, pluginsDir)),
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			dir, err := tt.setup()
-			if err != nil {
-				t.Fatalf("unexpected error %v", err)
-			}
-
-			defer func() {
-				err := tt.teardown(dir)
-				if err != nil {
-					t.Fatalf("unexpected error %v", err)
-				}
-			}()
+			t.Parallel()
 
 			xdg := xdgPath(cacheDir)
-			got, err := xdg.Find(tt.path)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("xdgPath.Find() error = %v, wantErr %v", err, tt.wantErr)
-				return
+			got, err := xdg.find(tt.path, tt.xdgDataHome, tt.xdgDataDirs)
+			gotErr := err != nil
+			wantErr := tt.want == ""
+			if gotErr != wantErr {
+				t.Fatalf("xdgPath.Find() error = %v, wantErr %v", err, wantErr)
 			}
-
-			want := tt.want(dir)
-			if got != want {
-				t.Errorf("xdgPath.Find() = %v, want %v", got, want)
+			if got != tt.want {
+				t.Errorf("xdgPath.Find() = %s, want %s", got, tt.want)
 			}
 		})
 	}