503 lines · 13.3 KB · TypeScript
RawDownload
import type {	NetworkAdapter,	Permissions,	SystemDriver,	VirtualFileSystem,} from "./runtime.js";import { bytesToBase64 } from "./encoding.js";import {	createCommandExecutorStub,	createEnosysError,	createFsStub,	createInMemoryFileSystem,	createNetworkStub,	wrapFileSystem,	wrapNetworkAdapter,} from "./runtime.js"; const S_IFREG = 0o100000;const S_IFDIR = 0o040000; const BROWSER_SYSTEM_DRIVER_OPTIONS = Symbol.for(	"secure-exec.browserSystemDriverOptions",); const LOOPBACK_DNS_NAMES = new Set([	"localhost",	"ip4-localhost",	"ip4-loopback",]);const LOOPBACK_IPV6_DNS_NAMES = new Set(["ip6-localhost", "ip6-loopback"]); export interface BrowserRuntimeSystemOptions {	filesystem: "opfs" | "memory";	networkEnabled: boolean;} type BrowserSystemDriver = SystemDriver & {	[BROWSER_SYSTEM_DRIVER_OPTIONS]?: BrowserRuntimeSystemOptions;}; function isIpv4Literal(hostname: string): boolean {	const parts = hostname.split(".");	return (		parts.length === 4 &&		parts.every((part) => {			if (!/^\d+$/.test(part)) return false;			const value = Number(part);			return value >= 0 && value <= 255 && String(value) === part;		})	);} function isIpv6Literal(hostname: string): boolean {	return hostname.includes(":") && /^[0-9a-fA-F:.]+$/.test(hostname);} function browserLocalDnsLookup(hostname: string): {	address?: string;	family?: number;	error?: string;	code?: string;} {	const normalized = hostname.trim().toLowerCase();	if (LOOPBACK_DNS_NAMES.has(normalized)) {		return { address: "127.0.0.1", family: 4 };	}	if (LOOPBACK_IPV6_DNS_NAMES.has(normalized)) {		return { address: "::1", family: 6 };	}	if (isIpv4Literal(normalized)) {		return { address: normalized, family: 4 };	}	if (isIpv6Literal(normalized)) {		return { address: normalized, family: 6 };	}	return { error: "DNS not supported in browser", code: "ENOSYS" };} function normalizePath(path: string): string {	if (!path) return "/";	let normalized = path.startsWith("/") ? path : `/${path}`;	normalized = normalized.replace(/\/+/g, "/");	if (normalized.length > 1 && normalized.endsWith("/")) {		normalized = normalized.slice(0, -1);	}	return normalized;} function splitPath(path: string): string[] {	const normalized = normalizePath(path);	return normalized === "/" ? [] : normalized.slice(1).split("/");} function dirname(path: string): string {	const parts = splitPath(path);	if (parts.length <= 1) return "/";	return `/${parts.slice(0, -1).join("/")}`;} async function getRootHandle(): Promise<FileSystemDirectoryHandle> {	if (!("storage" in navigator) || !("getDirectory" in navigator.storage)) {		throw createEnosysError("opfs");	}	return navigator.storage.getDirectory();} /** * VFS backed by the Origin Private File System (OPFS) API. Falls back to * InMemoryFileSystem when OPFS is unavailable. Rename is not supported * (throws ENOSYS) since OPFS doesn't provide atomic rename. */export class OpfsFileSystem implements VirtualFileSystem {	private rootPromise: Promise<FileSystemDirectoryHandle>; 	constructor() {		this.rootPromise = getRootHandle();	} 	private async getDirHandle(		path: string,		create = false,	): Promise<FileSystemDirectoryHandle> {		const root = await this.rootPromise;		const parts = splitPath(path);		let current = root;		for (const part of parts) {			current = await current.getDirectoryHandle(part, { create });		}		return current;	} 	private async getFileHandle(		path: string,		create = false,	): Promise<FileSystemFileHandle> {		const normalized = normalizePath(path);		const parent = dirname(normalized);		const name = normalized.split("/").pop() || "";		const dir = await this.getDirHandle(parent, create);		return dir.getFileHandle(name, { create });	} 	async readFile(path: string): Promise<Uint8Array> {		const handle = await this.getFileHandle(path);		const file = await handle.getFile();		const buffer = await file.arrayBuffer();		return new Uint8Array(buffer);	} 	async readTextFile(path: string): Promise<string> {		const handle = await this.getFileHandle(path);		const file = await handle.getFile();		return file.text();	} 	async readDir(path: string): Promise<string[]> {		const dir = await this.getDirHandle(path);		const entries: string[] = [];		for await (const [name] of dir.entries()) {			entries.push(name);		}		return entries;	} 	async readDirWithTypes(		path: string,	): Promise<Array<{ name: string; isDirectory: boolean }>> {		const dir = await this.getDirHandle(path);		const entries: Array<{ name: string; isDirectory: boolean }> = [];		for await (const [name, handle] of dir.entries()) {			entries.push({				name,				isDirectory: handle.kind === "directory",			});		}		return entries;	} 	async writeFile(path: string, content: string | Uint8Array): Promise<void> {		const normalized = normalizePath(path);		await this.mkdir(dirname(normalized));		const handle = await this.getFileHandle(normalized, true);		const writable = await handle.createWritable();		if (typeof content === "string") {			await writable.write(content);		} else {			await writable.write(content as unknown as FileSystemWriteChunkType);		}		await writable.close();	} 	async createDir(path: string): Promise<void> {		const normalized = normalizePath(path);		const parent = dirname(normalized);		await this.getDirHandle(parent, false);		await this.getDirHandle(normalized, true);	} 	async mkdir(path: string, _options?: { recursive?: boolean }): Promise<void> {		const parts = splitPath(path);		let current = "";		for (const part of parts) {			current += `/${part}`;			await this.getDirHandle(current, true);		}	} 	async exists(path: string): Promise<boolean> {		try {			await this.getFileHandle(path);			return true;		} catch {			try {				await this.getDirHandle(path);				return true;			} catch {				return false;			}		}	} 	async stat(path: string) {		try {			const handle = await this.getFileHandle(path);			const file = await handle.getFile();			return {				mode: S_IFREG | 0o644,				size: file.size,				blocks: file.size === 0 ? 0 : Math.ceil(file.size / 512),				dev: 1,				rdev: 0,				isDirectory: false,				isSymbolicLink: false,				atimeMs: file.lastModified,				mtimeMs: file.lastModified,				ctimeMs: file.lastModified,				birthtimeMs: file.lastModified,				ino: 0,				nlink: 1,				uid: 0,				gid: 0,			};		} catch {			const normalized = normalizePath(path);			try {				await this.getDirHandle(normalized);				const now = Date.now();				return {					mode: S_IFDIR | 0o755,					size: 4096,					blocks: 8,					dev: 1,					rdev: 0,					isDirectory: true,					isSymbolicLink: false,					atimeMs: now,					mtimeMs: now,					ctimeMs: now,					birthtimeMs: now,					ino: 0,					nlink: 2,					uid: 0,					gid: 0,				};			} catch {				throw new Error(					`ENOENT: no such file or directory, stat '${normalized}'`,				);			}		}	} 	async removeFile(path: string): Promise<void> {		const normalized = normalizePath(path);		const parent = dirname(normalized);		const name = normalized.split("/").pop() || "";		const dir = await this.getDirHandle(parent);		await dir.removeEntry(name);	} 	async removeDir(path: string): Promise<void> {		const normalized = normalizePath(path);		if (normalized === "/") {			throw new Error("EPERM: operation not permitted, rmdir '/'");		}		const parent = dirname(normalized);		const name = normalized.split("/").pop() || "";		const dir = await this.getDirHandle(parent);		await dir.removeEntry(name);	} 	async rename(_oldPath: string, _newPath: string): Promise<void> {		throw createEnosysError("rename");	} 	async symlink(_target: string, _linkPath: string): Promise<void> {		throw createEnosysError("symlink");	} 	async readlink(_path: string): Promise<string> {		throw createEnosysError("readlink");	} 	async lstat(path: string) {		return this.stat(path);	} 	async link(_oldPath: string, _newPath: string): Promise<void> {		throw createEnosysError("link");	} 	async chmod(_path: string, _mode: number): Promise<void> {		// No-op: OPFS does not support POSIX permissions	} 	async chown(_path: string, _uid: number, _gid: number): Promise<void> {		// No-op: OPFS does not support POSIX ownership	} 	async utimes(_path: string, _atime: number, _mtime: number): Promise<void> {		// No-op: OPFS does not support timestamp manipulation	} 	async truncate(path: string, length: number): Promise<void> {		const handle = await this.getFileHandle(path);		const writable = await handle.createWritable({ keepExistingData: true });		await writable.truncate(length);		await writable.close();	} 	async realpath(path: string): Promise<string> {		const normalized = normalizePath(path);		if (await this.exists(normalized)) return normalized;		throw new Error(			`ENOENT: no such file or directory, realpath '${normalized}'`,		);	} 	async pread(		path: string,		offset: number,		length: number,	): Promise<Uint8Array> {		const data = await this.readFile(path);		return data.slice(offset, offset + length);	} 	async pwrite(path: string, offset: number, data: Uint8Array): Promise<void> {		const content = await this.readFile(path);		const endPos = offset + data.length;		const newContent = new Uint8Array(Math.max(content.length, endPos));		newContent.set(content);		newContent.set(data, offset);		await this.writeFile(path, newContent);	}} export interface BrowserDriverOptions {	filesystem?: "opfs" | "memory";	permissions?: Permissions;	networkAdapter?: NetworkAdapter;	useDefaultNetwork?: boolean;} /** Create an OPFS-backed filesystem, falling back to in-memory if OPFS is unavailable. */export async function createOpfsFileSystem(): Promise<VirtualFileSystem> {	if (		!("storage" in navigator) ||		typeof navigator.storage.getDirectory !== "function"	) {		return createInMemoryFileSystem();	}	return new OpfsFileSystem();} // The platform fetch captured at module load — BEFORE any guest `fetch` global is// installed over globalThis.fetch. The guest fetch routes through this adapter, so the// adapter MUST call the real fetch (not the overridden global) or it recurses infinitely.const NATIVE_FETCH: typeof fetch | undefined =	typeof globalThis !== "undefined" && typeof globalThis.fetch === "function"		? globalThis.fetch.bind(globalThis)		: undefined; /** Network adapter that delegates to the browser's native `fetch`. DNS and http2 are unsupported. */export function createBrowserNetworkAdapter(): NetworkAdapter {	const platformFetch = NATIVE_FETCH ?? fetch;	return {		async fetch(url, options) {			const response = await platformFetch(url, {				method: options?.method || "GET",				headers: options?.headers,				body: options?.body as RequestInit["body"],			});			const headers: Record<string, string> = {};			response.headers.forEach((v, k) => {				headers[k] = v;			}); 			const contentType = response.headers.get("content-type") || "";			const isBinary =				contentType.includes("octet-stream") ||				contentType.includes("gzip") ||				url.endsWith(".tgz"); 			let body: string;			if (isBinary) {				const buffer = await response.arrayBuffer();				body = bytesToBase64(new Uint8Array(buffer));				headers["x-body-encoding"] = "base64";			} else {				body = await response.text();			} 			return {				ok: response.ok,				status: response.status,				statusText: response.statusText,				headers,				body,				url: response.url,				redirected: response.redirected,			};		}, 		async dnsLookup(hostname) {			return browserLocalDnsLookup(hostname);		}, 		async httpRequest(url, options) {			const response = await platformFetch(url, {				method: options?.method || "GET",				headers: options?.headers,				body: options?.body as RequestInit["body"],			});			const headers: Record<string, string> = {};			response.headers.forEach((v, k) => {				headers[k] = v;			});			const body = await response.text();			return {				status: response.status,				statusText: response.statusText,				headers,				body,				url: response.url,			};		},	};} /** Recover runtime-driver options from a browser SystemDriver instance. */export function getBrowserSystemDriverOptions(	systemDriver: SystemDriver,): BrowserRuntimeSystemOptions {	const options = (systemDriver as BrowserSystemDriver)[		BROWSER_SYSTEM_DRIVER_OPTIONS	];	if (options) {		return options;	}	return {		filesystem: "opfs",		networkEnabled: Boolean(systemDriver.network),	};} /** Assemble a browser-side SystemDriver with permission-wrapped adapters. */export async function createBrowserDriver(	options: BrowserDriverOptions = {},): Promise<SystemDriver> {	const permissions = options.permissions;	const filesystemMode = options.filesystem ?? "opfs";	const filesystem =		filesystemMode === "memory"			? createInMemoryFileSystem()			: await createOpfsFileSystem();	const rawNetworkAdapter =		options.networkAdapter ??		(options.useDefaultNetwork ? createBrowserNetworkAdapter() : undefined);	const networkAdapter = rawNetworkAdapter		? wrapNetworkAdapter(rawNetworkAdapter, permissions)		: undefined; 	const systemDriver: BrowserSystemDriver = {		filesystem: wrapFileSystem(filesystem, permissions),		network: networkAdapter,		commandExecutor: createCommandExecutorStub(),		permissions,		runtime: {			process: {},			os: {},		},	}; 	systemDriver[BROWSER_SYSTEM_DRIVER_OPTIONS] = {		filesystem: filesystemMode,		networkEnabled: Boolean(networkAdapter),	}; 	return systemDriver;

Lines 1–500 of 503.