package files import ( "context" "errors" "fmt" "os" "path/filepath" ) var ErrInstanceDataDirectoryUnavailable = errors.New("instance data directory is unavailable") // DirectoryPreparer verifies one explicitly supplied absolute directory. // It is used for program-owned locations such as the configured log folder. type DirectoryPreparer interface { PrepareDirectory(context.Context, string) error } // InstanceDataPreparer creates and verifies only the User Data Dir belonging // to the explicitly requested instance. It never discovers or changes other // browser data directories. type InstanceDataPreparer interface { PrepareInstanceDataDir(context.Context, string) error } type instanceDataPreparer struct { mkdirAll func(string, os.FileMode) error lstat func(string) (os.FileInfo, error) createTemp func(string, string) (*os.File, error) remove func(string) error } func NewInstanceDataPreparer() InstanceDataPreparer { return newInstanceDataPreparer(os.MkdirAll, os.Lstat, os.CreateTemp, os.Remove) } func NewDirectoryPreparer() DirectoryPreparer { return newInstanceDataPreparer(os.MkdirAll, os.Lstat, os.CreateTemp, os.Remove) } func newInstanceDataPreparer( mkdirAll func(string, os.FileMode) error, lstat func(string) (os.FileInfo, error), createTemp func(string, string) (*os.File, error), remove func(string) error, ) instanceDataPreparer { return instanceDataPreparer{mkdirAll: mkdirAll, lstat: lstat, createTemp: createTemp, remove: remove} } func (p instanceDataPreparer) PrepareInstanceDataDir(ctx context.Context, userDataDir string) error { return p.PrepareDirectory(ctx, userDataDir) } func (p instanceDataPreparer) PrepareDirectory(ctx context.Context, directory string) error { if err := ctx.Err(); err != nil { return err } path, err := normalizePreparatoryInstanceDataDir(directory) if err != nil { return err } if err := p.mkdirAll(path, 0o700); err != nil { return fmt.Errorf("%w: create", ErrInstanceDataDirectoryUnavailable) } if err := ctx.Err(); err != nil { return err } info, err := p.lstat(path) if err != nil { return fmt.Errorf("%w: inspect", ErrInstanceDataDirectoryUnavailable) } if info.Mode()&(os.ModeSymlink|os.ModeIrregular) != 0 || isReparsePoint(info) { return fmt.Errorf("%w: links and reparse points are not supported", ErrUnsafeInstanceDataDirectory) } if !info.IsDir() { return ErrInstanceDataNotDirectory } probe, err := p.createTemp(path, ".chub-write-*") if err != nil { return fmt.Errorf("%w: write", ErrInstanceDataDirectoryUnavailable) } probePath := probe.Name() if err := probe.Close(); err != nil { _ = p.remove(probePath) return fmt.Errorf("%w: close", ErrInstanceDataDirectoryUnavailable) } if err := p.remove(probePath); err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("%w: cleanup", ErrInstanceDataDirectoryUnavailable) } if err := ctx.Err(); err != nil { return err } return nil } // normalizePreparatoryInstanceDataDir is kept separate from deletion's // helper so callers can use a stable, intention-revealing validation entry. func normalizePreparatoryInstanceDataDir(value string) (string, error) { path, err := normalizeRemovableInstanceDataDir(value) if err != nil { return "", err } return filepath.Clean(path), nil }