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