Release Notes v0.11.0¶
Released: 2026-10-31
Highlights¶
Open a file a caller named, inside a directory, with the open itself confined.
fs.OpenInRoot checks the name as ResolveInRoot does and then opens it through
os.Root, so a symbolic link that leads out of the directory is refused at the
moment of the open. There is no gap between the check and the open for a link to
be swapped into, and the returned handle stays bound to the file it opened.
fs.OpenRegularInRoot and fs.CapReader add the rest of a safe read: the
handle is checked as a regular file within a size limit, and the read is capped
so a file that grows after the check cannot exceed it.
New Features¶
fs.OpenInRoot¶
root, err := fs.RealDir(uploadRoot) // once, at start-up
if err != nil {
return err
}
f, err := fs.OpenInRoot(root, req.Path)
if errors.Is(err, fs.ErrOutsideRoot) {
return fmt.Errorf("upload path: %w", err)
}
if err != nil {
return err // a missing file satisfies errors.Is(err, os.ErrNotExist)
}
defer f.Close()
The same names are refused as by ResolveInRoot, with an error wrapping
fs.ErrOutsideRoot, on every platform: an empty name, a NUL, a .. element
with either separator, and a rooted or drive-qualified name that is not inside
the root. On top of that, a symbolic link in any element of the name that leads
out of the root is refused by the open.
Where it differs from ResolveInRoot:
- A link whose target is relative and stays inside the root is followed. A link
whose target is an absolute path is refused, even when it points back into the
root, because
os.Rootdoes not follow absolute links. - The file must exist. A missing file satisfies
errors.Is(err, os.ErrNotExist)and does not wrapErrOutsideRoot. - An absolute name is accepted only if it is spelled inside the root, with the
root written the same way. Pass the result of
RealDiras the root.
Use OpenInRoot when you will read the file, and ResolveInRoot when you need
a path: to create a file that does not exist yet, or to hand to code that opens
paths itself.
fs.OpenRegularInRoot and fs.CapReader¶
f, err := fs.OpenRegularInRoot(root, req.Path, maxBytes)
if err != nil {
// fs.ErrOutsideRoot, os.ErrNotExist, fs.ErrNotRegular or fs.ErrTooLarge,
// each testable with errors.Is
return fmt.Errorf("upload path: %w", err)
}
defer f.Close()
body := fs.CapReader(f, maxBytes) // a file that grows after the check
data, err := io.ReadAll(body) // ends with fs.ErrTooLarge
OpenRegularInRoot opens as OpenInRoot does and returns the file only if the
handle it holds is a regular file of at most maxBytes. The check is made on the
open handle, so it describes the file you will read, and the file is closed for
you when it fails.
CapReader is io.LimitReader that reports fs.ErrTooLarge when the stream is
longer than the limit, instead of stopping silently, so an oversized file cannot
pass as a complete, shorter one. Bytes within the limit are delivered before the
error. Hand the capped reader to anything that takes an io.Reader, such as an
upload. A negative limit is treated as zero.
Documentation¶
- The fs guide explains
when to use
OpenInRootinstead ofResolveInRoot, with a complete example. - The README, the getting-started table and the portable-tests guide list the new functions.
Installation¶
Compatibility¶
Additive only. An API comparison against v0.10.0 reports compatible changes
only: the new functions and errors, and no incompatible changes. The module still has no
dependencies outside the standard library; os.Root needs Go 1.24, and the
module already requires Go 1.25.