/** * Every file operation the standard library performs on a path an Agency * program chose goes through this module. The rule it enforces: under an * approval that names directory D, no byte is read from or written to any * path outside D. A symlink in the caller's own spelling of D (a linked * /tmp, a linked home) resolves normally, once, in `root`. A symlink at * any component below D is refused, because the approver never named * where it points. * * How strong that rule is depends on the operation. Reads and writes, * `readStream`, and `copy`, which is built on them, move bytes only through a * descriptor that is validated after it is open, so a directory swapped * to a link while the operation runs cannot redirect them. `list`, * `stat`, `mkdir`, `remove`, and `move` act on a pathname that was * checked a moment earlier. Node has no openat, so a swap between that * check and the action is not closed here. Process containment is the * answer for that window. See docs/dev/stdlib/contained-files.md. */ import fs from "fs"; import type { Stats } from "fs"; export type { Root } from "../roots.js"; import type { Root } from "../roots.js"; /** A whole path the approval named, as its real parent plus a final name * that is never followed. */ export type Located = { root: Root; target: string; }; export declare function root(dir: string): Root; /** Join `target` under the root and validate it: no absolute path, no `~`, * no upward escape, and no symlink at any component strictly below the * root. Returns the absolute path to operate on. "" and "." mean the root * itself. A component that does not exist yet is accepted lexically. */ export declare function resolveUnder(root: Root, target: string): string; /** The root an approval already named, spelled the way the approver saw * it. Where `root` resolves a caller's spelling before the interrupt, * this runs after it and follows nothing: every existing component must * be a real directory, and a symlink anywhere in the spelling is refused, * because a link planted at the approved path while the prompt was * pending would otherwise become the new root. Components that do not * exist yet are kept as written. */ export declare function fixedRoot(real: string): Root; /** The whole-path twin of `fixedRoot`: the real parent the approver saw, * checked to still be spelled without links, plus the final name. */ export declare function fixedPath(p: string): Located; /** Split a whole path into its real parent and final name. `mkdir`, * `remove`, `copy`, `move`, and output files use this: the interrupt * named the whole path, the parent is the root, and the final name is * one component that is never followed. */ export declare function wholePath(p: string): Located; /** The `dir` and `filename` an interrupt payload shows for a single-file * operation such as `read` or `write`: `dir` is the real directory and * `filename` the normalized relative path, so the payload and the * operation see one spelling. An escape, a missing directory, or a symlink * below dir throws before any interrupt exists. Synchronous throughout: it * runs between a wrapper's call and its interrupt, and a real wait there * would hand the event loop to a concurrent branch. */ export declare function locateSync(dir: string, filename: string, operation: "read" | "write"): { dir: string; filename: string; }; /** The real spelling of a directory, for an interrupt payload. */ export declare function _realDir(dir: string): string; /** The real spelling of a whole path, for an interrupt payload. */ export declare function _realTarget(p: string): string; export declare const WRITE_MODES: readonly ["overwrite", "append", "create-only"]; export type WriteMode = (typeof WRITE_MODES)[number]; /** Test-only hook that runs between the open and the validation of a * descriptor, where a concurrent swap would land. */ export type Seams = { afterOpen?: () => void; }; export type WriteOptions = { mode?: WriteMode; fileMode?: number; seams?: Seams; }; export declare function readText(root: Root, target: string, seams?: Seams): string; /** Read through a validated descriptor: open without following a final * link, require a regular file, realpath after the open and require it * inside the root, and require the same (dev, ino) as the descriptor so a * swap undone after the open is caught too. */ export declare function readBytes(root: Root, target: string, seams?: Seams): Buffer; /** A read stream over a validated descriptor, for a file too large to * buffer, such as a downloaded model being hashed. The stream owns the * descriptor and closes it when it ends. */ export declare function readStream(root: Root, target: string, seams?: Seams): fs.ReadStream; export declare function writeText(root: Root, target: string, content: string, options?: WriteOptions): void; export declare function writeBytes(root: Root, target: string, data: Buffer, options?: WriteOptions): void; export type AppendableFile = { /** Write all of `data` at the end of the file, looping over a short * write. The descriptor was opened for appending, so two handles on * one file never write over each other. */ append(data: Uint8Array): void; close(): void; }; /** Open a file for appends and keep it open, for a writer that adds a * line at a time, such as the trace writer. The open is the same as * `append` mode in `writeBytes`: no following of a final link, a real * parent for a new file, and the descriptor validated before it is * handed back. */ export declare function openForAppend(root: Root, target: string, options?: WriteOptions): AppendableFile; /** A file held open for writes at any offset, for a download whose * chunks land out of order. The descriptor stays inside; call `close` * when the file is done. */ export type WritableFile = { /** Write all of `data` at `position`, looping over a short write. */ writeAt(data: Uint8Array, position: number): void; truncate(size: number): void; close(): void; }; /** Open an existing regular file for read-write, or create it, through * the same validated descriptor as every other write. */ export declare function openForWrite(root: Root, target: string, options?: WriteOptions): WritableFile; export type Entry = { name: string; type: "file" | "dir" | "other"; size: number; }; /** One level of a directory. Symlinked entries are left out: they are * never followed, so there is nothing to say about them. */ export declare function list(root: Root, target: string): Entry[]; /** lstat of a target under the root. Missing returns null. A symlink below * the root also returns null: it is hidden, the way `list` hides it. The * root itself is already real, so "." reports the directory. */ export declare function stat(root: Root, target: string): Stats | null; export declare function mkdir(root: Root, target: string): void; export declare function remove(root: Root, target: string): void; /** Copy a file or tree. Every file is read and written through a * validated descriptor, and a symlink anywhere in the source tree is * refused, so a copy can neither pull in bytes from outside the source * nor plant a link at the destination. An existing destination file is * replaced, as with cp. */ export declare function copy(from: Located, to: Located): void; /** Rename within one filesystem, or copy and remove across two. */ export declare function move(from: Located, to: Located): void; /** Every operation this module performs. The symlink battery runs each * one; adding an operation without a row here fails the registry test. */ export declare const PRIMITIVES: readonly ["readText", "readBytes", "readStream", "writeText", "writeBytes", "openForWrite", "openForAppend", "list", "stat", "mkdir", "remove", "copy", "move"]; /** Exports that are not operations: they resolve, they do not touch bytes. */ export declare const HELPERS: readonly ["root", "fixedRoot", "resolveUnder", "wholePath", "fixedPath", "locateSync", "_realDir", "_realTarget"];