@pgxsinkit/pgwasm
Version:
6,672 lines • 242 kB
JavaScript
// @bun
import {
createPgwasm2
} from "../chunk-gjf102c9.js";
import {
UnsupportedFeatureError2
} from "../chunk-cpsnmzrf.js";
import {
BaseFilesystem2
} from "../chunk-z0c40tja.js";
import {
registerStrictSync,
strictSyncOf
} from "../chunk-74gcrk47.js";
// packages/pgwasm/src/opfs/core/errors.ts
var FS_ERRNO = {
EBADF: 8,
EEXIST: 20,
EINVAL: 28,
EIO: 29,
EISDIR: 31,
ELOOP: 32,
ENOENT: 44,
ENOTDIR: 54,
ENOTEMPTY: 55,
EXDEV: 75
};
class FsError extends Error {
code;
operation;
path;
constructor(name, message, options = {}) {
super(message, { cause: options.cause });
this.name = "FsError";
this.code = FS_ERRNO[name];
this.operation = options.operation;
this.path = options.path;
}
}
class StoreLimitError extends Error {
storeCode = "STORE_LIMIT";
constructor(message, options) {
super(message, options);
this.name = "StoreLimitError";
}
}
class CorruptStoreError extends Error {
storeCode = "CORRUPT_STORE";
constructor(message, options) {
super(message, options);
this.name = "CorruptStoreError";
}
}
class StoreRecreationRequiredError extends Error {
storeCode = "STORE_RECREATION_REQUIRED";
constructor(message, options) {
super(message, options);
this.name = "StoreRecreationRequiredError";
}
}
class ExtentSizeMismatchError extends Error {
storeCode = "EXTENT_SIZE_MISMATCH";
constructor(expected, actual) {
super(`configured extent size ${expected} does not match persisted extent size ${actual}`);
this.name = "ExtentSizeMismatchError";
}
}
class StoreOwnedError extends Error {
storeCode = "STORE_OWNED";
constructor(options) {
super("the OPFS repacked store is already exclusively owned by another live instance", options);
this.name = "StoreOwnedError";
}
}
class UnexpectedStoreEntryError extends Error {
storeCode = "UNEXPECTED_STORE_ENTRY";
constructor(entryName) {
super(`the OPFS repacked store contains an unexpected entry: ${entryName}`);
this.name = "UnexpectedStoreEntryError";
}
}
class DurabilityModeMismatchError extends Error {
storeCode = "DURABILITY_MODE_MISMATCH";
constructor() {
super("the pgwasm host attempted a non-awaited OPFS repacked sync");
this.name = "DurabilityModeMismatchError";
}
}
class StoreClosedError extends Error {
storeCode = "STORE_CLOSED";
constructor() {
super("the OPFS repacked store is closed");
this.name = "StoreClosedError";
}
}
class StoreFailedError extends Error {
storeCode = "STORE_FAILED";
code = FS_ERRNO.EIO;
cause;
constructor(cause) {
super(`the OPFS repacked store is poisoned and must be reopened: ${String(cause)}`, { cause });
this.name = "StoreFailedError";
this.cause = cause;
}
}
// packages/pgwasm/src/opfs/opfs-port.ts
class OpfsRepackedPort {
#directory;
constructor(directory) {
this.#directory = directory;
}
async enumerate(_label) {
const entries = [];
for await (const entry of this.#directory.values()) {
entries.push({ name: entry.name, kind: entry.kind });
}
return entries;
}
async acquire(name, _label) {
const file = await this.#directory.getFileHandle(name, { create: true });
try {
return new OpfsRepackedFileHandle(name, await file.createSyncAccessHandle());
} catch (cause) {
if (isOwnershipFailure(cause))
throw new StoreOwnedError({ cause });
throw cause;
}
}
}
class OpfsRepackedFileHandle {
name;
#handle;
constructor(name, handle) {
this.name = name;
this.#handle = handle;
}
getSize(_label) {
return this.#handle.getSize();
}
read(target, at, _label) {
return this.#handle.read(target, { at });
}
write(source, at, _label) {
return this.#handle.write(source, { at });
}
truncate(size, _label) {
this.#handle.truncate(size);
}
flush(_label) {
this.#handle.flush();
}
close() {
this.#handle.close();
}
}
function isOwnershipFailure(cause) {
if (typeof cause !== "object" || cause === null || !("name" in cause))
return false;
return cause.name === "NoModificationAllowedError" || cause.name === "InvalidStateError";
}
// packages/pgwasm/src/opfs/core/checksum.ts
var CRC32_TABLE = new Uint32Array(256);
for (let value = 0;value < CRC32_TABLE.length; value += 1) {
let remainder = value;
for (let bit = 0;bit < 8; bit += 1) {
remainder = (remainder & 1) === 1 ? 3988292384 ^ remainder >>> 1 : remainder >>> 1;
}
CRC32_TABLE[value] = remainder >>> 0;
}
var CRC32_SLICES = [CRC32_TABLE];
for (let slice = 1;slice < 8; slice += 1) {
const previous = CRC32_SLICES[slice - 1];
const current = new Uint32Array(256);
for (let value = 0;value < current.length; value += 1) {
const remainder = previous[value];
current[value] = (CRC32_TABLE[remainder & 255] ^ remainder >>> 8) >>> 0;
}
CRC32_SLICES.push(current);
}
var CRC32_SLICE_1 = CRC32_SLICES[1];
var CRC32_SLICE_2 = CRC32_SLICES[2];
var CRC32_SLICE_3 = CRC32_SLICES[3];
var CRC32_SLICE_4 = CRC32_SLICES[4];
var CRC32_SLICE_5 = CRC32_SLICES[5];
var CRC32_SLICE_6 = CRC32_SLICES[6];
var CRC32_SLICE_7 = CRC32_SLICES[7];
function updateCrc32(remainder, bytes, start, end) {
let index = start;
while (index + 8 <= end) {
const word = remainder ^ (bytes[index] | bytes[index + 1] << 8 | bytes[index + 2] << 16 | bytes[index + 3] << 24);
remainder = CRC32_SLICE_7[word & 255] ^ CRC32_SLICE_6[word >>> 8 & 255] ^ CRC32_SLICE_5[word >>> 16 & 255] ^ CRC32_SLICE_4[word >>> 24] ^ CRC32_SLICE_3[bytes[index + 4]] ^ CRC32_SLICE_2[bytes[index + 5]] ^ CRC32_SLICE_1[bytes[index + 6]] ^ CRC32_TABLE[bytes[index + 7]];
index += 8;
}
for (;index < end; index += 1) {
remainder = CRC32_TABLE[(remainder ^ bytes[index]) & 255] ^ remainder >>> 8;
}
return remainder;
}
function updateCrc32WithZeros(remainder, length) {
while (length >= 8) {
remainder = CRC32_SLICE_7[remainder & 255] ^ CRC32_SLICE_6[remainder >>> 8 & 255] ^ CRC32_SLICE_5[remainder >>> 16 & 255] ^ CRC32_SLICE_4[remainder >>> 24];
length -= 8;
}
while (length > 0) {
remainder = CRC32_TABLE[remainder & 255] ^ remainder >>> 8;
length -= 1;
}
return remainder;
}
function crc32(bytes) {
const remainder = updateCrc32(4294967295, bytes, 0, bytes.byteLength);
return (remainder ^ 4294967295) >>> 0;
}
function crc32WithZeroedRange(bytes, offset, length) {
if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || offset < 0 || length < 0) {
throw new TypeError("checksum range must use non-negative safe integers");
}
if (offset + length > bytes.byteLength) {
throw new RangeError("checksum range exceeds the input");
}
const zeroEnd = offset + length;
let remainder = updateCrc32(4294967295, bytes, 0, offset);
remainder = updateCrc32WithZeros(remainder, length);
remainder = updateCrc32(remainder, bytes, zeroEnd, bytes.byteLength);
return (remainder ^ 4294967295) >>> 0;
}
// packages/pgwasm/src/opfs/core/limits.ts
var FORMAT_VERSION = 2;
var LIMITS_PROFILE_VERSION = 1;
var ARENA_HEADER_BYTES = 8192;
var MIN_EXTENT_BYTES = 8192;
var MAX_EXTENT_BYTES = 16 * 1024 * 1024;
var DEFAULT_EXTENT_BYTES = 64 * 1024;
var MAX_COMPONENT_BYTES = 255;
var MAX_PATH_BYTES = 1024;
var MAX_PATH_DEPTH = 32;
var MAX_SYMLINK_HOPS = 32;
var MAX_INODES = 65536;
var MAX_EXTENTS_PER_INODE = 2 ** 20;
var MAX_TOTAL_EXTENTS = 2 ** 22;
var MAX_METADATA_BASE_READER_BYTES = 64 * 1024 * 1024;
var MAX_METADATA_BASE_WRITER_BYTES = 32 * 1024 * 1024;
var MAX_FRAME_PAYLOAD_BYTES = 1024 * 1024;
var MAX_ACTIVE_LOG_BYTES = 16 * 1024 * 1024;
var SOFT_ACTIVE_LOG_BYTES = 8 * 1024 * 1024;
var MAX_ACTIVE_LOG_FRAMES = 32768;
var SOFT_ACTIVE_LOG_FRAMES = 16384;
var MAX_U64 = (1n << 64n) - 1n;
function validateExtentSize(value) {
if (!Number.isSafeInteger(value) || value < MIN_EXTENT_BYTES || value > MAX_EXTENT_BYTES || value % ARENA_HEADER_BYTES !== 0) {
throw new TypeError(`extentSize must be an ${ARENA_HEADER_BYTES}-byte multiple between ${MIN_EXTENT_BYTES} and ${MAX_EXTENT_BYTES}`);
}
return value;
}
function checkedU64(value, label) {
if (value < 0n || value > MAX_U64) {
throw new StoreLimitError(`${label} is outside the unsigned 64-bit range`);
}
return value;
}
function checkedSafeNumber(value, label) {
checkedU64(value, label);
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
throw new StoreLimitError(`${label} exceeds JavaScript's safe integer range`);
}
return Number(value);
}
function checkedAdd(left, right, label) {
return checkedU64(left + right, label);
}
function checkedMultiply(left, right, label) {
return checkedU64(left * right, label);
}
// packages/pgwasm/src/opfs/core/path.ts
function utf8Length(value, label) {
let bytes = 0;
for (let index = 0;index < value.length; index += 1) {
const codeUnit = value.charCodeAt(index);
if (codeUnit <= 127) {
bytes += 1;
} else if (codeUnit <= 2047) {
bytes += 2;
} else if (codeUnit >= 55296 && codeUnit <= 56319) {
const next = value.charCodeAt(index + 1);
if (index + 1 >= value.length || next < 56320 || next > 57343) {
throw new FsError("EINVAL", `${label} is not canonically representable as UTF-8`);
}
bytes += 4;
index += 1;
} else if (codeUnit >= 56320 && codeUnit <= 57343) {
throw new FsError("EINVAL", `${label} is not canonically representable as UTF-8`);
} else {
bytes += 3;
}
}
return bytes;
}
function encodedUtf8Length(value) {
return utf8Length(value, "string");
}
function validatePathComponent(component) {
if (component.length === 0 || component === "." || component === "..") {
throw new FsError("EINVAL", "path contains an empty or reserved component");
}
if (component.includes("/") || component.includes("\x00")) {
throw new FsError("EINVAL", "path component contains a forbidden character");
}
const bytes = utf8Length(component, "path component");
if (bytes > MAX_COMPONENT_BYTES) {
throw new FsError("EINVAL", `path component exceeds ${MAX_COMPONENT_BYTES} UTF-8 bytes`);
}
return bytes;
}
function validateSymlinkTarget(target) {
const bytes = utf8Length(target, "symlink target");
if (bytes === 0 || bytes > MAX_PATH_BYTES) {
throw new FsError("EINVAL", `symlink target must be 1 to ${MAX_PATH_BYTES} UTF-8 bytes`);
}
parsePath(target);
return bytes;
}
function parsePath(path) {
if (path === "/") {
return [];
}
if (!path.startsWith("/") || path.endsWith("/") || path.includes("//")) {
throw new FsError("EINVAL", "path must be absolute and canonical", { path });
}
if (utf8Length(path, "path") > MAX_PATH_BYTES) {
throw new FsError("EINVAL", `path exceeds ${MAX_PATH_BYTES} UTF-8 bytes`, { path });
}
const components = path.slice(1).split("/");
if (components.length > MAX_PATH_DEPTH) {
throw new FsError("EINVAL", `path exceeds ${MAX_PATH_DEPTH} components`, { path });
}
for (const component of components) {
validatePathComponent(component);
}
return components;
}
// packages/pgwasm/src/opfs/core/state-machine.ts
function otherMetadataFile(file) {
return file === "a" ? "b" : "a";
}
function makeOrphanRecord(inode) {
return {
inodeId: inode.id,
mode: inode.mode,
atimeMs: inode.atimeMs,
mtimeMs: inode.mtimeMs,
ctimeMs: inode.ctimeMs,
size: inode.size,
extents: [...inode.extents]
};
}
class IndexedExtentSet {
#items = [];
#indexes = new Map;
get size() {
return this.#items.length;
}
has(extentId) {
return this.#indexes.has(extentId);
}
add(extentId) {
if (this.#indexes.has(extentId)) {
return;
}
this.#indexes.set(extentId, this.#items.length);
this.#items.push(extentId);
}
delete(extentId) {
const index = this.#indexes.get(extentId);
if (index === undefined) {
return false;
}
const last = this.#items.pop();
this.#indexes.delete(extentId);
if (last !== undefined && index < this.#items.length) {
this.#items[index] = last;
this.#indexes.set(last, index);
}
return true;
}
values() {
return this.#items;
}
peekLast(count) {
if (!Number.isSafeInteger(count) || count < 0 || count > this.#items.length) {
throw new StoreLimitError("invalid available-extent selection count");
}
return this.#items.slice(this.#items.length - count).reverse();
}
clone() {
const copy = new IndexedExtentSet;
for (const extentId of this.#items) {
copy.add(extentId);
}
return copy;
}
}
var BASE_STATE_FIXED_BYTES = 20;
var DIRECTORY_INODE_FIXED_BYTES = 41;
var FILE_INODE_FIXED_BYTES = 49;
var SYMLINK_INODE_FIXED_BYTES = 39;
var CHILD_FIXED_BYTES = 10;
var SYMLINK_MODE = 41471;
function childBaseBytes(name) {
return CHILD_FIXED_BYTES + encodedUtf8Length(name);
}
function inodeBaseBytes(inode) {
if (inode.kind === "file") {
return FILE_INODE_FIXED_BYTES + inode.extents.length * 8;
}
if (inode.kind === "symlink") {
return SYMLINK_INODE_FIXED_BYTES + encodedUtf8Length(inode.target);
}
let bytes = DIRECTORY_INODE_FIXED_BYTES;
for (const name of inode.children.keys()) {
bytes += childBaseBytes(name);
}
return bytes;
}
function extentRunsPayloadBytes(runs) {
return 4 + runs.length * 12;
}
function estimateTxnPayloadBytes(record) {
switch (record.kind) {
case "createDirectories":
return 5 + record.entries.reduce((bytes, entry) => bytes + 8 + 2 + encodedUtf8Length(entry.name) + 8 + 4 + 24, 0);
case "createFile":
return 1 + 8 + 2 + encodedUtf8Length(record.name) + 8 + 4 + 24 + 8 + extentRunsPayloadBytes(record.extents);
case "removeFile":
case "removeDirectory":
return 1 + 8 + 2 + encodedUtf8Length(record.name) + 16;
case "changeMode":
return 1 + 8 + 4 + 8;
case "changeTimes":
return 1 + 8 + 24;
case "resizeFile":
return 1 + 1 + 8 + 8 + extentRunsPayloadBytes(record.allocated) + 16;
case "rename":
return 1 + 8 + 2 + encodedUtf8Length(record.sourceName) + 8 + 2 + encodedUtf8Length(record.destinationName) + 8;
case "createSymlink":
return 1 + 8 + 2 + encodedUtf8Length(record.name) + 8 + 4 + 24 + 2 + encodedUtf8Length(record.target);
case "reserveQuarantine":
return 1 + extentRunsPayloadBytes(record.extents);
}
}
function selectedExtentsAlreadyAvailable(state, runs) {
let count = 0;
for (const extentId of expandExtentRuns(runs, MAX_EXTENTS_PER_INODE)) {
if (extentId < state.allocator.totalExtents) {
count += 1;
}
}
return count;
}
function projectedBaseDelta(state, record) {
switch (record.kind) {
case "createDirectories":
return record.entries.reduce((bytes, entry) => bytes + DIRECTORY_INODE_FIXED_BYTES + childBaseBytes(entry.name), 0);
case "createFile": {
const extentCount = expandExtentRuns(record.extents, MAX_EXTENTS_PER_INODE).length;
const reused = selectedExtentsAlreadyAvailable(state, record.extents);
return FILE_INODE_FIXED_BYTES + extentCount * 8 + childBaseBytes(record.name) - reused * 8;
}
case "removeFile": {
const parent = directoryById(state, record.parentId);
const inodeId = parent.children.get(record.name);
const inode = inodeId === undefined ? undefined : state.inodes.get(inodeId);
if (inode === undefined || inode.kind === "directory") {
throw new FsError("ENOENT", "file does not exist");
}
const quarantined = inode.kind === "file" ? inode.extents.length * 16 : 0;
return -inodeBaseBytes(inode) - childBaseBytes(record.name) + quarantined;
}
case "changeMode":
case "changeTimes":
return 0;
case "resizeFile": {
const inode = fileById(state, record.inodeId);
const required = extentCountForSize(record.size, state.extentSize);
const allocatedCount = Math.max(0, required - inode.extents.length);
const releasedCount = Math.max(0, inode.extents.length - required);
const reused = selectedExtentsAlreadyAvailable(state, record.allocated);
return (allocatedCount - releasedCount) * 8 - reused * 8 + releasedCount * 16;
}
case "removeDirectory": {
const parent = directoryById(state, record.parentId);
const inodeId = parent.children.get(record.name);
const inode = inodeId === undefined ? undefined : state.inodes.get(inodeId);
if (inode?.kind !== "directory") {
throw new FsError("ENOENT", "directory does not exist");
}
return -inodeBaseBytes(inode) - childBaseBytes(record.name);
}
case "rename": {
const sourceParent = directoryById(state, record.sourceParentId);
const destinationParent = directoryById(state, record.destinationParentId);
const destinationId = destinationParent.children.get(record.destinationName);
const destination = destinationId === undefined ? undefined : state.inodes.get(destinationId);
let delta = -childBaseBytes(record.sourceName);
if (destination === undefined) {
delta += childBaseBytes(record.destinationName);
} else {
delta -= inodeBaseBytes(destination);
if (destination.kind === "file") {
delta += destination.extents.length * 16;
}
}
const sourceId = sourceParent.children.get(record.sourceName);
if (sourceId === undefined) {
throw new FsError("ENOENT", "rename source does not exist");
}
return delta;
}
case "createSymlink":
return SYMLINK_INODE_FIXED_BYTES + encodedUtf8Length(record.target) + childBaseBytes(record.name);
case "reserveQuarantine": {
const count = expandExtentRuns(record.extents, MAX_EXTENTS_PER_INODE).length;
const reused = selectedExtentsAlreadyAvailable(state, record.extents);
return count * 16 - reused * 8;
}
}
}
function preflightTxn(state, record, limits = {
maxFramePayloadBytes: MAX_FRAME_PAYLOAD_BYTES,
maxBasePayloadBytes: MAX_METADATA_BASE_WRITER_BYTES
}) {
const payloadBytes = estimateTxnPayloadBytes(record);
if (payloadBytes > limits.maxFramePayloadBytes) {
throw new StoreLimitError(`transaction payload would exceed ${limits.maxFramePayloadBytes} bytes`);
}
const projected = state.basePayloadBytes + projectedBaseDelta(state, record);
if (!Number.isSafeInteger(projected) || projected < 0 || projected > limits.maxBasePayloadBytes) {
throw new StoreLimitError(`metadata base would exceed ${limits.maxBasePayloadBytes} bytes`);
}
return projected;
}
var PREPARED_TXN_PROJECTION = Symbol("prepared transaction projection");
function prepareTxnProjection(state, record) {
return {
projectedBasePayloadBytes: preflightTxn(state, record),
state,
record,
[PREPARED_TXN_PROJECTION]: true
};
}
function checkedPlan(state, record) {
preflightTxn(state, record);
return { record };
}
function createInitialState(extentSize) {
validateExtentSize(extentSize);
const root = {
id: 1n,
kind: "directory",
mode: 16895,
atimeMs: 0n,
mtimeMs: 0n,
ctimeMs: 0n,
children: new Map
};
return {
generation: 1n,
nextInodeId: 2n,
rootInodeId: root.id,
extentSize,
activeMetadataFile: "a",
retainedGenerations: { a: 1n, b: null },
basePayloadBytes: BASE_STATE_FIXED_BYTES + DIRECTORY_INODE_FIXED_BYTES,
inodes: new Map([[root.id, root]]),
parentByInode: new Map,
allocator: {
totalExtents: 0n,
ownedBy: new Map,
available: new IndexedExtentSet,
quarantine: new Map
}
};
}
function validateMode(mode) {
if (!Number.isSafeInteger(mode) || mode < 0 || mode > 4294967295) {
throw new FsError("EINVAL", "mode must be an unsigned 32-bit integer");
}
return mode;
}
function validateTimestamp(timestamp) {
return checkedU64(timestamp, "timestamp");
}
function advanceNextInodeId(inodeId) {
if (inodeId === MAX_U64) {
throw new StoreLimitError("next inode ID is exhausted; the store must be recreated");
}
return checkedAdd(inodeId, 1n, "next inode ID");
}
function directoryById(state, inodeId) {
const inode = state.inodes.get(inodeId);
if (inode === undefined) {
throw new FsError("ENOENT", "directory does not exist");
}
if (inode.kind !== "directory") {
throw new FsError("ENOTDIR", "path component is not a directory");
}
return inode;
}
function inodeById(state, inodeId) {
const inode = state.inodes.get(inodeId);
if (inode === undefined) {
throw new FsError("ENOENT", "inode does not exist");
}
return inode;
}
function fileById(state, inodeId) {
const inode = inodeById(state, inodeId);
if (inode.kind !== "file") {
throw new FsError("EISDIR", "inode is not a file");
}
return inode;
}
function joinPath(base, rest) {
if (rest.length === 0)
return base;
return base === "/" ? `/${rest.join("/")}` : `${base}/${rest.join("/")}`;
}
function parentAndName(state, path) {
const components = parsePath(path);
const name = components.pop();
if (name === undefined) {
throw new FsError("EINVAL", "operation is not permitted on root", { path });
}
let parent = directoryById(state, state.rootInodeId);
for (const component of components) {
const childId = parent.children.get(component);
if (childId === undefined) {
throw new FsError("ENOENT", "parent directory does not exist", { path });
}
parent = directoryById(state, childId);
}
return { parent, name };
}
function extentCountForSize(size, extentSize) {
checkedU64(size, "file size");
if (size === 0n) {
return 0;
}
const count = (size + BigInt(extentSize) - 1n) / BigInt(extentSize);
if (count > BigInt(MAX_EXTENTS_PER_INODE)) {
throw new StoreLimitError(`file would exceed ${MAX_EXTENTS_PER_INODE} extents`);
}
return Number(count);
}
function toExtentRuns(extents) {
const runs = [];
for (const extentId of extents) {
checkedU64(extentId, "extent ID");
const last = runs.at(-1);
if (last !== undefined && last.start + BigInt(last.count) === extentId && last.count < 4294967295) {
last.count += 1;
} else {
runs.push({ start: extentId, count: 1 });
}
}
return runs;
}
function expandExtentRuns(runs, maximum) {
const extents = [];
for (const run of runs) {
checkedU64(run.start, "extent run start");
if (!Number.isSafeInteger(run.count) || run.count <= 0) {
throw new FsError("EINVAL", "extent run count must be a positive safe integer");
}
if (extents.length + run.count > maximum) {
throw new StoreLimitError(`extent list exceeds ${maximum} entries`);
}
for (let offset = 0;offset < run.count; offset += 1) {
extents.push(checkedAdd(run.start, BigInt(offset), "extent ID"));
}
}
return extents;
}
function chooseExtents(state, count) {
if (count > MAX_EXTENTS_PER_INODE) {
throw new StoreLimitError(`allocation exceeds ${MAX_EXTENTS_PER_INODE} extents`);
}
const availableCount = Math.min(count, state.allocator.available.size);
const chosen = state.allocator.available.peekLast(availableCount);
const freshCount = count - availableCount;
if (state.allocator.totalExtents + BigInt(freshCount) > BigInt(MAX_TOTAL_EXTENTS)) {
throw new StoreLimitError(`total extent count would exceed ${MAX_TOTAL_EXTENTS}`);
}
for (let offset = 0;offset < freshCount; offset += 1) {
chosen.push(state.allocator.totalExtents + BigInt(offset));
}
return chosen;
}
function getInodeAtPath(state, path) {
const components = parsePath(path);
let inode = state.inodes.get(state.rootInodeId);
if (inode === undefined) {
throw new StoreLimitError("root inode is missing");
}
for (const component of components) {
if (inode.kind !== "directory") {
throw new FsError("ENOTDIR", "path component is not a directory", { path });
}
const childId = inode.children.get(component);
if (childId === undefined) {
throw new FsError("ENOENT", "path does not exist", { path });
}
const child = state.inodes.get(childId);
if (child === undefined) {
throw new StoreLimitError("directory entry references a missing inode");
}
inode = child;
}
return inode;
}
function resolveLinks(state, path, follow) {
const root = state.inodes.get(state.rootInodeId);
if (root === undefined) {
throw new StoreLimitError("root inode is missing");
}
let pending = parsePath(path);
let index = 0;
let current = root;
const resolved = [];
let hops = 0;
while (index < pending.length) {
const name = pending[index];
if (current.kind !== "directory")
break;
const childId = current.children.get(name);
if (childId === undefined)
break;
const child = state.inodes.get(childId);
if (child === undefined) {
throw new StoreLimitError("directory entry references a missing inode");
}
if (child.kind === "symlink" && (follow || index < pending.length - 1)) {
hops += 1;
if (hops > MAX_SYMLINK_HOPS) {
throw new FsError("ELOOP", `path traverses more than ${MAX_SYMLINK_HOPS} symbolic links`, { path });
}
pending = parsePath(joinPath(child.target, pending.slice(index + 1)));
index = 0;
current = root;
resolved.length = 0;
continue;
}
resolved.push(name);
current = child;
index += 1;
}
return joinPath(`/${resolved.join("/")}`, pending.slice(index));
}
function planMkdir(state, path, options) {
const components = parsePath(path);
if (components.length === 0) {
throw new FsError("EEXIST", "root directory already exists", { operation: "mkdir", path });
}
const mode = validateMode(options.mode ?? 16895);
const nowMs = validateTimestamp(options.nowMs);
const entries = [];
let parent = directoryById(state, state.rootInodeId);
let nextInodeId = state.nextInodeId;
for (let index = 0;index < components.length; index += 1) {
const name = components[index];
const existingId = parent.children.get(name);
if (existingId !== undefined) {
const existing = state.inodes.get(existingId);
if (existing?.kind !== "directory") {
throw new FsError("ENOTDIR", "path component is not a directory", { operation: "mkdir", path });
}
if (index === components.length - 1) {
throw new FsError("EEXIST", "path already exists", { operation: "mkdir", path });
}
parent = existing;
continue;
}
if (!options.recursive && index !== components.length - 1) {
throw new FsError("ENOENT", "parent directory does not exist", { operation: "mkdir", path });
}
if (state.inodes.size + entries.length >= MAX_INODES) {
throw new StoreLimitError(`inode count would exceed ${MAX_INODES}`);
}
checkedU64(nextInodeId, "next inode ID");
const followingInodeId = advanceNextInodeId(nextInodeId);
const entry = {
parentId: parent.id,
name,
inodeId: nextInodeId,
mode,
atimeMs: nowMs,
mtimeMs: nowMs,
ctimeMs: nowMs
};
entries.push(entry);
parent = { ...entry, kind: "directory", id: entry.inodeId, children: new Map };
nextInodeId = followingInodeId;
}
if (entries.length === 0) {
throw new FsError("EEXIST", "path already exists", { operation: "mkdir", path });
}
return checkedPlan(state, { kind: "createDirectories", entries });
}
function planCreateFile(state, path, options) {
const { parent, name } = parentAndName(state, path);
if (parent.children.has(name)) {
throw new FsError("EEXIST", "path already exists", { operation: "writeFile", path });
}
if (state.inodes.size >= MAX_INODES) {
throw new StoreLimitError(`inode count would exceed ${MAX_INODES}`);
}
const inodeId = checkedU64(state.nextInodeId, "next inode ID");
advanceNextInodeId(inodeId);
const mode = validateMode(options.mode ?? 33206);
const nowMs = validateTimestamp(options.nowMs);
const size = checkedU64(options.size ?? 0n, "file size");
const extents = toExtentRuns(chooseExtents(state, extentCountForSize(size, state.extentSize)));
return checkedPlan(state, {
kind: "createFile",
parentId: parent.id,
name,
inodeId,
mode,
atimeMs: nowMs,
mtimeMs: nowMs,
ctimeMs: nowMs,
size,
extents
});
}
function planSymlink(state, path, target, options) {
const { parent, name } = parentAndName(state, path);
if (parent.children.has(name)) {
throw new FsError("EEXIST", "path already exists", { operation: "symlink", path });
}
if (state.inodes.size >= MAX_INODES) {
throw new StoreLimitError(`inode count would exceed ${MAX_INODES}`);
}
validateSymlinkTarget(target);
const inodeId = checkedU64(state.nextInodeId, "next inode ID");
advanceNextInodeId(inodeId);
const nowMs = validateTimestamp(options.nowMs);
return checkedPlan(state, {
kind: "createSymlink",
parentId: parent.id,
name,
inodeId,
mode: validateMode(options.mode ?? SYMLINK_MODE),
atimeMs: nowMs,
mtimeMs: nowMs,
ctimeMs: nowMs,
target
});
}
function planUnlink(state, path, nowMs) {
const { parent, name } = parentAndName(state, path);
const inodeId = parent.children.get(name);
const inode = inodeId === undefined ? undefined : state.inodes.get(inodeId);
if (inode === undefined) {
throw new FsError("ENOENT", "path does not exist", { operation: "unlink", path });
}
if (inode.kind === "directory") {
throw new FsError("EISDIR", "path is a directory", { operation: "unlink", path });
}
const timestamp = validateTimestamp(nowMs);
return checkedPlan(state, {
kind: "removeFile",
parentId: parent.id,
name,
parentMtimeMs: timestamp,
parentCtimeMs: timestamp
});
}
function planChmod(state, path, mode, nowMs) {
const inode = getInodeAtPath(state, path);
return checkedPlan(state, {
kind: "changeMode",
inodeId: inode.id,
mode: validateMode(mode),
ctimeMs: validateTimestamp(nowMs)
});
}
function planUtimes(state, path, atimeMs, mtimeMs, ctimeMs) {
const inode = getInodeAtPath(state, path);
return checkedPlan(state, {
kind: "changeTimes",
inodeId: inode.id,
atimeMs: validateTimestamp(atimeMs),
mtimeMs: validateTimestamp(mtimeMs),
ctimeMs: validateTimestamp(ctimeMs)
});
}
function planResizeFile(state, path, size, nowMs, operation) {
const inode = getInodeAtPath(state, path);
if (inode.kind !== "file") {
throw new FsError("EISDIR", "path is a directory", { operation, path });
}
return planResizeFileForInode(state, inode, size, nowMs, operation);
}
function planResizeFileForInode(state, inode, size, nowMs, operation) {
if (state.inodes.get(inode.id) !== inode) {
throw new StoreLimitError("resize planner requires the current file inode");
}
const checkedSize = checkedU64(size, "file size");
const required = extentCountForSize(checkedSize, state.extentSize);
const additional = Math.max(0, required - inode.extents.length);
const timestamp = validateTimestamp(nowMs);
return checkedPlan(state, {
kind: "resizeFile",
operation,
inodeId: inode.id,
size: checkedSize,
allocated: toExtentRuns(chooseExtents(state, additional)),
mtimeMs: timestamp,
ctimeMs: timestamp
});
}
function planRmdir(state, path, nowMs) {
const { parent, name } = parentAndName(state, path);
const inodeId = parent.children.get(name);
const inode = inodeId === undefined ? undefined : state.inodes.get(inodeId);
if (inode === undefined) {
throw new FsError("ENOENT", "directory does not exist", { operation: "rmdir", path });
}
if (inode.kind !== "directory") {
throw new FsError("ENOTDIR", "path is not a directory", { operation: "rmdir", path });
}
if (inode.children.size !== 0) {
throw new FsError("ENOTEMPTY", "directory is not empty", { operation: "rmdir", path });
}
const timestamp = validateTimestamp(nowMs);
return checkedPlan(state, {
kind: "removeDirectory",
parentId: parent.id,
name,
parentMtimeMs: timestamp,
parentCtimeMs: timestamp
});
}
function directoryContains(state, directory, candidateId) {
if (directory.id === candidateId) {
return true;
}
for (const childId of directory.children.values()) {
const child = inodeById(state, childId);
if (child.kind === "directory" && directoryContains(state, child, candidateId)) {
return true;
}
}
return false;
}
function childPathMetrics(parent, name) {
return {
depth: parent.depth + 1,
bytes: parent.bytes + (parent.depth === 0 ? 0 : 1) + encodedUtf8Length(name)
};
}
function findPathMetrics(state, targetId) {
let currentId = targetId;
let depth = 0;
let componentBytes = 0;
const visited = new Set;
while (currentId !== state.rootInodeId) {
if (visited.has(currentId)) {
throw new StoreLimitError("inode parent index contains a cycle");
}
visited.add(currentId);
const entry = state.parentByInode.get(currentId);
if (entry === undefined) {
throw new StoreLimitError("inode parent index does not reach root");
}
depth += 1;
componentBytes += encodedUtf8Length(entry.name);
currentId = entry.parentId;
}
return { depth, bytes: 1 + componentBytes + Math.max(0, depth - 1) };
}
function validateRenamedSubtreeBounds(state, source, destinationParent, destinationName) {
const visit = (inode, metrics) => {
if (metrics.depth > MAX_PATH_DEPTH) {
throw new FsError("EINVAL", `rename would exceed ${MAX_PATH_DEPTH} path components`);
}
if (metrics.bytes > MAX_PATH_BYTES) {
throw new FsError("EINVAL", `rename would exceed ${MAX_PATH_BYTES} UTF-8 path bytes`);
}
if (inode.kind === "directory") {
for (const [name, childId] of inode.children) {
visit(inodeById(state, childId), childPathMetrics(metrics, name));
}
}
};
const destinationMetrics = findPathMetrics(state, destinationParent.id);
visit(source, childPathMetrics(destinationMetrics, destinationName));
}
function validateRenameTypes(source, destination) {
if (destination === undefined) {
return;
}
if (source.kind !== "directory" && destination.kind === "directory") {
throw new FsError("EISDIR", "cannot replace a directory with a file");
}
if (source.kind === "directory" && destination.kind !== "directory") {
throw new FsError("ENOTDIR", "cannot replace a file with a directory");
}
if (destination.kind === "directory" && destination.children.size !== 0) {
throw new FsError("ENOTEMPTY", "destination directory is not empty");
}
}
function planRename(state, oldPath, newPath, nowMs) {
if (oldPath === newPath) {
throw new FsError("EINVAL", "source and destination are identical", { operation: "rename", path: oldPath });
}
const sourceLocation = parentAndName(state, oldPath);
const destinationLocation = parentAndName(state, newPath);
const sourceId = sourceLocation.parent.children.get(sourceLocation.name);
if (sourceId === undefined) {
throw new FsError("ENOENT", "rename source does not exist", { operation: "rename", path: oldPath });
}
const source = inodeById(state, sourceId);
const destinationId = destinationLocation.parent.children.get(destinationLocation.name);
const destination = destinationId === undefined ? undefined : inodeById(state, destinationId);
validateRenameTypes(source, destination);
if (source.kind === "directory" && directoryContains(state, source, destinationLocation.parent.id)) {
throw new FsError("EINVAL", "cannot move a directory into itself", { operation: "rename", path: newPath });
}
validateRenamedSubtreeBounds(state, source, destinationLocation.parent, destinationLocation.name);
return checkedPlan(state, {
kind: "rename",
sourceParentId: sourceLocation.parent.id,
sourceName: sourceLocation.name,
destinationParentId: destinationLocation.parent.id,
destinationName: destinationLocation.name,
timestampMs: validateTimestamp(nowMs)
});
}
function planReserveQuarantine(state, count) {
if (!Number.isSafeInteger(count) || count <= 0 || count > MAX_EXTENTS_PER_INODE) {
throw new StoreLimitError(`orphan reservation must contain 1 to ${MAX_EXTENTS_PER_INODE} extents`);
}
return checkedPlan(state, {
kind: "reserveQuarantine",
extents: toExtentRuns(chooseExtents(state, count))
});
}
function validateCreateDirectories(state, entries) {
if (entries.length === 0) {
throw new FsError("EINVAL", "createDirectories record is empty");
}
const staged = new Map;
const stagedDirectory = (inodeId) => {
const alreadyStaged = staged.get(inodeId);
if (alreadyStaged !== undefined) {
return alreadyStaged;
}
const existing = directoryById(state, inodeId);
const copy = { ...existing, children: new Map(existing.children) };
staged.set(inodeId, copy);
return copy;
};
let expectedInodeId = state.nextInodeId;
for (const entry of entries) {
validatePathComponent(entry.name);
validateMode(entry.mode);
validateTimestamp(entry.atimeMs);
validateTimestamp(entry.mtimeMs);
validateTimestamp(entry.ctimeMs);
if (entry.inodeId !== expectedInodeId || entry.inodeId > MAX_U64) {
throw new FsError("EINVAL", "createDirectories record has a non-canonical inode sequence");
}
const parent = stagedDirectory(entry.parentId);
if (parent.children.has(entry.name)) {
throw new FsError("EEXIST", "directory entry already exists");
}
const created = {
id: entry.inodeId,
kind: "directory",
mode: entry.mode,
atimeMs: entry.atimeMs,
mtimeMs: entry.mtimeMs,
ctimeMs: entry.ctimeMs,
children: new Map
};
parent.children.set(entry.name, entry.inodeId);
staged.set(entry.inodeId, created);
expectedInodeId = advanceNextInodeId(expectedInodeId);
}
if (state.inodes.size + entries.length > MAX_INODES) {
throw new StoreLimitError(`inode count would exceed ${MAX_INODES}`);
}
}
function validateExtentAssignment(state, runs, expectedCount) {
const extents = expandExtentRuns(runs, MAX_EXTENTS_PER_INODE);
if (extents.length !== expectedCount) {
throw new FsError("EINVAL", "extent choices do not cover the file size exactly");
}
const unique = new Set(extents);
if (unique.size !== extents.length) {
throw new FsError("EINVAL", "extent choices contain a duplicate");
}
let nextFresh = state.allocator.totalExtents;
for (const extentId of extents) {
if (extentId < state.allocator.totalExtents) {
if (!state.allocator.available.has(extentId)) {
throw new FsError("EINVAL", "extent choice is not safely available");
}
continue;
}
if (extentId !== nextFresh) {
throw new FsError("EINVAL", "fresh extent choices must grow the arena contiguously");
}
nextFresh += 1n;
}
if (nextFresh > BigInt(MAX_TOTAL_EXTENTS)) {
throw new StoreLimitError(`total extent count would exceed ${MAX_TOTAL_EXTENTS}`);
}
return extents;
}
function applyCreateFile(state, record) {
validatePathComponent(record.name);
const parent = directoryById(state, record.parentId);
if (parent.children.has(record.name)) {
throw new FsError("EEXIST", "directory entry already exists");
}
if (record.inodeId !== state.nextInodeId || state.inodes.has(record.inodeId)) {
throw new FsError("EINVAL", "createFile record has a non-canonical inode ID");
}
const nextInodeId = advanceNextInodeId(record.inodeId);
validateMode(record.mode);
validateTimestamp(record.atimeMs);
validateTimestamp(record.mtimeMs);
validateTimestamp(record.ctimeMs);
const size = checkedU64(record.size, "file size");
const extents = validateExtentAssignment(state, record.extents, extentCountForSize(size, state.extentSize));
if (state.inodes.size >= MAX_INODES) {
throw new StoreLimitError(`inode count would exceed ${MAX_INODES}`);
}
const inode = {
id: record.inodeId,
kind: "file",
mode: record.mode,
atimeMs: record.atimeMs,
mtimeMs: record.mtimeMs,
ctimeMs: record.ctimeMs,
size,
extents
};
for (const extentId of extents) {
if (extentId < state.allocator.totalExtents) {
state.allocator.available.delete(extentId);
} else {
state.allocator.totalExtents += 1n;
}
state.allocator.ownedBy.set(extentId, inode.id);
}
parent.children.set(record.name, inode.id);
state.inodes.set(inode.id, inode);
state.parentByInode.set(inode.id, { parentId: parent.id, name: record.name });
state.nextInodeId = nextInodeId;
}
function applyRemoveFile(state, record) {
validatePathComponent(record.name);
validateTimestamp(record.parentMtimeMs);
validateTimestamp(record.parentCtimeMs);
const parent = directoryById(state, record.parentId);
const inodeId = parent.children.get(record.name);
const inode = inodeId === undefined ? undefined : state.inodes.get(inodeId);
if (inode === undefined) {
throw new FsError("ENOENT", "file does not exist");
}
if (inode.kind === "directory") {
throw new FsError("EISDIR", "path is a directory");
}
const extents = inode.kind === "file" ? inode.extents : [];
for (const extentId of extents) {
if (state.allocator.ownedBy.get(extentId) !== inode.id || state.allocator.quarantine.has(extentId)) {
throw new StoreLimitError("file extent ownership is inconsistent");
}
}
parent.children.delete(record.name);
parent.mtimeMs = record.parentMtimeMs;
parent.ctimeMs = record.parentCtimeMs;
state.inodes.delete(inode.id);
state.parentByInode.delete(inode.id);
for (const extentId of extents) {
state.allocator.ownedBy.delete(extentId);
state.allocator.quarantine.set(extentId, null);
}
}
function applyCreateSymlink(state, record) {
validatePathComponent(record.name);
const parent = directoryById(state, record.parentId);
if (parent.children.has(record.name)) {
throw new FsError("EEXIST", "directory entry already exists");
}
if (record.inodeId !== state.nextInodeId || state.inodes.has(record.inodeId)) {
throw new FsError("EINVAL", "createSymlink record has a non-canonical inode ID");
}
const nextInodeId = advanceNextInodeId(record.inodeId);
validateMode(record.mode);
validateTimestamp(record.atimeMs);
validateTimestamp(record.mtimeMs);
validateTimestamp(record.ctimeMs);
validateSymlinkTarget(record.target);
if (state.inodes.size >= MAX_INODES) {
throw new StoreLimitError(`inode count would exceed ${MAX_INODES}`);
}
const inode = {
id: record.inodeId,
kind: "symlink",
mode: record.mode,
atimeMs: record.atimeMs,
mtimeMs: record.mtimeMs,
ctimeMs: record.ctimeMs,
target: record.target
};
parent.children.set(record.name, inode.id);
state.inodes.set(inode.id, inode);
state.parentByInode.set(inode.id, { parentId: parent.id, name: record.name });
state.nextInodeId = nextInodeId;
}
function applyChangeMode(state, record) {
const inode = inodeById(state, record.inodeId);
const mode = validateMode(record.mode);
const ctimeMs = validateTimestamp(record.ctimeMs);
inode.mode = mode;
inode.ctimeMs = ctimeMs;
}
function applyChangeTimes(state, record) {
const inode = inodeById(state, record.inodeId);
const atimeMs = validateTimestamp(record.atimeMs);
const mtimeMs = validateTimestamp(record.mtimeMs);
const ctimeMs = validateTimestamp(record.ctimeMs);
inode.atimeMs = atimeMs;
inode.mtimeMs = mtimeMs;
inode.ctimeMs = ctimeMs;
}
function applyResizeFile(state, record) {
const inode = fileById(state, record.inodeId);
const size = checkedU64(record.size, "file size");
const required = extentCountForSize(size, state.extentSize);
const additionalCount = Math.max(0, required - inode.extents.length);
const allocated = validateExtentAssignment(state, record.allocated, additionalCount);
const retained = required >= inode.extents.length ? inode.extents : inode.extents.slice(0, required);
const released = inode.extents.slice(required);
for (const extentId of released) {
if (state.allocator.ownedBy.get(extentId) !== inode.id || state.allocator.quarantine.has(extentId)) {
throw new StoreLimitError("file extent ownership is inconsistent");
}
}
const mtimeMs = validateTimestamp(record.mtimeMs);
const ctimeMs = validateTimestamp(record.ctimeMs);
for (const extentId of allocated) {
if (extentId < state.allocator.totalExtents) {
state.allocator.available.delete(extentId);
} else {
state.allocator.totalExtents += 1n;
}
state.allocator.ownedBy.set(extentId, inode.id);
retained.push(extentId);
}
for (const extentId of released) {
state.allocator.ownedBy.delete(extentId);
state.allocator.quarantine.set(extentId, null);
}
inode.size = size;
inode.extents = retained;
inode.mtimeMs = mtimeMs;
inode.ctimeMs = ctimeMs;
}
function applyRemoveDirectory(state, record) {
validatePathComponent(record.name);
const parent = directoryById(state, record.parentId);
const inodeId = parent.children.get(record.name);
const inode = inodeId === undefined ? undefined : state.inodes.get(inodeId);
if (inode === undefined) {
throw new FsError("ENOENT", "directory does not exist");
}
if (inode.kind !== "directory") {
throw new FsError("ENOTDIR", "path is not a directory");
}
if (inode.children.size !== 0) {
throw new FsError("ENOTEMPTY", "directory is not empty");
}
const mtimeMs = validateTimestamp(record.parentMtimeMs);
const ctimeMs = validateTimestamp(record.parentCtimeMs);
parent.children.delete(record.name);
parent.mtimeMs = mtimeMs;
parent.ctimeMs = ctimeMs;
state.inodes.delete(inode.id);
state.parentByInode.delete(inode.id);
}
function applyRename(state, record) {
validatePathComponent(record.sourceName);
validatePathComponent(record.destinationName);
if (record.sourceParentId === record.destinationParentId && record.sourceName === record.destinationName) {
throw new FsError("EINVAL", "rename source and destination are identical");
}
const timestampMs = validateTimestamp(record.timestampMs);
const sourceParent = directoryById(state, record.sourceParentId);
const destinationParent = directoryById(state, record.destinationParentId);
const sourceId = sourceParent.children.get(record.sourceName);
if (sourceId === undefined) {
throw new FsError("ENOENT", "rename source does not exist");
}
const source = inodeById(state, sourceId);
const destinationId = destinationParent.children.get(record.destinationName);
const destination = destinationId === undefined ? undefined : inodeById(state, destinationId);
validateRenameTypes(source, destination);
if (source.kind === "directory" && directoryContains(state, source, destinationParent.id)) {
throw new FsError("EINVAL", "cannot move a directory into itself");
}
validateRenamedSubtreeBounds(state, source, destinationParent, record.destinationName);
if (destination?.kind === "file") {
for (const extentId of destination.extents) {
if (state.allocator.ownedBy.get(extentId) !== destination.id || state.allocator.quarantine.has(extentId)) {
throw new StoreLimitError("replacement extent ownership is inconsistent");
}
}
}
sourceParent.children.delete(record.sourceName);
if (destination !== undefined) {
destinationParent.children.delete(record.destinationName);
state.inodes.delete(destination.id);
state.parentByInode.delete(destination.id);
if (destination.kind === "file") {
for (const extentId of destination.extents) {
state.allocator.ownedBy.delete(extentId);
state.allocator.quarantine.set(extentId, null);
}
}
}
destinationParent.children.set(record.destinationName, source.id);
state.parentByInode.set(source.id, {
parentId: destinationParent.id,
name: record.destinationName
});
source.ctimeMs = timestampMs;
sourceParent.mtimeMs = timestampMs;
sourceParent.ctimeMs = timestampMs;
destinationParent.mtimeMs = timestampMs;
destinationParent.ctimeMs = timestampMs;
}
function applyReserveQuarantine(state, record) {
const declaredCount = record.extents.reduce((sum, run) => sum + run.count, 0);
if (declaredCount <= 0 || declaredCount > MAX_EXTENTS_PER_INODE) {
throw new StoreLimitError("orphan reservation extent count is outside limits");
}
const extents = validateExtentAssignment(state, record.extents, declaredCount);
for (const extentId of extents) {
if (extentId < state.allocator.totalExtents) {
state.allocator.available.delete(extentId);
} else {
state.allocator.totalExtents += 1n;
}
state.allocator.quarantine.set(extentId, null);
}
}
function applyTxn(state, record, prepared) {
if (prepared !== undefined && (prepared[PREPARED_TXN_PROJECTION] !== true || prepared.state !== state || prepared.record !== record)) {
throw new StoreLimitError("prepared transaction projection does not match the live transition");
}
const projectedBasePayloadBytes = prepared?.projectedBasePayloadBytes ?? preflightTxn(state, record);
switch (record.kind) {
case "createDirectories":
validateCreateDirectories(state, record.entries);
for (const entry of record.entries) {
const parent = directoryById(state, entry.parentId);
const created = {
id: entry.inodeId,
kind: "directory",
mode: entry.mode,
atimeMs: entry.atimeMs,
mtimeMs: entry.mtimeMs,
ctimeMs: entry.ctimeMs,
children: new Map
};
parent.children.set(entry.name, created.id);
state.inodes.set(created.id, created);
state.parentByInode.set(created.id, { parentId: parent.id, name: entry.name });
state.nextInodeId = advanceNextInodeId(created.id);
}
break;
case "createFile":
applyCreateFile(state, record);
break;
case "removeFile":
applyRemoveFile(state, record);
break;
case "changeMode":
applyChangeMode(state, record);
break;
case "changeTimes":
applyChangeTimes(state, record);
break;
case "resizeFile":
applyResizeFile(state, record);
break;
case "removeDirectory":
applyRemoveDirectory(state, record);
break;
case "rename":
applyRename(state, record);
break;
case "createSymlink":
applyCreateSymlink(state, record);
break;
case "reserveQuarantine":
applyReserveQuarantine(state, record);
break;
}
state.basePayloadBytes = projectedBasePayloadBytes;
}
function projectRepackFromCopy(state, projected, pinnedQuarantine) {
if (state.generation === MAX_U64) {
throw new StoreLimitError("generation is exhausted; the store must be recreated");
}
let projectedBasePayloadBytes = state.basePayloadBytes;
const nextGeneration = state.generation + 1n;
const target = otherMetadataFile(state.activeMetadataFile);
const targetPreviousGeneration = state.retainedGenerations[target];
for (const [extentId, firstExcludedGeneration] of projected.allocator.quarantine) {
if (pinnedQuarantine.has(extentId)) {
projected.allocator.quarantine.set(extentId, null);
} else if (firstExcludedGeneration === null) {
projected.allocator.quarantine.set(extentId, nextGeneration);
} else if (targetPreviousGeneration === null || targetPreviousGeneration < firstExcludedGeneration) {
projected.allocator.quarantine.delete(extentId);
projected.allocator.available.add(extentId);
projectedBasePayloadBytes -= 8;
}
}
while (projected.allocator.totalExtents > 0n) {
const tail = projected.allocator.totalExtents - 1n;
if (!projected.allocator.available.delete(tail))
break;
projected.allocator.totalExtents = tail;
projectedBasePayloadBytes -= 8;
}
projected.generation = nextGeneration;
projected.activeMetadataFile = target;
projected.retainedGenerations[target] = nextGeneration;
projected.basePayloadBytes = projectedBasePayloadBytes;
validateState(projected);
return projected;
}
function projectRepackForActivation(state, pinnedQuarantine = new Set) {
const projected = {
...state,
retainedGenerations: { ...state.retainedGenerations },
inodes: state.inodes,
parentByInode: state.parentByInode,
allocator: {
totalExtents: state.allocator.totalExtents,
ownedBy: state.allocator.ownedBy,
available: state.allocator.available.clone(),
quarantine: new Map(state.allocator.quarantine)
}
};
return projectRepackFromCopy(state, projected, pinnedQuarantine);
}
function validateState(state) {
validateExtentSize(state.extentSize);
checkedU64(state.generation, "generation");
if (state.generation === 0n) {
throw new StoreLimitError("generation must be positive");
}
checkedU64(state.nextInodeId, "next inode ID");
const root = state.inodes.get(state.rootInodeId);
if (root?.kind !== "directory") {
throw new StoreLimitError("root inode is missing or is not a directory");
}
if (state.parentByInode.has(state.rootInodeId)) {
throw new StoreLimitError("root inode must not have a parent entry");
}
if (state.inodes.size > MAX_INODES) {
throw new StoreLimitError(`inode count exceeds ${MAX_INODES}`);
}
checkedU64(state.allocator.totalExtents, "total extent count");
if (state.allocator.totalExtents > BigInt(MAX_TOTAL_EXTENTS)) {
throw new StoreLimitError(`total extent count exceeds ${MAX_TOTAL_EXTENTS}`);
}
const classified = new Uint8Array(Number(state.allocator.totalExtents));
let classifiedCount = 0;
const classify = (extentId, label) => {
if (extentId < 0n || extentId >= state.allocator.totalExtents) {
throw new StoreLimitError(`${label} extent is out of range or multiply classified`);
}
const index = Number(extentId);
if (classified[index] !== 0) {
throw new StoreLimitError(`${label} extent is out of range or multiply classified`);
}
classified[index] = 1;
classifiedCount += 1;
};
const reached = new Set;
let calculatedBasePayloadBytes = BASE_STATE_FIXED_BYTES;
const visit = (inodeKey, inode, metrics) => {
checkedU64(inodeKey, "inode map key");
checkedU64(inode.id, "inode ID");
if (inodeKey !== inode.id) {
throw new StoreLimitError("inode map key does not match the inode ID");
}
if (inode.id >= state.nextInodeId) {
throw new StoreLimitError("inode ID is not below the unused next inode ID");
}
if (reached.has(inode.id)) {
throw new StoreLimitError("inode has more than one directory owner");
}
reached.add(inode.id);
validateMode(inode.mode);
validateTimestamp(inode.atimeMs);
validateTimestamp(inode.mtimeMs);
validateTimestamp(inode.ctimeMs);
if (inode.kind === "directory") {
calculatedBasePayloadBytes += DIRECTORY_INODE_FIXED_BYTES;
for (const [name, childId] of inode.children) {
const nameBytes = validatePathComponent(name);
calculatedBasePayloadBytes += CHILD_FIXED_BYTES + nameBytes;
const parentEntry = state.parentByInode.get(childId);
if (parentEntry?.parentId !== inode.id || parentEntry.name !== name) {
throw new StoreLimitError("inode parent index does not match its directory entry");
}
const childMetrics = {
depth: metrics.depth + 1,
bytes: metrics.bytes + (metrics.depth === 0 ? 0 : 1) + nameBytes
};
if (childMetrics.depth > MAX_PATH_DEPTH) {
throw new StoreLimitError(`inode path exceeds ${MAX_PATH_DEPTH} components`);
}
if (childMetrics.bytes > MAX_PATH_BYTES) {
throw new StoreLimitError(`inode path exceeds ${MAX_PATH_BYTES} UTF-8 bytes`);
}
const child = state.inodes.get(childId);
if (child === undefined) {
throw new StoreLimitError("directory entry references a missing inode");
}
visit(childId, child, childMetrics);
}
} else if (inode.kind === "symlink") {
let targetBytes;
try {
targetBytes = validateSymlinkTarget(inode.target);
} catch (cause) {
throw new StoreLimitError(`symlink target is invalid: ${String(cause)}`, { cause });
}
calculatedBasePayloadBytes += SYMLINK_INODE_FIXED_BYTES + targetBytes;
} else {
calculatedBasePayloadBytes += FILE_INODE_FIXED_BYTES + inode.extents.length * 8;
if (inode.extents.length !== extentCountForSize(inode.size, state.extentSize)) {
throw new StoreLimitError("file extent count does not cover its size exactly");
}
for (const extentId of inode.extents) {
classify(extentId, "owned");
if (state.allocator.ownedBy.get(extentId) !== inode.id) {
throw new StoreLimitError("owned extent does not map back to its inode");
}
}
}
};
visit(state.rootInodeId, root, { depth: 0, bytes: 1 });
if (reached.size !== state.inodes.size) {
throw new StoreLimitError("inode graph contains unreachable inodes");
}
if (state.parentByInode.size !== state.inodes.size - 1) {
throw new StoreLimitError("inode parent index does not cover every non-root inode");
}
if (classifiedCount !== state.allocator.ownedBy.size) {
throw new StoreLimitError("allocator contains an owner not referenced by a file");
}
calculatedBasePayloadBytes += state.allocator.available.size * 8 + state.allocator.quarantine.size * 16;
if (state.basePayloadBytes !== calculatedBasePayloadBytes) {
throw new StoreLimitError("tracked metadata-base size does not match canonical state size");
}
if (state.basePayloadBytes > MAX_METADATA_BASE_WRITER_BYTES) {
throw new StoreLimitError(`metadata base exceeds ${MAX_METADATA_BASE_WRITER_BYTES} bytes`);
}
if (state.retainedGenerations[state.activeMetadataFile] !== state.generation) {
throw new StoreLimitError("active metadata identity does not match the state generation");
}
const inactiveMetadataFile = otherMetadataFile(state.activeMetadataFile);
const inactiveGeneration = state.retainedGenerations[inactiveMetadataFile];
if (inactiveGeneration !== null) {
checkedU64(inactiveGeneration, "inactive retained generation");
if (inactiveGeneration === 0n || inactiveGeneration + 1n !== state.generation) {
throw new StoreLimitError("inactive retained generation is not the immediate predecessor");
}
}
for (const extentId of state.allocator.available.values()) {
classify(extentId, "available");
}
for (const [extentId, firstExcludedGeneration] of state.allocator.quarantine) {
classify(extentId, "quarantined");
if (firstExcludedGeneration !== null && firstExcludedGeneration !== state.generation) {
throw new StoreLimitError("quarantine generation is inconsistent with retained metadata");
}
}
if (BigInt(classifiedCount) !== state.allocator.totalExtents) {
throw new StoreLimitError("allocator partition does not cover every extent");
}
}
// packages/pgwasm/src/opfs/core/codec.ts
var textEncoder = new TextEncoder;
var textDecoder = new TextDecoder("utf-8", { fatal: true });
var ARENA_MAGIC = textEncoder.encode("PGXRPA01");
var METADATA_MAGIC = textEncoder.encode("PGXRPM01");
var FRAME_MAGIC = textEncoder.encode("PGXRPF01");
var ACTIVATION_MAGIC = textEncoder.encode("PGXRPK01");
var ARENA_HEADER_CHECKSUM_OFFSET = 24;
var METADATA_HEADER_BYTES = 80;
var TXN_FRAME_HEADER_BYTES = 48;
var ACTIVATION_SLOT_BYTES = 128;
var METADATA_HEADER_CHECKSUM_OFFSET = 64;
var FRAME_CHECKSUM_OFFSET = 40;
var ACTIVATION_CHECKSUM_OFFSET = 52;
class BinaryWriter {
bytes;
#view;
#offset = 0;
constructor(length, initialOffset = 0) {
if (!Number.isSafeInteger(length) || length < 0 || !Number.isSafeInteger(initialOffset) || initialOffset < 0 || initialOffset > length) {
throw new StoreLimitError("binary output length is invalid");
}
this.bytes = new Uint8Array(length);
this.#view = new DataView(this.bytes.buffer);
this.#offset = initialOffset;
}
u8(value) {
this.#require(1);
if (!Number.isSafeInteger(value) || value < 0 || value > 255) {
throw new StoreLimitError("unsigned 8-bit value is out of range");
}
this.#view.setUint8(this.#offset, value);
this.#offset += 1;
}
u16(value) {
this.#require(2);
if (!Number.isSafeInteger(value) || value < 0 || value > 65535) {
throw new StoreLimitError("unsigned 16-bit value is out of range");
}
this.#view.setUint16(this.#offset, value, true);
this.#offset += 2;
}
u32(value) {
this.#require(4);
if (!Number.isSafeInteger(value) || value < 0 || value > 4294967295) {
throw new StoreLimitError("unsigned 32-bit value is out of range");
}
this.#view.setUint32(this.#offset, value, true);
this.#offset += 4;
}
u64(value) {
this.#require(8);
this.#view.setBigUint64(this.#offset, checkedU64(value, "encoded integer"), true);
this.#offset += 8;
}
validatedU64(value) {
this.#require(8);
this.#view.setBigUint64(this.#offset, value, true);
this.#offset += 8;
}
raw(value) {
this.#require(value.byteLength);
this.bytes.set(value, this.#offset);
this.#offset += value.byteLength;
}
string(value) {
validatePathComponent(value);
this.validatedString(textEncoder.encode(value));
}
symlinkTarget(value) {
validateSymlinkTarget(value);
this.validatedString(textEncoder.encode(value));
}
validatedString(encoded) {
this.u16(encoded.byteLength);
this.raw(encoded);
}
finish() {
if (this.#offset !== this.bytes.byteLength) {
throw new StoreLimitError(`binary encoder wrote ${this.#offset} bytes into a ${this.bytes.byteLength}-byte output`);
}
return this.bytes;
}
#require(length) {
if (this.#offset + length > this.bytes.byteLength) {
throw new StoreLimitError("binary encoder exceeded its preflighted size");
}
}
}
class BinaryReader {
#bytes;
#view;
#offset = 0;
constructor(bytes) {
this.#bytes = bytes;
this.#view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
}
get remaining() {
return this.#bytes.byteLength - this.#offset;
}
u8() {
this.#require(1);
const value = this.#view.getUint8(this.#offset);
this.#offset += 1;
return value;
}
u16() {
this.#require(2);
const value = this.#view.getUint16(this.#offset, true);
this.#offset += 2;
return value;
}
u32() {
this.#require(4);
const value = this.#view.getUint32(this.#offset, true);
this.#offset += 4;
return value;
}
u64() {
this.#require(8);
const value = this.#view.getBigUint64(this.#offset, true);
this.#offset += 8;
return value;
}
raw(length) {
this.#require(length);
const value = this.#bytes.subarray(this.#offset, this.#offset + length);
this.#offset += length;
return value;
}
string() {
const length = this.u16();
if (length === 0 || length > MAX_COMPONENT_BYTES) {
throw new CorruptStoreError("encoded path component length is invalid");
}
let value;
try {
value = textDecoder.decode(this.raw(length));
} catch (cause) {
throw new CorruptStoreError("encoded path component is not valid UTF-8", { cause });
}
if (textEncoder.encode(value).byteLength !== length) {
throw new CorruptStoreError("encoded path component is not canonical UTF-8");
}
try {
validatePathComponent(value);
} catch (cause) {
throw new CorruptStoreError(`encoded path component is invalid: ${String(cause)}`, { cause });
}
return value;
}
symlinkTarget() {
const length = this.u16();
if (length === 0 || length > MAX_PATH_BYTES) {
throw new CorruptStoreError("encoded symlink target length is invalid");
}
let value;
try {
value = textDecoder.decode(this.raw(length));
} catch (cause) {
throw new CorruptStoreError("encoded symlink target is not valid UTF-8", { cause });
}
if (textEncoder.encode(value).byteLength !== length) {
throw new CorruptStoreError("encoded symlink target is not canonical UTF-8");
}
try {
validateSymlinkTarget(value);
} catch (cause) {
throw new CorruptStoreError(`encoded symlink target is invalid: ${String(cause)}`, { cause });
}
return value;
}
end() {
if (this.remaining !== 0) {
throw new CorruptStoreError("binary value has trailing bytes");
}
}
#require(length) {
if (!Number.isSafeInteger(length) || length < 0 || length > this.remaining) {
throw new CorruptStoreError("binary value is truncated");
}
}
}
function bytesEqual(left, right) {
if (left.byteLength !== right.byteLength) {
return false;
}
for (let index = 0;index < left.byteLength; index += 1) {
if (left[index] !== right[index]) {
return false;
}
}
return true;
}
function requireMagic(bytes, magic, label) {
if (!bytesEqual(bytes.subarray(0, magic.byteLength), magic)) {
throw new CorruptStoreError(`${label} magic is invalid`);
}
}
function requireZero(bytes, start, end, label) {
for (let offset = start;offset < end; offset += 1) {
if (bytes[offset] !== 0) {
throw new CorruptStoreError(`${label} padding is not canonical`);
}
}
}
function requireChecksum(bytes, offset, label) {
const stored = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset, true);
const calculated = crc32WithZeroedRange(bytes, offset, 4);
if (stored !== calculated) {
throw new CorruptStoreError(`${label} checksum is invalid`);
}
}
function requireCurrentIdentity(version, profile) {
if (version !== FORMAT_VERSION || profile !== LIMITS_PROFILE_VERSION) {
throw new StoreRecreationRequiredError(`store format ${version}/limits profile ${profile} is unsupported; delete the store and create it fresh`);
}
}
function encodeArenaHeader(options) {
const extentSize = validateExtentSize(options.extentSize);
const bytes = new Uint8Array(ARENA_HEADER_BYTES);
const view = new DataView(bytes.buffer);
bytes.set(ARENA_MAGIC, 0);
view.setUint32(8, FORMAT_VERSION, true);
view.setUint32(12, LIMITS_PROFILE_VERSION, true);
view.setUint32(16, extentSize, true);
view.setUint32(ARENA_HEADER_CHECKSUM_OFFSET, crc32WithZeroedRange(bytes, ARENA_HEADER_CHECKSUM_OFFSET, 4), true);
return bytes;
}
function decodeArenaHeader(bytes, configuredExtentSize) {
if (configuredExtentSize !== undefined) {
validateExtentSize(configuredExtentSize);
}
if (bytes.byteLength !== ARENA_HEADER_BYTES) {
throw new CorruptStoreError("arena header has the wrong length");
}
requireMagic(bytes, ARENA_MAGIC, "arena header");
requireZero(bytes, 20, 24, "arena header");
requireZero(bytes, 28, bytes.byteLength, "arena header");
requireChecksum(bytes, ARENA_HEADER_CHECKSUM_OFFSET, "arena header");
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const formatVersion = view.getUint32(8, true);
const limitsProfileVersion = view.getUint32(12, true);
requireCurrentIdentity(formatVersion, limitsProfileVersion);
const extentSize = view.getUint32(16, true);
try {
validateExtentSize(extentSize);
} catch (cause) {
throw new CorruptStoreError("arena header extent size is invalid", { cause });
}
if (configuredExtentSize !== undefined && configuredExtentSize !== extentSize) {
throw new ExtentSizeMismatchError(configuredExtentSize, extentSize);
}
return { extentSize, limitsProfileVersion, formatVersion };
}
function metadataFileCode(file) {
return file === "a" ? 0 : 1;
}
function decodeMetadataFile(value) {
if (value === 0)
return "a";
if (value === 1)
return "b";
throw new CorruptStoreError("metadata file selector is invalid");
}
function compareBigint(left, right) {
return left < right ? -1 : left > right ? 1 : 0;
}
function compareUtf8Names(left, right) {
let leftIndex = 0;
let rightIndex = 0;
while (leftIndex < left.length && rightIndex < right.length) {
const leftPoint = left.codePointAt(leftIndex);
const rightPoint = right.codePointAt(rightIndex);
if (leftPoint !== rightPoint)
return leftPoint - rightPoint;
leftIndex += leftPoint > 65535 ? 2 : 1;
rightIndex += rightPoint > 65535 ? 2 : 1;
}
return left.length - right.length;
}
function sortIfNeeded(values, compare) {
for (let index = 1;index < values.length; index += 1) {
if (compare(values[index - 1], values[index]) > 0) {
values.sort(compare);
break;
}
}
return values;
}
var INODE_KIND_DIRECTORY = 0;
var INODE_KIND_FILE = 1;
var INODE_KIND_SYMLINK = 2;
function writeInode(writer, inode) {
writer.validatedU64(inode.id);
writer.u8(inode.kind === "directory" ? INODE_KIND_DIRECTORY : inode.kind === "file" ? INODE_KIND_FILE : INODE_KIND_SYMLINK);
writer.u32(inode.mode);
writer.validatedU64(inode.atimeMs);
writer.validatedU64(inode.mtimeMs);
writer.validatedU64(inode.ctimeMs);
if (inode.kind === "directory") {
const children = sortIfNeeded([...inode.children], ([left], [right]) => compareUtf8Names(left, right));
writer.u32(children.length);
for (const [name, inodeId] of children) {
writer.validatedString(textEncoder.encode(name));
writer.validatedU64(inodeId);
}
} else if (inode.kind === "symlink") {
writer.symlinkTarget(inode.target);
} else {
writer.validatedU64(inode.size);
writer.u32(inode.extents.length);
for (const extentId of inode.extents)
writer.validatedU64(extentId);
}
}
function encodeMetadataPayload(state, length, prefixBytes = 0) {
if (length > MAX_METADATA_BASE_WRITER_BYTES) {
throw new StoreLimitError(`metadata base payload exceeds ${MAX_METADATA_BASE_WRITER_BYTES} bytes`);
}
const writer = new BinaryWriter(prefixBytes + length, prefixBytes);
writer.validatedU64(state.allocator.totalExtents);
writer.validatedU64(state.nextInodeId);
writer.u32(state.inodes.size);
const inodes = sortIfNeeded([...state.inodes.values()], (left, right) => compareBigint(left.id, right.id));
for (const inode of inodes) {
writeInode(writer, inode);
}
const available = sortIfNeeded([...state.allocator.available.values()], compareBigint);
for (const extentId of available)
writer.validatedU64(extentId);
const quarantine = sortIfNeeded([...state.allocator.quarantine], ([left], [right]) => compareBigint(left, right));
for (const [extentId, generation] of quarantine) {
writer.validatedU64(extentId);
writer.validatedU64(generation ?? 0n);
}
return writer.finish();
}
function inspectMetadataBaseHeader(header) {
if (header.byteLength !== METADATA_HEADER_BYTES) {
throw new CorruptStoreError("metadata base header has the wrong length");
}
requireMagic(header, METADATA_MAGIC, "metadata base");
requireZero(header, 29, 32, "metadata base");
requireZero(header, 52, 56, "metadata base");
requireZero(header, 68, 80, "metadata base");
const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
const payloadLength = view.getUint32(40, true);
if (payloadLength > MAX_METADATA_BASE_READER_BYTES) {
throw new CorruptStoreError("metadata base payload declaration exceeds the reader limit");
}
const baseEnd = view.getBigUint64(56, true);
if (baseEnd !== BigInt(METADATA_HEADER_BYTES + payloadLength)) {
throw new CorruptStoreError("metadata base end offset is inconsistent");
}
const availableCount = view.getUint32(44, true);
const quarantineCount = view.getUint32(48, true);
return {
formatVersion: view.getUint32(8, true),
limitsProfileVersion: view.getUint32(12, true),
generation: view.getBigUint64(16, true),
extentSize: view.getUint32(24, true),
metadataFileCode: view.getUint8(28),
rootInodeId: view.getBigUint64(32, true),
payloadLength,
availableCount,
quarantineCount,
baseEnd
};
}
function encodeMetadataBase(state) {
validateState(state);
return encodeValidatedMetadataBase(state);
}
function encodeValidatedMetadataBase(state) {
const baseEnd = METADATA_HEADER_BYTES + state.basePayloadBytes;
const bytes = encodeMetadataPayload(state, state.basePayloadBytes, METADATA_HEADER_BYTES);
const view = new DataView(bytes.buffer);
bytes.set(METADATA_MAGIC, 0);
view.setUint32(8, FORMAT_VERSION, true);
view.setUint32(12, LIMITS_PROFILE_VERSION, true);
view.setBigUint64(16, state.generation, true);
view.setUint32(24, state.extentSize, true);
view.setUint8(28, metadataFileCode(state.activeMetadataFile));
view.setBigUint64(32, state.rootInodeId, true);
view.setUint32(40, state.basePayloadBytes, true);
view.setUint32(44, state.allocator.available.size, true);
view.setUint32(48, state.allocator.quarantine.size, true);
view.setBigUint64(56, BigInt(baseEnd), true);
view.setUint32(METADATA_HEADER_CHECKSUM_OFFSET, crc32WithZeroedRange(bytes, METADATA_HEADER_CHECKSUM_OFFSET, 4), true);
return bytes;
}
function readBoundedCount(reader, maximum, label) {
const count = reader.u32();
if (count > maximum) {
throw new CorruptStoreError(`${label} count exceeds ${maximum}`);
}
return count;
}
function readInode(reader) {
const id = reader.u64();
const kind = reader.u8();
const mode = reader.u32();
const atimeMs = reader.u64();
const mtimeMs = reader.u64();
const ctimeMs = reader.u64();
if (kind === INODE_KIND_DIRECTORY) {
const childCount = readBoundedCount(reader, MAX_INODES, "directory child");
const children = new Map;
let previousName;
for (let index = 0;index < childCount; index += 1) {
const name = reader.string();
if (previousName !== undefined && compareUtf8Names(previousName, name) >= 0) {
throw new CorruptStoreError("directory children are not in canonical order");
}
children.set(name, reader.u64());
previousName = name;
}
const inode = { id, kind: "directory", mode, atimeMs, mtimeMs, ctimeMs, children };
return inode;
}
if (kind === INODE_KIND_FILE) {
const size = reader.u64();
const extentCount = readBoundedCount(reader, MAX_EXTENTS_PER_INODE, "file extent");
const extents = [];
for (let index = 0;index < extentCount; index += 1)
extents.push(reader.u64());
const inode = { id, kind: "file", mode, atimeMs, mtimeMs, ctimeMs, size, extents };
return inode;
}
if (kind === INODE_KIND_SYMLINK) {
const inode = {
id,
kind: "symlink",
mode,
atimeMs,
mtimeMs,
ctimeMs,
target: reader.symlinkTarget()
};
return inode;
}
throw new CorruptStoreError("inode type is invalid");
}
function decodeMetadataBase(bytes) {
if (bytes.byteLength < METADATA_HEADER_BYTES) {
throw new CorruptStoreError("metadata base header is truncated");
}
const inspected = inspectMetadataBaseHeader(bytes.subarray(0, METADATA_HEADER_BYTES));
const { payloadLength, baseEnd } = inspected;
if (bytes.byteLength !== Number(baseEnd)) {
throw new CorruptStoreError("metadata base end offset is inconsistent");
}
requireChecksum(bytes, METADATA_HEADER_CHECKSUM_OFFSET, "metadata base");
const payload = bytes.subarray(METADATA_HEADER_BYTES);
requireCurrentIdentity(inspected.formatVersion, inspected.limitsProfileVersion);
const { generation, extentSize, rootInodeId, availableCount, quarantineCount } = inspected;
if (generation === 0n)
throw new CorruptStoreError("metadata generation must be positive");
try {
validateExtentSize(extentSize);
} catch (cause) {
throw new CorruptStoreError("metadata base extent size is invalid", { cause });
}
const activeMetadataFile = decodeMetadataFile(inspected.metadataFileCode);
if (availableCount > MAX_TOTAL_EXTENTS || quarantineCount > MAX_TOTAL_EXTENTS) {
throw new CorruptStoreError("metadata allocator count exceeds the extent limit");
}
try {
const reader = new BinaryReader(payload);
const totalExtents = reader.u64();
if (totalExtents > BigInt(MAX_TOTAL_EXTENTS)) {
throw new CorruptStoreError("metadata total extent count exceeds the extent limit");
}
const nextInodeId = reader.u64();
const inodeCount = readBoundedCount(reader, MAX_INODES, "inode");
const inodes = new Map;
const ownedBy = new Map;
let previousInodeId;
for (let index = 0;index < inodeCount; index += 1) {
const inode = readInode(reader);
if (previousInodeId !== undefined && inode.id <= previousInodeId) {
throw new CorruptStoreError("inodes are not in canonical order");
}
inodes.set(inode.id, inode);
previousInodeId = inode.id;
if (inode.kind === "file") {
for (const extentId of inode.extents) {
if (ownedBy.has(extentId))
throw new CorruptStoreError("metadata contains duplicate extent ownership");
ownedBy.set(extentId, inode.id);
}
}
}
const available = new IndexedExtentSet;
let previousAvailable;
for (let index = 0;index < availableCount; index += 1) {
const extentId = reader.u64();
if (previousAvailable !== undefined && extentId <= previousAvailable) {
throw new CorruptStoreError("available extents are not in canonical order");
}
available.add(extentId);
previousAvailable = extentId;
}
const quarantine = new Map;
let previousQuarantine;
for (let index = 0;index < quarantineCount; index += 1) {
const extentId = reader.u64();
const encodedGeneration = reader.u64();
if (previousQuarantine !== undefined && extentId <= previousQuarantine) {
throw new CorruptStoreError("quarantine extents are not in canonical order");
}
quarantine.set(extentId, encodedGeneration === 0n ? null : encodedGeneration);
previousQuarantine = extentId;
}
reader.end();
const parentByInode = new Map;
for (const inode of inodes.values()) {
if (inode.kind !== "directory")
continue;
for (const [name, childId] of inode.children) {
if (parentByInode.has(childId)) {
throw new CorruptStoreError("metadata inode has multiple directory owners");
}
parentByInode.set(childId, { parentId: inode.id, name });
}
}
const retainedGenerations = { a: null, b: null };
retainedGenerations[activeMetadataFile] = generation;
const state = {
generation,
nextInodeId,
rootInodeId,
extentSize,
activeMetadataFile,
retainedGenerations,
basePayloadBytes: payloadLength,
inodes,
parentByInode,
allocator: { totalExtents, ownedBy, available, quarantine }
};
validateState(state);
return { state, payloadBytes: payloadLength, baseEnd, baseDigest: crc32(bytes) };
} catch (cause) {
if (cause instanceof CorruptStoreError)
throw cause;
throw new CorruptStoreError(`metadata base is semantically invalid: ${String(cause)}`, { cause });
}
}
function writeExtentRuns(writer, runs) {
writer.u32(runs.length);
for (const run of runs) {
writer.u64(run.start);
writer.u32(run.count);
}
}
function readExtentRuns(reader) {
const runCount = readBoundedCount(reader, MAX_EXTENTS_PER_INODE, "extent run");
const runs = [];
let extentCount = 0;
for (let index = 0;index < runCount; index += 1) {
const start = reader.u64();
const count = reader.u32();
if (count === 0 || extentCount + count > MAX_EXTENTS_PER_INODE) {
throw new CorruptStoreError("extent run count is invalid");
}
extentCount += count;
runs.push({ start, count });
}
return runs;
}
var RECORD_CODES = {
createDirectories: 1,
createFile: 2,
removeFile: 3,
changeMode: 4,
changeTimes: 5,
resizeFile: 6,
removeDirectory: 7,
rename: 8,
reserveQuarantine: 9,
createSymlink: 10
};
function encodeTxnRecord(record) {
const writer = new BinaryWriter(estimateTxnPayloadBytes(record));
writer.u8(RECORD_CODES[record.kind]);
switch (record.kind) {
case "createDirectories":
writer.u32(record.entries.length);
for (const entry of record.entries) {
writer.u64(entry.parentId);
writer.string(entry.name);
writer.u64(entry.inodeId);
writer.u32(entry.mode);
writer.u64(entry.atimeMs);
writer.u64(entry.mtimeMs);
writer.u64(entry.ctimeMs);
}
break;
case "createFile":
writer.u64(record.parentId);
writer.string(record.name);
writer.u64(record.inodeId);
writer.u32(record.mode);
writer.u64(record.atimeMs);
writer.u64(record.mtimeMs);
writer.u64(record.ctimeMs);
writer.u64(record.size);
writeExtentRuns(writer, record.extents);
break;
case "removeFile":
case "removeDirectory":
writer.u64(record.parentId);
writer.string(record.name);
writer.u64(record.parentMtimeMs);
writer.u64(record.parentCtimeMs);
break;
case "changeMode":
writer.u64(record.inodeId);
writer.u32(record.mode);
writer.u64(record.ctimeMs);
break;
case "changeTimes":
writer.u64(record.inodeId);
writer.u64(record.atimeMs);
writer.u64(record.mtimeMs);
writer.u64(record.ctimeMs);
break;
case "resizeFile":
writer.u8(record.operation === "truncate" ? 0 : 1);
writer.u64(record.inodeId);
writer.u64(record.size);
writeExtentRuns(writer, record.allocated);
writer.u64(record.mtimeMs);
writer.u64(record.ctimeMs);
break;
case "rename":
writer.u64(record.sourceParentId);
writer.string(record.sourceName);
writer.u64(record.destinationParentId);
writer.string(record.destinationName);
writer.u64(record.timestampMs);
break;
case "createSymlink":
writer.u64(record.parentId);
writer.string(record.name);
writer.u64(record.inodeId);
writer.u32(record.mode);
writer.u64(record.atimeMs);
writer.u64(record.mtimeMs);
writer.u64(record.ctimeMs);
writer.symlinkTarget(record.target);
break;
case "reserveQuarantine":
writeExtentRuns(writer, record.extents);
break;
}
return writer.finish();
}
function decodeTxnRecord(bytes) {
const reader = new BinaryReader(bytes);
const type = reader.u8();
let record;
switch (type) {
case 1: {
const count = readBoundedCount(reader, MAX_INODES, "created directory");
if (count === 0)
throw new CorruptStoreError("directory transaction must not be empty");
const entries = [];
for (let index = 0;index < count; index += 1) {
entries.push({
parentId: reader.u64(),
name: reader.string(),
inodeId: reader.u64(),
mode: reader.u32(),
atimeMs: reader.u64(),
mtimeMs: reader.u64(),
ctimeMs: reader.u64()
});
}
record = { kind: "createDirectories", entries };
break;
}
case 2:
record = {
kind: "createFile",
parentId: reader.u64(),
name: reader.string(),
inodeId: reader.u64(),
mode: reader.u32(),
atimeMs: reader.u64(),
mtimeMs: reader.u64(),
ctimeMs: reader.u64(),
size: reader.u64(),
extents: readExtentRuns(reader)
};
break;
case 3:
case 7:
record = {
kind: type === 3 ? "removeFile" : "removeDirectory",
parentId: reader.u64(),
name: reader.string(),
parentMtimeMs: reader.u64(),
parentCtimeMs: reader.u64()
};
break;
case 4:
record = { kind: "changeMode", inodeId: reader.u64(), mode: reader.u32(), ctimeMs: reader.u64() };
break;
case 5:
record = {
kind: "changeTimes",
inodeId: reader.u64(),
atimeMs: reader.u64(),
mtimeMs: reader.u64(),
ctimeMs: reader.u64()
};
break;
case 6: {
const operation = reader.u8();
if (operation !== 0 && operation !== 1)
throw new CorruptStoreError("resize operation is invalid");
record = {
kind: "resizeFile",
operation: operation === 0 ? "truncate" : "write",
inodeId: reader.u64(),
size: reader.u64(),
allocated: readExtentRuns(reader),
mtimeMs: reader.u64(),
ctimeMs: reader.u64()
};
break;
}
case 8:
record = {
kind: "rename",
sourceParentId: reader.u64(),
sourceName: reader.string(),
destinationParentId: reader.u64(),
destinationName: reader.string(),
timestampMs: reader.u64()
};
break;
case 9:
record = { kind: "reserveQuarantine", extents: readExtentRuns(reader) };
break;
case 10:
record = {
kind: "createSymlink",
parentId: reader.u64(),
name: reader.string(),
inodeId: reader.u64(),
mode: reader.u32(),
atimeMs: reader.u64(),
mtimeMs: reader.u64(),
ctimeMs: reader.u64(),
target: reader.symlinkTarget()
};
break;
default:
throw new CorruptStoreError("transaction record type is invalid");
}
reader.end();
return record;
}
function txnFrameBytes(record) {
const payloadBytes = estimateTxnPayloadBytes(record);
if (payloadBytes > MAX_FRAME_PAYLOAD_BYTES) {
throw new StoreLimitError(`transaction payload exceeds ${MAX_FRAME_PAYLOAD_BYTES} bytes`);
}
return TXN_FRAME_HEADER_BYTES + payloadBytes;
}
function inspectTxnFrameHeader(header) {
if (header.byteLength !== TXN_FRAME_HEADER_BYTES) {
throw new CorruptStoreError("transaction frame header has the wrong length");
}
const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
requireMagic(header, FRAME_MAGIC, "transaction frame");
requireZero(header, 36, 40, "transaction frame");
requireZero(header, 44, 48, "transaction frame");
const payloadLength = view.getUint32(32, true);
if (payloadLength > MAX_FRAME_PAYLOAD_BYTES) {
throw new CorruptStoreError("transaction payload declaration exceeds the frame limit");
}
if (view.getUint32(8, true) !== FORMAT_VERSION || view.getUint32(12, true) !== LIMITS_PROFILE_VERSION) {
throw new CorruptStoreError("transaction frame format is invalid");
}
return {
generation: view.getBigUint64(16, true),
sequence: view.getBigUint64(24, true),
payloadLength,
frameBytes: TXN_FRAME_HEADER_BYTES + payloadLength
};
}
function encodeTxnFrame(frame) {
const payload = encodeTxnRecord(frame.record);
if (payload.byteLength > MAX_FRAME_PAYLOAD_BYTES) {
throw new StoreLimitError(`transaction payload exceeds ${MAX_FRAME_PAYLOAD_BYTES} bytes`);
}
const bytes = new Uint8Array(TXN_FRAME_HEADER_BYTES + payload.byteLength);
const view = new DataView(bytes.buffer);
bytes.set(FRAME_MAGIC, 0);
view.setUint32(8, FORMAT_VERSION, true);
view.setUint32(12, LIMITS_PROFILE_VERSION, true);
view.setBigUint64(16, checkedU64(frame.generation, "frame generation"), true);
view.setBigUint64(24, checkedU64(frame.sequence, "frame sequence"), true);
view.setUint32(32, payload.byteLength, true);
bytes.set(payload, TXN_FRAME_HEADER_BYTES);
view.setUint32(FRAME_CHECKSUM_OFFSET, crc32WithZeroedRange(bytes, FRAME_CHECKSUM_OFFSET, 4), true);
return bytes;
}
function decodeTxnFrame(bytes) {
if (bytes.byteLength < TXN_FRAME_HEADER_BYTES)
throw new CorruptStoreError("transaction frame is truncated");
const inspected = inspectTxnFrameHeader(bytes.subarray(0, TXN_FRAME_HEADER_BYTES));
if (bytes.byteLength !== inspected.frameBytes) {
throw new CorruptStoreError("transaction frame length is inconsistent");
}
requireChecksum(bytes, FRAME_CHECKSUM_OFFSET, "transaction frame");
return {
generation: inspected.generation,
sequence: inspected.sequence,
record: decodeTxnRecord(bytes.subarray(TXN_FRAME_HEADER_BYTES))
};
}
function encodeActivationSlot(record) {
if (record.sequence === 0n || record.generation === 0n || record.baseEnd < BigInt(METADATA_HEADER_BYTES)) {
throw new StoreLimitError("activation identity values are outside the encodable domain");
}
const bytes = new Uint8Array(ACTIVATION_SLOT_BYTES);
const view = new DataView(bytes.buffer);
bytes.set(ACTIVATION_MAGIC, 0);
view.setUint32(8, FORMAT_VERSION, true);
view.setUint32(12, LIMITS_PROFILE_VERSION, true);
view.setBigUint64(16, checkedU64(record.sequence, "activation sequence"), true);
view.setUint8(24, metadataFileCode(record.metadataFile));
view.setBigUint64(32, checkedU64(record.generation, "activation generation"), true);
view.setBigUint64(40, checkedU64(record.baseEnd, "activation base end"), true);
if (!Number.isSafeInteger(record.baseDigest) || record.baseDigest < 0 || record.baseDigest > 4294967295) {
throw new StoreLimitError("activation base digest is outside the unsigned 32-bit range");
}
view.setUint32(48, record.baseDigest, true);
view.setUint32(ACTIVATION_CHECKSUM_OFFSET, crc32WithZeroedRange(bytes, ACTIVATION_CHECKSUM_OFFSET, 4), true);
return bytes;
}
function decodeActivationSlot(bytes) {
if (bytes.byteLength !== ACTIVATION_SLOT_BYTES) {
throw new CorruptStoreError("activation slot has the wrong length");
}
requireMagic(bytes, ACTIVATION_MAGIC, "activation slot");
requireZero(bytes, 25, 32, "activation slot");
requireZero(bytes, 56, bytes.byteLength, "activation slot");
requireChecksum(bytes, ACTIVATION_CHECKSUM_OFFSET, "activation slot");
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
requireCurrentIdentity(view.getUint32(8, true), view.getUint32(12, true));
const sequence = view.getBigUint64(16, true);
const generation = view.getBigUint64(32, true);
const baseEnd = view.getBigUint64(40, true);
if (sequence === 0n || generation === 0n || baseEnd < BigInt(METADATA_HEADER_BYTES)) {
throw new CorruptStoreError("activation slot contains invalid identity values");
}
return {
sequence,
metadataFile: decodeMetadataFile(view.getUint8(24)),
generation,
baseEnd,
baseDigest: view.getUint32(48, true)
};
}
function tryDecodeActivationSlot(bytes) {
try {
return decodeActivationSlot(bytes);
} catch (cause) {
if (cause instanceof CorruptStoreError)
return null;
throw cause;
}
}
function selectActivationPair(leftBytes, rightBytes) {
const left = tryDecodeActivationSlot(leftBytes);
const right = tryDecodeActivationSlot(rightBytes);
if (left === null && right === null) {
throw new CorruptStoreError("both activation slots are invalid");
}
if (left === null)
return { selected: right, other: null };
if (right === null)
return { selected: left, other: null };
const older = left.sequence < right.sequence ? left : right;
const newer = older === left ? right : left;
if (newer.sequence !== older.sequence + 1n || newer.metadataFile === older.metadataFile) {
throw new CorruptStoreError("activation slots do not form one exact alternating progression");
}
return { selected: newer, other: older };
}
function metadataBaseDigest(bytes) {
return crc32(bytes);
}
// packages/pgwasm/src/opfs/core/port.ts
var OWNED_FILE_NAMES = ["arena.bin", "metadata-a.bin", "metadata-b.bin", "activation.bin"];
function metadataFileName(file) {
return file === "a" ? "metadata-a.bin" : "metadata-b.bin";
}
class PortWriteError extends Error {
constructor(label) {
super(`${label} made no valid write progress`);
this.name = "PortWriteError";
}
}
function checkedRange(offset, length, label) {
checkedU64(offset, `${label} offset`);
if (!Number.isSafeInteger(length) || length < 0) {
throw new StoreLimitError(`${label} length is invalid`);
}
checkedSafeNumber(checkedAdd(offset, BigInt(length), `${label} end`), `${label} end`);
return checkedSafeNumber(offset, `${label} offset`);
}
function readExact(handle, offset, length, maximumLength, label) {
if (!Number.isSafeInteger(maximumLength) || maximumLength < 0 || length > maximumLength) {
throw new StoreLimitError(`${label} length exceeds the bounded read limit`);
}
const start = checkedRange(offset, length, label);
const output = new Uint8Array(length);
let completed = 0;
while (completed < output.byteLength) {
const count = handle.read(output.subarray(completed), start + completed, label);
if (!Number.isSafeInteger(count) || count <= 0 || count > output.byteLength - completed) {
throw new CorruptStoreError(`${label} ended before the declared byte length`);
}
completed += count;
}
return output;
}
function writeExact(handle, offset, bytes, label) {
const start = checkedRange(offset, bytes.byteLength, label);
let completed = 0;
while (completed < bytes.byteLength) {
const count = handle.write(bytes.subarray(completed), start + completed, label);
if (!Number.isSafeInteger(count) || count <= 0 || count > bytes.byteLength - completed) {
throw new PortWriteError(label);
}
completed += count;
}
}
function truncateChecked(handle, size, label) {
handle.truncate(checkedSafeNumber(size, `${label} size`), label);
}
function arenaByteOffset(extentId, extentSize, withinExtent = 0) {
validateExtentSize(extentSize);
checkedU64(extentId, "extent ID");
if (extentId >= BigInt(MAX_TOTAL_EXTENTS)) {
throw new StoreLimitError(`extent ID exceeds ${MAX_TOTAL_EXTENTS - 1}`);
}
if (!Number.isSafeInteger(withinExtent) || withinExtent < 0 || withinExtent >= extentSize) {
throw new StoreLimitError("within-extent offset is outside the extent");
}
return checkedAdd(checkedAdd(BigInt(ARENA_HEADER_BYTES), checkedMultiply(extentId, BigInt(extentSize), "arena offset"), "arena offset"), BigInt(withinExtent), "arena offset");
}
// packages/pgwasm/src/opfs/core/metadata-store.ts
function validateEntries(entries) {
for (const entry of entries) {
if (!OWNED_FILE_NAMES.includes(entry.name) || entry.kind !== "file") {
throw new UnexpectedStoreEntryError(entry.name);
}
}
}
function closeHandles(handles) {
for (let index = handles.length - 1;index >= 0; index -= 1) {
try {
handles[index].close();
} catch {}
}
}
async function acquireHandles(port) {
validateEntries(await port.enumerate("store.enumerate.initial"));
const acquired = [];
try {
const activation = await port.acquire("activation.bin", "store.acquire.activation");
acquired.push(activation);
validateEntries(await port.enumerate("store.enumerate.locked"));
for (const name of ["arena.bin", "metadata-a.bin", "metadata-b.bin"]) {
acquired.push(await port.acquire(name, `store.acquire.${name}`));
}
return Object.fromEntries(acquired.map((handle) => [handle.name, handle]));
} catch (cause) {
closeHandles(acquired);
throw cause;
}
}
function getSizes(handles) {
return Object.fromEntries(OWNED_FILE_NAMES.map((name) => [name, handles[name].getSize(`store.size.${name}`)]));
}
function readSlot(handle, size, index) {
const offset = index * ACTIVATION_SLOT_BYTES;
if (offset >= size)
return new Uint8Array;
const length = Math.min(ACTIVATION_SLOT_BYTES, size - offset);
return readExact(handle, BigInt(offset), length, ACTIVATION_SLOT_BYTES, `activation.read.slot-${index}`);
}
function isPrefix(bytes, canonical) {
if (bytes.byteLength > canonical.byteLength)
return false;
for (let index = 0;index < bytes.byteLength; index += 1) {
if (bytes[index] !== canonical[index])
return false;
}
return true;
}
function readWhole(handle, size, maximum, label) {
return readExact(handle, 0n, size, maximum, label);
}
function bootstrapExtentSize(arena, arenaSize, configuredExtentSize) {
if (arenaSize === 0)
return configuredExtentSize ?? DEFAULT_EXTENT_BYTES;
if (arenaSize >= ARENA_HEADER_BYTES) {
const header = readExact(arena, 0n, ARENA_HEADER_BYTES, ARENA_HEADER_BYTES, "arena.read.bootstrap");
const extentSize = decodeArenaHeader(header).extentSize;
if (arenaSize > ARENA_HEADER_BYTES) {
throw new CorruptStoreError("unactivated arena contains extent payload");
}
return extentSize;
}
const bytes = readWhole(arena, arenaSize, ARENA_HEADER_BYTES, "arena.read.bootstrap");
let partialExtentSize;
if (arenaSize >= 20) {
const declared = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(16, true);
try {
partialExtentSize = validateExtentSize(declared);
} catch (cause) {
if (!(cause instanceof TypeError))
throw cause;
partialExtentSize = undefined;
}
}
const candidates = new Set;
if (configuredExtentSize !== undefined)
candidates.add(configuredExtentSize);
if (partialExtentSize !== undefined)
candidates.add(partialExtentSize);
candidates.add(DEFAULT_EXTENT_BYTES);
if (arenaSize < 20) {
for (let candidate = MIN_EXTENT_BYTES;candidate <= MAX_EXTENT_BYTES; candidate += ARENA_HEADER_BYTES) {
candidates.add(candidate);
}
}
const matchingExtentSize = [...candidates].find((candidate) => isPrefix(bytes, encodeArenaHeader({ extentSize: candidate })));
if (matchingExtentSize === undefined) {
throw new CorruptStoreError("unactivated arena is not canonical bootstrap residue");
}
return partialExtentSize ?? matchingExtentSize;
}
function detectArenaVersion(handle, size) {
if (size < ARENA_HEADER_BYTES)
return;
const bytes = readExact(handle, 0n, ARENA_HEADER_BYTES, ARENA_HEADER_BYTES, "arena.inspect.identity");
try {
decodeArenaHeader(bytes);
} catch (cause) {
if (cause instanceof StoreRecreationRequiredError)
throw cause;
if (!(cause instanceof CorruptStoreError))
throw cause;
}
}
function detectCompleteMetadataVersion(handle, size, label) {
if (size < METADATA_HEADER_BYTES)
return;
let header;
try {
header = inspectMetadataBaseHeader(readExact(handle, 0n, METADATA_HEADER_BYTES, METADATA_HEADER_BYTES, label));
} catch (cause) {
if (cause instanceof CorruptStoreError)
return;
throw cause;
}
if (header.baseEnd > BigInt(size))
return;
const base = readExact(handle, 0n, Number(header.baseEnd), METADATA_HEADER_BYTES + MAX_METADATA_BASE_READER_BYTES, `${label}.base`);
try {
decodeMetadataBase(base);
} catch (cause) {
if (cause instanceof StoreRecreationRequiredError)
throw cause;
if (!(cause instanceof CorruptStoreError))
throw cause;
}
}
function classifyBootstrapResidue(handles, sizes, configuredExtentSize) {
const extentSize = bootstrapExtentSize(handles["arena.bin"], sizes["arena.bin"], configuredExtentSize);
const initialA = createInitialState(extentSize);
const baseA = encodeMetadataBase(initialA);
const initialB = createInitialState(extentSize);
initialB.activeMetadataFile = "b";
initialB.retainedGenerations = { a: null, b: 1n };
const baseB = encodeMetadataBase(initialB);
for (const [name, canonical] of [
["metadata-a.bin", baseA],
["metadata-b.bin", baseB]
]) {
const size = sizes[name];
detectCompleteMetadataVersion(handles[name], size, `${name}.inspect.bootstrap`);
if (size > canonical.byteLength) {
throw new CorruptStoreError(`${name} exceeds the empty-root bootstrap envelope`);
}
const bytes = readWhole(handles[name], size, canonical.byteLength, `${name}.read.bootstrap`);
if (!isPrefix(bytes, canonical)) {
throw new CorruptStoreError(`${name} is not canonical bootstrap residue`);
}
}
if (sizes["activation.bin"] > ACTIVATION_SLOT_BYTES * 2) {
throw new CorruptStoreError("unactivated manifest exceeds its fixed envelope");
}
const initialSlot = encodeActivationSlot({
sequence: 1n,
metadataFile: "a",
generation: 1n,
baseEnd: BigInt(baseA.byteLength),
baseDigest: metadataBaseDigest(baseA)
});
const canonicalManifest = new Uint8Array(ACTIVATION_SLOT_BYTES * 2);
canonicalManifest.set(initialSlot);
const manifest = readWhole(handles["activation.bin"], sizes["activation.bin"], canonicalManifest.byteLength, "activation.read.bootstrap");
if (!isPrefix(manifest, canonicalManifest)) {
throw new CorruptStoreError("manifest is not canonical bootstrap residue");
}
if (sizes["arena.bin"] === ARENA_HEADER_BYTES && configuredExtentSize !== undefined && configuredExtentSize !== extentSize) {
throw new ExtentSizeMismatchError(configuredExtentSize, extentSize);
}
return sizes["arena.bin"] < ARENA_HEADER_BYTES && configuredExtentSize !== undefined ? configuredExtentSize : extentSize;
}
function bootstrap(handles, extentSize) {
const state = createInitialState(extentSize);
const arenaBytes = encodeArenaHeader({ extentSize });
const baseBytes = encodeMetadataBase(state);
const activationBytes = encodeActivationSlot({
sequence: 1n,
metadataFile: "a",
generation: 1n,
baseEnd: BigInt(baseBytes.byteLength),
baseDigest: metadataBaseDigest(baseBytes)
});
const arena = handles["arena.bin"];
arena.truncate(0, "bootstrap.arena.reset");
writeExact(arena, 0n, arenaBytes, "bootstrap.arena.write");
arena.flush("bootstrap.arena.flush");
const metadataA = handles["metadata-a.bin"];
metadataA.truncate(0, "bootstrap.metadata-a.reset");
writeExact(metadataA, 0n, baseBytes, "bootstrap.metadata-a.write");
metadataA.flush("bootstrap.metadata-a.flush");
handles["metadata-b.bin"].truncate(0, "bootstrap.metadata-b.reset");
const activation = handles["activation.bin"];
activation.truncate(0, "bootstrap.activation.reset");
writeExact(activation, 0n, activationBytes, "bootstrap.activation.write");
activation.flush("bootstrap.activation.flush");
const decoded = decodeMetadataBase(baseBytes);
return {
handles,
state: decoded.state,
activeLogEnd: decoded.baseEnd,
nextSequence: 1n,
activeLogBytes: 0,
activeLogFrames: 0,
activationSequence: 1n,
arenaHighWaterExtents: 0n,
arenaSize: arenaBytes.byteLength,
arenaDirty: false,
activeMetadataDirty: false
};
}
function metadataHandle(handles, file) {
return handles[metadataFileName(file)];
}
function verifyInactiveVersion(handles, sizes, selectedFile) {
const inactiveName = metadataFileName(otherMetadataFile(selectedFile));
detectCompleteMetadataVersion(handles[inactiveName], sizes[inactiveName], `${inactiveName}.inspect.inactive`);
}
function readSelectedBase(handle, fileSize, selected) {
if (fileSize < METADATA_HEADER_BYTES)
throw new CorruptStoreError("selected metadata base is missing or short");
const headerBytes = readExact(handle, 0n, METADATA_HEADER_BYTES, METADATA_HEADER_BYTES, "metadata.read.header");
const header = inspectMetadataBaseHeader(headerBytes);
if (header.baseEnd !== selected.baseEnd || header.baseEnd > BigInt(fileSize)) {
throw new CorruptStoreError("selected metadata base end does not match activation");
}
const baseBytes = readExact(handle, 0n, Number(header.baseEnd), METADATA_HEADER_BYTES + MAX_METADATA_BASE_READER_BYTES, "metadata.read.base");
const decoded = decodeMetadataBase(baseBytes);
if (decoded.state.activeMetadataFile !== selected.metadataFile || decoded.state.generation !== selected.generation || decoded.baseEnd !== selected.baseEnd || decoded.baseDigest !== selected.baseDigest) {
throw new CorruptStoreError("selected metadata base identity does not match activation");
}
return { ...decoded, header };
}
function requiredArenaEnd(state) {
return checkedAdd(BigInt(ARENA_HEADER_BYTES), checkedMultiply(state.allocator.totalExtents, BigInt(state.extentSize), "required arena bytes"), "required arena end");
}
function replayLog(handle, fileSize, state, baseEnd) {
let cursor = checkedSafeNumber(baseEnd, "active log start");
let sequence = 1n;
let frameCount = 0;
while (cursor < fileSize && frameCount < MAX_ACTIVE_LOG_FRAMES) {
const logBytes = cursor - Number(baseEnd);
const remaining = fileSize - cursor;
if (remaining < TXN_FRAME_HEADER_BYTES || logBytes + TXN_FRAME_HEADER_BYTES > MAX_ACTIVE_LOG_BYTES)
break;
const headerBytes = readExact(handle, BigInt(cursor), TXN_FRAME_HEADER_BYTES, TXN_FRAME_HEADER_BYTES, "metadata.log.read-header");
let inspected;
try {
inspected = inspectTxnFrameHeader(headerBytes);
} catch (cause) {
if (cause instanceof CorruptStoreError)
break;
throw cause;
}
if (inspected.frameBytes > remaining || logBytes + inspected.frameBytes > MAX_ACTIVE_LOG_BYTES)
break;
const frameBytes = readExact(handle, BigInt(cursor), inspected.frameBytes, TXN_FRAME_HEADER_BYTES + inspected.payloadLength, "metadata.log.read-frame");
try {
const frame = decodeTxnFrame(frameBytes);
if (frame.generation !== state.generation || frame.sequence !== sequence)
break;
applyTxn(state, frame.record);
} catch (cause) {
if (cause instanceof CorruptStoreError || cause instanceof FsError || cause instanceof StoreLimitError) {
break;
}
throw cause;
}
cursor += inspected.frameBytes;
sequence += 1n;
frameCount += 1;
}
validateState(state);
return { validEnd: BigInt(cursor), nextSequence: sequence, requiredArenaEnd: requiredArenaEnd(state) };
}
function recoverActivated(handles, sizes, leftBytes, rightBytes, configuredExtentSize) {
if (sizes["arena.bin"] < ARENA_HEADER_BYTES) {
throw new CorruptStoreError("activated arena header is missing or short");
}
const arenaHeader = decodeArenaHeader(readExact(handles["arena.bin"], 0n, ARENA_HEADER_BYTES, ARENA_HEADER_BYTES, "arena.read.header"));
const selection = selectActivationPair(leftBytes, rightBytes);
verifyInactiveVersion(handles, sizes, selection.selected.metadataFile);
const selectedHandle = metadataHandle(handles, selection.selected.metadataFile);
const selectedName = selectedHandle.name;
const decoded = readSelectedBase(selectedHandle, sizes[selectedName], selection.selected);
if (decoded.state.extentSize !== arenaHeader.extentSize || decoded.header.limitsProfileVersion !== arenaHeader.limitsProfileVersion) {
throw new CorruptStoreError("arena and metadata identities disagree");
}
if (selection.other !== null) {
decoded.state.retainedGenerations[selection.other.metadataFile] = selection.other.generation;
}
try {
validateState(decoded.state);
} catch (cause) {
if (cause instanceof StoreLimitError) {
throw new CorruptStoreError(`activation retained-generation facts are invalid: ${cause.message}`, { cause });
}
throw cause;
}
const replayed = replayLog(selectedHandle, sizes[selectedName], decoded.state, decoded.baseEnd);
if (configuredExtentSize !== undefined && configuredExtentSize !== arenaHeader.extentSize) {
throw new ExtentSizeMismatchError(configuredExtentSize, arenaHeader.extentSize);
}
const requiredEnd = checkedSafeNumber(replayed.requiredArenaEnd, "required arena end");
const arenaDirty = sizes["arena.bin"] < requiredEnd;
if (arenaDirty) {
truncateChecked(handles["arena.bin"], replayed.requiredArenaEnd, "recovery.arena.grow");
}
const activeMetadataDirty = replayed.validEnd < BigInt(sizes[selectedName]);
if (activeMetadataDirty) {
truncateChecked(selectedHandle, replayed.validEnd, "recovery.metadata.discard-suffix");
}
const physicalArenaExtents = (BigInt(Math.max(0, sizes["arena.bin"] - ARENA_HEADER_BYTES)) + BigInt(arenaHeader.extentSize) - 1n) / BigInt(arenaHeader.extentSize);
return {
handles,
state: decoded.state,
activeLogEnd: replayed.validEnd,
nextSequence: replayed.nextSequence,
activeLogBytes: Number(replayed.validEnd - decoded.baseEnd),
activeLogFrames: Number(replayed.nextSequence - 1n),
activationSequence: selection.selected.sequence,
arenaHighWaterExtents: physicalArenaExtents > decoded.state.allocator.totalExtents ? physicalArenaExtents : decoded.state.allocator.totalExtents,
arenaSize: arenaDirty ? requiredEnd : sizes["arena.bin"],
arenaDirty,
activeMetadataDirty
};
}
async function openMetadataStore(port, options) {
const configuredExtentSize = options.extentSize;
if (configuredExtentSize !== undefined)
validateExtentSize(configuredExtentSize);
const handles = await acquireHandles(port);
try {
const sizes = getSizes(handles);
const leftBytes = readSlot(handles["activation.bin"], sizes["activation.bin"], 0);
const rightBytes = readSlot(handles["activation.bin"], sizes["activation.bin"], 1);
detectArenaVersion(handles["arena.bin"], sizes["arena.bin"]);
detectCompleteMetadataVersion(handles["metadata-a.bin"], sizes["metadata-a.bin"], "metadata-a.bin.inspect.identity");
detectCompleteMetadataVersion(handles["metadata-b.bin"], sizes["metadata-b.bin"], "metadata-b.bin.inspect.identity");
const left = tryDecodeActivationSlot(leftBytes);
const right = tryDecodeActivationSlot(rightBytes);
if (sizes["activation.bin"] > ACTIVATION_SLOT_BYTES * 2) {
throw new CorruptStoreError("manifest exceeds its fixed envelope");
}
if (left !== null || right !== null) {
return recoverActivated(handles, sizes, leftBytes, rightBytes, configuredExtentSize);
}
const extentSize = classifyBootstrapResidue(handles, sizes, configuredExtentSize);
return bootstrap(handles, extentSize);
} catch (cause) {
closeHandles(Object.values(handles));
throw cause;
}
}
// packages/pgwasm/src/opfs/core/repacked-vfs.ts
var REPACK_INTERVAL_MS = 30000;
var AMORTIZED_ARENA_FLUSH_BYTES = 4 * 1024 * 1024;
var BASE_REPACK_PRESSURE_BYTES = Math.floor(MAX_METADATA_BASE_WRITER_BYTES * 0.75);
var QUARANTINE_PRESSURE_EXTENTS = 64;
class ArenaGrowthQuotaError extends Error {
cause;
constructor(cause) {
super("arena growth exhausted the storage quota");
this.name = "ArenaGrowthQuotaError";
this.cause = cause;
}
}
function isQuotaExceeded(cause) {
return typeof cause === "object" && cause !== null && "name" in cause && cause.name === "QuotaExceededError";
}
function mergeExtentRuns(left, right) {
const merged = left.map((run) => ({ ...run }));
for (const run of right) {
const last = merged.at(-1);
if (last !== undefined && last.start + BigInt(last.count) === run.start && last.count + run.count <= 4294967295) {
last.count += run.count;
} else {
merged.push({ ...run });
}
}
return merged;
}
class RepackedVfs {
#store;
#status = "open";
#failure;
#descriptors = new Map;
#nextDescriptor = 3;
#pendingResizeCommit;
#arenaSize;
#arenaDirty;
#arenaDirtyBytesSinceFlush = 0;
#metadataDirtySinceFlush;
#repacking = false;
#lastRepackReason = null;
#lastRepackDurationMs = null;
#pendingRepackReason = null;
#repackCount = 0;
#lastRepackAtMs = Date.now();
#flushes = { amortized: 0, arena: 0, metadata: 0, zeroBarrier: 0, manifest: 0 };
constructor(store) {
this.#store = store;
this.#arenaSize = store.arenaSize;
this.#arenaDirty = store.arenaDirty;
this.#metadataDirtySinceFlush = store.activeMetadataDirty;
}
static async open(port, options = {}) {
return new RepackedVfs(await openMetadataStore(port, options));
}
strictSync() {
this.#assertOpen();
try {
if (this.#arenaDirty)
this.#flush(this.#store.handles["arena.bin"], "sync.arena.flush", "arena");
this.#materializePendingResize();
if (this.#metadataDirtySinceFlush) {
const activeName = metadataFileName(this.#store.state.activeMetadataFile);
this.#flush(this.#store.handles[activeName], "sync.metadata.flush", "metadata");
}
this.#arenaDirty = false;
this.#arenaDirtyBytesSinceFlush = 0;
this.#metadataDirtySinceFlush = false;
} catch (cause) {
this.#poison(cause);
throw cause;
}
}
assertHealthy() {
this.#assertOpen();
}
fail(cause) {
this.#assertOpen();
this.#poison(cause);
throw cause;
}
cleanupFailedInit() {
if (this.#status === "closed")
return;
this.#status = "closed";
this.#descriptors.clear();
const closeError = this.#closeHandles();
if (closeError !== undefined)
throw closeError;
}
metrics() {
this.#assertOpen();
return {
generation: this.#store.state.generation,
totalExtents: this.#store.state.allocator.totalExtents,
availableExtents: this.#store.state.allocator.available.size,
quarantineExtents: this.#store.state.allocator.quarantine.size,
quarantineBytes: BigInt(this.#store.state.allocator.quarantine.size) * BigInt(this.#store.state.extentSize),
activeLogBytes: this.#store.activeLogBytes,
activeLogFrames: this.#store.activeLogFrames,
lastRepackReason: this.#lastRepackReason,
lastRepackDurationMs: this.#lastRepackDurationMs,
repackCount: this.#repackCount,
pendingRepackReason: this.#pendingRepackReason,
flushes: { ...this.#flushes }
};
}
repack(reason = "manual") {
this.#assertOpen();
if (this.#repacking)
throw new Error("OPFS repacked VFS repack is already running");
checkedAdd(this.#store.state.generation, 1n, "generation");
const activationSequence = checkedAdd(this.#store.activationSequence, 1n, "activation sequence");
this.#repacking = true;
const startedAt = performance.now();
try {
this.#materializePendingResize();
const pinned = new Set;
for (const descriptor of this.#descriptors.values()) {
if (descriptor.orphan === undefined)
continue;
for (const extentId of descriptor.orphan.extents)
pinned.add(extentId);
}
const projected = projectRepackForActivation(this.#store.state, pinned);
const base = encodeValidatedMetadataBase(projected);
const activation = encodeActivationSlot({
sequence: activationSequence,
metadataFile: projected.activeMetadataFile,
generation: projected.generation,
baseEnd: BigInt(base.byteLength),
baseDigest: metadataBaseDigest(base)
});
this.#flush(this.#store.handles["arena.bin"], "repack.arena.flush", "arena");
this.#arenaDirty = false;
this.#arenaDirtyBytesSinceFlush = 0;
const inactiveName = metadataFileName(projected.activeMetadataFile);
const inactive = this.#store.handles[inactiveName];
inactive.truncate(0, "repack.metadata.reset");
writeExact(inactive, 0n, base, "repack.metadata.write");
this.#flush(inactive, "repack.metadata.flush", "metadata");
const activationOffset = (activationSequence - 1n) % 2n * BigInt(ACTIVATION_SLOT_BYTES);
try {
writeExact(this.#store.handles["activation.bin"], activationOffset, activation, "repack.activation.write");
this.#flush(this.#store.handles["activation.bin"], "repack.activation.flush", "manifest");
} catch (cause) {
this.#poison(cause);
throw cause;
}
try {
this.#store.state = projected;
this.#store.activeLogEnd = BigInt(base.byteLength);
this.#store.nextSequence = 1n;
this.#store.activeLogBytes = 0;
this.#store.activeLogFrames = 0;
this.#store.activationSequence = activationSequence;
this.#metadataDirtySinceFlush = false;
this.#lastRepackReason = reason;
this.#pendingRepackReason = null;
this.#repackCount += 1;
this.#lastRepackAtMs = Date.now();
this.#scheduleRepack();
} catch (cause) {
this.#poison(cause);
throw cause;
}
const requiredArenaEnd = checkedAdd(BigInt(ARENA_HEADER_BYTES), checkedMultiply(projected.allocator.totalExtents, BigInt(projected.extentSize), "arena byte length"), "arena byte length");
try {
if (BigInt(this.#arenaSizeChecked("repack.arena.size-before-trim")) > requiredArenaEnd) {
this.#arenaDirty = true;
this.#truncateArena(requiredArenaEnd, "repack.arena.trim");
}
} catch {}
this.#lastRepackDurationMs = performance.now() - startedAt;
} finally {
this.#repacking = false;
}
}
runScheduledRepack(nowMs = Date.now()) {
this.#assertOpen();
if (!Number.isSafeInteger(nowMs) || nowMs < 0)
throw new TypeError("repack scheduler time is invalid");
this.#materializePendingResize();
if (this.#arenaDirtyBytesSinceFlush >= AMORTIZED_ARENA_FLUSH_BYTES) {
try {
this.#flush(this.#store.handles["arena.bin"], "sync.arena.amortized.flush", "amortized");
this.#arenaDirty = false;
this.#arenaDirtyBytesSinceFlush = 0;
} catch (cause) {
this.#poison(cause);
throw cause;
}
}
const reason = this.#dueRepackReason(nowMs);
if (reason === null)
return false;
this.repack(reason);
this.#lastRepackAtMs = nowMs;
return true;
}
resolvePath(path, follow = true) {
this.#assertOpen();
return this.#resolve(path, follow);
}
stat(requestPath) {
this.#assertOpen();
return this.#statValue(getInodeAtPath(this.#store.state, this.#resolve(requestPath, true)));
}
lstat(requestPath) {
this.#assertOpen();
return this.#statValue(getInodeAtPath(this.#store.state, this.#resolve(requestPath, false)));
}
symlink(target, requestPath, nowMs) {
this.#assertOpen();
const path = this.#resolve(requestPath, false);
this.#commit(planSymlink(this.#store.state, path, target, { nowMs }).record);
}
readlink(requestPath) {
this.#assertOpen();
const path = this.#resolve(requestPath, false);
const inode = getInodeAtPath(this.#store.state, path);
if (inode.kind !== "symlink")
throw new FsError("EINVAL", "path is not a symbolic link", { path });
return inode.target;
}
readdir(requestPath) {
this.#assertOpen();
const path = this.#resolve(requestPath, true);
const inode = getInodeAtPath(this.#store.state, path);
if (inode.kind !== "directory")
throw new FsError("ENOTDIR", "path is not a directory", { path });
return [...inode.children.keys()].sort();
}
mkdir(requestPath, options) {
this.#assertOpen();
this.#commit(planMkdir(this.#store.state, this.#resolve(requestPath, false), options).record);
}
writeFile(requestPath, data, options) {
this.#assertOpen();
const path = this.#resolve(requestPath, true);
const flags = this.#parseFlags(options.flag ?? "w");
if (!flags.writable)
throw new FsError("EBADF", "writeFile flags are not writable", { operation: "writeFile", path });
let bytes;
if (typeof data === "string") {
this.#validateEncoding(options.encoding ?? "utf8", path);
bytes = new TextEncoder().encode(data);
} else {
bytes = data;
}
checkedU64(options.nowMs, "timestamp");
let existing;
try {
const inode = getInodeAtPath(this.#store.state, path);
if (inode.kind !== "file")
throw new FsError("EISDIR", "path is a directory", { path });
existing = inode;
} catch (cause) {
if (!(cause instanceof FsError) || cause.code !== FS_ERRNO.ENOENT)
throw cause;
}
if (existing !== undefined && flags.create && flags.exclusive) {
throw new FsError("EEXIST", "path already exists", { operation: "writeFile", path });
}
if (existing === undefined && !flags.create) {
throw new FsError("ENOENT", "path does not exist", { operation: "writeFile", path });
}
const wasExisting = existing !== undefined;
if (bytes.byteLength === 0 && existing !== undefined && !flags.truncate)
return;
const buildPlan = () => {
let current;
if (wasExisting) {
const inode = getInodeAtPath(this.#store.state, path);
if (inode.kind !== "file")
throw new FsError("EISDIR", "path is a directory", { path });
current = inode;
}
const currentStart = current !== undefined && flags.append ? current.size : 0n;
const targetSize = current === undefined ? BigInt(bytes.byteLength) : flags.append ? checkedAdd(current.size, BigInt(bytes.byteLength), "writeFile size") : flags.truncate ? BigInt(bytes.byteLength) : current.size > BigInt(bytes.byteLength) ? current.size : BigInt(bytes.byteLength);
const currentPlan = current === undefined ? planCreateFile(this.#store.state, path, {
...options.mode === undefined ? {} : { mode: options.mode },
nowMs: options.nowMs,
size: targetSize
}) : planResizeFile(this.#store.state, path, targetSize, options.nowMs, "write");
return {
existing: current,
start: currentStart,
plan: currentPlan,
preparedCommit: this.#preflightCommit(currentPlan.record),
allocated: expandExtentRuns(currentPlan.record.kind === "createFile" ? currentPlan.record.extents : currentPlan.record.allocated, MAX_EXTENTS_PER_INODE)
};
};
const prepared = this.#prepareAllocationWithQuotaRetry(buildPlan(), buildPlan);
existing = prepared.existing;
const { start, plan, preparedCommit, allocated } = prepared;
if (bytes.byteLength === 0) {
this.#commit(plan.record, preparedCommit);
return;
}
const progress = this.#writeLogical(existing?.extents ?? [], start, bytes, "arena.write-file", allocated);
if (progress.bytes === 0)
this.#failStore(progress.error);
const partialSize = existing !== undefined && !flags.truncate ? existing.size > checkedAdd(start, BigInt(progress.bytes), "partial writeFile size") ? existing.size : checkedAdd(start, BigInt(progress.bytes), "partial writeFile size") : BigInt(progress.bytes);
const committedPlan = progress.bytes === bytes.byteLength ? plan : existing === undefined ? planCreateFile(this.#store.state, path, {
...options.mode === undefined ? {} : { mode: options.mode },
nowMs: options.nowMs,
size: partialSize
}) : planResizeFile(this.#store.state, path, partialSize, options.nowMs, "write");
if (progress.error === undefined || partialSize !== existing?.size) {
this.#commit(committedPlan.record, committedPlan === plan ? preparedCommit : undefined);
}
if (progress.error !== undefined)
throw progress.error;
}
readFile(requestPath) {
this.#assertOpen();
const path = this.#resolve(requestPath, true);
const inode = getInodeAtPath(this.#store.state, path);
if (inode.kind !== "file")
throw new FsError("EISDIR", "path is a directory", { path });
const output = new Uint8Array(checkedSafeNumber(inode.size, "file read size"));
this.#readLogical(inode.extents, 0n, output);
return output;
}
truncate(requestPath, size, nowMs) {
this.#assertOpen();
const path = this.#resolve(requestPath, true);
let inode = getInodeAtPath(this.#store.state, path);
if (inode.kind !== "file")
throw new FsError("EISDIR", "path is a directory", { path });
if (size === inode.size)
return;
let plan = planResizeFile(this.#store.state, path, size, nowMs, "truncate");
let preparedCommit = this.#preflightCommit(plan.record);
if (size > inode.size) {
const buildPlan = () => {
const current = getInodeAtPath(this.#store.state, path);
if (current.kind !== "file")
throw new FsError("EISDIR", "path is a directory", { path });
const currentPlan = planResizeFile(this.#store.state, path, size, nowMs, "truncate");
return {
inode: current,
plan: currentPlan,
preparedCommit: this.#preflightCommit(currentPlan.record),
allocated: expandExtentRuns(currentPlan.record.allocated, MAX_EXTENTS_PER_INODE),
arenaSizeBefore: this.#arenaSizeChecked("arena.size.before-extension")
};
};
const prepared = this.#prepareAllocationWithQuotaRetry({
inode,
plan,
preparedCommit,
allocated: expandExtentRuns(plan.record.allocated, MAX_EXTENTS_PER_INODE),
arenaSizeBefore: this.#arenaSizeChecked("arena.size.before-extension")
}, buildPlan);
inode = prepared.inode;
plan = prepared.plan;
preparedCommit = prepared.preparedCommit;
const touchedExisting = this.#zeroLogical(inode.extents, inode.size, size - inode.size, prepared.arenaSizeBefore, prepared.allocated);
if (touchedExisting)
this.#flushPreparedArena("arena.gap-zero.flush");
}
this.#commit(plan.record, preparedCommit);
}
open(requestPath, flags = "r", mode = 33206, nowMs = 0n) {
this.#assertOpen();
const path = this.#resolve(requestPath, true);
const parsed = this.#parseFlags(flags);
let inode;
try {
const found = getInodeAtPath(this.#store.state, path);
if (found.kind !== "file")
throw new FsError("EISDIR", "path is a directory", { path });
if (parsed.exclusive && parsed.create)
throw new FsError("EEXIST", "path already exists", { path });
inode = found;
if (parsed.truncate) {
this.truncate(path, 0n, nowMs);
inode = getInodeAtPath(this.#store.state, path);
}
} catch (cause) {
if (!(cause instanceof FsError) || cause.code !== FS_ERRNO.ENOENT || !parsed.create)
throw cause;
this.#commit(planCreateFile(this.#store.state, path, { mode, nowMs, size: 0n }).record);
inode = getInodeAtPath(this.#store.state, path);
}
const fd = this.#nextDescriptor++;
this.#descriptors.set(fd, {
inodeId: inode.id,
offset: parsed.append ? inode.size : 0n,
readable: parsed.readable,
writable: parsed.writable,
append: parsed.append
});
return fd;
}
fstat(fd) {
this.#assertOpen();
const descriptor = this.#descriptor(fd);
const inode = descriptor.orphan ?? this.#linkedFile(descriptor.inodeId);
return this.#statValue(inode);
}
read(fd, buffer, offset, length, position) {
this.#assertOpen();
this.#validateBufferRange(buffer, offset, length);
const descriptor = this.#descriptor(fd);
if (!descriptor.readable)
throw new FsError("EBADF", "descriptor is not readable");
const inode = descriptor.orphan ?? this.#linkedFile(descriptor.inodeId);
const start = position ?? descriptor.offset;
if (start < 0n)
throw new FsError("EINVAL", "read position is negative");
const available = start >= inode.size ? 0 : Math.min(length, checkedSafeNumber(inode.size - start, "read length"));
if (available > 0)
this.#readLogical(inode.extents, start, buffer.subarray(offset, offset + available));
if (position === undefined)
descriptor.offset = start + BigInt(available);
return available;
}
write(fd, buffer, offset, length, position, nowMs) {
this.#assertOpen();
this.#validateBufferRange(buffer, offset, length);
checkedU64(nowMs, "timestamp");
const descriptor = this.#descriptor(fd);
if (!descriptor.writable)
throw new FsError("EBADF", "descriptor is not writable");
if (descriptor.orphan !== undefined) {
return this.#writeOrphan(descriptor, buffer.subarray(offset, offset + length), position, nowMs);
}
let inode = this.#linkedFile(descriptor.inodeId);
const start = descriptor.append ? inode.size : position ?? descriptor.offset;
if (start < 0n)
throw new FsError("EINVAL", "write position is negative");
if (length === 0)
return 0;
const requestedEnd = checkedAdd(start, BigInt(length), "write end");
let admittedLength = length;
let allocated = [];
let plan;
let preparedCommit;
if (requestedEnd > inode.size) {
const buildPlan = () => {
const current = this.#linkedFile(descriptor.inodeId);
const admitted = this.#planLinkedWrite(current, start, length, nowMs);
return {
inode: current,
admitted,
allocated: admitted.plan === undefined ? [] : expandExtentRuns(admitted.plan.record.allocated, MAX_EXTENTS_PER_INODE),
arenaSizeBefore: start > current.size ? this.#arenaSizeChecked("arena.size.before-write-extension") : 0
};
};
const prepared = this.#prepareAllocationWithQuotaRetry(buildPlan(), buildPlan);
inode = prepared.inode;
plan = prepared.admitted.plan;
preparedCommit = prepared.admitted.preparedCommit;
admittedLength = prepared.admitted.length;
if (plan !== undefined) {
allocated = prepared.allocated;
if (start > inode.size) {
const touchedExisting = this.#zeroLogical(inode.extents, inode.size, start - inode.size, prepared.arenaSizeBefore, allocated);
if (touchedExisting)
this.#flushPreparedArena("arena.gap-zero.flush");
}
}
}
const source = buffer.subarray(offset, offset + admittedLength);
const progress = this.#writeLogical(inode.extents, start, source, "arena.write", allocated);
if (progress.bytes === 0)
this.#failStore(progress.error);
const completedEnd = checkedAdd(start, BigInt(progress.bytes), "completed write end");
if (completedEnd > inode.size) {
const committedPlan = plan !== undefined && completedEnd === plan.record.size ? plan : planResizeFileForInode(this.#store.state, inode, completedEnd, nowMs, "write");
this.#commit(committedPlan.record, committedPlan === plan ? preparedCommit : undefined);
}
if (position === undefined)
descriptor.offset = completedEnd;
return progress.bytes;
}
chmod(requestPath, mode, nowMs) {
this.#assertOpen();
this.#commit(planChmod(this.#store.state, this.#resolve(requestPath, true), mode, nowMs).record);
}
utimes(requestPath, atimeMs, mtimeMs, ctimeMs) {
this.#assertOpen();
const path = this.#resolve(requestPath, true);
this.#commit(planUtimes(this.#store.state, path, atimeMs, mtimeMs, ctimeMs).record);
}
unlink(requestPath, nowMs) {
this.#assertOpen();
const path = this.#resolve(requestPath, false);
const inode = getInodeAtPath(this.#store.state, path);
if (inode.kind === "directory")
throw new FsError("EISDIR", "path is a directory", { path });
const affected = inode.kind === "file" ? [...this.#descriptors.values()].filter((descriptor) => descriptor.orphan === undefined && descriptor.inodeId === inode.id) : [];
const orphan = inode.kind === "file" && affected.length > 0 ? makeOrphanRecord(inode) : undefined;
this.#commit(planUnlink(this.#store.state, path, nowMs).record);
if (orphan !== undefined)
for (const descriptor of affected)
descriptor.orphan = orphan;
}
rmdir(requestPath, nowMs) {
this.#assertOpen();
this.#commit(planRmdir(this.#store.state, this.#resolve(requestPath, false), nowMs).record);
}
rename(oldRequestPath, newRequestPath, nowMs) {
this.#assertOpen();
const oldPath = this.#resolve(oldRequestPath, false);
const newPath = this.#resolve(newRequestPath, false);
let destination;
try {
const inode = getInodeAtPath(this.#store.state, newPath);
if (inode.kind === "file")
destination = inode;
} catch (cause) {
if (!(cause instanceof FsError) || cause.code !== FS_ERRNO.ENOENT)
throw cause;
}
const affected = destination === undefined ? [] : [...this.#descriptors.values()].filter((descriptor) => descriptor.orphan === undefined && descriptor.inodeId === destination.id);
const orphan = destination !== undefined && affected.length > 0 ? makeOrphanRecord(destination) : undefined;
this.#commit(planRename(this.#store.state, oldPath, newPath, nowMs).record);
if (orphan !== undefined)
for (const descriptor of affected)
descriptor.orphan = orphan;
}
close(fd) {
if (fd !== undefined) {
this.#assertOpen();
if (!this.#descriptors.delete(fd))
throw new FsError("EBADF", "descriptor is invalid");
return;
}
if (this.#status === "closed")
return;
let firstError;
if (this.#status === "open") {
try {
this.strictSync();
} catch (cause) {
firstError = cause;
}
} else {
firstError = new StoreFailedError(this.#failure);
}
this.#status = "closed";
this.#descriptors.clear();
const closeError = this.#closeHandles();
firstError ??= closeError;
if (firstError !== undefined)
throw firstError;
}
#closeHandles() {
let firstError;
for (const name of ["metadata-b.bin", "metadata-a.bin", "arena.bin", "activation.bin"]) {
try {
this.#store.handles[name].close();
} catch (cause) {
firstError ??= cause;
}
}
return firstError;
}
#prepareAllocated(extents) {
if (extents.length === 0)
return;
const totalBefore = this.#store.state.allocator.totalExtents;
const highWaterBefore = this.#store.arenaHighWaterExtents;
const fresh = extents.filter((extentId) => extentId >= totalBefore);
const arenaSizeBefore = this.#arenaSizeChecked("arena.size.before-allocation");
if (fresh.length > 0) {
const last = fresh[fresh.length - 1];
const requiredExtents = checkedAdd(last, 1n, "arena extent count");
const requiredEnd = checkedAdd(BigInt(ARENA_HEADER_BYTES), checkedMultiply(requiredExtents, BigInt(this.#store.state.extentSize), "arena byte length"), "arena byte length");
if (requiredEnd > BigInt(arenaSizeBefore)) {
this.#arenaDirty = true;
if (requiredExtents > this.#store.arenaHighWaterExtents) {
this.#store.arenaHighWaterExtents = requiredExtents;
}
try {
this.#truncateArena(requiredEnd, "arena.grow");
} catch (cause) {
if (isQuotaExceeded(cause))
throw new ArenaGrowthQuotaError(cause);
throw cause;
}
}
}
const reused = extents.filter((extentId) => extentId < totalBefore || extentId < highWaterBefore);
if (reused.length === 0)
return;
const zeros = new Uint8Array(Math.min(this.#store.state.extentSize, 64 * 1024));
for (const extentId of reused) {
let within = 0;
while (within < this.#store.state.extentSize) {
const count = Math.min(zeros.byteLength, this.#store.state.extentSize - within);
this.#arenaDirty = true;
this.#writeArenaExact(arenaByteOffset(extentId, this.#store.state.extentSize, within), zeros.subarray(0, count), "arena.allocation.zero");
this.#recordArenaWrite(count);
within += count;
}
}
this.#flushPreparedArena("arena.allocation.flush");
}
#readLogical(extents, position, target) {
checkedU64(position, "read position");
let completed = 0;
while (completed < target.byteLength) {
const logical = checkedAdd(position, BigInt(completed), "read position");
const extentIndex = checkedSafeNumber(logical / BigInt(this.#store.state.extentSize), "extent index");
const extentId = this.#extentAt(extents, [], extentIndex);
if (extentId === undefined)
throw new CorruptStoreError("file metadata does not cover its visible data");
const within = Number(logical % BigInt(this.#store.state.extentSize));
const count = Math.min(target.byteLength - completed, this.#store.state.extentSize - within);
let extentCompleted = 0;
while (extentCompleted < count) {
const read = this.#store.handles["arena.bin"].read(target.subarray(completed + extentCompleted, completed + count), checkedSafeNumber(arenaByteOffset(extentId, this.#store.state.extentSize, within + extentCompleted), "arena read offset"), "arena.read");
if (!Number.isSafeInteger(read) || read <= 0 || read > count - extentCompleted) {
throw new CorruptStoreError("arena data ended before the visible file range");
}
extentCompleted += read;
}
completed += count;
}
}
#writeLogical(extents, position, source, label, allocated = []) {
checkedU64(position, "write position");
let completed = 0;
while (completed < source.byteLength) {
const logical = checkedAdd(position, BigInt(completed), "write position");
const extentIndex = checkedSafeNumber(logical / BigInt(this.#store.state.extentSize), "extent index");
const extentId = this.#extentAt(extents, allocated, extentIndex);
if (extentId === undefined)
throw new StoreLimitError("planned extents do not cover the write range");
const within = Number(logical % BigInt(this.#store.state.extentSize));
const requested = Math.min(source.byteLength - completed, this.#store.state.extentSize - within);
try {
this.#arenaDirty = true;
const written = this.#writeArena(source.subarray(completed, completed + requested), checkedSafeNumber(arenaByteOffset(extentId, this.#store.state.extentSize, within), "arena write offset"), label);
if (!Number.isSafeInteger(written) || written <= 0 || written > requested) {
throw new PortWriteError(label);
}
this.#recordArenaWrite(written);
completed += written;
} catch (error) {
return { bytes: completed, error };
}
}
return { bytes: completed };
}
#zeroLogical(extents, position, length, arenaSizeBefore, allocated = []) {
checkedU64(position, "zero position");
checkedU64(length, "zero length");
const zeros = new Uint8Array(64 * 1024);
const previousEnd = BigInt(arenaSizeBefore);
let completed = 0n;
let touchedExisting = false;
while (completed < length) {
const logical = checkedAdd(position, completed, "zero position");
const extentIndex = checkedSafeNumber(logical / BigInt(this.#store.state.extentSize), "extent index");
const extentId = this.#extentAt(extents, allocated, extentIndex);
if (extentId === undefined)
throw new StoreLimitError("planned extents do not cover the zero range");
const within = Number(logical % BigInt(this.#store.state.extentSize));
const remainingInExtent = this.#store.state.extentSize - within;
const count = Math.min(zeros.byteLength, remainingInExtent, checkedSafeNumber(length - completed, "remaining zero length"));
const physical = arenaByteOffset(extentId, this.#store.state.extentSize, within);
if (physical < previousEnd) {
const existingCount = Math.min(count, checkedSafeNumber(previousEnd - physical, "existing zero length"));
this.#arenaDirty = true;
this.#writeArenaExact(physical, zeros.subarray(0, existingCount), "arena.gap-zero.write");
this.#recordArenaWrite(existingCount);
touchedExisting = true;
}
completed += BigInt(count);
}
return touchedExisting;
}
#extentAt(head, tail, index) {
return index < head.length ? head[index] : tail[index - head.length];
}
#planLinkedWrite(inode, start, requestedLength, nowMs) {
const attempt = (length) => {
const end = checkedAdd(start, BigInt(length), "write end");
if (end <= inode.size)
return;
const plan = planResizeFileForInode(this.#store.state, inode, end, nowMs, "write");
return { plan, preparedCommit: this.#preflightCommit(plan.record) };
};
try {
const admitted = attempt(requestedLength);
return {
length: requestedLength,
plan: admitted?.plan,
preparedCommit: admitted?.preparedCommit
};
} catch (cause) {
if (!(cause instanceof StoreLimitError))
throw cause;
let low = 0;
let high = requestedLength - 1;
let admitted = 0;
let admittedPlan;
let admittedCommit;
while (low <= high) {
const candidate = low + Math.floor((high - low) / 2);
try {
const candidatePlan = attempt(candidate);
admitted = candidate;
admittedPlan = candidatePlan?.plan;
admittedCommit = candidatePlan?.preparedCommit;
low = candidate + 1;
} catch (candidateCause) {
if (!(candidateCause instanceof StoreLimitError))
throw candidateCause;
high = candidate - 1;
}
}
if (admitted === 0)
throw cause;
return { length: admitted, plan: admittedPlan, preparedCommit: admittedCommit };
}
}
#writeOrphan(descriptor, source, position, nowMs) {
const orphan = descriptor.orphan;
const start = descriptor.append ? orphan.size : position ?? descriptor.offset;
if (start < 0n)
throw new FsError("EINVAL", "write position is negative");
if (source.byteLength === 0)
return 0;
const buildPlan = () => {
const admitted = this.#planOrphanWrite(orphan, start, source.byteLength);
return {
admitted,
allocated: admitted.reservation === undefined ? [] : expandExtentRuns(admitted.reservation.record.extents, MAX_EXTENTS_PER_INODE),
arenaSizeBefore: this.#arenaSizeChecked("arena.size.before-orphan-extension")
};
};
const prepared = this.#prepareAllocationWithQuotaRetry(buildPlan(), buildPlan);
const { admitted, allocated, arenaSizeBefore } = prepared;
if (admitted.reservation !== undefined) {
this.#commit(admitted.reservation.record);
orphan.extents.push(...allocated);
}
if (start > orphan.size) {
const touchedExisting = this.#zeroLogical(orphan.extents, orphan.size, start - orphan.size, arenaSizeBefore);
if (touchedExisting)
this.#flushPreparedArena("arena.gap-zero.flush");
}
const progress = this.#writeLogical(orphan.extents, start, source.subarray(0, admitted.length), "arena.write");
if (progress.bytes === 0)
this.#failStore(progress.error);
const completedEnd = checkedAdd(start, BigInt(progress.bytes), "completed orphan write end");
if (completedEnd > orphan.size)
orphan.size = completedEnd;
orphan.mtimeMs = nowMs;
orphan.ctimeMs = nowMs;
if (position === undefined)
descriptor.offset = completedEnd;
return progress.bytes;
}
#planOrphanWrite(orphan, start, requestedLength) {
const attempt = (length) => {
const end = checkedAdd(start, BigInt(length), "orphan write end");
const required = this.#extentCount(end);
const additional = Math.max(0, required - orphan.extents.length);
if (additional === 0)
return;
const reservation = planReserveQuarantine(this.#store.state, additional);
this.#preflightCommit(reservation.record);
return reservation;
};
try {
return { length: requestedLength, reservation: attempt(requestedLength) };
} catch (cause) {
if (!(cause instanceof StoreLimitError))
throw cause;
let low = 0;
let high = requestedLength - 1;
let admitted = 0;
let reservation;
while (low <= high) {
const candidate = low + Math.floor((high - low) / 2);
try {
const candidateReservation = attempt(candidate);
admitted = candidate;
reservation = candidateReservation;
low = candidate + 1;
} catch (candidateCause) {
if (!(candidateCause instanceof StoreLimitError))
throw candidateCause;
high = candidate - 1;
}
}
if (admitted === 0)
throw cause;
return { length: admitted, reservation };
}
}
#extentCount(size) {
checkedU64(size, "file size");
if (size === 0n)
return 0;
const count = (size + BigInt(this.#store.state.extentSize) - 1n) / BigInt(this.#store.state.extentSize);
const result = checkedSafeNumber(count, "file extent count");
if (result > MAX_EXTENTS_PER_INODE) {
throw new StoreLimitError(`file extent count exceeds ${MAX_EXTENTS_PER_INODE}`);
}
return result;
}
#parseFlags(flags) {
const parsed = {
r: { readable: true, writable: false, create: false, exclusive: false, truncate: false, append: false },
"r+": { readable: true, writable: true, create: false, exclusive: false, truncate: false, append: false },
w: { readable: false, writable: true, create: true, exclusive: false, truncate: true, append: false },
"w+": { readable: true, writable: true, create: true, exclusive: false, truncate: true, append: false },
wx: { readable: false, writable: true, create: true, exclusive: true, truncate: true, append: false },
"wx+": { readable: true, writable: true, create: true, exclusive: true, truncate: true, append: false },
a: { readable: false, writable: true, create: true, exclusive: false, truncate: false, append: true },
"a+": { readable: true, writable: true, create: true, exclusive: false, truncate: false, append: true },
ax: { readable: false, writable: true, create: true, exclusive: true, truncate: false, append: true },
"ax+": { readable: true, writable: true, create: true, exclusive: true, truncate: false, append: true }
};
const result = parsed[flags];
if (result === undefined)
throw new FsError("EINVAL", `unsupported open flags: ${flags}`);
return result;
}
#validateEncoding(encoding, path) {
if (encoding !== "utf8" && encoding !== "utf-8") {
throw new FsError("EINVAL", `unsupported writeFile encoding: ${encoding}`, { operation: "writeFile", path });
}
return "utf8";
}
#flushPreparedArena(label) {
this.#flush(this.#store.handles["arena.bin"], label, "zeroBarrier");
this.#arenaDirty = false;
this.#arenaDirtyBytesSinceFlush = 0;
}
#recordArenaWrite(bytes) {
if (!Number.isSafeInteger(bytes) || bytes < 0)
throw new TypeError("arena dirty byte count is invalid");
this.#arenaDirtyBytesSinceFlush = Math.min(Number.MAX_SAFE_INTEGER, this.#arenaDirtyBytesSinceFlush + bytes);
}
#arenaSizeChecked(label) {
if (this.#arenaSize !== undefined)
return this.#arenaSize;
const size = this.#store.handles["arena.bin"].getSize(label);
if (!Number.isSafeInteger(size) || size < 0)
throw new Error(`${label} returned an invalid arena size`);
this.#arenaSize = size;
return size;
}
#writeArena(source, at, label) {
const sizeBefore = this.#arenaSize;
this.#arenaSize = undefined;
const written = this.#store.handles["arena.bin"].write(source, at, label);
if (sizeBefore !== undefined && Number.isSafeInteger(written) && written >= 0 && written <= source.byteLength) {
const end = at + written;
if (!Number.isSafeInteger(end))
return written;
this.#arenaSize = Math.max(sizeBefore, end);
}
return written;
}
#writeArenaExact(at, source, label) {
const sizeBefore = this.#arenaSize;
this.#arenaSize = undefined;
writeExact(this.#store.handles["arena.bin"], at, source, label);
if (sizeBefore !== undefined) {
this.#arenaSize = Math.max(sizeBefore, checkedSafeNumber(checkedAdd(at, BigInt(source.byteLength), "arena write end"), "arena write end"));
}
}
#truncateArena(size, label) {
this.#arenaSize = undefined;
truncateChecked(this.#store.handles["arena.bin"], size, label);
this.#arenaSize = checkedSafeNumber(size, "arena truncate size");
}
#flush(handle, label, kind) {
this.#flushes[kind] += 1;
handle.flush(label);
}
#prepareAllocationWithQuotaRetry(initial, retry) {
try {
this.#prepareAllocated(initial.allocated);
return initial;
} catch (cause) {
if (!(cause instanceof ArenaGrowthQuotaError))
throw cause;
this.repack("quota");
this.repack("quota");
const retried = retry();
try {
this.#prepareAllocated(retried.allocated);
} catch (retryCause) {
if (retryCause instanceof ArenaGrowthQuotaError)
throw retryCause.cause;
throw retryCause;
}
return retried;
}
}
#descriptor(fd) {
if (!Number.isSafeInteger(fd))
throw new FsError("EBADF", "descriptor is invalid");
const descriptor = this.#descriptors.get(fd);
if (descriptor === undefined)
throw new FsError("EBADF", "descriptor is invalid");
return descriptor;
}
#resolve(path, follow) {
return resolveLinks(this.#store.state, path, follow);
}
#linkedFile(inodeId) {
const inode = this.#store.state.inodes.get(inodeId);
if (inode?.kind !== "file")
throw new FsError("EBADF", "descriptor no longer references a file");
return inode;
}
#statValue(inode) {
const kind = "kind" in inode ? inode.kind : "file";
return {
kind,
mode: inode.mode,
size: kind === "file" ? inode.size : kind === "symlink" ? BigInt(encodedUtf8Length(inode.target)) : 0n,
atimeMs: inode.atimeMs,
mtimeMs: inode.mtimeMs,
ctimeMs: inode.ctimeMs
};
}
#validateBufferRange(buffer, offset, length) {
if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || offset < 0 || length < 0 || offset + length > buffer.byteLength) {
throw new FsError("EINVAL", "buffer range is invalid");
}
}
#preflightCommit(record) {
this.#assertOpen();
const projection = prepareTxnProjection(this.#store.state, record);
const deferResize = this.#isDeferrableResize(record);
const pending = this.#pendingResizeCommit;
const replacesPendingResize = deferResize && pending !== undefined && record.kind === "resizeFile" && pending.record.inodeId === record.inodeId;
const materializedRecord = replacesPendingResize ? {
...record,
allocated: mergeExtentRuns(pending.record.allocated, record.allocated)
} : record;
const sequence = replacesPendingResize ? pending.sequence : this.#store.nextSequence;
const frameBytes = txnFrameBytes(materializedRecord);
const frame = deferResize ? undefined : encodeTxnFrame({
generation: this.#store.state.generation,
sequence,
record: materializedRecord
});
const projectedLogBytes = replacesPendingResize ? this.#store.activeLogBytes - pending.frameBytes + frameBytes : this.#store.activeLogBytes + frameBytes;
if (!replacesPendingResize && this.#store.activeLogFrames >= MAX_ACTIVE_LOG_FRAMES || projectedLogBytes > MAX_ACTIVE_LOG_BYTES) {
throw new StoreLimitError("active metadata log reached its hard limit");
}
return {
frame,
frameBytes,
projection,
materializedRecord,
deferResize,
replacesPendingResize
};
}
#commit(record, preparedCommit) {
this.#assertOpen();
const prepared = preparedCommit ?? this.#preflightCommit(record);
if (prepared.replacesPendingResize) {
const pending = this.#pendingResizeCommit;
if (pending === undefined || prepared.materializedRecord.kind !== "resizeFile") {
const cause = new Error("pending resize preflight no longer matches live state");
this.#poison(cause);
throw cause;
}
try {
applyTxn(this.#store.state, record, prepared.projection);
} catch (cause) {
this.#poison(cause);
throw cause;
}
this.#store.activeLogBytes += prepared.frameBytes - pending.frameBytes;
this.#pendingResizeCommit = {
record: prepared.materializedRecord,
frameBytes: prepared.frameBytes,
sequence: pending.sequence
};
this.#scheduleRepack();
return;
}
this.#materializePendingResize();
if (prepared.deferResize && prepared.materializedRecord.kind === "resizeFile") {
try {
applyTxn(this.#store.state, record, prepared.projection);
} catch (cause) {
this.#poison(cause);
throw cause;
}
this.#pendingResizeCommit = {
record: prepared.materializedRecord,
frameBytes: prepared.frameBytes,
sequence: this.#store.nextSequence
};
this.#store.activeLogBytes += prepared.frameBytes;
this.#store.activeLogFrames += 1;
this.#store.nextSequence += 1n;
this.#scheduleRepack();
return;
}
if (prepared.frame === undefined) {
const cause = new Error("immediate commit has no encoded transaction frame");
this.#poison(cause);
throw cause;
}
this.#appendFrame(prepared.frame);
try {
applyTxn(this.#store.state, record, prepared.projection);
} catch (cause) {
this.#poison(cause);
throw cause;
}
this.#store.activeLogBytes += prepared.frameBytes;
this.#store.activeLogFrames += 1;
this.#store.nextSequence += 1n;
this.#scheduleRepack();
}
#isDeferrableResize(record) {
if (record.kind !== "resizeFile" || record.operation !== "write")
return false;
const inode = this.#store.state.inodes.get(record.inodeId);
return inode?.kind === "file" && record.size > inode.size;
}
#appendFrame(frame) {
const activeName = metadataFileName(this.#store.state.activeMetadataFile);
try {
writeExact(this.#store.handles[activeName], this.#store.activeLogEnd, frame, "metadata.log.append");
this.#metadataDirtySinceFlush = true;
this.#store.activeLogEnd += BigInt(frame.byteLength);
} catch (cause) {
this.#failStore(cause);
}
}
#materializePendingResize() {
const pending = this.#pendingResizeCommit;
if (pending === undefined)
return;
let frame;
try {
frame = encodeTxnFrame({
generation: this.#store.state.generation,
sequence: pending.sequence,
record: pending.record
});
if (frame.byteLength !== pending.frameBytes) {
throw new Error("pending resize frame length changed after preflight");
}
} catch (cause) {
this.#poison(cause);
throw cause;
}
this.#appendFrame(frame);
this.#pendingResizeCommit = undefined;
}
#scheduleRepack() {
if (this.#store.activeLogBytes >= SOFT_ACTIVE_LOG_BYTES) {
this.#pendingRepackReason = "log-bytes";
} else if (this.#store.activeLogFrames >= SOFT_ACTIVE_LOG_FRAMES) {
this.#pendingRepackReason = "log-frames";
} else if (this.#store.state.basePayloadBytes >= BASE_REPACK_PRESSURE_BYTES) {
this.#pendingRepackReason = "writer-limit";
} else if (this.#store.state.allocator.quarantine.size >= QUARANTINE_PRESSURE_EXTENTS || this.#store.state.allocator.quarantine.size > 0 && this.#store.state.allocator.available.size === 0) {
this.#pendingRepackReason ??= "quarantine-pressure";
}
}
#dueRepackReason(nowMs) {
if (this.#pendingRepackReason !== null)
return this.#pendingRepackReason;
if (this.#store.activeLogBytes >= SOFT_ACTIVE_LOG_BYTES)
return "log-bytes";
if (this.#store.activeLogFrames >= SOFT_ACTIVE_LOG_FRAMES)
return "log-frames";
if (this.#store.state.basePayloadBytes >= BASE_REPACK_PRESSURE_BYTES) {
return "writer-limit";
}
return this.#store.activeLogFrames > 0 && nowMs - this.#lastRepackAtMs >= REPACK_INTERVAL_MS ? "time" : null;
}
#assertOpen() {
if (this.#status === "failed")
throw new StoreFailedError(this.#failure);
if (this.#status === "closed")
throw new StoreClosedError;
}
#poison(cause) {
if (this.#status === "open") {
this.#status = "failed";
this.#failure = cause;
}
}
#failStore(cause) {
this.#poison(cause);
throw new StoreFailedError(this.#failure);
}
}
// packages/pgwasm/src/opfs/opfs-repacked-fs.ts
var DESCRIPTION = Object.freeze({ name: "opfs-repacked", persistent: true });
class OpfsRepackedFS extends BaseFilesystem2 {
#vfs;
#durability;
constructor(vfs, durability) {
super();
this.#vfs = vfs;
this.#durability = durability;
}
get description() {
return DESCRIPTION;
}
async initialSyncFs() {
this.#vfs.assertHealthy();
}
async syncToFs(relaxedDurability = false) {
this.#vfs.assertHealthy();
if (relaxedDurability)
this.#vfs.fail(new DurabilityModeMismatchError);
this.#vfs.runScheduledRepack();
if (this.#durability === "strict")
this.#vfs.strictSync();
}
strictSync() {
this.#vfs.strictSync();
}
async cleanupFailedInit() {
this.#vfs.cleanupFailedInit();
}
async closeFs() {
this.#vfs.close();
}
chmod(path, mode) {
this.#vfs.chmod(hostPath(path), mode, nowMs());
}
close(fd) {
this.#vfs.close(fd);
}
fstat(fd) {
return toFsStats(this.#vfs.fstat(fd));
}
lstat(path) {
return toFsStats(this.#vfs.lstat(hostPath(path)));
}
mkdir(path, options) {
this.#vfs.mkdir(hostPath(path), {
...options?.recursive === undefined ? {} : { recursive: options.recursive },
...options?.mode === undefined ? {} : { mode: options.mode },
nowMs: nowMs()
});
}
open(path, flags = "r+", mode = 33206) {
return this.#vfs.open(hostPath(path), flags, mode, nowMs());
}
readdir(path) {
return this.#vfs.readdir(hostPath(path));
}
read(fd, buffer, offset, length, position) {
return this.#vfs.read(fd, buffer, offset, length, toFileOffset(position));
}
rename(oldPath, newPath) {
this.#vfs.rename(hostPath(oldPath), hostPath(newPath), nowMs());
}
rmdir(path) {
this.#vfs.rmdir(hostPath(path), nowMs());
}
truncate(path, length) {
this.#vfs.truncate(hostPath(path), toFileOffset(length), nowMs());
}
unlink(path) {
this.#vfs.unlink(hostPath(path), nowMs());
}
utimes(path, atime, mtime) {
this.#vfs.utimes(hostPath(path), toTimestamp(atime), toTimestamp(mtime), nowMs());
}
writeFile(path, data, options) {
this.#vfs.writeFile(hostPath(path), data, {
...options?.encoding === undefined ? {} : { encoding: options.encoding },
...options?.mode === undefined ? {} : { mode: options.mode },
...options?.flag === undefined ? {} : { flag: options.flag },
nowMs: nowMs()
});
}
write(fd, buffer, offset, length, position) {
if (buffer instanceof Uint8Array) {
return this.#vfs.write(fd, buffer, offset, length, toFileOffset(position), nowMs());
}
const source = new Uint8Array(buffer, offset, length);
return this.#vfs.write(fd, source, 0, source.byteLength, toFileOffset(position), nowMs());
}
}
class ConcreteOpfsRepackedFS extends OpfsRepackedFS {
constructor(vfs, durability) {
super(vfs, durability);
}
}
async function openOpfsRepackedFsForPort(port, options = {}) {
const durability = options.durability ?? "relaxed";
if (durability !== "relaxed" && durability !== "strict") {
throw new TypeError(`unsupported OPFS repacked durability: ${String(durability)}`);
}
const vfs = await RepackedVfs.open(port, {
...options.extentSize === undefined ? {} : { extentSize: options.extentSize }
});
return new ConcreteOpfsRepackedFS(vfs, durability);
}
function nowMs() {
return BigInt(Date.now());
}
function hostPath(path) {
return path === "" ? "/" : path;
}
function toFileOffset(value) {
if (!Number.isSafeInteger(value) || value < 0)
throw new TypeError("file offset is not a non-negative safe integer");
return BigInt(value);
}
function toTimestamp(value) {
const floored = Math.floor(value);
if (!Number.isSafeInteger(floored) || floored < 0) {
throw new TypeError("timestamp is not a non-negative finite millisecond value");
}
return BigInt(floored);
}
function toFsStats(stat) {
const size = toSafeNumber(stat.size, "file size");
const blksize = 4096;
return {
dev: 0,
ino: 0,
mode: stat.mode,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
size,
blksize,
blocks: Math.ceil(size / blksize),
atime: toSafeNumber(stat.atimeMs, "access time"),
mtime: toSafeNumber(stat.mtimeMs, "modification time"),
ctime: toSafeNumber(stat.ctimeMs, "change time")
};
}
function toSafeNumber(value, label) {
if (value > BigInt(Number.MAX_SAFE_INTEGER))
throw new StoreLimitError(`${label} exceeds safe integer range`);
return Number(value);
}
// packages/pgwasm/src/opfs/create.ts
async function createOpfsPgwasm(options) {
assertHostOptions(options.pgwasm);
const store = await openOpfsRepackedFsForPort(new OpfsRepackedPort(options.directory), filesystemOptions(options));
options.onPhase?.("store-opened");
let pg;
try {
pg = await createPgwasm2({
...options.pgwasm,
build: options.build,
fs: store,
relaxedDurability: false
});
registerStrictSync(pg, () => store.strictSync());
options.onPhase?.("pgwasm-ready");
store.strictSync();
return pg;
} catch (cause) {
try {
await pg?.close();
} catch {}
try {
await store.cleanupFailedInit();
} catch {}
throw cause;
}
}
async function strictSync(pg) {
const sync = strictSyncOf(pg);
if (sync === undefined) {
throw new UnsupportedFeatureError2("strictSync needs a database on an OPFS-repacked store (created by createOpfsPgwasm).");
}
await pg.runExclusive(async () => {
sync();
});
}
function filesystemOptions(options) {
return {
...options.extentSize === undefined ? {} : { extentSize: options.extentSize },
...options.durability === undefined ? {} : { durability: options.durability }
};
}
function assertHostOptions(options) {
if (options === undefined)
return;
for (const reserved of ["build", "dataDir", "fs", "relaxedDurability"]) {
if (reserved in options) {
throw new TypeError(`pgwasm.${reserved} is owned by createOpfsPgwasm`);
}
}
}
// packages/pgwasm/src/opfs/core/memory-port.ts
function clone(bytes) {
return bytes.slice();
}
function applyWrite(current, at, source) {
const required = at + source.byteLength;
const next = required <= current.byteLength ? clone(current) : new Uint8Array(required);
if (next.byteLength > current.byteLength)
next.set(current);
next.set(source, at);
return next;
}
function applyTruncate(current, size) {
if (size === current.byteLength)
return clone(current);
const next = new Uint8Array(size);
next.set(current.subarray(0, Math.min(size, current.byteLength)));
return next;
}
function resizeImage(current, size) {
if (size <= current.byteLength)
return new Uint8Array(current.buffer, 0, size);
if (size <= current.buffer.byteLength) {
const grown = new Uint8Array(current.buffer, 0, size);
grown.fill(0, current.byteLength, size);
return grown;
}
const capacity = Math.max(size, current.buffer.byteLength * 2, 64);
const grown = new Uint8Array(new ArrayBuffer(capacity), 0, size);
grown.set(current);
return grown;
}
function isOwnedFileName(name) {
return OWNED_FILE_NAMES.includes(name);
}
class MemoryHandle {
name;
#port;
#epoch;
#closed = false;
constructor(port, name, epoch) {
this.#port = port;
this.name = name;
this.#epoch = epoch;
}
getSize(label) {
this.#assertOpen();
return this.#port.handleGetSize(this.name, label);
}
read(target, at, label) {
this.#assertOpen();
return this.#port.handleRead(this.name, target, at, label);
}
write(source, at, label) {
this.#assertOpen();
return this.#port.handleWrite(this.name, source, at, label);
}
truncate(size, label) {
this.#assertOpen();
this.#port.handleTruncate(this.name, size, label);
}
flush(label) {
this.#assertOpen();
this.#port.handleFlush(this.name, label);
}
close() {
if (this.#closed)
return;
this.#closed = true;
if (this.#epoch === this.#port.epoch)
this.#port.handleClose(this.name);
}
#assertOpen() {
if (this.#closed || this.#epoch !== this.#port.epoch) {
throw new Error(`memory handle ${this.name} is closed`);
}
}
}
class MemoryRepackedPort {
#durable = new Map;
#volatile = new Map;
#entryKinds = new Map;
#open = new Set;
#faults = [];
#operations = [];
#effects = [];
#nextEffectId = 1;
#epoch = 1;
get epoch() {
return this.#epoch;
}
async enumerate(label) {
this.#recordOperation("enumerate", undefined, label);
const fault = this.#takeFault("enumerate", undefined, label);
if (fault !== undefined)
throw new Error(`injected enumerate failure ${fault.outcome}`);
return [...this.#entryKinds].sort(([left], [right]) => left.localeCompare(right)).map(([name, kind]) => ({
name,
kind
}));
}
async acquire(name, label) {
this.#recordOperation("acquire", name, label);
const fault = this.#takeFault("acquire", name, label);
if (fault?.outcome === "throw-before")
throw new Error("injected acquire failure before effect");
if (this.#open.has(name))
throw new StoreOwnedError;
if (this.#entryKinds.get(name) === "directory")
throw new Error(`memory entry ${name} is not a file`);
if (!this.#volatile.has(name)) {
this.#volatile.set(name, new Uint8Array);
this.#durable.set(name, new Uint8Array);
this.#entryKinds.set(name, "file");
}
if (fault !== undefined)
throw new Error(`injected acquire failure ${fault.outcome}`);
this.#open.add(name);
return new MemoryHandle(this, name, this.#epoch);
}
injectEntry(name, kind) {
if (isOwnedFileName(name) && this.#open.has(name)) {
throw new Error(`cannot replace acquired memory entry ${name}`);
}
this.#entryKinds.set(name, kind);
if (isOwnedFileName(name)) {
this.#effects = this.#effects.filter((effect) => effect.file !== name);
if (kind === "file") {
this.#volatile.set(name, new Uint8Array);
this.#durable.set(name, new Uint8Array);
} else {
this.#volatile.delete(name);
this.#durable.delete(name);
}
}
}
injectFault(fault) {
if (fault.outcome === "quota" && fault.operation !== "truncate") {
throw new TypeError("quota faults are supported only for arena growth truncates");
}
if (fault.bytes !== undefined && (!Number.isSafeInteger(fault.bytes) || fault.bytes < 0)) {
throw new TypeError("fault byte count must be a non-negative safe integer");
}
if (fault.occurrence !== undefined && (!Number.isSafeInteger(fault.occurrence) || fault.occurrence < 0)) {
throw new TypeError("fault occurrence must be a non-negative safe integer");
}
this.#faults.push({ ...fault });
}
pendingEffects() {
return this.#effects.map((effect) => ({
id: effect.id,
file: effect.file,
operation: effect.kind,
bytes: effect.kind === "write" ? effect.data.byteLength : effect.size
}));
}
openHandleCount() {
return this.#open.size;
}
observedOperations() {
return Object.freeze(this.#operations.map((operation) => Object.freeze({ ...operation })));
}
clearObservedOperations() {
this.#operations.length = 0;
}
durableBytes(name) {
return clone(this.#durable.get(name) ?? new Uint8Array);
}
terminate(decisions = {}) {
const materialized = new Map;
for (const [name, bytes] of this.#durable)
materialized.set(name, clone(bytes));
for (const effect of this.#effects) {
const decision = decisions[effect.id] ?? "absent";
if (decision === "absent")
continue;
const current = materialized.get(effect.file) ?? new Uint8Array;
if (effect.kind === "truncate") {
if (decision !== "full") {
throw new TypeError("truncate termination decisions must be absent or full");
}
materialized.set(effect.file, applyTruncate(current, effect.size));
} else {
const bytes = decision === "full" ? effect.data.byteLength : decision;
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > effect.data.byteLength) {
throw new TypeError("write termination prefix is outside the effect");
}
if (bytes > 0)
materialized.set(effect.file, applyWrite(current, effect.at, effect.data.subarray(0, bytes)));
}
}
this.#durable.clear();
this.#volatile.clear();
for (const [name, bytes] of materialized) {
this.#durable.set(name, clone(bytes));
this.#volatile.set(name, clone(bytes));
}
this.#effects = [];
this.#faults.length = 0;
this.#open.clear();
this.#epoch += 1;
}
handleGetSize(name, label) {
this.#recordOperation("getSize", name, label);
const fault = this.#takeFault("getSize", name, label);
if (fault !== undefined)
throw new Error(`injected getSize failure ${fault.outcome}`);
return this.#file(name).byteLength;
}
handleRead(name, target, at, label) {
this.#validateRange(at, target.byteLength);
this.#recordOperation("read", name, label);
const fault = this.#takeFault("read", name, label);
if (fault?.outcome === "throw-before" || fault?.outcome === "throw-after") {
throw new Error(`injected read failure ${fault.outcome === "throw-before" ? "before" : "after"} effect`);
}
const source = this.#file(name);
const available = Math.max(0, Math.min(target.byteLength, source.byteLength - at));
const count = fault?.outcome === "short" ? Math.min(available, fault.bytes ?? 0) : available;
target.set(source.subarray(at, at + count));
return count;
}
handleWrite(name, source, at, label) {
this.#validateRange(at, source.byteLength);
this.#recordOperation("write", name, label);
const fault = this.#takeFault("write", name, label);
if (fault?.outcome === "throw-before")
throw new Error("injected write failure before effect");
const count = fault?.outcome === "short" || fault?.outcome === "throw-after" ? Math.min(source.byteLength, fault.bytes ?? source.byteLength) : source.byteLength;
if (count > 0)
this.#recordWrite(name, at, source.subarray(0, count));
if (fault?.outcome === "throw-after")
throw new Error("injected write failure after effect");
return count;
}
handleTruncate(name, size, label) {
this.#validateRange(size, 0);
this.#recordOperation("truncate", name, label);
const fault = this.#takeFault("truncate", name, label);
if (fault?.outcome === "quota")
throw new DOMException("injected arena quota exhaustion", "QuotaExceededError");
if (fault?.outcome === "throw-before")
throw new Error("injected truncate failure before effect");
this.#volatile.set(name, resizeImage(this.#file(name), size));
this.#effects.push({ id: this.#nextEffectId++, file: name, kind: "truncate", size });
if (fault?.outcome === "throw-after")
throw new Error("injected truncate failure after effect");
}
handleFlush(name, label) {
const record = this.#recordOperation("flush", name, label);
record.copiedBytes = 0;
const fault = this.#takeFault("flush", name, label);
if (fault?.outcome === "throw-before")
throw new Error("injected flush failure before effect");
record.copiedBytes = this.#makeDurable(name);
if (fault?.outcome === "throw-after")
throw new Error("injected flush failure after effect");
}
handleClose(name) {
this.#open.delete(name);
}
#file(name) {
const bytes = this.#volatile.get(name);
if (bytes === undefined)
throw new Error(`memory file ${name} was not acquired`);
return bytes;
}
#recordWrite(name, at, source) {
const data = clone(source);
const current = this.#file(name);
const required = at + data.byteLength;
const target = required > current.byteLength ? resizeImage(current, required) : current;
target.set(data, at);
this.#volatile.set(name, target);
this.#effects.push({ id: this.#nextEffectId++, file: name, kind: "write", at, data });
}
#makeDurable(name) {
let image = this.#durable.get(name) ?? new Uint8Array;
const remaining = [];
let copied = 0;
for (const effect of this.#effects) {
if (effect.file !== name) {
remaining.push(effect);
continue;
}
if (effect.kind === "truncate") {
const resized = resizeImage(image, effect.size);
if (resized.buffer !== image.buffer)
copied += image.byteLength;
image = resized;
continue;
}
const required = effect.at + effect.data.byteLength;
if (required > image.byteLength) {
const grown = resizeImage(image, required);
if (grown.buffer !== image.buffer)
copied += image.byteLength;
image = grown;
}
image.set(effect.data, effect.at);
copied += effect.data.byteLength;
}
this.#durable.set(name, image);
this.#effects = remaining;
return copied;
}
#takeFault(operation, file, label) {
const index = this.#faults.findIndex((fault) => fault.operation === operation && (fault.file === undefined || fault.file === file) && (fault.label === undefined || fault.label === label));
if (index < 0)
return;
const fault = this.#faults[index];
if ((fault.occurrence ?? 0) > 0) {
this.#faults[index] = { ...fault, occurrence: fault.occurrence - 1 };
return;
}
return this.#faults.splice(index, 1)[0];
}
#recordOperation(operation, file, label) {
const record = { operation, file, label };
this.#operations.push(record);
return record;
}
#validateRange(at, length) {
if (!Number.isSafeInteger(at) || !Number.isSafeInteger(length) || at < 0 || length < 0) {
throw new RangeError("memory port range is invalid");
}
if (!Number.isSafeInteger(at + length))
throw new RangeError("memory port range exceeds safe integers");
}
}
// packages/pgwasm/src/opfs/core/mounted-vfs.ts
function joinPath2(base, rest) {
if (rest.length === 0)
return base;
return base === "/" ? `/${rest.join("/")}` : `${base}/${rest.join("/")}`;
}
function isMissing(cause) {
return cause instanceof FsError && (cause.code === FS_ERRNO.ENOENT || cause.code === FS_ERRNO.ENOTDIR);
}
class MountedRepackedVfs {
#root;
#mounts;
#descriptors = new Map;
#nextDescriptor = 3;
constructor(options) {
this.#root = options.root;
const nowMs = (options.nowMs ?? (() => BigInt(Date.now())))();
const mounts = [];
for (const mount of options.mounts) {
if (parsePath(mount.prefix).length === 0) {
throw new FsError("EINVAL", "a mount prefix must be an absolute path below the root", {
path: mount.prefix
});
}
for (const existing of mounts) {
if (existing.prefix === mount.prefix || mount.prefix.startsWith(existing.boundary) || existing.prefix.startsWith(`${mount.prefix}/`)) {
throw new FsError("EINVAL", `mount prefix ${mount.prefix} overlaps ${existing.prefix}`, {
path: mount.prefix
});
}
}
mounts.push({
prefix: mount.prefix,
boundary: `${mount.prefix}/`,
vfs: mount.vfs,
durable: mount.durable ?? true
});
}
this.#mounts = mounts;
for (const mount of mounts) {
try {
this.#root.mkdir(mount.prefix, { recursive: true, nowMs });
} catch (cause) {
if (!(cause instanceof FsError) || cause.code !== FS_ERRNO.EEXIST)
throw cause;
}
}
}
get mounts() {
return this.#mounts.map((mount) => ({ prefix: mount.prefix, vfs: mount.vfs, durable: mount.durable }));
}
get root() {
return this.#root;
}
#route(path) {
parsePath(path);
for (const mount of this.#mounts) {
if (path === mount.prefix)
return { fs: mount.vfs, path: "/", mount };
if (path.startsWith(mount.boundary)) {
return { fs: mount.vfs, path: path.slice(mount.prefix.length), mount };
}
}
return { fs: this.#root, path, mount: undefined };
}
resolvePath(path, follow = true) {
const route = this.#route(path);
if (route.fs.resolvePath(route.path, follow) === route.path)
return path;
return this.#walk(path, follow);
}
#walk(path, follow) {
let pending = parsePath(path);
let index = 0;
const resolved = [];
let hops = 0;
while (index < pending.length) {
const candidate = joinPath2(`/${resolved.join("/")}`, [pending[index]]);
const route = this.#route(candidate);
let stat;
try {
stat = route.fs.lstat(route.path);
} catch (cause) {
if (isMissing(cause))
break;
throw cause;
}
if (stat.kind === "symlink" && (follow || index < pending.length - 1)) {
hops += 1;
if (hops > MAX_SYMLINK_HOPS) {
throw new FsError("ELOOP", `path traverses more than ${MAX_SYMLINK_HOPS} symbolic links`, { path });
}
pending = parsePath(joinPath2(route.fs.readlink(route.path), pending.slice(index + 1)));
index = 0;
resolved.length = 0;
continue;
}
resolved.push(pending[index]);
index += 1;
}
return joinPath2(`/${resolved.join("/")}`, pending.slice(index));
}
#at(path, follow) {
return this.#route(this.resolvePath(path, follow));
}
#descriptor(fd) {
const entry = this.#descriptors.get(fd);
if (entry === undefined)
throw new FsError("EBADF", "descriptor is invalid");
return entry;
}
strictSync() {
this.#root.strictSync();
for (const mount of this.#mounts) {
if (mount.durable)
mount.vfs.strictSync();
}
}
assertHealthy() {
this.#root.assertHealthy();
for (const mount of this.#mounts)
mount.vfs.assertHealthy();
}
fail(cause) {
for (const mount of this.#mounts) {
try {
mount.vfs.fail(cause);
} catch {}
}
return this.#root.fail(cause);
}
metrics() {
return this.#root.metrics();
}
repack(reason) {
this.#root.repack(reason);
for (const mount of this.#mounts)
mount.vfs.repack(reason);
}
runScheduledRepack(nowMs) {
let repacked = this.#root.runScheduledRepack(nowMs);
for (const mount of this.#mounts) {
if (mount.vfs.runScheduledRepack(nowMs))
repacked = true;
}
return repacked;
}
close(fd) {
if (fd !== undefined) {
const entry = this.#descriptor(fd);
this.#descriptors.delete(fd);
entry.fs.close(entry.fd);
return;
}
this.#descriptors.clear();
let firstError;
for (const mount of this.#mounts) {
try {
mount.vfs.close();
} catch (cause) {
firstError ??= cause;
}
}
try {
this.#root.close();
} catch (cause) {
firstError ??= cause;
}
if (firstError !== undefined)
throw firstError;
}
stat(path) {
const route = this.#at(path, true);
return route.fs.stat(route.path);
}
lstat(path) {
const route = this.#at(path, false);
return route.fs.lstat(route.path);
}
symlink(target, path, nowMs) {
const route = this.#at(path, false);
route.fs.symlink(target, route.path, nowMs);
}
readlink(path) {
const route = this.#at(path, false);
return route.fs.readlink(route.path);
}
readdir(path) {
const route = this.#at(path, true);
return route.fs.readdir(route.path);
}
mkdir(path, options) {
const route = this.#at(path, false);
route.fs.mkdir(route.path, options);
}
writeFile(path, data, options) {
const route = this.#at(path, true);
route.fs.writeFile(route.path, data, options);
}
readFile(path) {
const route = this.#at(path, true);
return route.fs.readFile(route.path);
}
truncate(path, size, nowMs) {
const route = this.#at(path, true);
route.fs.truncate(route.path, size, nowMs);
}
chmod(path, mode, nowMs) {
const route = this.#at(path, true);
route.fs.chmod(route.path, mode, nowMs);
}
utimes(path, atimeMs, mtimeMs, ctimeMs) {
const route = this.#at(path, true);
route.fs.utimes(route.path, atimeMs, mtimeMs, ctimeMs);
}
unlink(path, nowMs) {
const route = this.#at(path, false);
route.fs.unlink(route.path, nowMs);
}
rmdir(path, nowMs) {
const route = this.#at(path, false);
route.fs.rmdir(route.path, nowMs);
}
rename(oldPath, newPath, nowMs) {
const source = this.#at(oldPath, false);
const destination = this.#at(newPath, false);
if (source.mount !== destination.mount) {
throw new FsError("EXDEV", "rename crosses a mount boundary", { operation: "rename", path: newPath });
}
source.fs.rename(source.path, destination.path, nowMs);
}
open(path, flags = "r", mode = 33206, nowMs = 0n) {
const route = this.#at(path, true);
const inner = route.fs.open(route.path, flags, mode, nowMs);
const fd = this.#nextDescriptor++;
this.#descriptors.set(fd, { fs: route.fs, fd: inner });
return fd;
}
fstat(fd) {
const entry = this.#descriptor(fd);
return entry.fs.fstat(entry.fd);
}
read(fd, buffer, offset, length, position) {
const entry = this.#descriptor(fd);
return entry.fs.read(entry.fd, buffer, offset, length, position);
}
write(fd, buffer, offset, length, position, nowMs) {
const entry = this.#descriptor(fd);
return entry.fs.write(entry.fd, buffer, offset, length, position, nowMs);
}
}
// packages/pgwasm/src/opfs/file-port.ts
var CREATED_FILE_MODE = 438;
async function loadNodeFileSystem() {
const runtime = globalThis;
if (runtime.process?.versions?.["node"] === undefined) {
throw new Error("FileRepackedPort needs node:fs and this scope has none (no process.versions.node); " + "a browser store belongs on OpfsRepackedPort");
}
return await import("fs");
}
function joinPath3(directory, name) {
return `${directory.replace(/\/+$/, "")}/${name}`;
}
class FileRepackedPort {
#directory;
#open = new Set;
#fs;
constructor(directory) {
if (directory.trim() === "")
throw new TypeError("FileRepackedPort needs a directory path");
this.#directory = directory;
}
get directory() {
return this.#directory;
}
async enumerate(_label) {
const fs = await this.#ready();
return fs.readdirSync(this.#directory, { withFileTypes: true }).map((entry) => ({
name: entry.name,
kind: entry.isDirectory() ? "directory" : "file"
}));
}
async acquire(name, _label) {
const fs = await this.#ready();
if (this.#open.has(name))
throw new StoreOwnedError;
const fd = fs.openSync(joinPath3(this.#directory, name), fs.constants.O_RDWR | fs.constants.O_CREAT, CREATED_FILE_MODE);
this.#open.add(name);
return new FileRepackedFileHandle(fs, name, fd, () => this.#open.delete(name));
}
async#ready() {
if (this.#fs === undefined) {
const fs = await loadNodeFileSystem();
fs.mkdirSync(this.#directory, { recursive: true });
this.#fs = fs;
}
return this.#fs;
}
}
class FileRepackedFileHandle {
name;
#fs;
#fd;
#released;
#closed = false;
constructor(fs, name, fd, released) {
this.#fs = fs;
this.name = name;
this.#fd = fd;
this.#released = released;
}
getSize(_label) {
this.#assertOpen();
return this.#fs.fstatSync(this.#fd).size;
}
read(target, at, _label) {
this.#assertOpen();
return this.#fs.readSync(this.#fd, target, 0, target.byteLength, at);
}
write(source, at, _label) {
this.#assertOpen();
return this.#fs.writeSync(this.#fd, source, 0, source.byteLength, at);
}
truncate(size, _label) {
this.#assertOpen();
this.#fs.ftruncateSync(this.#fd, size);
}
flush(_label) {
this.#assertOpen();
this.#fs.fsyncSync(this.#fd);
}
close() {
if (this.#closed)
return;
this.#closed = true;
this.#released();
this.#fs.closeSync(this.#fd);
}
#assertOpen() {
if (this.#closed)
throw new Error(`file handle ${this.name} is closed`);
}
}
// packages/pgwasm/src/opfs/broker/protocol.ts
var HEADER_STATE = 0;
var HEADER_OPCODE = 1;
var HEADER_REQUEST = 2;
var HEADER_RESPONSE = 3;
var HEADER_ERRNO = 4;
var HEADER_RESULT_LO = 5;
var HEADER_RESULT_HI = 6;
var HEADER_SEQUENCE = 7;
var HEADER_FAULT = 8;
var HEADER_PAYLOAD = 9;
var CHANNEL_HEADER_SLOTS = 16;
var CHANNEL_HEADER_BYTES = CHANNEL_HEADER_SLOTS * 4;
var STATE_IDLE = 0;
var STATE_REQUEST = 1;
var STATE_RESPONSE = 2;
var STATE_DETACHED = 3;
var DOORBELL_TICKET = 0;
var DOORBELL_RUNNING = 1;
var DOORBELL_SLOTS = 4;
var DOORBELL_BYTES = DOORBELL_SLOTS * 4;
var DEFAULT_PAYLOAD_BYTES = 64 * 1024;
var MIN_PAYLOAD_BYTES = 1024;
var FAULT_NONE = 0;
var FAULT_PROTOCOL = 1;
var FAULT_STORE = 2;
var FAULT_DETACHED = 3;
var OPCODE_OPEN = 1;
var OPCODE_CLOSE = 2;
var OPCODE_READ = 3;
var OPCODE_WRITE = 4;
var OPCODE_FSYNC = 5;
var OPCODE_FSTAT = 6;
var OPCODE_STAT = 7;
var OPCODE_LSTAT = 8;
var OPCODE_READDIR = 9;
var OPCODE_MKDIR = 10;
var OPCODE_RMDIR = 11;
var OPCODE_UNLINK = 12;
var OPCODE_RENAME = 13;
var OPCODE_TRUNCATE = 14;
var OPCODE_SIZE = 15;
var OPCODE_SYMLINK = 16;
var OPCODE_READLINK = 17;
var MIN_OPCODE = OPCODE_OPEN;
var MAX_OPCODE = OPCODE_READLINK;
function isKnownOpcode(opcode) {
return Number.isInteger(opcode) && opcode >= MIN_OPCODE && opcode <= MAX_OPCODE;
}
var O_RDONLY = 0;
var O_WRONLY = 1;
var O_RDWR = 2;
var O_ACCMODE = 3;
var O_CREAT = 64;
var O_EXCL = 128;
var O_TRUNC = 512;
var O_APPEND = 1024;
var O_NOFOLLOW = 131072;
var O_KNOWN = O_ACCMODE | O_CREAT | O_EXCL | O_TRUNC | O_APPEND | O_NOFOLLOW;
var STAT_KIND_FILE = 0;
var STAT_KIND_DIRECTORY = 1;
var STAT_KIND_SYMLINK = 2;
var STAT_BYTES = 1 + 4 + 8 * 4;
var READDIR_DONE = -1;
function planOpen(flags) {
if (!Number.isInteger(flags) || flags < 0 || (flags & ~O_KNOWN) !== 0) {
throw new FsError("EINVAL", `unsupported open flags: ${flags}`);
}
const access = flags & O_ACCMODE;
if (access === O_ACCMODE)
throw new FsError("EINVAL", "open access mode is invalid");
const readable = access === O_RDONLY || access === O_RDWR;
const writable = access === O_WRONLY || access === O_RDWR;
const create = (flags & O_CREAT) !== 0;
const exclusive = (flags & O_EXCL) !== 0;
const truncate = (flags & O_TRUNC) !== 0;
const append = (flags & O_APPEND) !== 0;
if (exclusive && !create)
throw new FsError("EINVAL", "O_EXCL requires O_CREAT");
if (truncate && append)
throw new FsError("EINVAL", "O_TRUNC and O_APPEND are contradictory");
if (!writable && (create || truncate || append)) {
throw new FsError("EINVAL", "O_CREAT, O_TRUNC and O_APPEND require write access");
}
const plan = (coreFlags, extra = {}) => ({
coreFlags,
fallbackFlags: undefined,
truncateFirst: false,
requireExisting: false,
readable,
writable,
follow: (flags & O_NOFOLLOW) === 0,
...extra
});
if (exclusive)
return plan(append ? readable ? "ax+" : "ax" : readable ? "wx+" : "wx");
if (append && create)
return plan(readable ? "a+" : "a");
if (truncate && create)
return plan(readable ? "w+" : "w");
if (append)
return plan(readable ? "a+" : "a", { requireExisting: true });
if (truncate)
return plan("r+", { truncateFirst: true });
if (create)
return plan("r+", { fallbackFlags: "w+" });
return plan(readable && !writable ? "r" : "r+");
}
var textEncoder2 = new TextEncoder;
var textDecoder2 = new TextDecoder("utf-8", { fatal: true });
class PayloadOverflowError extends Error {
constructor(needed, capacity) {
super(`broker payload needs ${needed} bytes but the channel region holds ${capacity}`);
this.name = "PayloadOverflowError";
}
}
class PayloadDecodeError extends Error {
constructor(message) {
super(`broker payload is malformed: ${message}`);
this.name = "PayloadDecodeError";
}
}
class PayloadWriter {
#view;
#bytes;
#cursor = 0;
constructor(payload) {
this.#bytes = payload;
this.#view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
}
get length() {
return this.#cursor;
}
get remaining() {
return this.#bytes.byteLength - this.#cursor;
}
u8(value) {
this.#view.setUint8(this.#reserve(1), value);
}
u32(value) {
this.#view.setUint32(this.#reserve(4), value, true);
}
i32(value) {
this.#view.setInt32(this.#reserve(4), value, true);
}
u64(value) {
this.#view.setBigUint64(this.#reserve(8), value, true);
}
bytes(source) {
this.#bytes.set(source, this.#reserve(source.byteLength));
}
string(value) {
const encoded = textEncoder2.encode(value);
this.u32(encoded.byteLength);
this.bytes(encoded);
}
rewind(to) {
if (!Number.isSafeInteger(to) || to < 0 || to > this.#cursor) {
throw new RangeError("the payload rewind target is outside what has been written");
}
this.#cursor = to;
}
patch(at, count) {
if (!Number.isSafeInteger(at) || !Number.isSafeInteger(count) || at < 0 || count < 0 || at + count > this.#cursor) {
throw new RangeError("the payload patch range is outside what has been written");
}
return new DataView(this.#bytes.buffer, this.#bytes.byteOffset + at, count);
}
#reserve(count) {
if (count > this.remaining)
throw new PayloadOverflowError(this.#cursor + count, this.#bytes.byteLength);
const at = this.#cursor;
this.#cursor += count;
return at;
}
}
class PayloadReader {
#view;
#bytes;
#cursor = 0;
constructor(payload, length) {
if (!Number.isSafeInteger(length) || length < 0 || length > payload.byteLength) {
throw new PayloadDecodeError(`declared length ${length} is outside the payload region`);
}
this.#bytes = payload.subarray(0, length);
this.#view = new DataView(payload.buffer, payload.byteOffset, length);
}
get remaining() {
return this.#bytes.byteLength - this.#cursor;
}
u8() {
return this.#view.getUint8(this.#take(1));
}
u32() {
return this.#view.getUint32(this.#take(4), true);
}
i32() {
return this.#view.getInt32(this.#take(4), true);
}
u64() {
return this.#view.getBigUint64(this.#take(8), true);
}
bytes(count) {
return this.#bytes.subarray(this.#take(count), this.#cursor);
}
string() {
const length = this.u32();
const raw = this.bytes(length);
try {
return textDecoder2.decode(raw.slice());
} catch (cause) {
throw new PayloadDecodeError(`a string is not valid UTF-8: ${String(cause)}`);
}
}
#take(count) {
if (!Number.isSafeInteger(count) || count < 0)
throw new PayloadDecodeError(`length ${count} is invalid`);
if (count > this.remaining)
throw new PayloadDecodeError("the payload ended mid-field");
const at = this.#cursor;
this.#cursor += count;
return at;
}
}
function splitResult(value) {
if (value < 0n || value > 0xffffffffffffffffn) {
throw new RangeError(`broker result ${value} is outside the unsigned 64-bit range`);
}
return { lo: Number(value & 0xffffffffn) | 0, hi: Number(value >> 32n) | 0 };
}
function joinResult(lo, hi) {
return BigInt(hi >>> 0) << 32n | BigInt(lo >>> 0);
}
function writeStat(writer, stat) {
writer.u8(stat.kind === "directory" ? STAT_KIND_DIRECTORY : stat.kind === "symlink" ? STAT_KIND_SYMLINK : STAT_KIND_FILE);
writer.u32(stat.mode);
writer.u64(stat.size);
writer.u64(stat.atimeMs);
writer.u64(stat.mtimeMs);
writer.u64(stat.ctimeMs);
}
function readStat(reader) {
const code = reader.u8();
const kind = code === STAT_KIND_DIRECTORY ? "directory" : code === STAT_KIND_SYMLINK ? "symlink" : "file";
return {
kind,
mode: reader.u32(),
size: reader.u64(),
atimeMs: reader.u64(),
mtimeMs: reader.u64(),
ctimeMs: reader.u64()
};
}
class RepackedDoorbell {
buffer;
#slots;
constructor(buffer) {
if (buffer.byteLength < DOORBELL_BYTES) {
throw new RangeError(`a broker doorbell needs at least ${DOORBELL_BYTES} bytes`);
}
this.buffer = buffer;
this.#slots = new Int32Array(buffer, 0, DOORBELL_SLOTS);
}
static create() {
const doorbell = new RepackedDoorbell(new SharedArrayBuffer(DOORBELL_BYTES));
Atomics.store(doorbell.#slots, DOORBELL_RUNNING, 1);
return doorbell;
}
static attach(buffer) {
return new RepackedDoorbell(buffer);
}
ticket() {
return Atomics.load(this.#slots, DOORBELL_TICKET);
}
ring() {
Atomics.add(this.#slots, DOORBELL_TICKET, 1);
Atomics.notify(this.#slots, DOORBELL_TICKET);
}
running() {
return Atomics.load(this.#slots, DOORBELL_RUNNING) === 1;
}
requestStop() {
Atomics.store(this.#slots, DOORBELL_RUNNING, 0);
this.ring();
}
resume() {
Atomics.store(this.#slots, DOORBELL_RUNNING, 1);
}
wait(observed, timeoutMs) {
return Atomics.wait(this.#slots, DOORBELL_TICKET, observed, timeoutMs);
}
waitAsync(observed, timeoutMs) {
const result = Atomics.waitAsync(this.#slots, DOORBELL_TICKET, observed, timeoutMs);
return result.async ? result.value : Promise.resolve(result.value);
}
}
class RepackedChannel {
id;
buffer;
doorbell;
header;
payload;
constructor(id, buffer, doorbell) {
this.id = id;
this.buffer = buffer;
this.doorbell = doorbell;
this.header = new Int32Array(buffer, 0, CHANNEL_HEADER_SLOTS);
this.payload = new Uint8Array(buffer, CHANNEL_HEADER_BYTES);
}
static create(options) {
const payloadBytes = options.payloadBytes ?? DEFAULT_PAYLOAD_BYTES;
if (!Number.isSafeInteger(payloadBytes) || payloadBytes < MIN_PAYLOAD_BYTES) {
throw new RangeError(`a broker channel payload must be at least ${MIN_PAYLOAD_BYTES} bytes`);
}
if (!Number.isSafeInteger(options.id) || options.id < 0) {
throw new RangeError("a broker channel id must be a non-negative safe integer");
}
const channel = new RepackedChannel(options.id, new SharedArrayBuffer(CHANNEL_HEADER_BYTES + payloadBytes), options.doorbell);
Atomics.store(channel.header, HEADER_PAYLOAD, payloadBytes);
Atomics.store(channel.header, HEADER_STATE, STATE_IDLE);
return channel;
}
static attach(transfer) {
const doorbell = RepackedDoorbell.attach(transfer.doorbell);
if (transfer.channel.byteLength < CHANNEL_HEADER_BYTES + MIN_PAYLOAD_BYTES) {
throw new RangeError("the broker channel buffer is too small to be a channel");
}
const channel = new RepackedChannel(transfer.id, transfer.channel, doorbell);
const declared = Atomics.load(channel.header, HEADER_PAYLOAD);
if (declared !== channel.payload.byteLength) {
throw new RangeError(`the broker channel declares ${declared} payload bytes but carries a different region`);
}
return channel;
}
transfer() {
return { id: this.id, channel: this.buffer, doorbell: this.doorbell.buffer };
}
get payloadBytes() {
return this.payload.byteLength;
}
state() {
return Atomics.load(this.header, HEADER_STATE);
}
}
function fsErrorNameOf(code) {
for (const name of Object.keys(FS_ERRNO)) {
if (FS_ERRNO[name] === code)
return name;
}
return;
}
function errnoName(code) {
return fsErrorNameOf(code) ?? `errno ${code}`;
}
// packages/pgwasm/src/opfs/broker/server.ts
var OK = { errno: 0, result: 0n, responseBytes: 0 };
function ok(result = 0n, responseBytes = 0) {
return { errno: 0, result: typeof result === "bigint" ? result : BigInt(result), responseBytes };
}
class ProtocolViolation extends Error {
constructor(message) {
super(message);
this.name = "ProtocolViolation";
}
}
class RepackedSyncBroker {
doorbell;
#vfs;
#now;
#log;
#pollIntervalMs;
#clients = new Map;
#serving = false;
constructor(options) {
this.#vfs = options.vfs;
this.doorbell = options.doorbell;
this.#now = options.now ?? (() => BigInt(Date.now()));
this.#log = options.log ?? ((message) => console.warn(message));
this.#pollIntervalMs = options.pollIntervalMs ?? 250;
}
attachedIds() {
return [...this.#clients.keys()].sort((left, right) => left - right);
}
openFdCount(channelId) {
if (channelId === undefined) {
let total = 0;
for (const client of this.#clients.values())
total += client.fds.size;
return total;
}
return this.#clients.get(channelId)?.fds.size ?? 0;
}
attach(channel) {
if (this.#clients.has(channel.id))
throw new Error(`broker channel ${channel.id} is already attached`);
if (channel.doorbell.buffer !== this.doorbell.buffer) {
throw new Error(`broker channel ${channel.id} rings a different doorbell`);
}
this.#clients.set(channel.id, { channel, fds: new Map });
}
detach(channel, reason = "detached by the host") {
this.#detach(typeof channel === "number" ? channel : channel.id, reason, FAULT_DETACHED);
}
#detach(id, reason, fault) {
const client = this.#clients.get(id);
if (client === undefined)
return;
this.#clients.delete(id);
this.#releaseFds(client);
const header = client.channel.header;
Atomics.store(header, HEADER_FAULT, fault);
Atomics.store(header, HEADER_ERRNO, 0);
Atomics.store(header, HEADER_RESPONSE, 0);
Atomics.store(header, HEADER_STATE, STATE_DETACHED);
Atomics.notify(header, HEADER_STATE);
this.#log(`repacked broker detached channel ${id}: ${reason}`);
}
detachAll(reason = "broker shutting down") {
for (const id of this.attachedIds())
this.detach(id, reason);
}
serveOnce() {
let served = 0;
for (const client of [...this.#clients.values()]) {
if (Atomics.load(client.channel.header, HEADER_STATE) !== STATE_REQUEST)
continue;
this.#answer(client);
served += 1;
}
return served;
}
serveForever() {
this.#enterLoop();
try {
while (this.doorbell.running()) {
const observed = this.doorbell.ticket();
if (this.serveOnce() > 0)
continue;
this.doorbell.wait(observed, this.#pollIntervalMs);
}
} finally {
this.#serving = false;
}
}
async serve() {
this.#enterLoop();
try {
while (this.doorbell.running()) {
const observed = this.doorbell.ticket();
if (this.serveOnce() > 0)
continue;
await this.doorbell.waitAsync(observed, this.#pollIntervalMs);
}
} finally {
this.#serving = false;
}
}
#enterLoop() {
if (this.#serving)
throw new Error("the repacked broker is already serving");
this.#serving = true;
}
#releaseFds(client) {
for (const entry of client.fds.values()) {
try {
this.#vfs.close(entry.fd);
} catch {}
}
client.fds.clear();
}
#answer(client) {
const header = client.channel.header;
const opcode = Atomics.load(header, HEADER_OPCODE);
const requestBytes = Atomics.load(header, HEADER_REQUEST);
let fault = FAULT_NONE;
let answer = OK;
try {
if (!isKnownOpcode(opcode))
throw new ProtocolViolation(`unknown opcode ${opcode}`);
if (requestBytes < 0 || requestBytes > client.channel.payloadBytes) {
throw new ProtocolViolation(`request length ${requestBytes} is outside the payload region`);
}
const reader = new PayloadReader(client.channel.payload, requestBytes);
const writer = new PayloadWriter(client.channel.payload);
answer = this.#dispatch(client, opcode, reader, writer);
} catch (cause) {
if (cause instanceof ProtocolViolation || cause instanceof PayloadDecodeError) {
this.#detach(client.channel.id, cause.message, FAULT_PROTOCOL);
return;
}
if (cause instanceof FsError) {
answer = { errno: cause.code, result: 0n, responseBytes: 0 };
} else {
fault = FAULT_STORE;
answer = this.#faultAnswer(client, cause);
}
}
const { lo, hi } = splitResult(answer.result);
Atomics.store(header, HEADER_ERRNO, answer.errno);
Atomics.store(header, HEADER_RESULT_LO, lo);
Atomics.store(header, HEADER_RESULT_HI, hi);
Atomics.store(header, HEADER_RESPONSE, answer.responseBytes);
Atomics.store(header, HEADER_FAULT, fault);
Atomics.store(header, HEADER_STATE, STATE_RESPONSE);
Atomics.notify(header, HEADER_STATE);
}
#faultAnswer(client, cause) {
const message = cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause);
const writer = new PayloadWriter(client.channel.payload);
const room = client.channel.payloadBytes - 4;
try {
writer.string(message.length > room ? message.slice(0, Math.max(0, room)) : message);
} catch {
return { errno: 0, result: 0n, responseBytes: 0 };
}
return { errno: 0, result: 0n, responseBytes: writer.length };
}
#dispatch(client, opcode, reader, writer) {
switch (opcode) {
case OPCODE_OPEN:
return this.#open(client, reader);
case OPCODE_CLOSE:
return this.#close(client, reader);
case OPCODE_READ:
return this.#read(client, reader);
case OPCODE_WRITE:
return this.#write(client, reader);
case OPCODE_FSYNC:
return this.#fsync(client, reader);
case OPCODE_FSTAT:
return this.#stat(writer, this.#vfs.fstat(this.#fd(client, reader.u32()).fd));
case OPCODE_STAT:
return this.#stat(writer, this.#vfs.stat(reader.string()));
case OPCODE_LSTAT:
return this.#stat(writer, this.#vfs.lstat(reader.string()));
case OPCODE_READDIR:
return this.#readdir(reader, writer);
case OPCODE_MKDIR:
return this.#mkdir(reader);
case OPCODE_RMDIR:
this.#vfs.rmdir(reader.string(), this.#now());
return OK;
case OPCODE_UNLINK:
this.#vfs.unlink(reader.string(), this.#now());
return OK;
case OPCODE_RENAME: {
const oldPath = reader.string();
this.#vfs.rename(oldPath, reader.string(), this.#now());
return OK;
}
case OPCODE_TRUNCATE: {
const path = reader.string();
this.#vfs.truncate(path, reader.u64(), this.#now());
return OK;
}
case OPCODE_SIZE:
return this.#size(reader);
case OPCODE_SYMLINK: {
const target = reader.string();
this.#vfs.symlink(target, reader.string(), this.#now());
return OK;
}
case OPCODE_READLINK:
return this.#readlink(reader, writer);
default:
throw new ProtocolViolation(`unhandled opcode ${opcode}`);
}
}
#open(client, reader) {
const path = reader.string();
const flags = reader.u32();
const mode = reader.u32();
const plan = planOpen(flags);
const nowMs = this.#now();
if (!plan.follow) {
try {
if (this.#vfs.lstat(path).kind === "symlink") {
throw new FsError("ELOOP", "path is a symbolic link and O_NOFOLLOW was requested", { path });
}
} catch (cause) {
if (!(cause instanceof FsError) || cause.code !== FS_ERRNO.ENOENT)
throw cause;
}
}
if (plan.requireExisting)
this.#vfs.lstat(path);
if (plan.truncateFirst)
this.#vfs.truncate(path, 0n, nowMs);
let fd;
try {
fd = this.#vfs.open(path, plan.coreFlags, mode, nowMs);
} catch (cause) {
if (plan.fallbackFlags === undefined || !(cause instanceof FsError) || cause.code !== FS_ERRNO.ENOENT) {
throw cause;
}
fd = this.#vfs.open(path, plan.fallbackFlags, mode, nowMs);
}
client.fds.set(fd, { fd, readable: plan.readable, writable: plan.writable });
return ok(fd);
}
#close(client, reader) {
const entry = this.#fd(client, reader.u32());
this.#vfs.close(entry.fd);
client.fds.delete(entry.fd);
return OK;
}
#read(client, reader) {
const entry = this.#fd(client, reader.u32());
if (!entry.readable)
throw new FsError("EBADF", "descriptor was not opened for reading");
const hasPosition = reader.u8() === 1;
const position = reader.u64();
const length = reader.u32();
if (length > client.channel.payloadBytes) {
throw new ProtocolViolation(`read length ${length} exceeds the payload region`);
}
const target = client.channel.payload.subarray(0, length);
const count = hasPosition ? this.#vfs.read(entry.fd, target, 0, length, position) : this.#vfs.read(entry.fd, target, 0, length);
return ok(count, count);
}
#write(client, reader) {
const entry = this.#fd(client, reader.u32());
if (!entry.writable)
throw new FsError("EBADF", "descriptor was not opened for writing");
const hasPosition = reader.u8() === 1;
const position = reader.u64();
const length = reader.u32();
const source = reader.bytes(length);
const count = this.#vfs.write(entry.fd, source, 0, length, hasPosition ? position : undefined, this.#now());
return ok(count);
}
#fsync(client, reader) {
if (reader.u8() === 1)
this.#fd(client, reader.u32());
this.#vfs.strictSync();
return OK;
}
#stat(writer, stat) {
writeStat(writer, stat);
return ok(stat.size, writer.length);
}
#readdir(reader, writer) {
const path = reader.string();
const cursor = reader.u32();
const names = this.#vfs.readdir(path);
if (cursor > names.length)
throw new ProtocolViolation(`readdir cursor ${cursor} is past the listing`);
const countAt = writer.length;
writer.u32(0);
writer.i32(READDIR_DONE);
let emitted = 0;
let next = READDIR_DONE;
for (let index = cursor;index < names.length; index += 1) {
const before = writer.length;
try {
writer.string(names[index]);
} catch (cause) {
if (!(cause instanceof PayloadOverflowError))
throw cause;
writer.rewind(before);
if (emitted === 0)
throw cause;
next = index;
break;
}
emitted += 1;
}
const counters = writer.patch(countAt, 8);
counters.setUint32(0, emitted, true);
counters.setInt32(4, next, true);
return ok(emitted, writer.length);
}
#mkdir(reader) {
const path = reader.string();
const recursive = reader.u8() === 1;
const mode = reader.u32();
this.#vfs.mkdir(path, { recursive, mode, nowMs: this.#now() });
return OK;
}
#readlink(reader, writer) {
writer.string(this.#vfs.readlink(reader.string()));
return ok(writer.length, writer.length);
}
#size(reader) {
const stat = this.#vfs.stat(reader.string());
if (stat.kind !== "file")
throw new FsError("EISDIR", "size is a file query");
return ok(stat.size);
}
#fd(client, fd) {
const entry = client.fds.get(fd);
if (entry === undefined)
throw new FsError("EBADF", `descriptor ${fd} is not owned by this client`);
return entry;
}
}
// packages/pgwasm/src/opfs/broker/client.ts
class RepackedBrokerTransportError extends Error {
brokerCode;
constructor(brokerCode, message) {
super(message);
this.name = "RepackedBrokerTransportError";
this.brokerCode = brokerCode;
}
}
class RepackedBrokerStoreError extends Error {
brokerCode = "store";
constructor(message) {
super(`the repacked store behind the broker failed: ${message}`);
this.name = "RepackedBrokerStoreError";
}
}
var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
var TRANSFER_OVERHEAD_BYTES = 4 + 1 + 8 + 4;
class RepackedSyncClient {
channel;
#timeoutMs;
#reply;
#sequence = 0;
constructor(channel, options = {}) {
this.channel = channel;
this.#timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
this.#reply = new Uint8Array(channel.payloadBytes);
}
get maxTransferBytes() {
return this.channel.payloadBytes - TRANSFER_OVERHEAD_BYTES;
}
open(path, flags, mode = 33206) {
const reply = this.#call(OPCODE_OPEN, (writer) => {
writer.string(path);
writer.u32(flags);
writer.u32(mode);
});
return { errno: reply.errno, fd: reply.errno === 0 ? Number(reply.result) : -1 };
}
close(fd) {
return { errno: this.#call(OPCODE_CLOSE, (writer) => writer.u32(fd)).errno };
}
read(fd, length, position) {
if (!Number.isSafeInteger(length) || length < 0) {
throw new RangeError("a broker read length must be a non-negative safe integer");
}
const output = new Uint8Array(length);
let filled = 0;
let errno = 0;
while (filled < length) {
const want = Math.min(length - filled, this.maxTransferBytes);
const at = position === undefined ? undefined : position + BigInt(filled);
const reply = this.#call(OPCODE_READ, (writer) => {
writer.u32(fd);
writer.u8(at === undefined ? 0 : 1);
writer.u64(at ?? 0n);
writer.u32(want);
});
if (reply.errno !== 0) {
errno = reply.errno;
break;
}
output.set(reply.bytes, filled);
filled += reply.bytes.byteLength;
if (reply.bytes.byteLength < want)
break;
}
return { errno, count: filled, bytes: output.subarray(0, filled) };
}
write(fd, bytes, position) {
let written = 0;
let errno = 0;
while (written < bytes.byteLength) {
const chunk = bytes.subarray(written, written + Math.min(bytes.byteLength - written, this.maxTransferBytes));
const at = position === undefined ? undefined : position + BigInt(written);
const reply = this.#call(OPCODE_WRITE, (writer) => {
writer.u32(fd);
writer.u8(at === undefined ? 0 : 1);
writer.u64(at ?? 0n);
writer.u32(chunk.byteLength);
writer.bytes(chunk);
});
if (reply.errno !== 0) {
errno = reply.errno;
break;
}
const count = Number(reply.result);
written += count;
if (count < chunk.byteLength)
break;
}
return { errno, count: written };
}
fsync(fd) {
return {
errno: this.#call(OPCODE_FSYNC, (writer) => {
writer.u8(fd === undefined ? 0 : 1);
writer.u32(fd ?? 0);
}).errno
};
}
fstat(fd) {
return this.#statCall(OPCODE_FSTAT, (writer) => writer.u32(fd));
}
stat(path) {
return this.#statCall(OPCODE_STAT, (writer) => writer.string(path));
}
lstat(path) {
return this.#statCall(OPCODE_LSTAT, (writer) => writer.string(path));
}
symlink(target, path) {
return {
errno: this.#call(OPCODE_SYMLINK, (writer) => {
writer.string(target);
writer.string(path);
}).errno
};
}
readlink(path) {
const reply = this.#call(OPCODE_READLINK, (writer) => writer.string(path));
if (reply.errno !== 0)
return { errno: reply.errno, target: undefined };
return { errno: 0, target: new PayloadReader(reply.bytes, reply.bytes.byteLength).string() };
}
readdir(path) {
const entries = [];
let cursor = 0;
while (cursor !== undefined) {
const page = this.readdirPage(path, cursor);
if (page.errno !== 0)
return { errno: page.errno, entries: [] };
entries.push(...page.entries);
cursor = page.nextCursor;
}
return { errno: 0, entries };
}
readdirPage(path, cursor = 0) {
const reply = this.#call(OPCODE_READDIR, (writer) => {
writer.string(path);
writer.u32(cursor);
});
if (reply.errno !== 0)
return { errno: reply.errno, entries: [], nextCursor: undefined };
const reader = new PayloadReader(reply.bytes, reply.bytes.byteLength);
const count = reader.u32();
const next = reader.i32();
const entries = [];
for (let index = 0;index < count; index += 1)
entries.push(reader.string());
return { errno: 0, entries, nextCursor: next === READDIR_DONE ? undefined : next };
}
mkdir(path, options = {}) {
return {
errno: this.#call(OPCODE_MKDIR, (writer) => {
writer.string(path);
writer.u8(options.recursive === true ? 1 : 0);
writer.u32(options.mode ?? 16895);
}).errno
};
}
rmdir(path) {
return { errno: this.#call(OPCODE_RMDIR, (writer) => writer.string(path)).errno };
}
unlink(path) {
return { errno: this.#call(OPCODE_UNLINK, (writer) => writer.string(path)).errno };
}
rename(oldPath, newPath) {
return {
errno: this.#call(OPCODE_RENAME, (writer) => {
writer.string(oldPath);
writer.string(newPath);
}).errno
};
}
truncate(path, size) {
return {
errno: this.#call(OPCODE_TRUNCATE, (writer) => {
writer.string(path);
writer.u64(size);
}).errno
};
}
size(path) {
const reply = this.#call(OPCODE_SIZE, (writer) => writer.string(path));
return { errno: reply.errno, size: reply.errno === 0 ? reply.result : 0n };
}
#statCall(opcode, encode) {
const reply = this.#call(opcode, encode);
if (reply.errno !== 0)
return { errno: reply.errno, stat: undefined };
return { errno: 0, stat: readStat(new PayloadReader(reply.bytes, reply.bytes.byteLength)) };
}
#call(opcode, encode) {
const header = this.channel.header;
const state = Atomics.load(header, HEADER_STATE);
if (state === STATE_DETACHED) {
throw new RepackedBrokerTransportError("detached", "the repacked broker detached this client");
}
if (state === STATE_REQUEST) {
throw new RepackedBrokerTransportError("protocol", "a repacked broker request is already in flight");
}
const writer = new PayloadWriter(this.channel.payload);
encode(writer);
this.#sequence = this.#sequence + 1 | 0;
Atomics.store(header, HEADER_SEQUENCE, this.#sequence);
Atomics.store(header, HEADER_OPCODE, opcode);
Atomics.store(header, HEADER_REQUEST, writer.length);
Atomics.store(header, HEADER_RESPONSE, 0);
Atomics.store(header, HEADER_ERRNO, 0);
Atomics.store(header, HEADER_FAULT, FAULT_NONE);
Atomics.store(header, HEADER_STATE, STATE_REQUEST);
this.channel.doorbell.ring();
while (Atomics.load(header, HEADER_STATE) === STATE_REQUEST) {
const outcome = Atomics.wait(header, HEADER_STATE, STATE_REQUEST, this.#timeoutMs);
if (outcome === "timed-out" && Atomics.load(header, HEADER_STATE) === STATE_REQUEST) {
throw new RepackedBrokerTransportError("timeout", `the repacked broker did not answer opcode ${opcode} within ${this.#timeoutMs} ms`);
}
}
const responseBytes = Atomics.load(header, HEADER_RESPONSE);
const fault = Atomics.load(header, HEADER_FAULT);
const errno = Atomics.load(header, HEADER_ERRNO);
const result = joinResult(Atomics.load(header, HEADER_RESULT_LO), Atomics.load(header, HEADER_RESULT_HI));
let bytes = new Uint8Array;
if (responseBytes > 0 && responseBytes <= this.channel.payloadBytes) {
this.#reply.set(this.channel.payload.subarray(0, responseBytes));
bytes = this.#reply.subarray(0, responseBytes);
}
if (Atomics.load(header, HEADER_STATE) === STATE_DETACHED) {
throw new RepackedBrokerTransportError(fault === FAULT_PROTOCOL ? "protocol" : "detached", fault === FAULT_PROTOCOL ? `the repacked broker rejected opcode ${opcode} as a protocol violation and detached this client` : "the repacked broker detached this client");
}
if (Atomics.load(header, HEADER_STATE) !== STATE_RESPONSE) {
throw new RepackedBrokerTransportError("protocol", "the repacked broker left the channel in an unknown state");
}
Atomics.store(header, HEADER_STATE, STATE_IDLE);
if (fault === FAULT_STORE)
throw new RepackedBrokerStoreError(this.#faultMessage(bytes));
if (fault !== FAULT_NONE) {
throw new RepackedBrokerTransportError("protocol", `the repacked broker reported fault ${fault}`);
}
return { errno, result, bytes };
}
#faultMessage(bytes) {
if (bytes.byteLength === 0)
return "no detail was reported";
try {
return new PayloadReader(bytes, bytes.byteLength).string();
} catch {
return "the fault detail was malformed";
}
}
}
function throwOnErrno(result, operation, path) {
if (result.errno === 0)
return;
const name = fsErrorNameOf(result.errno);
const detail = `${operation} failed with ${errnoName(result.errno)}`;
if (name === undefined)
throw new RepackedBrokerTransportError("protocol", `${detail} (unknown to the core)`);
throw new FsError(name, detail, path === undefined ? { operation } : { operation, path });
}
// packages/pgwasm/src/opfs/wasi/preview1.ts
var WASI_ERRNO = {
SUCCESS: 0,
ACCES: 2,
BADF: 8,
EXIST: 20,
INVAL: 28,
IO: 29,
ISDIR: 31,
LOOP: 32,
NOENT: 44,
NOSYS: 52,
NOTDIR: 54,
NOTEMPTY: 55,
NOTSUP: 58,
OVERFLOW: 61,
PERM: 63,
SPIPE: 70,
NOTCAPABLE: 76
};
var WASI_FILETYPE = {
UNKNOWN: 0,
BLOCK_DEVICE: 1,
CHARACTER_DEVICE: 2,
DIRECTORY: 3,
REGULAR_FILE: 4,
SOCKET_DGRAM: 5,
SOCKET_STREAM: 6,
SYMBOLIC_LINK: 7
};
var OFLAGS_CREAT = 1;
var OFLAGS_DIRECTORY = 2;
var OFLAGS_EXCL = 4;
var OFLAGS_TRUNC = 8;
var FDFLAGS_APPEND = 1;
var FDFLAGS_DSYNC = 2;
var FDFLAGS_NONBLOCK = 4;
var FDFLAGS_RSYNC = 8;
var FDFLAGS_SYNC = 16;
var LOOKUPFLAGS_SYMLINK_FOLLOW = 1;
var RIGHTS_FD_READ = 1n << 1n;
var RIGHTS_FD_WRITE = 1n << 6n;
var RIGHTS_ALL = 0xffffffffffffffffn;
var WHENCE_SET = 0;
var WHENCE_CUR = 1;
var WHENCE_END = 2;
var FILESTAT_BYTES = 64;
var DIRENT_HEADER_BYTES = 24;
var DIRECTORY_SIZE = 4096n;
var NS_PER_MS = 1000000n;
var FD_ARGUMENT_INDEX = {
fd_prestat_get: 0,
fd_prestat_dir_name: 0,
fd_close: 0,
fd_read: 0,
fd_pread: 0,
fd_write: 0,
fd_pwrite: 0,
fd_seek: 0,
fd_tell: 0,
fd_fdstat_get: 0,
fd_fdstat_set_flags: 0,
fd_fdstat_set_rights: 0,
fd_filestat_get: 0,
fd_filestat_set_size: 0,
fd_filestat_set_times: 0,
fd_readdir: 0,
fd_sync: 0,
fd_datasync: 0,
fd_allocate: 0,
fd_advise: 0,
path_open: 0,
path_filestat_get: 0,
path_filestat_set_times: 0,
path_create_directory: 0,
path_remove_directory: 0,
path_unlink_file: 0,
path_rename: 0,
path_readlink: 0,
path_symlink: 2,
path_link: 0
};
var textEncoder3 = new TextEncoder;
var textDecoder3 = new TextDecoder("utf-8", { fatal: false });
function normalizeWasiPath(path) {
let value = path.replace(/\0+$/u, "");
if (value === "" || value === ".")
return "/";
if (!value.startsWith("/"))
value = `/${value}`;
const parts = [];
for (const segment of value.split("/")) {
if (segment === "" || segment === ".")
continue;
if (segment === "..") {
parts.pop();
continue;
}
parts.push(segment);
}
return `/${parts.join("/")}`;
}
function inodeOf(path) {
let hash = 0xcbf29ce484222325n;
const bytes = textEncoder3.encode(path);
for (const byte of bytes) {
hash = BigInt.asUintN(64, (hash ^ BigInt(byte)) * 0x1000000001b3n);
}
return hash === 0n ? 1n : hash;
}
function toBigInt(value) {
return typeof value === "bigint" ? value : BigInt(Math.trunc(value));
}
function createWasiPreview1Fs(options) {
const client = options.client;
const preopenFd = options.preopenFd ?? 3;
const preopenPath = normalizeWasiPath(options.preopenPath ?? "/");
const fdBase = options.fdBase ?? preopenFd + 1;
const now = options.now ?? (() => BigInt(Date.now()));
const onError = options.onError ?? ((call, cause) => {
const detail = cause instanceof Error ? cause.stack ?? `${cause.name}: ${cause.message}` : String(cause);
console.error(`[wasi-preview1 ${call} @${now()}] ${detail}`);
});
if (!Number.isSafeInteger(preopenFd) || preopenFd < 3) {
throw new RangeError("a WASI preopen fd must be a safe integer of at least 3 (0-2 are stdio)");
}
if (!Number.isSafeInteger(fdBase) || fdBase <= preopenFd) {
throw new RangeError("the WASI adapter fd base must be a safe integer above the preopen fd");
}
const table = new Map;
let nextFd = fdBase;
const preopenEntry = () => ({
fd: preopenFd,
path: preopenPath,
isDir: true,
clientFd: undefined,
offset: 0n,
fdflags: 0,
rightsBase: RIGHTS_ALL,
rightsInheriting: RIGHTS_ALL,
readable: true,
writable: false,
listing: undefined
});
table.set(preopenFd, preopenEntry());
const bytes = () => new Uint8Array(options.memory());
const view = () => new DataView(options.memory());
function readGuestString(ptr, length) {
return textDecoder3.decode(bytes().slice(ptr, ptr + length));
}
function* iovecs(ptr, count) {
const data = view();
for (let index = 0;index < count; index += 1) {
const base = ptr + index * 8;
yield { ptr: data.getUint32(base, true), len: data.getUint32(base + 4, true) };
}
}
function owns(fd) {
return fd === preopenFd || Number.isSafeInteger(fd) && fd >= fdBase;
}
function entryOf(fd) {
return table.get(fd);
}
function allocate(entry) {
const fd = nextFd;
nextFd += 1;
table.set(fd, { ...entry, fd });
return fd;
}
function resolve(dirfd, ptr, length) {
const dir = entryOf(dirfd);
if (dir === undefined || !dir.isDir)
return;
const relative = readGuestString(ptr, length);
if (relative.startsWith("/"))
return normalizeWasiPath(relative);
return normalizeWasiPath(dir.path === "/" ? `/${relative}` : `${dir.path}/${relative}`);
}
function filetypeOf(kind) {
if (kind === "directory")
return WASI_FILETYPE.DIRECTORY;
return kind === "symlink" ? WASI_FILETYPE.SYMBOLIC_LINK : WASI_FILETYPE.REGULAR_FILE;
}
function writeFilestat(ptr, path, stat) {
bytes().fill(0, ptr, ptr + FILESTAT_BYTES);
const data = view();
const isDirectory = stat.kind === "directory";
const times = [stat.atimeMs, stat.mtimeMs, stat.ctimeMs].map((ms) => ms * NS_PER_MS);
data.setBigUint64(ptr + 0, 1n, true);
data.setBigUint64(ptr + 8, inodeOf(path), true);
data.setUint8(ptr + 16, filetypeOf(stat.kind));
data.setBigUint64(ptr + 24, 1n, true);
data.setBigUint64(ptr + 32, isDirectory ? DIRECTORY_SIZE : stat.size, true);
data.setBigUint64(ptr + 40, times[0], true);
data.setBigUint64(ptr + 48, times[1], true);
data.setBigUint64(ptr + 56, times[2], true);
}
function sizeOf(entry) {
if (entry.clientFd === undefined)
return { errno: 0, size: DIRECTORY_SIZE };
const stat = client.fstat(entry.clientFd);
if (stat.errno !== 0 || stat.stat === undefined)
return { errno: stat.errno || WASI_ERRNO.IO, size: 0n };
return { errno: 0, size: stat.stat.size };
}
const fs = {
fd_prestat_get(fd, resultPtr) {
if (fd !== preopenFd)
return WASI_ERRNO.BADF;
const data = view();
data.setUint8(resultPtr, 0);
data.setUint32(resultPtr + 4, textEncoder3.encode(preopenPath).byteLength, true);
return WASI_ERRNO.SUCCESS;
},
fd_prestat_dir_name(fd, pathPtr, pathLen) {
if (fd !== preopenFd)
return WASI_ERRNO.BADF;
const encoded = textEncoder3.encode(preopenPath);
if (pathLen < encoded.byteLength)
return WASI_ERRNO.INVAL;
bytes().set(encoded, pathPtr);
return WASI_ERRNO.SUCCESS;
},
fd_close(fd) {
if (fd === preopenFd)
return WASI_ERRNO.SUCCESS;
const entry = entryOf(fd);
if (entry === undefined)
return WASI_ERRNO.BADF;
table.delete(fd);
if (entry.clientFd === undefined)
return WASI_ERRNO.SUCCESS;
return client.close(entry.clientFd).errno;
},
fd_read(fd, iovsPtr, iovsLen, nreadPtr) {
const entry = entryOf(fd);
if (entry === undefined)
return WASI_ERRNO.BADF;
if (entry.clientFd === undefined)
return WASI_ERRNO.ISDIR;
if (!entry.readable)
return WASI_ERRNO.NOTCAPABLE;
let total = 0;
let errno = WASI_ERRNO.SUCCESS;
for (const { ptr, len } of iovecs(iovsPtr, iovsLen)) {
if (len === 0)
continue;
const chunk = client.read(entry.clientFd, len, entry.offset + BigInt(total));
if (chunk.count > 0)
bytes().set(chunk.bytes, ptr);
total += chunk.count;
if (chunk.errno !== 0) {
errno = chunk.errno;
break;
}
if (chunk.count < len)
break;
}
entry.offset += BigInt(total);
view().setUint32(nreadPtr, total, true);
return total > 0 ? WASI_ERRNO.SUCCESS : errno;
},
fd_pread(fd, iovsPtr, iovsLen, offset, nreadPtr) {
const entry = entryOf(fd);
if (entry === undefined)
return WASI_ERRNO.BADF;
if (entry.clientFd === undefined)
return WASI_ERRNO.ISDIR;
if (!entry.readable)
return WASI_ERRNO.NOTCAPABLE;
let at = toBigInt(offset);
let total = 0;
let errno = WASI_ERRNO.SUCCESS;
for (const { ptr, len } of iovecs(iovsPtr, iovsLen)) {
if (len === 0)
continue;
const chunk = client.read(entry.clientFd, len, at);
if (chunk.count > 0)
bytes().set(chunk.bytes, ptr);
at += BigInt(chunk.count);
total += chunk.count;
if (chunk.errno !== 0) {
errno = chunk.errno;
break;
}
if (chunk.count < len)
break;
}
view().setUint32(nreadPtr, total, true);
return total > 0 ? WASI_ERRNO.SUCCESS : errno;
},
fd_write(fd, iovsPtr, iovsLen, nwrittenPtr) {
const entry = entryOf(fd);
if (entry === undefined)
return WASI_ERRNO.BADF;
if (entry.clientFd === undefined)
return WASI_ERRNO.ISDIR;
if (!entry.writable)
return WASI_ERRNO.NOTCAPABLE;
if ((entry.fdflags & FDFLAGS_APPEND) !== 0) {
const end = sizeOf(entry);
if (end.errno !== 0)
return end.errno;
entry.offset = end.size;
}
let total = 0;
let errno = WASI_ERRNO.SUCCESS;
for (const { ptr, len } of iovecs(iovsPtr, iovsLen)) {
if (len === 0)
continue;
const source = bytes().subarray(ptr, ptr + len);
const written = client.write(entry.clientFd, source, entry.offset + BigInt(total));
total += written.count;
if (written.errno !== 0) {
errno = written.errno;
break;
}
if (written.count < len)
break;
}
entry.offset += BigInt(total);
view().setUint32(nwrittenPtr, total, true);
return total > 0 ? WASI_ERRNO.SUCCESS : errno;
},
fd_pwrite(fd, iovsPtr, iovsLen, offset, nwrittenPtr) {
const entry = entryOf(fd);
if (entry === undefined)
return WASI_ERRNO.BADF;
if (entry.clientFd === undefined)
return WASI_ERRNO.ISDIR;
if (!entry.writable)
return WASI_ERRNO.NOTCAPABLE;
let at = toBigInt(offset);
let total = 0;
let errno = WASI_ERRNO.SUCCESS;
for (const { ptr, len } of iovecs(iovsPtr, iovsLen)) {
if (len === 0)
continue;
const written = client.write(entry.clientFd, bytes().subarray(ptr, ptr + len), at);
at += BigInt(written.count);
total += written.count;
if (written.errno !== 0) {
errno = written.errno;
break;
}
if (written.count < len)
break;
}
view().setUint32(nwrittenPtr, total, true);
return total > 0 ? WASI_ERRNO.SUCCESS : errno;
},
fd_seek(fd, offset, whence, resultPtr) {
const entry = entryOf(fd);
if (entry === undefined)
return WASI_ERRNO.BADF;
if (entry.clientFd === undefined)
return WASI_ERRNO.BADF;
const delta = toBigInt(offset);
let target;
if (whence === WHENCE_SET)
target = delta;
else if (whence === WHENCE_CUR)
target = entry.offset + delta;
else if (whence === WHENCE_END) {
const end = sizeOf(entry);
if (end.errno !== 0)
return end.errno;
target = end.size + delta;
} else
return WASI_ERRNO.INVAL;
if (target < 0n)
return WASI_ERRNO.INVAL;
entry.offset = target;
view().setBigUint64(resultPtr, target, true);
return WASI_ERRNO.SUCCESS;
},
fd_tell(fd, resultPtr) {
const entry = entryOf(fd);
if (entry === undefined)
return WASI_ERRNO.BADF;
if (entry.clientFd === undefined)
return WASI_ERRNO.BADF;
view().setBigUint64(resultPtr, entry.offset, true);
return WASI_ERRNO.SUCCESS;
},
fd_fdstat_get(fd, resultPtr) {
const entry = entryOf(fd);
if (entry === undefined)
return WASI_ERRNO.BADF;
const data = view();
data.setUint8(resultPtr, entry.isDir ? WASI_FILETYPE.DIRECTORY : WASI_FILETYPE.REGULAR_FILE);
data.setUint8(resultPtr + 1, 0);
data.setUint16(resultPtr + 2, entry.fdflags, true);
data.setUint32(resultPtr + 4, 0, true);
data.setBigUint64(resultPtr + 8, entry.rightsBase, true);
data.setBigUint64(resultPtr + 16, entry.rightsInheriting, true);
return WASI_ERRNO.SUCCESS;
},
fd_fdstat_set_flags(fd, fdflags) {
const entry = entryOf(fd);
if (entry === undefined)
return WASI_ERRNO.BADF;
entry.fdflags = fdflags & (FDFLAGS_APPEND | FDFLAGS_DSYNC | FDFLAGS_NONBLOCK | FDFLAGS_RSYNC | FDFLAGS_SYNC);
return WASI_ERRNO.SUCCESS;
},
fd_fdstat_set_rights(fd, rightsBase, rightsInheriting) {
const entry = entryOf(fd);
if (entry === undefined)
return WASI_ERRNO.BADF;
const base = toBigInt(rightsBase);
const inheriting = toBigInt(rightsInheriting);
if ((base & ~entry.rightsBase) !== 0n || (inheriting & ~entry.rightsInheriting) !== 0n) {
return WASI_ERRNO.NOTCAPABLE;
}
entry.rightsBase = base;
entry.rightsInheriting = inheriting;
return WASI_ERRNO.SUCCESS;
},
fd_filestat_get(fd, resultPtr) {
const entry = entryOf(fd);
if (entry === undefined)
return WASI_ERRNO.BADF;
const stat = entry.clientFd === undefined ? client.stat(entry.path) : client.fstat(entry.clientFd);
if (stat.errno !== 0 || stat.stat === undefined)
return stat.errno || WASI_ERRNO.IO;
writeFilestat(resultPtr, entry.path, stat.stat);
return WASI_ERRNO.SUCCESS;
},
fd_filestat_set_size(fd, size) {
const entry = entryOf(fd);
if (entry === undefined)
return WASI_ERRNO.BADF;
if (entry.clientFd === undefined)
return WASI_ERRNO.ISDIR;
if (!entry.writable)
return WASI_ERRNO.NOTCAPABLE;
return client.truncate(entry.path, toBigInt(size)).errno;
},
fd_filestat_set_times(fd, _atim, _mtim, fstflags) {
const entry = entryOf(fd);
if (entry === undefined)
return WASI_ERRNO.BADF;
if ((fstflags & ~15) !== 0)
return WASI_ERRNO.INVAL;
if (fstflags === 0)
return WASI_ERRNO.SUCCESS;
return WASI_ERRNO.NOTSUP;
},
fd_readdir(fd, bufPtr, bufLen, cookie, bufusedPtr) {
const entry = entryOf(fd);
if (entry === undefined)
return WASI_ERRNO.BADF;
if (!entry.isDir)
return WASI_ERRNO.NOTDIR;
const start = Number(toBigInt(cookie));
if (!Number.isSafeInteger(start) || start < 0)
return WASI_ERRNO.INVAL;
if (start === 0 || entry.listing === undefined) {
const listed = client.readdir(entry.path);
if (listed.errno !== 0)
return listed.errno;
entry.listing = { names: listed.entries, filetypes: new Map };
}
const listing = entry.listing;
const data = view();
const memory = bytes();
let used = 0;
for (let index = start;index < listing.names.length; index += 1) {
const name = listing.names[index];
const nameBytes = textEncoder3.encode(name);
if (used + DIRENT_HEADER_BYTES >= bufLen) {
used = bufLen;
break;
}
let filetype = listing.filetypes.get(name);
if (filetype === undefined) {
const child = client.lstat(entry.path === "/" ? `/${name}` : `${entry.path}/${name}`);
filetype = child.errno !== 0 || child.stat === undefined ? WASI_FILETYPE.UNKNOWN : filetypeOf(child.stat.kind);
listing.filetypes.set(name, filetype);
}
const at = bufPtr + used;
data.setBigUint64(at, BigInt(index + 1), true);
data.setBigUint64(at + 8, inodeOf(entry.path === "/" ? `/${name}` : `${entry.path}/${name}`), true);
data.setUint32(at + 16, nameBytes.byteLength, true);
data.setUint8(at + 20, filetype);
data.setUint8(at + 21, 0);
data.setUint16(at + 22, 0, true);
const room = bufLen - used - DIRENT_HEADER_BYTES;
const copied = Math.min(nameBytes.byteLength, room);
memory.set(nameBytes.subarray(0, copied), at + DIRENT_HEADER_BYTES);
used += DIRENT_HEADER_BYTES + copied;
if (copied < nameBytes.byteLength)
break;
}
data.setUint32(bufusedPtr, used, true);
return WASI_ERRNO.SUCCESS;
},
fd_sync(fd) {
const entry = entryOf(fd);
if (entry === undefined)
return WASI_ERRNO.BADF;
return entry.clientFd === undefined ? client.fsync().errno : client.fsync(entry.clientFd).errno;
},
fd_datasync(fd) {
return fs.fd_sync(fd);
},
fd_allocate(fd, offset, length) {
const entry = entryOf(fd);
if (entry === undefined)
return WASI_ERRNO.BADF;
if (entry.clientFd === undefined)
return WASI_ERRNO.ISDIR;
if (!entry.writable)
return WASI_ERRNO.NOTCAPABLE;
const end = toBigInt(offset) + toBigInt(length);
if (end < 0n)
return WASI_ERRNO.INVAL;
const current = sizeOf(entry);
if (current.errno !== 0)
return current.errno;
if (end <= current.size)
return WASI_ERRNO.SUCCESS;
return client.truncate(entry.path, end).errno;
},
fd_advise(fd, _offset, _length, _advice) {
return entryOf(fd) === undefined ? WASI_ERRNO.BADF : WASI_ERRNO.SUCCESS;
},
path_open(dirfd, dirflags, pathPtr, pathLen, oflags, rightsBase, rightsInheriting, fdflags, resultPtr) {
const path = resolve(dirfd, pathPtr, pathLen);
if (path === undefined)
return WASI_ERRNO.BADF;
const followLinks = (dirflags & LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0;
const base = toBigInt(rightsBase);
const inheriting = toBigInt(rightsInheriting);
const wantsRead = (base & RIGHTS_FD_READ) !== 0n;
const wantsWrite = (base & RIGHTS_FD_WRITE) !== 0n;
const create = (oflags & OFLAGS_CREAT) !== 0;
const exclusive = (oflags & OFLAGS_EXCL) !== 0;
const truncate = (oflags & OFLAGS_TRUNC) !== 0;
const directory = (oflags & OFLAGS_DIRECTORY) !== 0;
const readable = wantsRead || !wantsWrite;
const writable = wantsWrite;
const openDirectory = () => {
const stat = followLinks ? client.stat(path) : client.lstat(path);
if (stat.errno !== 0 || stat.stat === undefined)
return stat.errno || WASI_ERRNO.IO;
if (stat.stat.kind === "symlink")
return WASI_ERRNO.LOOP;
if (stat.stat.kind !== "directory")
return WASI_ERRNO.NOTDIR;
if (create && exclusive)
return WASI_ERRNO.EXIST;
const fd = allocate({
path,
isDir: true,
clientFd: undefined,
offset: 0n,
fdflags: 0,
rightsBase: base === 0n ? RIGHTS_ALL : base,
rightsInheriting: inheriting === 0n ? RIGHTS_ALL : inheriting,
readable: true,
writable: false,
listing: undefined
});
view().setUint32(resultPtr, fd, true);
return WASI_ERRNO.SUCCESS;
};
if (directory)
return openDirectory();
const posixWrite = writable || create || truncate;
const posixRead = readable || !posixWrite;
let flags = posixRead && posixWrite ? O_RDWR : posixWrite ? O_WRONLY : O_RDONLY;
if (create)
flags |= O_CREAT;
if (exclusive)
flags |= O_EXCL;
if (truncate)
flags |= O_TRUNC;
if (!followLinks)
flags |= O_NOFOLLOW;
const opened = client.open(path, flags);
if (opened.errno !== 0) {
if (opened.errno === WASI_ERRNO.ISDIR)
return create && exclusive ? WASI_ERRNO.EXIST : openDirectory();
return opened.errno;
}
const fd = allocate({
path,
isDir: false,
clientFd: opened.fd,
offset: 0n,
fdflags: fdflags & (FDFLAGS_APPEND | FDFLAGS_DSYNC | FDFLAGS_NONBLOCK | FDFLAGS_RSYNC | FDFLAGS_SYNC),
rightsBase: base === 0n ? RIGHTS_ALL : base,
rightsInheriting: inheriting === 0n ? RIGHTS_ALL : inheriting,
readable,
writable,
listing: undefined
});
view().setUint32(resultPtr, fd, true);
return WASI_ERRNO.SUCCESS;
},
path_filestat_get(dirfd, flags, pathPtr, pathLen, resultPtr) {
const path = resolve(dirfd, pathPtr, pathLen);
if (path === undefined)
return WASI_ERRNO.BADF;
const follow = (flags & LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0;
const stat = follow ? client.stat(path) : client.lstat(path);
if (stat.errno !== 0 || stat.stat === undefined)
return stat.errno || WASI_ERRNO.IO;
writeFilestat(resultPtr, path, stat.stat);
return WASI_ERRNO.SUCCESS;
},
path_filestat_set_times(dirfd, _flags, pathPtr, pathLen, _atim, _mtim, fstflags) {
const path = resolve(dirfd, pathPtr, pathLen);
if (path === undefined)
return WASI_ERRNO.BADF;
if ((fstflags & ~15) !== 0)
return WASI_ERRNO.INVAL;
const stat = client.lstat(path);
if (stat.errno !== 0)
return stat.errno;
if (fstflags === 0)
return WASI_ERRNO.SUCCESS;
return WASI_ERRNO.NOTSUP;
},
path_create_directory(dirfd, pathPtr, pathLen) {
const path = resolve(dirfd, pathPtr, pathLen);
if (path === undefined)
return WASI_ERRNO.BADF;
return client.mkdir(path).errno;
},
path_remove_directory(dirfd, pathPtr, pathLen) {
const path = resolve(dirfd, pathPtr, pathLen);
if (path === undefined)
return WASI_ERRNO.BADF;
return client.rmdir(path).errno;
},
path_unlink_file(dirfd, pathPtr, pathLen) {
const path = resolve(dirfd, pathPtr, pathLen);
if (path === undefined)
return WASI_ERRNO.BADF;
return client.unlink(path).errno;
},
path_rename(dirfd, oldPtr, oldLen, newDirfd, newPtr, newLen) {
const oldPath = resolve(dirfd, oldPtr, oldLen);
const newPath = resolve(newDirfd, newPtr, newLen);
if (oldPath === undefined || newPath === undefined)
return WASI_ERRNO.BADF;
const result = client.rename(oldPath, newPath);
if (result.errno !== 0)
return result.errno;
const prefix = oldPath === "/" ? "/" : `${oldPath}/`;
for (const entry of table.values()) {
if (entry.path === oldPath)
entry.path = newPath;
else if (entry.path.startsWith(prefix))
entry.path = newPath + entry.path.slice(oldPath.length);
}
return WASI_ERRNO.SUCCESS;
},
path_readlink(dirfd, pathPtr, pathLen, bufPtr, bufLen, bufusedPtr) {
const path = resolve(dirfd, pathPtr, pathLen);
if (path === undefined)
return WASI_ERRNO.BADF;
const link = client.readlink(path);
if (link.errno !== 0 || link.target === undefined)
return link.errno || WASI_ERRNO.IO;
const encoded = textEncoder3.encode(link.target);
const copied = Math.min(encoded.byteLength, bufLen);
if (copied > 0)
bytes().set(encoded.subarray(0, copied), bufPtr);
view().setUint32(bufusedPtr, copied, true);
return WASI_ERRNO.SUCCESS;
},
path_symlink(oldPtr, oldLen, dirfd, newPtr, newLen) {
const path = resolve(dirfd, newPtr, newLen);
if (path === undefined)
return WASI_ERRNO.BADF;
const target = readGuestString(oldPtr, oldLen).replace(/\0+$/u, "");
return client.symlink(target, path).errno;
},
path_link(oldDirfd, _oldFlags, _oldPtr, _oldLen, newDirfd, _newPtr, _newLen) {
if (entryOf(oldDirfd)?.isDir !== true || entryOf(newDirfd)?.isDir !== true)
return WASI_ERRNO.BADF;
return WASI_ERRNO.NOTSUP;
}
};
const guarded = {};
for (const name of Object.keys(fs)) {
const inner = fs[name];
guarded[name] = (...args) => {
try {
return inner(...args);
} catch (cause) {
onError(name, cause);
return WASI_ERRNO.IO;
}
};
}
const adapter = guarded;
adapter.owns = owns;
adapter.openFdCount = () => table.size - (table.has(preopenFd) ? 1 : 0);
adapter.closeAll = () => {
let released = 0;
for (const entry of table.values()) {
if (entry.clientFd === undefined)
continue;
client.close(entry.clientFd);
released += 1;
}
table.clear();
table.set(preopenFd, preopenEntry());
nextFd = fdBase;
return released;
};
adapter.compose = (base) => {
const merged = { ...base };
for (const [name, index] of Object.entries(FD_ARGUMENT_INDEX)) {
const mine = guarded[name];
const theirs = base[name];
if (typeof theirs !== "function") {
merged[name] = (...args) => owns(Number(args[index])) ? mine(...args) : WASI_ERRNO.BADF;
continue;
}
const fallback = theirs;
merged[name] = (...args) => owns(Number(args[index])) ? mine(...args) : fallback(...args);
}
return merged;
};
return adapter;
}
export {
CorruptStoreError,
DEFAULT_PAYLOAD_BYTES,
DurabilityModeMismatchError,
ExtentSizeMismatchError,
FileRepackedPort,
FsError,
MemoryRepackedPort,
MountedRepackedVfs,
O_APPEND,
O_CREAT,
O_EXCL,
O_NOFOLLOW,
O_RDONLY,
O_RDWR,
O_TRUNC,
O_WRONLY,
OpfsRepackedFS,
OpfsRepackedPort,
RepackedBrokerStoreError,
RepackedBrokerTransportError,
RepackedChannel,
RepackedDoorbell,
RepackedSyncBroker,
RepackedSyncClient,
RepackedVfs,
StoreClosedError,
StoreFailedError,
StoreLimitError,
StoreOwnedError,
StoreRecreationRequiredError,
UnexpectedStoreEntryError,
WASI_ERRNO,
WASI_FILETYPE,
createOpfsPgwasm,
createWasiPreview1Fs,
errnoName,
fsErrorNameOf,
normalizeWasiPath,
planOpen,
strictSync,
throwOnErrno
};
//# debugId=C6DDC6F2B59C0DC864756E2164756E21