char/avril

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

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

Charlotte Somruntime: add native POSIX and Windows path modules92a4f7e

main
5.3 KiB98 linesraw

avril:path

Lexical path manipulation with no filesystem access: joining, normalization, parsing, and file: URL conversion. The algorithms follow Node's path semantics for POSIX and Windows, including drive roots, drive-relative forms, UNC paths, extended \\?\ / \\.\ namespaces, case-insensitive drive and component comparison for win32.relative, and the parse/format dot and extension rules. There is deliberately no glob support and no filesystem lookup of any kind; realPath stays in avril:std, where symlinks and permissions live.

import * as path from "avril:path";
import * as posix from "avril:path/posix";
import * as win32 from "avril:path/win32";

path.join("/a", "b", "../c"); // "/a/c" on POSIX hosts
win32.join("C:\\a", "b", "..\\c"); // "C:\\a\\c" on any host
posix.relative("/a/b/c", "/a/d/e"); // "../../d/e"

path.toFileUrl("/a b/c#d").href; // "file:///a%20b/c%23d"
path.fromFileUrl(new URL("file:///a%20b/c")); // "/a b/c"

avril:path exports the native flavor for the target OS directly (join, resolve, normalize, relative, dirname, basename, extname, isAbsolute, parse, format, toNamespacedPath, fromFileUrl, toFileUrl, sep, delimiter) plus frozen posix and win32 namespaces with the same surface. avril:path/posix and avril:path/win32 export the same names pinned to one flavor on any host, so explicit Windows paths work lexically on Unix and vice versa. There is no default export; import * as path is idiomatic. posix/win32 namespace objects carry no nested posix/win32 fields.

Authority

Pure operations (join, normalize, dirname, basename, extname, isAbsolute, parse, format, and the POSIX toNamespacedPath passthrough) never touch the working directory and run under avril run -0 with no grants. resolve, relative, and win32.toNamespacedPath (which resolves its input first) consult the working directory only when relative, root-relative, or drive-relative input actually needs it: inputs that are already fully qualified need no read grant, and importing the module itself never requires any authority. Each lookup rechecks live capability state, so a pledge() narrowing read access is honored by the next call; there is no cached directory. A missing grant throws the normal PermissionDenied, and toFileUrl/fromFileUrl need no permissions at all.

Windows notes

Windows resolution never reads per-drive hidden environment entries and needs no environment permission: when the requested drive differs from the working directory's drive (or the directory has no drive letter, as on POSIX hosts), resolution falls back to that drive's root instead of consulting another drive's remembered directory. Explicit posix calls on Windows use the POSIX view of the working directory (separators converted, drive indicator truncated), matching Node.

Extended-namespace paths (\\?\, \\.\, and their forward-slash spellings) work in every lexical operation but are rejected by toFileUrl and fromFileUrl with a TypeError. UNC servers are validated strictly (control characters, spaces, and #?%@/:[] are rejected rather than left for the URL parser to strip or reinterpret, except inside bracketed IPv6 literals like \\[::1]\share, which are accepted); ASCII case folds for a deterministic representation, localhost is refused as a server (WHATWG would collapse it and lose the share — including Unicode aliases that normalize to it, which are rechecked on the constructed URL), and other non-ASCII names pass through IDNA to their punycode form.

File URLs

toFileUrl(path) requires an absolute path: /-rooted for POSIX, or a fully qualified drive (C:\…) or UNC (\\server\share\…) path for Windows. The Windows input is normalized with the lexical win32 algorithm first, so ./.. can never cross a drive or share root (unlike a bare URL parse, which does not know the share boundary); explicit trailing slashes are preserved. Drive-relative (C:foo), root-only (\foo), and relative inputs throw TypeError — as does the bare drive C:, which Node accepts but which names no directory here — along with NUL bytes and lone surrogates. Conversion is purely lexical and never consults the working directory. The percent-encoding table matches Node's pathToFileURL exactly (verified differentially over the full ASCII range, including ~, |, ^, [, ]); ordinary separators and coherent trailing slashes survive, and ./.. normalize per URL semantics.

fromFileUrl(url) accepts a genuine URL instance or an absolute file: URL string; forged objects and other schemes throw TypeError. Raw strings are rejected up front for lone surrogates, NUL bytes, and tab/newline characters (the URL parser would otherwise normalize those silently); a URL instance is already parsed, so a surrogate it normalized to U+FFFD on construction stays one. Non-local POSIX hosts are rejected (localhost is local); any other Windows host becomes a UNC path, with localhost meaning the local machine (file://server/ with no share, and drive-relative file:///C: / file:///C:foo, are rejected as incomplete). Encoded separators (%2F everywhere, %5C for Windows), malformed escapes, invalid UTF-8, NUL bytes, and surrogate escapes throw instead of decoding quietly. Unlike Node, a nonempty query or fragment is rejected rather than silently dropped.

1# `avril:path`
2
3Lexical path manipulation with no filesystem access: joining, normalization,
4parsing, and `file:` URL conversion. The algorithms follow Node's `path`
5semantics for POSIX and Windows, including drive roots, drive-relative forms,
6UNC paths, extended `\\?\` / `\\.\` namespaces, case-insensitive drive and
7component comparison for `win32.relative`, and the `parse`/`format` dot and
8extension rules. There is deliberately no glob support and no filesystem
9lookup of any kind; `realPath` stays in `avril:std`, where symlinks and
10permissions live.
11
12```ts
13import * as path from "avril:path";
14import * as posix from "avril:path/posix";
15import * as win32 from "avril:path/win32";
16
17path.join("/a", "b", "../c"); // "/a/c" on POSIX hosts
18win32.join("C:\\a", "b", "..\\c"); // "C:\\a\\c" on any host
19posix.relative("/a/b/c", "/a/d/e"); // "../../d/e"
20
21path.toFileUrl("/a b/c#d").href; // "file:///a%20b/c%23d"
22path.fromFileUrl(new URL("file:///a%20b/c")); // "/a b/c"
23```
24
25`avril:path` exports the native flavor for the target OS directly
26(`join`, `resolve`, `normalize`, `relative`, `dirname`, `basename`,
27`extname`, `isAbsolute`, `parse`, `format`, `toNamespacedPath`,
28`fromFileUrl`, `toFileUrl`, `sep`, `delimiter`) plus frozen `posix` and
29`win32` namespaces with the same surface. `avril:path/posix` and
30`avril:path/win32` export the same names pinned to one flavor on any host,
31so explicit Windows paths work lexically on Unix and vice versa. There is no
32default export; `import * as path` is idiomatic. `posix`/`win32` namespace
33objects carry no nested `posix`/`win32` fields.
34
35## Authority
36
37Pure operations (`join`, `normalize`, `dirname`, `basename`, `extname`,
38`isAbsolute`, `parse`, `format`, and the POSIX `toNamespacedPath` passthrough)
39never touch the working directory and run under `avril run -0` with no grants.
40`resolve`, `relative`, and `win32.toNamespacedPath` (which resolves its input
41first) consult the working directory only when relative, root-relative, or
42drive-relative input actually needs it: inputs that are already fully qualified
43need no read grant, and importing the module itself never requires any
44authority. Each lookup rechecks live capability state, so a `pledge()`
45narrowing read access is honored by the next call; there is no cached directory.
46A missing grant throws the normal `PermissionDenied`, and
47`toFileUrl`/`fromFileUrl` need no permissions at all.
48
49## Windows notes
50
51Windows resolution never reads per-drive hidden environment entries and
52needs no environment permission: when the requested drive differs from the
53working directory's drive (or the directory has no drive letter, as on POSIX
54hosts), resolution falls back to that drive's root instead of consulting
55another drive's remembered directory. Explicit `posix` calls on Windows use
56the POSIX view of the working directory (separators converted, drive
57indicator truncated), matching Node.
58
59Extended-namespace paths (`\\?\`, `\\.\`, and their forward-slash spellings)
60work in every lexical operation but are rejected by `toFileUrl` and
61`fromFileUrl` with a `TypeError`. UNC servers are validated strictly (control
62characters, spaces, and `#?%@/:[]` are rejected rather than left for the URL
63parser to strip or reinterpret, except inside bracketed IPv6 literals like
64`\\[::1]\share`, which are accepted); ASCII case folds for a deterministic
65representation, `localhost` is refused as a server (WHATWG would collapse it
66and lose the share — including Unicode aliases that normalize to it, which are
67rechecked on the constructed URL), and other non-ASCII names pass through IDNA
68to their punycode form.
69
70## File URLs
71
72`toFileUrl(path)` requires an absolute path: `/`-rooted for POSIX, or a
73fully qualified drive (`C:\…`) or UNC (`\\server\share\…`) path for Windows.
74The Windows input is normalized with the lexical `win32` algorithm first, so
75`.`/`..` can never cross a drive or share root (unlike a bare URL parse, which
76does not know the share boundary); explicit trailing slashes are preserved.
77Drive-relative (`C:foo`), root-only (`\foo`), and relative inputs throw
78`TypeError` — as does the bare drive `C:`, which Node accepts but which names
79no directory here — along with NUL bytes and lone surrogates. Conversion is
80purely lexical and never consults the working directory. The percent-encoding
81table matches Node's `pathToFileURL` exactly (verified differentially over the
82full ASCII range, including `~`, `|`, `^`, `[`, `]`); ordinary separators and
83coherent trailing slashes survive, and `.`/`..` normalize per URL semantics.
84
85`fromFileUrl(url)` accepts a genuine `URL` instance or an absolute `file:`
86URL string; forged objects and other schemes throw `TypeError`. Raw strings are
87rejected up front for lone surrogates, NUL bytes, and tab/newline characters
88(the URL parser would otherwise normalize those silently); a `URL` instance is
89already parsed, so a surrogate it normalized to U+FFFD on construction stays
90one. Non-local
91POSIX hosts are rejected (`localhost` is local); any other Windows host
92becomes a UNC path, with `localhost` meaning the local machine (`file://server/`
93with no share, and drive-relative `file:///C:` / `file:///C:foo`, are rejected as
94incomplete). Encoded
95separators (`%2F` everywhere, `%5C` for Windows), malformed escapes, invalid
96UTF-8, NUL bytes, and surrogate escapes throw instead of decoding quietly.
97Unlike Node, a nonempty query or fragment is rejected rather than silently
98dropped.