package fileops import ( "fmt" "io" "os" "path/filepath" ) // ValidateConfinedDirectory opens path without following symbolic links and // applies validate to the opened directory's metadata. func ValidateConfinedDirectory(path string, validate func(os.FileInfo) error) error { root, err := openConfinedDirectory(path) if err != nil { return err } defer func() { _ = root.Close() }() info, err := root.Stat(".") if err != nil { return fmt.Errorf("inspect root directory: %w", err) } if !info.IsDir() { return fmt.Errorf("root path is not a directory") } if validate != nil { return validate(info) } return nil } // ReadRegularFileUnderRoot reads a bounded regular file after opening root and // every relative-path ancestor without following symbolic links. validateRoot // and validateFile may enforce caller-owned access policy while their handles // are still verified. func ReadRegularFileUnderRoot( rootPath, relativePath string, maxBytes int64, validateRoot, validateFile func(os.FileInfo) error, ) ([]byte, error) { if maxBytes < 0 { return nil, fmt.Errorf("maximum byte count must not be negative") } if filepath.IsAbs(relativePath) { return nil, fmt.Errorf("relative file path must not be absolute") } parts, err := relativePathParts(relativePath) if err != nil { return nil, fmt.Errorf("invalid relative file path: %w", err) } root, err := openConfinedDirectory(rootPath) if err != nil { return nil, err } defer func() { _ = root.Close() }() rootInfo, err := root.Stat(".") if err != nil { return nil, fmt.Errorf("inspect root directory: %w", err) } if validateRoot != nil { if err := validateRoot(rootInfo); err != nil { return nil, err } } for _, part := range parts[:len(parts)-1] { child, err := openConfinedChild(root, part, false, 0) if err != nil { return nil, err } _ = root.Close() root = child } name := parts[len(parts)-1] inspected, err := root.Lstat(name) if err != nil { return nil, fmt.Errorf("inspect file %q: %w", name, err) } if inspected.Mode()&os.ModeSymlink != 0 || !inspected.Mode().IsRegular() { return nil, fmt.Errorf("file %q is not a regular file", name) } file, err := root.Open(name) if err != nil { return nil, fmt.Errorf("open file %q: %w", name, err) } defer func() { _ = file.Close() }() opened, err := file.Stat() if err != nil { return nil, fmt.Errorf("inspect opened file %q: %w", name, err) } current, err := root.Lstat(name) if err != nil || current.Mode()&os.ModeSymlink != 0 || !current.Mode().IsRegular() || !os.SameFile(opened, current) { if err != nil { return nil, fmt.Errorf("reinspect file %q: %w", name, err) } return nil, fmt.Errorf("file %q changed while being opened", name) } if validateFile != nil { if err := validateFile(opened); err != nil { return nil, err } } content, err := io.ReadAll(io.LimitReader(file, maxBytes+1)) if err != nil { return nil, fmt.Errorf("read file %q: %w", name, err) } if int64(len(content)) > maxBytes { return nil, fmt.Errorf("file %q exceeds %d-byte limit", name, maxBytes) } return content, nil } // ReadRegularFile reads a bounded regular file without following symbolic links // in its parent hierarchy. It is intended for externally produced results; // callers own the limit and semantic validation contract. func ReadRegularFile(path string, maxBytes int64) ([]byte, error) { clean := filepath.Clean(path) if clean == "." || filepath.Base(clean) == "." { return nil, fmt.Errorf("file path is required") } return ReadRegularFileUnderRoot(filepath.Dir(clean), filepath.Base(clean), maxBytes, nil, nil) }