aboutsummaryrefslogtreecommitdiff
path: root/crates/chat-crypto/browser/wasi.mjs
blob: d27fe4b39debdeab60266bc040291460e4c348eb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0

export async function runProbe(bytes) {
    let memory;
    let output = '';
    let errors = '';
    let randomCalls = 0;
    let randomBytes = 0;
    const data = () => new DataView(memory.buffer);
    const put32 = (address, value) => data().setUint32(address, value, true);
    const noEntries = (count, size) => { put32(count, 0); put32(size, 0); return 0; };
    class Exit extends Error {
        constructor(code) { super(`WASI exit ${code}`); this.code = code; }
    }
    const wasi = {
        args_sizes_get: noEntries,
        environ_sizes_get: noEntries,
        args_get: () => 0,
        environ_get: () => 0,
        random_get(address, length) {
            const buffer = new Uint8Array(memory.buffer, address, length);
            for (let offset = 0; offset < length; offset += 65536) {
                crypto.getRandomValues(buffer.subarray(offset, Math.min(offset + 65536, length)));
            }
            randomCalls += 1;
            randomBytes += length;
            return 0;
        },
        clock_time_get(clock, precision, address) {
            if (clock !== 0 && clock !== 1) return 52;
            const milliseconds = clock === 0 ? Date.now() : performance.now();
            data().setBigUint64(address, BigInt(Math.floor(milliseconds * 1000000)), true);
            return 0;
        },
        clock_res_get(clock, address) { data().setBigUint64(address, 1000000n, true); return 0; },
        fd_write(fd, vectors, count, written) {
            if (fd !== 1 && fd !== 2) return 8;
            let total = 0;
            for (let index = 0; index < count; index += 1) {
                const address = data().getUint32(vectors + index * 8, true);
                const length = data().getUint32(vectors + index * 8 + 4, true);
                const text = new TextDecoder().decode(new Uint8Array(memory.buffer, address, length));
                if (fd === 1) output += text; else errors += text;
                total += length;
                if (output.length + errors.length > 65536) throw new Error('Probe output exceeded its limit');
            }
            put32(written, total);
            return 0;
        },
        fd_read(fd, vectors, count, read) { if (fd !== 0) return 8; put32(read, 0); return 0; },
        fd_fdstat_get(fd, address) {
            if (fd > 2) return 8;
            new Uint8Array(memory.buffer, address, 24).fill(0);
            data().setUint8(address, 2);
            return 0;
        },
        fd_fdstat_set_flags: fd => fd <= 2 ? 0 : 8,
        fd_prestat_get: () => 8,
        fd_prestat_dir_name: () => 8,
        fd_close: fd => fd <= 2 ? 0 : 8,
        fd_seek: () => 70,
        fd_tell: () => 70,
        fd_filestat_get: () => 8,
        path_open: () => 76,
        sched_yield: () => 0,
        proc_exit(code) { throw new Exit(code); },
    };
    for (const name of [
        'fd_readdir', 'fd_advise', 'fd_allocate', 'fd_datasync', 'fd_sync',
        'fd_filestat_set_size', 'fd_filestat_set_times', 'fd_fdstat_set_rights',
        'path_filestat_get', 'path_filestat_set_times', 'path_readlink', 'path_unlink_file',
        'path_rename', 'path_create_directory', 'path_remove_directory', 'path_link', 'path_symlink',
        'sock_accept', 'sock_recv', 'sock_send', 'sock_shutdown', 'poll_oneoff',
    ]) wasi[name] = () => 76;
    const module = await WebAssembly.compile(bytes);
    for (const entry of WebAssembly.Module.imports(module)) {
        if (entry.module !== 'wasi_snapshot_preview1' || typeof wasi[entry.name] !== 'function') {
            throw new Error(`Unsupported WASI import: ${entry.module}.${entry.name}`);
        }
    }
    const instance = await WebAssembly.instantiate(module, { wasi_snapshot_preview1: wasi });
    memory = instance.exports.memory;
    try { instance.exports._start(); }
    catch (error) { if (!(error instanceof Exit) || error.code !== 0) throw new Error(`${error.message}: ${errors}`); }
    if (!output.startsWith('PASS: backend=OpenSSL ') || randomCalls === 0) {
        throw new Error(`OpenSSL probe did not pass: ${errors || output}`);
    }
    return { output, randomCalls, randomBytes };
}

// @license-end