char/avril

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

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

Charlotte Somgenerate std typedefs from typescript impl38d7f6c

main
10.2 KiB213 linesraw
1mod common;
2
3#[test]
4fn std_facades_preserve_namespaces_and_class_bindings() {
5    let output = common::run(
6        r#"
7        const std = await import('avril:std'), sync = await import('avril:std/sync');
8        const expected = [
9            'ChildProcess', 'Conn', 'DatagramConn', 'FileHandle', 'FsWatcher', 'Listener',
10            'OutputLimitError', 'ProcessAbortError', 'ProcessError', 'ProcessTimeoutError',
11            'SeekMode', 'addSignalListener', 'avril', 'chmod', 'chown', 'connect', 'copyFile',
12            'create', 'env', 'errors', 'inspect', 'lchown', 'link', 'listen', 'listenDatagram',
13            'lstat', 'lutime', 'mkdir', 'open', 'pledge', 'process', 'readDir', 'readLink',
14            'realPath', 'remove', 'removeSignalListener', 'rename', 'run', 'runSync', 'runtime',
15            'serve', 'serveFile', 'spawn', 'stat', 'stderr', 'stdin', 'stdout', 'symlink',
16            'truncate', 'upgradeWebSocket', 'utime', 'watchFs',
17        ];
18        const expectedSync = [
19            'FileHandle', 'SeekMode', 'chmod', 'chown', 'copyFile', 'create', 'errors', 'lchown',
20            'link', 'lstat', 'lutime', 'mkdir', 'open', 'readDir', 'readLink', 'realPath',
21            'remove', 'rename', 'stat', 'stderr', 'stdin', 'stdout', 'symlink', 'truncate', 'utime',
22        ];
23        if (Object.keys(std).sort().join() !== expected.join()) throw Error('std exports');
24        if (Object.keys(sync).sort().join() !== expectedSync.join()) throw Error('sync exports');
25        for (const name of ['errors', 'FileHandle', 'SeekMode']) {
26            if (std[name] !== sync[name]) throw Error('sync identity: ' + name);
27        }
28        for (const name of ['Conn', 'Listener', 'DatagramConn', 'FileHandle', 'FsWatcher',
29            'ChildProcess', 'ProcessError', 'OutputLimitError', 'ProcessTimeoutError', 'ProcessAbortError']) {
30            if (Object.hasOwn(std[name], Symbol.hasInstance)) throw Error('Runtime hasInstance: ' + name);
31        }
32        if (std.Conn.length !== 0 || std.Listener.length !== 0) throw Error('Constructor arity');
33        for (const name of ['run', 'runSync', 'spawn']) {
34            if (std[name].length !== 1 || Object.hasOwn(std[name], 'prototype')) throw Error('Function shape: ' + name);
35        }
36        const result = {code: 1, signal: null, success: false, reason: null, error: null, stdout: null, stderr: null};
37        const error = new std.OutputLimitError(result);
38        if (!(error instanceof std.OutputLimitError) || !(error instanceof std.ProcessError) ||
39            !(error instanceof Error) || error.result !== result) throw Error('Error inheritance');
40        for (const name of ['ProcessError', 'OutputLimitError', 'ProcessTimeoutError', 'ProcessAbortError']) {
41            if (std[name].isError !== Error.isError || !std[name].isError(error) || std[name].isError({}))
42                throw Error('Error statics: ' + name);
43        }
44        console.log('facades');
45    "#,
46        &[],
47    );
48    assert_eq!(output.stdout, b"facades\n");
49}
50
51#[test]
52fn base_snapshots_and_pledge_conversion_boundaries_are_preserved() {
53    let output = common::run(
54        r#"
55        import {env, pledge, process, runtime} from 'avril:std';
56        const reads = [];
57        const grant = {toString() {reads.push('string'); return 'AVRIL_TYPED_BASE';}};
58        const grants = [];
59        Object.defineProperty(grants, 0, {get() {reads.push('grant'); return grant;}});
60        grants[Symbol.iterator] = () => {throw Error('Pledge iterated grants');};
61        pledge({get env() {
62            reads.push('env');
63            return {
64                get allow() {reads.push('allow'); return grants;},
65                get deny() {reads.push('deny'); return [];},
66            };
67        }});
68        if (reads.join(',') !== 'env,allow,grant,string,deny') throw Error('Pledge reads: ' + reads);
69        const name = {toString() {reads.push('name'); return 'AVRIL_TYPED_BASE';}};
70        const value = {toString() {reads.push('value'); return 'saved';}};
71        env.set(name, value);
72        if (reads.slice(-2).join(',') !== 'name,value') throw Error('Environment conversion');
73        const snapshot = env.toObject();
74        env.delete(name);
75        if (Object.getPrototypeOf(snapshot) !== null || snapshot.AVRIL_TYPED_BASE !== 'saved' || env.has(name))
76            throw Error('Environment snapshot');
77        if (!Object.isFrozen(env) || !Object.isFrozen(process) || !Object.isFrozen(process.args) ||
78            !Object.isFrozen(runtime) || typeof process.pid !== 'number' || typeof runtime.v8 !== 'string')
79            throw Error('Runtime initialization');
80        let read = false;
81        try {
82            pledge({unknown: false, get env() {read = true; return false;}});
83            throw Error('Unknown pledge accepted');
84        } catch (error) {if (!(error instanceof TypeError)) throw error;}
85        if (read) throw Error('Pledge read values before validating names');
86        if (env.set.length !== 2 || env.get.length !== 1 || pledge.length !== 1) throw Error('Base arity');
87        console.log('base conversion');
88    "#,
89        &[],
90    );
91    assert_eq!(output.stdout, b"base conversion\n");
92}
93
94#[test]
95fn stdlib_exports_retained_bindings_not_mutable_global_lookups() {
96    let output = common::run(
97        r#"
98        const avril = Symbol.for('runtime:avril');
99        if (Symbol.for('__avril.base') in globalThis) throw new Error('Leaked base');
100        if ('pledge' in globalThis) throw new Error('Unexpected global pledge');
101        const poison = () => { throw new Error('Poisoned global'); };
102        globalThis.pledge = poison;
103        Symbol.for = poison; globalThis.Symbol = poison;
104        globalThis.Promise = poison; Object.freeze = poison;
105        const std = await import('avril:std');
106        const again = await import(new URL('avril:std'));
107        try { std.pledge = poison; throw new Error('Unsealed std'); }
108        catch(e) { if (!(e instanceof TypeError)) throw e; }
109        if (Object.isFrozen(std.stdin) || Object.isFrozen(std.stdout) || Object.isFrozen(std.stderr)) throw new Error('Frozen streams');
110        if (std.avril !== avril) throw new Error('Wrong avril symbol');
111        if (std !== again || std.stdin !== again.stdin
112            || std.stdout !== again.stdout || std.stderr !== again.stderr) throw new Error('Wrong bindings');
113        std.pledge({jsExec:false});
114        std.pledge({net:false,read:false,jsExec:false});
115        if ((await import('avril:std')) !== std) throw new Error('Lost builtin access');
116        console.log('retained');
117    "#,
118        &[],
119    );
120    assert_eq!(output.stdout, b"retained\n");
121}
122
123#[test]
124fn typescript_imports_stdlib_and_invalid_exports_fail_normally() {
125    let dir = tempfile::tempdir().unwrap();
126    let path = dir.path().join("main.ts");
127    std::fs::write(
128        &path,
129        r#"
130        import { pledge, stdout } from 'avril:std';
131        import type { Pledge } from 'avril:std';
132        const restrictions: Pledge = {net:false};
133        pledge(restrictions);
134        await stdout.getWriter().write(new Uint8Array([1,2,3]));
135        for (const name of ['avril:missing','avril:std?query','avril:std#fragment']) {
136            try { await import(name); throw new Error('Unexpected builtin'); }
137            catch(e) { if (!e.message.includes('Unsupported module specifier')) throw e; }
138        }
139    "#,
140    )
141    .unwrap();
142    let output = std::process::Command::new(env!("CARGO_BIN_EXE_avril"))
143        .arg("run")
144        .args(["-0", path.to_str().unwrap()])
145        .output()
146        .unwrap();
147    assert!(
148        output.status.success(),
149        "{}",
150        String::from_utf8_lossy(&output.stderr)
151    );
152    assert_eq!(output.stdout, [1, 2, 3]);
153    std::fs::write(
154        &path,
155        "import { missing } from 'avril:std'; console.log(missing);",
156    )
157    .unwrap();
158    let output = std::process::Command::new(env!("CARGO_BIN_EXE_avril"))
159        .arg("run")
160        .arg(path)
161        .output()
162        .unwrap();
163    assert!(!output.status.success());
164    assert!(
165        String::from_utf8_lossy(&output.stderr)
166            .contains("does not provide an export named 'missing'")
167    );
168}
169
170#[test]
171fn embedded_modules_stay_private_with_and_without_ambient_permissions() {
172    let dir = tempfile::tempdir().unwrap();
173    let path = dir.path().join("main.js");
174    for zero in [false, true] {
175        std::fs::write(&path,r#"
176            import {pledge} from 'avril:std';
177            async function denied(promise) {
178                try {await promise;} catch(e) {if(e.message.includes('private')) return;throw e;}
179                throw Error('Extension module escaped');
180            }
181            for(const name of ['ext:core','ext:core/ops','ext:core/primordials','ext:web/idl.js','ext:web/events.js','ext:http_types/headers.js','ext:http_types/body.js','ext:encoding/main.js']) await denied(import(name));
182            new Headers({a:"globalThis.fromEval=import('ext:fetch/main.js')"}).forEach(eval);
183            await denied(fromEval);
184            new URLSearchParams('a='+encodeURIComponent("globalThis.fromParams=import('ext:web/main.js')")).forEach(eval);
185            await denied(fromParams);
186            pledge({net:false,read:false,jsExec:false});
187            if ((await import('avril:std')).pledge !== pledge) throw Error('Lost public builtin');
188            if (new TextDecoder().decode(new TextEncoder().encode('private')) !== 'private') throw Error('Lost encoding');
189            console.log('private');
190        "#).unwrap();
191        let mut command = std::process::Command::new(env!("CARGO_BIN_EXE_avril"));
192        command.arg("run");
193        if zero {
194            command.arg("-0");
195        }
196        let output = command.arg(&path).output().unwrap();
197        assert!(
198            output.status.success(),
199            "{}",
200            String::from_utf8_lossy(&output.stderr)
201        );
202        assert_eq!(output.stdout, b"private\n");
203        std::fs::write(&path, "import 'ext:web/idl.js'; throw Error('Escaped');").unwrap();
204        let mut command = std::process::Command::new(env!("CARGO_BIN_EXE_avril"));
205        command.arg("run");
206        if zero {
207            command.arg("-0");
208        }
209        let output = command.arg(&path).output().unwrap();
210        assert!(!output.status.success());
211        assert!(String::from_utf8_lossy(&output.stderr).contains("Extension modules are private"));
212    }
213}