// Package tempdir allocates disposable storage outside a source worktree. package tempdir import ( "errors" "fmt" "os" "path/filepath" "strings" ) // Outside uses system temporary storage and falls back to the repository's // parent when TMPDIR resolves inside the worktree. Cleanup belongs to the caller. func Outside(repositoryDir, pattern string) (string, error) { return outside(repositoryDir, pattern, filepath.Abs, filepath.EvalSymlinks, os.RemoveAll) } func outside(repositoryDir, pattern string, abs, resolve func(string) (string, error), remove func(string) error) (string, error) { repository, err := abs(repositoryDir) if err != nil { return "", err } repository, err = resolve(repository) if err != nil { return "", err } directory, err := os.MkdirTemp("", pattern) if err != nil { return "", err } created := directory directory, err = abs(created) if err != nil { return "", cleanupError(created, err, remove) } directory, err = resolve(directory) if err != nil { return "", cleanupError(created, err, remove) } outside, err := isOutside(repository, directory) if err != nil { return "", cleanupError(created, err, remove) } if outside { return directory, nil } if err := remove(created); err != nil { return "", fmt.Errorf("remove temporary storage %q: %w", created, err) } directory, err = os.MkdirTemp(filepath.Dir(repository), pattern) if err != nil { return "", fmt.Errorf("create temporary storage outside repository %q: %w", repository, err) } created = directory directory, err = abs(created) if err != nil { return "", cleanupError(created, err, remove) } directory, err = resolve(directory) if err != nil { return "", cleanupError(created, err, remove) } outside, err = isOutside(repository, directory) if err != nil { return "", cleanupError(created, err, remove) } if !outside { return "", cleanupError(created, fmt.Errorf("temporary storage %q is inside repository %q", directory, repository), remove) } return directory, nil } func cleanupError(path string, cause error, remove func(string) error) error { if err := remove(path); err != nil { return errors.Join(cause, fmt.Errorf("remove temporary storage %q: %w", path, err)) } return cause } func isOutside(repository, directory string) (bool, error) { if !strings.EqualFold(filepath.VolumeName(repository), filepath.VolumeName(directory)) { return true, nil } relative, err := filepath.Rel(repository, directory) if err != nil { return false, err } return relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)), nil }