@pgxsinkit/pgwasm
Version:
83 lines (77 loc) • 3.1 kB
text/typescript
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;
}