char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somgenerate fs typedefs from typescript impl766372c

main
11.6 KiB359 linesraw
1import { safePromise } from "ext:core/promises";
2import type { CopyInfo } from "ext:core/ops";
3import {
4  ObjectCreate,
5  RangeError,
6  Set,
7  SetPrototypeAdd as add,
8  SetPrototypeDelete as deleteKey,
9  SetPrototypeHas as has,
10  StringPrototypeCharCodeAt as charCodeAt,
11  StringPrototypeSlice as slice,
12  TypedArrayPrototypeGetBuffer as bufferOf,
13  TypedArrayPrototypeGetByteLength as length,
14  TypedArrayPrototypeGetByteOffset as offsetOf,
15  TypedArrayPrototypeGetTag as tag,
16  TypeError,
17  Uint8Array as Uint8ArrayCtor,
18  Uint8ArrayPrototypeSet as setBytes,
19  uncurryThis,
20} from "ext:core/primordials";
21import {
22  op_file_copy_dir_mode as dirMode,
23  op_file_copy_link as copyLink,
24  op_file_ensure_dir as ensureDir,
25  op_file_path_join as join,
26  op_file_path_parent as parent,
27  op_file_prepare_copy as prepare,
28  op_file_read_tree_dir as readTreeDir,
29  op_file_tmp_dir as tmpDir,
30  op_path_platform as platform,
31} from "ext:core/ops";
32import {
33  args,
34  copyFile,
35  FileHandle,
36  integer,
37  mkdir,
38  open,
39  path,
40  remove,
41  rename,
42  stat,
43  writeOptions,
44} from "./main.js";
45import { errorCode, failure, result } from "./errors.js";
46import { dictionary, required } from "ext:web/idl.js";
47
48export interface WriteFileOptions {
49  append?: boolean;
50  create?: boolean;
51  createNew?: boolean;
52  mode?: number;
53}
54
55export interface MakeTempOptions {
56  dir?: string | URL;
57  prefix?: string;
58  suffix?: string;
59}
60
61export interface CopyOptions {
62  dereference?: boolean;
63}
64
65const read = uncurryThis(FileHandle.prototype.read),
66  write = uncurryThis(FileHandle.prototype.write),
67  dispose = uncurryThis(FileHandle.prototype[Symbol.dispose]);
68const Encoder = globalThis.TextEncoder, Decoder = globalThis.TextDecoder;
69const encode = uncurryThis(Encoder.prototype.encode),
70  decode = uncurryThis(Decoder.prototype.decode);
71const encoder = new Encoder(), decoder = new Decoder();
72const systemCrypto = globalThis.crypto,
73  getRandomValues = uncurryThis(systemCrypto.getRandomValues);
74const windowsHost = platform() === "win32";
75const digits = "0123456789abcdef";
76
77function checkTempComponent(value: string, suffix: boolean): void {
78  for (let i = 0; i < value.length; i++) {
79    const code = charCodeAt(value, i);
80    if (code < 32 || (code >= 127 && code <= 159)) {
81      throw failure(
82        "InvalidInput",
83        "Invalid control character in temp prefix or suffix",
84      );
85    }
86    if (
87      code === 34 || code === 42 || code === 47 || code === 58 ||
88      code === 60 || code === 62 || code === 63 || code === 92 || code === 124
89    ) {
90      throw failure(
91        "InvalidInput",
92        "Invalid character in temp prefix or suffix",
93      );
94    }
95  }
96  // Windows strips trailing dots and spaces, so the created name would not
97  // match the returned one.
98  if (suffix && windowsHost && value.length > 0) {
99    const last = charCodeAt(value, value.length - 1);
100    if (last === 46 || last === 32) {
101      throw failure(
102        "InvalidInput",
103        "Invalid trailing character in temp suffix",
104      );
105    }
106  }
107}
108
109function randomHex(): string {
110  const bytes = new Uint8ArrayCtor(8);
111  getRandomValues(systemCrypto, bytes);
112  let text = "";
113  for (let i = 0; i < 8; i++) {
114    text += digits[bytes[i] >> 4] + digits[bytes[i] & 15];
115  }
116  // Like Rust's `{value:08x}`: at least 8 digits.
117  let start = 0;
118  while (start < 8 && text[start] === "0") start++;
119  return start === 0 ? text : slice(text, start);
120}
121
122function tempOptions(options: unknown): [string, string, string] {
123  const settings = dictionary(options);
124  const dir = settings.dir,
125    prefix = settings.prefix,
126    suffix = settings.suffix;
127  const base = dir === undefined ? result(tmpDir()) : path(dir);
128  if (prefix !== undefined && typeof prefix !== "string") {
129    throw new TypeError("prefix must be string");
130  }
131  if (suffix !== undefined && typeof suffix !== "string") {
132    throw new TypeError("suffix must be string");
133  }
134  const head = prefix === undefined ? "" : prefix as string;
135  const tail = suffix === undefined ? "" : suffix as string;
136  checkTempComponent(head, false);
137  checkTempComponent(tail, true);
138  return [base, head, tail];
139}
140
141export async function readFile(
142  pathname: string | URL,
143): Promise<Uint8Array> {
144  required(arguments.length, 1);
145  const file = await safePromise(open(pathname)),
146    chunks = ObjectCreate(null) as Record<number, Uint8Array<ArrayBuffer>>;
147  let count = 0, total = 0;
148  try {
149    for (;;) {
150      const buffer = new Uint8ArrayCtor(65536),
151        size = await safePromise(read(file, buffer));
152      if (size === null) break;
153      chunks[count++] = new Uint8ArrayCtor(bufferOf(buffer), 0, size);
154      total += size;
155    }
156  } finally {
157    dispose(file);
158  }
159  const output = new Uint8ArrayCtor(total);
160  let offset = 0;
161  for (let i = 0; i < count; i++) {
162    setBytes(output, chunks[i], offset);
163    offset += length(chunks[i]);
164  }
165  return output;
166}
167export async function readTextFile(pathname: string | URL): Promise<string> {
168  required(arguments.length, 1);
169  return decode(decoder, await safePromise(readFile(pathname))) as string;
170}
171export async function writeFile(
172  pathname: string | URL,
173  data: Uint8Array,
174  options: WriteFileOptions | undefined = undefined,
175): Promise<void> {
176  required(arguments.length, 2);
177  const filename = path(pathname);
178  if (tag(data) !== "Uint8Array") {
179    throw new TypeError("writeFile requires a Uint8Array");
180  }
181  const snapshot = new Uint8ArrayCtor(data as Uint8Array<ArrayBufferLike>),
182    flags = writeOptions(options);
183  const file = await safePromise(open(filename, flags));
184  try {
185    let offset = 0;
186    while (offset < length(snapshot)) {
187      const buffer = new Uint8ArrayCtor(
188        bufferOf(snapshot),
189        offsetOf(snapshot) + offset,
190        length(snapshot) - offset,
191      );
192      const size = await safePromise(write(file, buffer));
193      if (size <= 0) throw failure("IoError", "File write made no progress");
194      offset += size;
195    }
196  } finally {
197    dispose(file);
198  }
199}
200export async function writeTextFile(
201  pathname: string | URL,
202  text: string,
203  options: WriteFileOptions | undefined = undefined,
204): Promise<void> {
205  required(arguments.length, 2);
206  if (typeof text !== "string") {
207    throw new TypeError("writeTextFile requires a string");
208  }
209  await safePromise(writeFile(pathname, encode(encoder, text), options));
210}
211/** False only for missing paths; permission and other errors propagate. */
212export async function exists(pathname: string | URL): Promise<boolean> {
213  required(arguments.length, 1);
214  try {
215    await safePromise(stat(pathname));
216    return true;
217  } catch (error) {
218    if (errorCode(error) === "NotFound") return false;
219    throw error;
220  }
221}
222export async function mkdirs(
223  pathname: string | URL,
224  options: { mode?: number } | undefined = undefined,
225): Promise<void> {
226  required(arguments.length, 1);
227  const filename = path(pathname), mode = dictionary(options).mode;
228  if (mode !== undefined && integer(mode) > 0o7777) {
229    throw new RangeError("Invalid directory mode");
230  }
231  const nativeMode = mode === undefined ? null : mode as number;
232  async function ensure(filename: string): Promise<void> {
233    try {
234      result(await safePromise(ensureDir(args(filename, nativeMode))));
235    } catch (error) {
236      if (errorCode(error) !== "NotFound") throw error;
237      await safePromise(ensure(result<string>(parent(filename))));
238      result(await safePromise(ensureDir(args(filename, nativeMode))));
239    }
240  }
241  await safePromise(ensure(filename));
242}
243/** Creates a uniquely-named empty directory with mode 0o700; the caller removes it. Collisions retry; missing parents are an error. */
244export async function makeTempDir(
245  options: MakeTempOptions | undefined = undefined,
246): Promise<string> {
247  const [base, prefix, suffix] = tempOptions(options);
248  for (let attempt = 0; attempt < 10; attempt++) {
249    const candidate = result(join(args(base, prefix + randomHex() + suffix)));
250    try {
251      await safePromise(mkdir(candidate, { mode: 0o700 }));
252      return candidate;
253    } catch (error) {
254      if (errorCode(error) !== "AlreadyExists") throw error;
255    }
256  }
257  throw failure("AlreadyExists", "too many temp dirs exist");
258}
259/** Creates a uniquely-named empty file with mode 0o600; the caller removes it. Collisions retry; missing parents are an error. */
260export async function makeTempFile(
261  options: MakeTempOptions | undefined = undefined,
262): Promise<string> {
263  const [base, prefix, suffix] = tempOptions(options);
264  for (let attempt = 0; attempt < 10; attempt++) {
265    const candidate = result(join(args(base, prefix + randomHex() + suffix)));
266    try {
267      const file = await safePromise(
268        open(candidate, { write: true, createNew: true, mode: 0o600 }),
269      );
270      dispose(file);
271      return candidate;
272    } catch (error) {
273      if (errorCode(error) !== "AlreadyExists") throw error;
274    }
275  }
276  throw failure("AlreadyExists", "too many temp files exist");
277}
278/** Preserves symlinks by default and ordinary mode bits, including existing destination directories. Following links rejects ancestor cycles but may duplicate shared subtrees. */
279export async function copy(
280  source: string | URL,
281  destination: string | URL,
282  options: CopyOptions | undefined = undefined,
283): Promise<void> {
284  required(arguments.length, 2);
285  const from = path(source),
286    to = path(destination),
287    dereference = dictionary(options).dereference;
288  if (dereference !== undefined && typeof dereference !== "boolean") {
289    throw new TypeError("dereference must be boolean");
290  }
291  const ancestors = new Set<string>();
292  async function visit(source: string, destination: string): Promise<void> {
293    const info = result<CopyInfo>(
294      await safePromise(
295        prepare(args(source, destination, false, dereference === true)),
296      ),
297    );
298    const actualSource = info[3];
299    if (info[0] === 1) {
300      if (has(ancestors, info[2])) {
301        throw failure(
302          "InvalidInput",
303          "Directory cycle while copying symbolic links",
304        );
305      }
306      add(ancestors, info[2]);
307      try {
308        await safePromise(mkdirs(destination, { mode: 0o700 }));
309        result(await safePromise(dirMode(args(destination, 0o700))));
310        const entries = result<string[]>(
311          await safePromise(readTreeDir(actualSource)),
312        );
313        for (let i = 0; i < entries.length; i++) {
314          await safePromise(visit(
315            result(join(args(actualSource, entries[i][0]))),
316            result(join(args(destination, entries[i][0]))),
317          ));
318        }
319        result(await safePromise(dirMode(args(destination, info[1]))));
320      } finally {
321        deleteKey(ancestors, info[2]);
322      }
323    } else {
324      async function transfer(): Promise<void> {
325        if (info[0] === 2) {
326          result(await safePromise(copyLink(args(actualSource, destination))));
327        } else await safePromise(copyFile(actualSource, destination));
328      }
329      try {
330        await safePromise(transfer());
331      } catch (error) {
332        if (errorCode(error) !== "NotFound") throw error;
333        await safePromise(mkdirs(result(parent(destination))));
334        await safePromise(transfer());
335      }
336    }
337  }
338  await safePromise(visit(from, to));
339}
340/** Rename first; cross-device fallback copies then removes and is not atomic. */
341export async function move(
342  source: string | URL,
343  destination: string | URL,
344): Promise<void> {
345  required(arguments.length, 2);
346  const from = path(source), to = path(destination);
347  try {
348    await safePromise(rename(from, to));
349    return;
350  } catch (error) {
351    if (errorCode(error) !== "CrossDevice") throw error;
352  }
353  const directory = result<CopyInfo>(
354    await safePromise(prepare(args(from, to, true, false))),
355  )[0] === 1;
356  await safePromise(copy(from, to));
357  result(await safePromise(prepare(args(from, to, true, false))));
358  await safePromise(remove(from, { recursive: directory }));
359}