mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
fix: Fix sandbox policy generator for MacOS min permissions
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// DANGEROUS_FILES are files that should always be blocked from write access
|
||||
// to prevent credential theft and security compromise.
|
||||
var DANGEROUS_FILES = []string{
|
||||
".env",
|
||||
".env.*",
|
||||
".aws",
|
||||
".gcloud",
|
||||
".kube",
|
||||
".ssh",
|
||||
".gnupg",
|
||||
".docker/config.json",
|
||||
}
|
||||
|
||||
// GetMandatoryDenyPatterns returns filesystem paths that should always be blocked
|
||||
// from write access for security reasons. These are automatically injected into
|
||||
// all sandbox policies regardless of user configuration.
|
||||
//
|
||||
// Parameters:
|
||||
// - allowGitConfig: if false, blocks write access to .git/config (recommended)
|
||||
//
|
||||
// Returns patterns in both absolute (from HOME) and glob forms for comprehensive coverage.
|
||||
func GetMandatoryDenyPatterns(allowGitConfig bool) []string {
|
||||
patterns := []string{}
|
||||
|
||||
// Get current working directory for CWD-relative patterns
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
// Fallback to basic patterns if we can't get CWD
|
||||
cwd = "."
|
||||
}
|
||||
|
||||
// Get home directory for HOME-relative patterns
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
// If we can't get home, skip home-based patterns
|
||||
home = ""
|
||||
}
|
||||
|
||||
// Add dangerous files from CWD
|
||||
for _, fileName := range DANGEROUS_FILES {
|
||||
// Absolute path in CWD
|
||||
patterns = append(patterns, filepath.Join(cwd, fileName))
|
||||
// Glob pattern to catch in subdirectories
|
||||
patterns = append(patterns, filepath.Join("**", fileName))
|
||||
}
|
||||
|
||||
// Add dangerous files from HOME (if available)
|
||||
if home != "" {
|
||||
for _, fileName := range DANGEROUS_FILES {
|
||||
patterns = append(patterns, filepath.Join(home, fileName))
|
||||
}
|
||||
}
|
||||
|
||||
// Git hooks are ALWAYS blocked for security (can execute arbitrary code)
|
||||
patterns = append(patterns, filepath.Join(cwd, ".git/hooks"))
|
||||
patterns = append(patterns, filepath.Join(cwd, ".git/hooks/**"))
|
||||
patterns = append(patterns, "**/.git/hooks")
|
||||
patterns = append(patterns, "**/.git/hooks/**")
|
||||
|
||||
// Git config is conditionally blocked
|
||||
if !allowGitConfig {
|
||||
patterns = append(patterns, filepath.Join(cwd, ".git/config"))
|
||||
patterns = append(patterns, "**/.git/config")
|
||||
}
|
||||
|
||||
return patterns
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetMandatoryDenyPatterns(t *testing.T) {
|
||||
t.Run("always blocks dangerous files", func(t *testing.T) {
|
||||
patterns := GetMandatoryDenyPatterns(false)
|
||||
|
||||
// Should contain patterns for each dangerous file
|
||||
assert.Contains(t, patterns, "**/.env")
|
||||
assert.Contains(t, patterns, "**/.ssh")
|
||||
assert.Contains(t, patterns, "**/.aws")
|
||||
assert.Contains(t, patterns, "**/.gcloud")
|
||||
assert.Contains(t, patterns, "**/.kube")
|
||||
assert.Contains(t, patterns, "**/.gnupg")
|
||||
assert.Contains(t, patterns, "**/.docker/config.json")
|
||||
})
|
||||
|
||||
t.Run("always blocks git hooks", func(t *testing.T) {
|
||||
patterns := GetMandatoryDenyPatterns(false)
|
||||
|
||||
// Should block git hooks
|
||||
assert.Contains(t, patterns, "**/.git/hooks")
|
||||
assert.Contains(t, patterns, "**/.git/hooks/**")
|
||||
})
|
||||
|
||||
t.Run("blocks git config when allowGitConfig is false", func(t *testing.T) {
|
||||
patterns := GetMandatoryDenyPatterns(false)
|
||||
|
||||
// Should block git config
|
||||
assert.Contains(t, patterns, "**/.git/config")
|
||||
})
|
||||
|
||||
t.Run("allows git config when allowGitConfig is true", func(t *testing.T) {
|
||||
patterns := GetMandatoryDenyPatterns(true)
|
||||
|
||||
// Should NOT block git config
|
||||
for _, pattern := range patterns {
|
||||
assert.NotContains(t, pattern, ".git/config")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("includes CWD-relative patterns", func(t *testing.T) {
|
||||
cwd, err := os.Getwd()
|
||||
assert.NoError(t, err)
|
||||
|
||||
patterns := GetMandatoryDenyPatterns(false)
|
||||
|
||||
// Should include absolute paths in CWD
|
||||
assert.Contains(t, patterns, filepath.Join(cwd, ".env"))
|
||||
assert.Contains(t, patterns, filepath.Join(cwd, ".ssh"))
|
||||
assert.Contains(t, patterns, filepath.Join(cwd, ".git/hooks"))
|
||||
})
|
||||
|
||||
t.Run("includes HOME-relative patterns", func(t *testing.T) {
|
||||
home, err := os.UserHomeDir()
|
||||
assert.NoError(t, err)
|
||||
|
||||
patterns := GetMandatoryDenyPatterns(false)
|
||||
|
||||
// Should include absolute paths in HOME
|
||||
assert.Contains(t, patterns, filepath.Join(home, ".env"))
|
||||
assert.Contains(t, patterns, filepath.Join(home, ".ssh"))
|
||||
assert.Contains(t, patterns, filepath.Join(home, ".aws"))
|
||||
})
|
||||
|
||||
t.Run("includes glob patterns for env variants", func(t *testing.T) {
|
||||
patterns := GetMandatoryDenyPatterns(false)
|
||||
|
||||
// Should include pattern for .env.* files
|
||||
assert.Contains(t, patterns, "**/.env.*")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GlobToRegex converts a glob pattern to a Seatbelt-compatible regular expression.
|
||||
//
|
||||
// This implements gitignore-style pattern matching to match the behavior used
|
||||
// in filesystem permission systems.
|
||||
//
|
||||
// Supported patterns:
|
||||
// - * matches any characters except / (e.g., *.ts matches foo.ts but not foo/bar.ts)
|
||||
// - ** matches any characters including / (e.g., src/**/*.ts matches all .ts files in src/)
|
||||
// - ? matches any single character except / (e.g., file?.txt matches file1.txt)
|
||||
// - [abc] matches any character in the set (e.g., file[0-9].txt matches file3.txt)
|
||||
//
|
||||
// Note: This is designed for macOS sandbox (regex ...) syntax. The resulting regex
|
||||
// will be used in sandbox profiles like: (deny file-write* (regex "pattern"))
|
||||
//
|
||||
// Examples:
|
||||
// - "/path/to/*.txt" -> "^/path/to/[^/]*\\.txt$"
|
||||
// - "/path/**/file" -> "^/path/(.*/)?file$"
|
||||
// - "/tmp/file?.log" -> "^/tmp/file[^/]\\.log$"
|
||||
func GlobToRegex(globPattern string) string {
|
||||
result := globPattern
|
||||
|
||||
// Escape regex special characters (except glob chars * ? [ ])
|
||||
// We need to escape: . ^ $ + { } ( ) | \
|
||||
result = escapeRegexChars(result)
|
||||
|
||||
// Escape unclosed brackets (no matching ])
|
||||
// This handles edge cases like "[abc" which should be treated literally
|
||||
result = escapeUnclosedBrackets(result)
|
||||
|
||||
// Convert glob patterns to regex (order matters - ** before *)
|
||||
// Use placeholders to avoid double-conversion
|
||||
|
||||
// 1. Handle **/ (globstar with slash)
|
||||
result = strings.ReplaceAll(result, "**/", "__GLOBSTAR_SLASH__")
|
||||
|
||||
// 2. Handle ** (globstar standalone)
|
||||
result = strings.ReplaceAll(result, "**", "__GLOBSTAR__")
|
||||
|
||||
// 3. Handle * (wildcard)
|
||||
result = strings.ReplaceAll(result, "*", "[^/]*")
|
||||
|
||||
// 4. Handle ? (single char wildcard)
|
||||
result = strings.ReplaceAll(result, "?", "[^/]")
|
||||
|
||||
// 5. Restore placeholders
|
||||
result = strings.ReplaceAll(result, "__GLOBSTAR_SLASH__", "(.*/)?")
|
||||
result = strings.ReplaceAll(result, "__GLOBSTAR__", ".*")
|
||||
|
||||
// Add anchors for exact matching
|
||||
return "^" + result + "$"
|
||||
}
|
||||
|
||||
// escapeRegexChars escapes regex special characters except glob wildcards.
|
||||
// Escapes: . ^ $ + { } ( ) |
|
||||
// Preserves: * ? [ ] \
|
||||
// Note: We don't escape backslash because it shouldn't appear in file path glob patterns
|
||||
func escapeRegexChars(s string) string {
|
||||
// Characters that need escaping in regex (excluding glob chars)
|
||||
// We don't include backslash here because:
|
||||
// 1. File paths on Unix don't contain backslashes
|
||||
// 2. We use backslash to escape regex chars, so escaping backslash would double them
|
||||
specialChars := []string{".", "^", "$", "+", "{", "}", "(", ")", "|"}
|
||||
|
||||
result := s
|
||||
for _, char := range specialChars {
|
||||
result = strings.ReplaceAll(result, char, "\\"+char)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
var escapeUnclosedBracketsRegex = regexp.MustCompile(`\[([^\]]*?)$`)
|
||||
|
||||
// escapeUnclosedBrackets escapes bracket expressions that don't have a closing bracket.
|
||||
// Example: "[abc" -> "\[abc"
|
||||
func escapeUnclosedBrackets(s string) string {
|
||||
// Find all opening brackets that don't have a closing bracket
|
||||
return escapeUnclosedBracketsRegex.ReplaceAllString(s, `\[$1`)
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGlobToRegex(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
pattern string
|
||||
shouldMatch []string
|
||||
shouldNotMatch []string
|
||||
}{
|
||||
{
|
||||
name: "simple asterisk wildcard",
|
||||
pattern: "/path/to/*.txt",
|
||||
shouldMatch: []string{
|
||||
"/path/to/file.txt",
|
||||
"/path/to/test.txt",
|
||||
"/path/to/a.txt",
|
||||
"/path/to/.txt", // * matches zero or more chars (standard glob behavior)
|
||||
},
|
||||
shouldNotMatch: []string{
|
||||
"/path/to/file.log",
|
||||
"/path/to/sub/file.txt",
|
||||
"/path/file.txt",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "globstar pattern",
|
||||
pattern: "/path/**/*.txt",
|
||||
shouldMatch: []string{
|
||||
"/path/file.txt",
|
||||
"/path/to/file.txt",
|
||||
"/path/to/sub/file.txt",
|
||||
"/path/a/b/c/file.txt",
|
||||
},
|
||||
shouldNotMatch: []string{
|
||||
"/path/file.log",
|
||||
"/other/path/file.txt",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "globstar with trailing slash",
|
||||
pattern: "/src/**/",
|
||||
shouldMatch: []string{
|
||||
"/src/",
|
||||
"/src/a/",
|
||||
"/src/a/b/",
|
||||
"/src/deep/nested/path/",
|
||||
},
|
||||
shouldNotMatch: []string{
|
||||
"/src",
|
||||
"/other/",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "question mark wildcard",
|
||||
pattern: "/tmp/file?.log",
|
||||
shouldMatch: []string{
|
||||
"/tmp/file1.log",
|
||||
"/tmp/file2.log",
|
||||
"/tmp/filea.log",
|
||||
},
|
||||
shouldNotMatch: []string{
|
||||
"/tmp/file.log",
|
||||
"/tmp/file12.log",
|
||||
"/tmp/file/.log",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bracket wildcard",
|
||||
pattern: "/tmp/test[123].txt",
|
||||
shouldMatch: []string{
|
||||
"/tmp/test1.txt",
|
||||
"/tmp/test2.txt",
|
||||
"/tmp/test3.txt",
|
||||
},
|
||||
shouldNotMatch: []string{
|
||||
"/tmp/test4.txt",
|
||||
"/tmp/testa.txt",
|
||||
"/tmp/test.txt",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bracket range",
|
||||
pattern: "/tmp/file[0-9].log",
|
||||
shouldMatch: []string{
|
||||
"/tmp/file0.log",
|
||||
"/tmp/file5.log",
|
||||
"/tmp/file9.log",
|
||||
},
|
||||
shouldNotMatch: []string{
|
||||
"/tmp/filea.log",
|
||||
"/tmp/file10.log",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "regex special characters escaped",
|
||||
pattern: "/path/to/file.txt",
|
||||
shouldMatch: []string{
|
||||
"/path/to/file.txt",
|
||||
},
|
||||
shouldNotMatch: []string{
|
||||
"/path/to/fileXtxt",
|
||||
"/path/to/file_txt",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple wildcards",
|
||||
pattern: "/path/*/sub/*.txt",
|
||||
shouldMatch: []string{
|
||||
"/path/a/sub/file.txt",
|
||||
"/path/b/sub/test.txt",
|
||||
},
|
||||
shouldNotMatch: []string{
|
||||
"/path/sub/file.txt",
|
||||
"/path/a/sub/deep/file.txt",
|
||||
"/path/a/b/sub/file.txt",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "globstar in middle",
|
||||
pattern: "/usr/**/bin/node",
|
||||
shouldMatch: []string{
|
||||
"/usr/bin/node",
|
||||
"/usr/local/bin/node",
|
||||
"/usr/a/b/c/bin/node",
|
||||
},
|
||||
shouldNotMatch: []string{
|
||||
"/usr/node",
|
||||
"/usr/bin/npm",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exact path (no wildcards)",
|
||||
pattern: "/etc/passwd",
|
||||
shouldMatch: []string{
|
||||
"/etc/passwd",
|
||||
},
|
||||
shouldNotMatch: []string{
|
||||
"/etc/passwd.bak",
|
||||
"/etc/shadow",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wildcard at beginning",
|
||||
pattern: "*.txt",
|
||||
shouldMatch: []string{
|
||||
"file.txt",
|
||||
"test.txt",
|
||||
},
|
||||
shouldNotMatch: []string{
|
||||
"file.log",
|
||||
"dir/file.txt",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "complex pattern with parens and dots",
|
||||
pattern: "/path/to/file(1).txt",
|
||||
shouldMatch: []string{
|
||||
"/path/to/file(1).txt",
|
||||
},
|
||||
shouldNotMatch: []string{
|
||||
"/path/to/file1.txt",
|
||||
"/path/to/file(1)Xtxt",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
regexPattern := GlobToRegex(tt.pattern)
|
||||
re, err := regexp.Compile(regexPattern)
|
||||
assert.NoError(t, err, "Generated regex should be valid")
|
||||
|
||||
for _, path := range tt.shouldMatch {
|
||||
assert.True(t, re.MatchString(path), "Pattern %s should match %s (regex: %s)", tt.pattern, path, regexPattern)
|
||||
}
|
||||
|
||||
for _, path := range tt.shouldNotMatch {
|
||||
assert.False(t, re.MatchString(path), "Pattern %s should not match %s (regex: %s)", tt.pattern, path, regexPattern)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobToRegexPatterns(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
pattern string
|
||||
expectedRegex string
|
||||
}{
|
||||
{
|
||||
name: "simple asterisk",
|
||||
pattern: "*.txt",
|
||||
expectedRegex: `^[^/]*\.txt$`,
|
||||
},
|
||||
{
|
||||
name: "globstar",
|
||||
pattern: "**/*.txt",
|
||||
expectedRegex: `^(.*/)?[^/]*\.txt$`,
|
||||
},
|
||||
{
|
||||
name: "question mark",
|
||||
pattern: "file?.txt",
|
||||
expectedRegex: `^file[^/]\.txt$`,
|
||||
},
|
||||
{
|
||||
name: "absolute path with wildcard",
|
||||
pattern: "/path/to/*.txt",
|
||||
expectedRegex: `^/path/to/[^/]*\.txt$`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := GlobToRegex(tt.pattern)
|
||||
assert.Equal(t, tt.expectedRegex, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapeRegexChars(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "dot escaping",
|
||||
input: "file.txt",
|
||||
expected: `file\.txt`,
|
||||
},
|
||||
{
|
||||
name: "multiple special chars",
|
||||
input: "file(1).txt",
|
||||
expected: `file\(1\)\.txt`,
|
||||
},
|
||||
{
|
||||
name: "glob chars not escaped but dots are",
|
||||
input: "*.txt",
|
||||
expected: `*\.txt`,
|
||||
},
|
||||
{
|
||||
name: "brackets not escaped",
|
||||
input: "[0-9]",
|
||||
expected: "[0-9]",
|
||||
},
|
||||
{
|
||||
name: "question mark not escaped but dots are",
|
||||
input: "file?.txt",
|
||||
expected: `file?\.txt`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := escapeRegexChars(tt.input)
|
||||
assert.Equal(t, tt.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapeUnclosedBrackets(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "unclosed bracket at end",
|
||||
input: "test[abc",
|
||||
expected: `test\[abc`,
|
||||
},
|
||||
{
|
||||
name: "closed bracket",
|
||||
input: "test[abc]",
|
||||
expected: "test[abc]",
|
||||
},
|
||||
{
|
||||
name: "no brackets",
|
||||
input: "test",
|
||||
expected: "test",
|
||||
},
|
||||
{
|
||||
name: "multiple closed brackets",
|
||||
input: "[a-z][0-9]",
|
||||
expected: "[a-z][0-9]",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := escapeUnclosedBrackets(tt.input)
|
||||
assert.Equal(t, tt.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var tmpdirPatternRegex = regexp.MustCompile(`^/(private/)?var/folders/[^/]{2}/[^/]+/T/?$`)
|
||||
|
||||
// GetTmpdirParent returns the parent directory of TMPDIR if it matches the macOS pattern.
|
||||
// On macOS, TMPDIR is typically /var/folders/XX/YYY/T/ where XX and YYY are random.
|
||||
//
|
||||
// Returns both /var/ and /private/var/ versions since /var is a symlink to /private/var.
|
||||
// This is needed because package managers may reference either path.
|
||||
//
|
||||
// Returns empty slice if TMPDIR doesn't match the expected macOS pattern.
|
||||
func GetTmpdirParent() []string {
|
||||
tmpdir := os.Getenv("TMPDIR")
|
||||
if tmpdir == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// macOS TMPDIR pattern: /var/folders/XX/YYY/T/ or /private/var/folders/XX/YYY/T/
|
||||
// where XX is 2 chars and YYY is random string
|
||||
pattern := tmpdirPatternRegex
|
||||
if !pattern.MatchString(tmpdir) {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Remove trailing /T or /T/
|
||||
parent := strings.TrimSuffix(tmpdir, "/")
|
||||
parent = strings.TrimSuffix(parent, "/T")
|
||||
|
||||
// Return both /var/ and /private/var/ versions
|
||||
if strings.HasPrefix(parent, "/private/var/") {
|
||||
// Already has /private prefix
|
||||
withoutPrivate := strings.Replace(parent, "/private", "", 1)
|
||||
return []string{parent, withoutPrivate}
|
||||
} else if strings.HasPrefix(parent, "/var/") {
|
||||
// Missing /private prefix
|
||||
withPrivate := "/private" + parent
|
||||
return []string{parent, withPrivate}
|
||||
}
|
||||
|
||||
return []string{parent}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetTmpdirParent(t *testing.T) {
|
||||
// Save original TMPDIR
|
||||
originalTmpdir := os.Getenv("TMPDIR")
|
||||
defer os.Setenv("TMPDIR", originalTmpdir)
|
||||
|
||||
t.Run("macOS pattern with /var prefix", func(t *testing.T) {
|
||||
os.Setenv("TMPDIR", "/var/folders/ab/cd1234ef/T/")
|
||||
parents := GetTmpdirParent()
|
||||
|
||||
assert.Len(t, parents, 2)
|
||||
assert.Contains(t, parents, "/var/folders/ab/cd1234ef")
|
||||
assert.Contains(t, parents, "/private/var/folders/ab/cd1234ef")
|
||||
})
|
||||
|
||||
t.Run("macOS pattern with /private/var prefix", func(t *testing.T) {
|
||||
os.Setenv("TMPDIR", "/private/var/folders/xy/z9876543/T/")
|
||||
parents := GetTmpdirParent()
|
||||
|
||||
assert.Len(t, parents, 2)
|
||||
assert.Contains(t, parents, "/private/var/folders/xy/z9876543")
|
||||
assert.Contains(t, parents, "/var/folders/xy/z9876543")
|
||||
})
|
||||
|
||||
t.Run("macOS pattern without trailing slash", func(t *testing.T) {
|
||||
os.Setenv("TMPDIR", "/var/folders/12/abcdefgh/T")
|
||||
parents := GetTmpdirParent()
|
||||
|
||||
assert.Len(t, parents, 2)
|
||||
assert.Contains(t, parents, "/var/folders/12/abcdefgh")
|
||||
assert.Contains(t, parents, "/private/var/folders/12/abcdefgh")
|
||||
})
|
||||
|
||||
t.Run("non-macOS pattern returns empty", func(t *testing.T) {
|
||||
testCases := []string{
|
||||
"/tmp",
|
||||
"/var/tmp",
|
||||
"/custom/temp",
|
||||
"/var/folders/",
|
||||
"/var/folders/ab/",
|
||||
"/var/folders/abc/def/T/", // XX should be 2 chars, not 3
|
||||
}
|
||||
|
||||
for _, tmpdir := range testCases {
|
||||
os.Setenv("TMPDIR", tmpdir)
|
||||
parents := GetTmpdirParent()
|
||||
assert.Empty(t, parents, "Expected empty result for TMPDIR=%s", tmpdir)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty TMPDIR returns empty", func(t *testing.T) {
|
||||
os.Setenv("TMPDIR", "")
|
||||
parents := GetTmpdirParent()
|
||||
assert.Empty(t, parents)
|
||||
})
|
||||
|
||||
t.Run("unset TMPDIR returns empty", func(t *testing.T) {
|
||||
os.Unsetenv("TMPDIR")
|
||||
parents := GetTmpdirParent()
|
||||
assert.Empty(t, parents)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user