import { ObjectCreate, RangeError, Set, SetPrototypeAdd as add, SetPrototypeDelete as deleteKey, SetPrototypeHas as has, StringPrototypeCharCodeAt as charCodeAt, StringPrototypeSlice as slice, TypedArrayPrototypeGetBuffer as bufferOf, TypedArrayPrototypeGetByteLength as length, TypedArrayPrototypeGetByteOffset as offsetOf, TypedArrayPrototypeGetTag as tag, TypeError, Uint8Array as Uint8ArrayCtor, Uint8ArrayPrototypeSet as setBytes, uncurryThis, } from "ext:core/primordials"; import { op_file_copy_dir_mode_sync as dirMode, op_file_copy_link_sync as copyLink, op_file_ensure_dir_sync as ensureDir, op_file_path_join as join, op_file_path_parent as parent, op_file_prepare_copy_sync as prepare, op_file_read_tree_dir_sync as readTreeDir, op_file_tmp_dir as tmpDir, op_path_platform as platform, } from "ext:core/ops"; import { copyFile, FileHandle, mkdir, open, remove, rename, stat, } from "./sync.js"; import type { CopyInfo } from "ext:core/ops"; import type { CopyOptions, MakeTempOptions, WriteFileOptions } from "avril:fs"; export type { CopyOptions, MakeTempOptions, WriteFileOptions } from "avril:fs"; import { args, integer, path, writeOptions } from "./main.js"; import { errorCode, failure, result } from "./errors.js"; import { dictionary, required } from "ext:web/idl.js"; const read = uncurryThis(FileHandle.prototype.readSync), write = uncurryThis(FileHandle.prototype.writeSync), dispose = uncurryThis(FileHandle.prototype[Symbol.dispose]); const Encoder = globalThis.TextEncoder, Decoder = globalThis.TextDecoder; const encode = uncurryThis(Encoder.prototype.encode), decode = uncurryThis(Decoder.prototype.decode); const encoder = new Encoder(), decoder = new Decoder(); const systemCrypto = globalThis.crypto, getRandomValues = uncurryThis(systemCrypto.getRandomValues); const windowsHost = platform() === "win32"; const digits = "0123456789abcdef"; function checkTempComponent(value: string, suffix: boolean): void { for (let i = 0; i < value.length; i++) { const code = charCodeAt(value, i); if (code < 32 || (code >= 127 && code <= 159)) { throw failure( "InvalidInput", "Invalid control character in temp prefix or suffix", ); } if ( code === 34 || code === 42 || code === 47 || code === 58 || code === 60 || code === 62 || code === 63 || code === 92 || code === 124 ) { throw failure( "InvalidInput", "Invalid character in temp prefix or suffix", ); } } // Windows strips trailing dots and spaces, so the created name would not // match the returned one. if (suffix && windowsHost && value.length > 0) { const last = charCodeAt(value, value.length - 1); if (last === 46 || last === 32) { throw failure( "InvalidInput", "Invalid trailing character in temp suffix", ); } } } function randomHex(): string { const bytes = new Uint8ArrayCtor(8); getRandomValues(systemCrypto, bytes); let text = ""; for (let i = 0; i < 8; i++) { text += digits[bytes[i] >> 4] + digits[bytes[i] & 15]; } // Like Rust's `{value:08x}`: at least 8 digits. let start = 0; while (start < 8 && text[start] === "0") start++; return start === 0 ? text : slice(text, start); } function tempOptions(options: unknown): [string, string, string] { const settings = dictionary(options); const dir = settings.dir, prefix = settings.prefix, suffix = settings.suffix; const base = dir === undefined ? result(tmpDir()) : path(dir); if (prefix !== undefined && typeof prefix !== "string") { throw new TypeError("prefix must be string"); } if (suffix !== undefined && typeof suffix !== "string") { throw new TypeError("suffix must be string"); } const head = prefix === undefined ? "" : prefix as string; const tail = suffix === undefined ? "" : suffix as string; checkTempComponent(head, false); checkTempComponent(tail, true); return [base, head, tail]; } export function readFile(pathname: string | URL): Uint8Array { required(arguments.length, 1); const file = open(pathname), chunks = ObjectCreate(null) as Record>; let count = 0, total = 0; try { for (;;) { const buffer = new Uint8ArrayCtor(65536), size = read(file, buffer); if (size === null) break; chunks[count++] = new Uint8ArrayCtor(bufferOf(buffer), 0, size); total += size; } } finally { dispose(file); } const output = new Uint8ArrayCtor(total); let offset = 0; for (let i = 0; i < count; i++) { setBytes(output, chunks[i], offset); offset += length(chunks[i]); } return output; } export function readTextFile(pathname: string | URL): string { required(arguments.length, 1); return decode(decoder, readFile(pathname)) as string; } export function writeFile( pathname: string | URL, data: Uint8Array, options: WriteFileOptions | undefined = undefined, ): void { required(arguments.length, 2); const filename = path(pathname); if (tag(data) !== "Uint8Array") { throw new TypeError("writeFile requires a Uint8Array"); } const snapshot = new Uint8ArrayCtor(data as Uint8Array), flags = writeOptions(options); const file = open(filename, flags); try { let offset = 0; while (offset < length(snapshot)) { const buffer = new Uint8ArrayCtor( bufferOf(snapshot), offsetOf(snapshot) + offset, length(snapshot) - offset, ); const size = write(file, buffer); if (size <= 0) throw failure("IoError", "File write made no progress"); offset += size; } } finally { dispose(file); } } export function writeTextFile( pathname: string | URL, text: string, options: WriteFileOptions | undefined = undefined, ): void { required(arguments.length, 2); if (typeof text !== "string") { throw new TypeError("writeTextFile requires a string"); } writeFile(pathname, encode(encoder, text), options); } /** False only for missing paths; permission and other errors propagate. */ export function exists(pathname: string | URL): boolean { required(arguments.length, 1); try { stat(pathname); return true; } catch (error) { if (errorCode(error) === "NotFound") return false; throw error; } } export function mkdirs( pathname: string | URL, options: { mode?: number } | undefined = undefined, ): void { required(arguments.length, 1); const filename = path(pathname), mode = dictionary(options).mode; if (mode !== undefined && integer(mode) > 0o7777) { throw new RangeError("Invalid directory mode"); } const nativeMode = mode === undefined ? null : mode as number; function ensure(filename: string): void { try { result(ensureDir(args(filename, nativeMode))); } catch (error) { if (errorCode(error) !== "NotFound") throw error; ensure(result(parent(filename))); result(ensureDir(args(filename, nativeMode))); } } ensure(filename); } /** Creates a uniquely-named empty directory with mode 0o700; the caller removes it. Collisions retry; missing parents are an error. */ export function makeTempDir( options: MakeTempOptions | undefined = undefined, ): string { const [base, prefix, suffix] = tempOptions(options); for (let attempt = 0; attempt < 10; attempt++) { const candidate = result(join(args(base, prefix + randomHex() + suffix))); try { mkdir(candidate, { mode: 0o700 }); return candidate; } catch (error) { if (errorCode(error) !== "AlreadyExists") throw error; } } throw failure("AlreadyExists", "too many temp dirs exist"); } /** Creates a uniquely-named empty file with mode 0o600; the caller removes it. Collisions retry; missing parents are an error. */ export function makeTempFile( options: MakeTempOptions | undefined = undefined, ): string { const [base, prefix, suffix] = tempOptions(options); for (let attempt = 0; attempt < 10; attempt++) { const candidate = result(join(args(base, prefix + randomHex() + suffix))); try { const file = open(candidate, { write: true, createNew: true, mode: 0o600, }); dispose(file); return candidate; } catch (error) { if (errorCode(error) !== "AlreadyExists") throw error; } } throw failure("AlreadyExists", "too many temp files exist"); } /** Preserves symlinks by default and ordinary mode bits, including existing destination directories. Following links rejects ancestor cycles but may duplicate shared subtrees. */ export function copy( source: string | URL, destination: string | URL, options: CopyOptions | undefined = undefined, ): void { required(arguments.length, 2); const from = path(source), to = path(destination), dereference = dictionary(options).dereference; if (dereference !== undefined && typeof dereference !== "boolean") { throw new TypeError("dereference must be boolean"); } const ancestors = new Set(); function visit(source: string, destination: string): void { const info = result( prepare(args(source, destination, false, dereference === true)), ); const actualSource = info[3]; if (info[0] === 1) { if (has(ancestors, info[2])) { throw failure( "InvalidInput", "Directory cycle while copying symbolic links", ); } add(ancestors, info[2]); try { mkdirs(destination, { mode: 0o700 }); result(dirMode(args(destination, 0o700))); const entries = result(readTreeDir(actualSource)); for (let i = 0; i < entries.length; i++) { visit( result(join(args(actualSource, entries[i][0]))), result(join(args(destination, entries[i][0]))), ); } result(dirMode(args(destination, info[1]))); } finally { deleteKey(ancestors, info[2]); } } else { const transfer = (): void => { if (info[0] === 2) { result(copyLink(args(actualSource, destination))); } else copyFile(actualSource, destination); }; try { transfer(); } catch (error) { if (errorCode(error) !== "NotFound") throw error; mkdirs(result(parent(destination))); transfer(); } } } visit(from, to); } /** Rename first; cross-device fallback copies then removes and is not atomic. */ export function move(source: string | URL, destination: string | URL): void { required(arguments.length, 2); const from = path(source), to = path(destination); try { rename(from, to); return; } catch (error) { if (errorCode(error) !== "CrossDevice") throw error; } const directory = result(prepare(args(from, to, true, false)))[0] === 1; copy(from, to); result(prepare(args(from, to, true, false))); remove(from, { recursive: directory }); }