UNPKG

@pgxsinkit/pgwasm

Version:
83 lines (77 loc) • 3.1 kB
import { FsError } from "./errors"; import { MAX_COMPONENT_BYTES, MAX_PATH_BYTES, MAX_PATH_DEPTH } from "./limits"; function utf8Length(value: string, label: string): number { let bytes = 0; for (let index = 0; index < value.length; index += 1) { const codeUnit = value.charCodeAt(index); if (codeUnit <= 0x7f) { bytes += 1; } else if (codeUnit <= 0x7ff) { bytes += 2; } else if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) { const next = value.charCodeAt(index + 1); if (index + 1 >= value.length || next < 0xdc00 || next > 0xdfff) { throw new FsError("EINVAL", `${label} is not canonically representable as UTF-8`); } bytes += 4; index += 1; } else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) { throw new FsError("EINVAL", `${label} is not canonically representable as UTF-8`); } else { bytes += 3; } } return bytes; } export function encodedUtf8Length(value: string): number { return utf8Length(value, "string"); } export function validatePathComponent(component: string): number { if (component.length === 0 || component === "." || component === "..") { throw new FsError("EINVAL", "path contains an empty or reserved component"); } if (component.includes("/") || component.includes("\0")) { 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; } /** * Validate a symbolic link's target and report its UTF-8 byte length. * * Targets are ABSOLUTE ONLY, and deliberately so: an absolute target resolves by restarting the * walk at the store root, which needs no notion of "the directory the link lives in" and therefore * survives a rename of the link. It is also all a Postgres datadir ever writes — `CREATE TABLESPACE` * links `pg_tblspc/<oid>` at the absolute `LOCATION`. A relative target is `EINVAL`, not silently * reinterpreted. */ export function validateSymlinkTarget(target: string): number { 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; } export function parsePath(path: string): string[] { 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; }