| | | 1 | | // Package sandbox implements ports.SandboxFactory. Each sandbox creates a |
| | | 2 | | // temporary directory tree that mirrors the XDG base directory structure and |
| | | 3 | | // sets the corresponding XDG_* environment variables so Neovim uses them |
| | | 4 | | // exclusively, without touching the user's real config. |
| | | 5 | | package sandbox |
| | | 6 | | |
| | | 7 | | import ( |
| | | 8 | | "context" |
| | | 9 | | "errors" |
| | | 10 | | "fmt" |
| | | 11 | | "os" |
| | | 12 | | "path/filepath" |
| | | 13 | | |
| | | 14 | | "github.com/jedi-knights/neospec/internal/ports" |
| | | 15 | | ) |
| | | 16 | | |
| | | 17 | | // fsOps abstracts the OS filesystem operations used by Factory.Create. Holding |
| | | 18 | | // them in an interface lets tests inject fakes that trigger the MkdirTemp and |
| | | 19 | | // MkdirAll error branches without manipulating real filesystem state. |
| | | 20 | | type fsOps interface { |
| | | 21 | | MkdirTemp(dir, pattern string) (string, error) |
| | | 22 | | MkdirAll(path string, perm os.FileMode) error |
| | | 23 | | RemoveAll(path string) error |
| | | 24 | | } |
| | | 25 | | |
| | | 26 | | // realFS is the production fsOps implementation that delegates to the os package. |
| | | 27 | | type realFS struct{} |
| | | 28 | | |
| | | 29 | | func (realFS) MkdirTemp(dir, pattern string) (string, error) { return os.MkdirTemp(dir, pattern) } |
| | | 30 | | func (realFS) MkdirAll(path string, perm os.FileMode) error { return os.MkdirAll(path, perm) } |
| | | 31 | | func (realFS) RemoveAll(path string) error { return os.RemoveAll(path) } |
| | | 32 | | |
| | | 33 | | // xdgSandbox is a single-use XDG environment tied to a temporary directory. |
| | | 34 | | type xdgSandbox struct { |
| | | 35 | | root string |
| | | 36 | | fs fsOps |
| | | 37 | | } |
| | | 38 | | |
| | | 39 | | // Factory creates XDG sandboxes. |
| | | 40 | | type Factory struct { |
| | | 41 | | fs fsOps |
| | | 42 | | } |
| | | 43 | | |
| | | 44 | | // NewFactory creates a Factory backed by the real OS filesystem. |
| | 4 | 45 | | func NewFactory() *Factory { |
| | 4 | 46 | | return &Factory{fs: realFS{}} |
| | 4 | 47 | | } |
| | | 48 | | |
| | | 49 | | // Create creates a new sandbox with a unique temporary root directory. |
| | | 50 | | func (f *Factory) Create(_ context.Context) (ports.Sandbox, error) { |
| | | 51 | | root, err := f.fs.MkdirTemp("", "neospec-sandbox-*") |
| | | 52 | | if err != nil { |
| | | 53 | | return nil, fmt.Errorf("creating sandbox temp dir: %w", err) |
| | | 54 | | } |
| | | 55 | | |
| | | 56 | | // Pre-create all XDG subdirectories so Neovim doesn't encounter missing dirs. |
| | | 57 | | for _, sub := range []string{"data", "config", "state", "cache", "runtime"} { |
| | | 58 | | if err := f.fs.MkdirAll(filepath.Join(root, sub), 0o700); err != nil { |
| | | 59 | | mkdirErr := fmt.Errorf("creating xdg subdir %s: %w", sub, err) |
| | | 60 | | if cerr := f.fs.RemoveAll(root); cerr != nil { |
| | | 61 | | return nil, errors.Join(mkdirErr, fmt.Errorf("cleaning up sandbox root: %w", cerr)) |
| | | 62 | | } |
| | | 63 | | return nil, mkdirErr |
| | | 64 | | } |
| | | 65 | | } |
| | | 66 | | |
| | | 67 | | return &xdgSandbox{root: root, fs: f.fs}, nil |
| | | 68 | | } |
| | | 69 | | |
| | | 70 | | // Env returns the environment variables that activate this sandbox. |
| | | 71 | | func (s *xdgSandbox) Env() []string { |
| | | 72 | | return []string{ |
| | | 73 | | "XDG_DATA_HOME=" + filepath.Join(s.root, "data"), |
| | | 74 | | "XDG_CONFIG_HOME=" + filepath.Join(s.root, "config"), |
| | | 75 | | "XDG_STATE_HOME=" + filepath.Join(s.root, "state"), |
| | | 76 | | "XDG_CACHE_HOME=" + filepath.Join(s.root, "cache"), |
| | | 77 | | "XDG_RUNTIME_DIR=" + filepath.Join(s.root, "runtime"), |
| | | 78 | | // Prevent Neovim from reading system init files. |
| | | 79 | | "NVIM_APPNAME=neospec-isolated", |
| | | 80 | | // HOME override ensures no ~/.config/nvim fallback. |
| | | 81 | | "HOME=" + s.root, |
| | | 82 | | } |
| | | 83 | | } |
| | | 84 | | |
| | | 85 | | // Dir returns the root temporary directory of the sandbox. |
| | | 86 | | func (s *xdgSandbox) Dir() string { |
| | | 87 | | return s.root |
| | | 88 | | } |
| | | 89 | | |
| | | 90 | | // Close removes all temporary directories created for this sandbox. |
| | | 91 | | func (s *xdgSandbox) Close() error { |
| | | 92 | | return s.fs.RemoveAll(s.root) |
| | | 93 | | } |