Skip to main content

nanvm_effects_node/
native.rs

1//! The console, files and directories over `std`, performed for the loop of [`crate::run`].
2//!
3//! A [`Native`] is generic over its three streams so that it can be proven
4//! without touching the process's own; [`Native::stdio`] is the one over the
5//! real ones. Its [`perform`](Native::perform) is the `perform` the loop takes:
6//! it reads the request from VM values, runs the operation and answers the
7//! `Result` the language reads, as `fjs/effects/node/module.mjs` does.
8
9use crate::{
10    codec::{
11        Malformed, argument, arity, decode_array, decode_bytes, decode_choice, decode_flag,
12        decode_literal, decode_nullable, decode_number, decode_object, decode_optional,
13        decode_string, decode_true, encode_array, encode_bool, encode_bytes, encode_error,
14        encode_nothing, encode_nullable, encode_number, encode_object, encode_ok, encode_result,
15        encode_string, encode_tuple, member, optional_argument, required,
16    },
17    common,
18    files::{self, Dirent, IoError, Stat},
19    inflate,
20    resolve::{FileModule, resolve_file_module},
21};
22use nanvm_lib::vm::{
23    Any, Array, IVm,
24    unstable::{number_text, string_any},
25};
26use std::{
27    collections::HashMap,
28    fs::File,
29    io::{self, Cursor, ErrorKind, Read, Write},
30};
31
32/// `NodeOp`'s commands, printed from `nodeCommands` in
33/// `fjs/effects/node/module.f.mjs`: a command outside them is a malformed
34/// request, not a missing capability.
35#[path = "gen.commands.rs"]
36mod commands;
37use commands::COMMANDS;
38
39/// A host over `std`, reading console input from `R` and writing console
40/// output to `O` and `E`.
41#[derive(Debug)]
42pub struct Native<R, O, E> {
43    stdin: R,
44    stdout: O,
45    stderr: E,
46    /// The open files, by handle. A handle is the count of handles given before
47    /// it, so none is reused for another file, and a closed one is simply
48    /// absent: nothing is kept for it.
49    files: HashMap<u64, File>,
50    /// How many handles have been given.
51    opened: u64,
52}
53
54impl Native<io::Stdin, io::Stdout, io::Stderr> {
55    /// The host over the real standard streams.
56    pub fn stdio() -> Self {
57        Self::new(io::stdin(), io::stdout(), io::stderr())
58    }
59}
60
61impl Native<Cursor<Vec<u8>>, Vec<u8>, Vec<u8>> {
62    /// A host in memory: standard input reads `input`, and the output streams
63    /// collect what is written to them.
64    pub fn buffered(input: &[u8]) -> Self {
65        Self::new(Cursor::new(input.to_vec()), Vec::new(), Vec::new())
66    }
67}
68
69impl<R, O, E> Native<R, O, E> {
70    pub fn new(stdin: R, stdout: O, stderr: E) -> Self {
71        Self {
72            stdin,
73            stdout,
74            stderr,
75            files: HashMap::new(),
76            opened: 0,
77        }
78    }
79
80    /// What has been written to the standard output stream.
81    pub fn stdout(&self) -> &O {
82        &self.stdout
83    }
84
85    /// What has been written to the standard error stream.
86    pub fn stderr(&self) -> &E {
87        &self.stderr
88    }
89}
90
91/// What a host answers a command it has no operation for: `['error',
92/// ['notImplemented', command]]`, through the ordinary continuation, so the
93/// program decides what that means.
94fn not_implemented<A: IVm>(command: &str) -> Any<A> {
95    encode_error(encode_tuple(
96        "notImplemented",
97        encode_string(command.to_string()),
98    ))
99}
100
101fn malformed_command<T>(command: &str) -> Result<T, Malformed> {
102    Err(Malformed(format!("`{command}` is not a command")))
103}
104
105/// An operation's answer as the language reads a `Result`: a failure is
106/// `['error', ['ioError', {code?, message}]]`.
107fn answer<A: IVm, T>(result: Result<T, IoError>, ok: impl FnOnce(T) -> Any<A>) -> Any<A> {
108    encode_result(result.map(ok).map_err(encode_io_error))
109}
110
111/// `['ioError', {code?, message}]`.
112fn encode_io_error<A: IVm>(IoError { code, message }: IoError) -> Any<A> {
113    encode_tuple(
114        "ioError",
115        encode_object([
116            code.map(|code| ("code", encode_string(code))),
117            Some(("message", encode_string(message))),
118        ]),
119    )
120}
121
122fn encode_file_module<A: IVm>(module: FileModule) -> Any<A> {
123    encode_object([
124        Some(("id", encode_string(module.id))),
125        Some(("path", encode_string(module.path))),
126    ])
127}
128
129fn encode_stat<A: IVm>(stat: Stat) -> Any<A> {
130    encode_object([
131        Some(("size", encode_number(stat.size as f64))),
132        Some(("isFile", encode_bool(stat.is_file))),
133        Some(("isDirectory", encode_bool(stat.is_directory))),
134    ])
135}
136
137fn encode_dirent<A: IVm>(dirent: Dirent) -> Any<A> {
138    encode_object([
139        Some(("name", encode_string(dirent.name))),
140        Some(("parentPath", encode_string(dirent.parent_path))),
141        Some(("isFile", encode_bool(dirent.is_file))),
142        Some(("isDirectory", encode_bool(dirent.is_directory))),
143    ])
144}
145
146impl<R: Read, O: Write, E: Write> Native<R, O, E> {
147    /// Performs `command` with `payload`: the answer is a `Result` for the
148    /// continuation. A request that is not one the operation takes is thrown
149    /// in the language, as the JavaScript runners' `assert` ends their loop.
150    pub fn perform<A: IVm>(&mut self, command: Any<A>, payload: Any<A>) -> Result<Any<A>, Any<A>> {
151        self.dispatch(command, payload)
152            .map_err(|Malformed(what)| string_any(&what))
153    }
154
155    fn dispatch<A: IVm>(&mut self, command: Any<A>, payload: Any<A>) -> Result<Any<A>, Malformed> {
156        let command = decode_string(command)?;
157        let Ok(payload) = Array::try_from(payload) else {
158            return Err(Malformed("a payload that is not an array".to_string()));
159        };
160        let payload = &payload;
161        match command.as_str() {
162            "write" => {
163                arity(payload, 2)?;
164                let stream = decode_choice(argument(payload, 0, "stream")?, &["stdout", "stderr"])?;
165                let data = decode_bytes(argument(payload, 1, "data")?)?;
166                self.write(stream == 1, &data);
167                Ok(encode_ok(encode_nothing(())))
168            }
169            "read" => {
170                arity(payload, 1)?;
171                decode_literal(argument(payload, 0, "stream")?, "stdin")?;
172                Ok(encode_ok(encode_nullable(self.read(), encode_number)))
173            }
174            "mkdir" => {
175                arity(payload, 2)?;
176                let path = decode_string(argument(payload, 0, "path")?)?;
177                let recursive = decode_optional(optional_argument(payload, 1), |options| {
178                    let options = decode_object(options, &["recursive"])?;
179                    decode_true(required(&options, "recursive")?)
180                })?
181                .is_some();
182                Ok(answer(files::mkdir(&path, recursive), encode_nothing))
183            }
184            "readFile" => {
185                arity(payload, 1)?;
186                let path = decode_string(argument(payload, 0, "path")?)?;
187                Ok(answer(files::read_file(&path), encode_bytes))
188            }
189            "readWhole" => {
190                arity(payload, 1)?;
191                let path = decode_string(argument(payload, 0, "path")?)?;
192                Ok(answer(files::read_whole(&path), |windows| {
193                    encode_array(windows, encode_bytes)
194                }))
195            }
196            "readdir" => {
197                arity(payload, 2)?;
198                let path = decode_string(argument(payload, 0, "path")?)?;
199                let options = decode_object(argument(payload, 1, "options")?, &["recursive"])?;
200                let recursive = decode_flag(member(&options, "recursive"))?;
201                Ok(answer(files::readdir(&path, recursive), |entries| {
202                    encode_array(entries, encode_dirent)
203                }))
204            }
205            "writeFile" => {
206                arity(payload, 2)?;
207                let path = decode_string(argument(payload, 0, "path")?)?;
208                let data = decode_bytes(argument(payload, 1, "data")?)?;
209                Ok(answer(files::write_file(&path, &data), encode_nothing))
210            }
211            "writeBytes" => {
212                arity(payload, 3)?;
213                let path = decode_string(argument(payload, 0, "path")?)?;
214                let offset = decode_number(argument(payload, 1, "offset")?)?;
215                let data = decode_bytes(argument(payload, 2, "data")?)?;
216                Ok(answer(
217                    files::write_bytes(&path, offset, &data),
218                    encode_nothing,
219                ))
220            }
221            "resolveFileModule" => {
222                arity(payload, 2)?;
223                let name = decode_string(argument(payload, 0, "name")?)?;
224                let parent = decode_nullable(argument(payload, 1, "parent")?, decode_string)?;
225                Ok(answer(
226                    resolve_file_module(&name, parent.as_deref()),
227                    |module| encode_file_module(module),
228                ))
229            }
230            "stat" => {
231                arity(payload, 1)?;
232                let path = decode_string(argument(payload, 0, "path")?)?;
233                Ok(answer(files::stat(&path), encode_stat))
234            }
235            "access" => {
236                arity(payload, 1)?;
237                let path = decode_string(argument(payload, 0, "path")?)?;
238                Ok(answer(files::access(&path), encode_nothing))
239            }
240            "rename" => {
241                arity(payload, 2)?;
242                let src = decode_string(argument(payload, 0, "src")?)?;
243                let dst = decode_string(argument(payload, 1, "dst")?)?;
244                Ok(answer(files::rename(&src, &dst), encode_nothing))
245            }
246            "rmdir" => {
247                arity(payload, 1)?;
248                let path = decode_string(argument(payload, 0, "path")?)?;
249                Ok(answer(files::rmdir(&path), encode_nothing))
250            }
251            "readBytes" => {
252                arity(payload, 3)?;
253                let path = decode_string(argument(payload, 0, "path")?)?;
254                let offset = decode_number(argument(payload, 1, "offset")?)?;
255                let size = decode_number(argument(payload, 2, "size")?)?;
256                Ok(answer(files::read_bytes(&path, offset, size), encode_bytes))
257            }
258            "inflate" => {
259                arity(payload, 1)?;
260                let data = decode_bytes(argument(payload, 0, "data")?)?;
261                Ok(answer(inflate::inflate(&data), encode_bytes))
262            }
263            "createExclusive" => {
264                arity(payload, 1)?;
265                let path = decode_string(argument(payload, 0, "path")?)?;
266                Ok(answer(files::create_exclusive(&path), encode_nothing))
267            }
268            "writeExclusive" => {
269                arity(payload, 2)?;
270                let path = decode_string(argument(payload, 0, "path")?)?;
271                let data = decode_array(argument(payload, 1, "data")?, decode_bytes)?;
272                Ok(answer(files::write_exclusive(&path, &data), encode_nothing))
273            }
274            "open" => {
275                arity(payload, 1)?;
276                let path = decode_string(argument(payload, 0, "path")?)?;
277                let opened = files::open(&path).map(|file| {
278                    self.files.insert(self.opened, file);
279                    self.opened += 1;
280                    (self.opened - 1) as f64
281                });
282                Ok(answer(opened, encode_number))
283            }
284            "fstat" => {
285                arity(payload, 1)?;
286                let handle = self.handle(argument(payload, 0, "handle")?)?;
287                Ok(answer(handle.and_then(files::fstat), encode_stat))
288            }
289            "pread" => {
290                arity(payload, 3)?;
291                let handle = argument(payload, 0, "handle")?;
292                let offset = decode_number(argument(payload, 1, "offset")?)?;
293                let size = decode_number(argument(payload, 2, "size")?)?;
294                // The window first, as the Node runner: whatever the handle is,
295                // a bad window answers the window.
296                if let Err(refused) = files::check_window(offset, size) {
297                    return Ok(answer(Err(refused), encode_bytes));
298                }
299                let handle = self.handle(handle)?;
300                // An empty window reads nothing, so a closed handle answers it
301                // too, as the Node runner's `fill` does.
302                let read = if size == 0.0 {
303                    Ok(Vec::new())
304                } else {
305                    handle.and_then(|file| files::pread(file, offset, size))
306                };
307                Ok(answer(read, encode_bytes))
308            }
309            "close" => {
310                arity(payload, 1)?;
311                let handle = self.slot(argument(payload, 0, "handle")?)?;
312                // Closing twice is not an error, as `FileHandle.close()`.
313                self.files.remove(&handle);
314                Ok(encode_ok(encode_nothing(())))
315            }
316            "catch" => {
317                arity(payload, 1)?;
318                Ok(common::catch(argument(payload, 0, "f")?))
319            }
320            "sandbox" if common::CLOCK => {
321                arity(payload, 1)?;
322                Ok(common::sandbox(argument(payload, 0, "f")?))
323            }
324            "now" if common::CLOCK => {
325                arity(payload, 0)?;
326                Ok(common::now())
327            }
328            "randomInt" if common::RANDOM => {
329                arity(payload, 0)?;
330                Ok(common::random_int())
331            }
332            "rm" => {
333                arity(payload, 1)?;
334                let path = decode_string(argument(payload, 0, "path")?)?;
335                Ok(answer(files::rm(&path), encode_nothing))
336            }
337            _ if COMMANDS.contains(&command.as_str()) => Ok(not_implemented(&command)),
338            _ => malformed_command(&command),
339        }
340    }
341
342    /// The number a handle is. A value that is no handle this host gave is
343    /// thrown, as the Node runner's `asFileHandle` does.
344    fn slot<A: IVm>(&self, handle: Any<A>) -> Result<u64, Malformed> {
345        let number = decode_number(handle)?;
346        if number.fract() == 0.0 && number >= 0.0 && number < self.opened as f64 {
347            Ok(number as u64)
348        } else {
349            Err(Malformed(format!(
350                "{} is not a file handle",
351                number_text(number)
352            )))
353        }
354    }
355
356    /// The open file a handle names, or `EBADF` for one that was closed.
357    fn handle<A: IVm>(&self, handle: Any<A>) -> Result<Result<&File, IoError>, Malformed> {
358        let slot = self.slot(handle)?;
359        Ok(self.files.get(&slot).ok_or_else(files::bad_descriptor))
360    }
361
362    /// Writes all of `data` and flushes, so that it is out when this answers.
363    /// The operation has no failure of its own, and a stream that refuses the
364    /// data ends the process, as a stream error nobody handles ends Node's.
365    fn write(&mut self, stderr: bool, data: &[u8]) {
366        if stderr {
367            self.stderr
368                .write_all(data)
369                .and_then(|_| self.stderr.flush())
370        } else {
371            self.stdout
372                .write_all(data)
373                .and_then(|_| self.stdout.flush())
374        }
375        .expect("a console stream refused a write");
376    }
377
378    /// The next byte of input, and `None` at its end. A read that fails ends
379    /// the process, as for `write`.
380    fn read(&mut self) -> Option<f64> {
381        let mut byte = [0u8];
382        loop {
383            match self.stdin.read(&mut byte) {
384                Ok(0) => return None,
385                Ok(_) => return Some(f64::from(byte[0])),
386                Err(e) if e.kind() == ErrorKind::Interrupted => {}
387                Err(e) => panic!("console input failed: {e}"),
388            }
389        }
390    }
391}
392
393#[cfg(test)]
394mod test {
395    use super::*;
396    use crate::codec::decode_result;
397    #[cfg(not(target_family = "wasm"))]
398    use crate::scratch::Scratch;
399    use nanvm_lib::{
400        naive::Naive,
401        vm::{
402            Nullish, Number, ToAny, ToArray, ToObject,
403            unstable::{bigint_any, bigint_any_words, string_key},
404        },
405    };
406
407    type V = Any<Naive>;
408    type Host = Native<Cursor<Vec<u8>>, Vec<u8>, Vec<u8>>;
409
410    fn array<const N: usize>(items: [V; N]) -> V {
411        items.to_array().to_any()
412    }
413
414    /// The value of an `['ok', value]` answer.
415    fn ok(result: Result<V, V>) -> V {
416        decode_result(result.unwrap()).unwrap().unwrap()
417    }
418
419    /// `"hi"` as the language spells bytes: the bits with a stop bit in front,
420    /// negated where the first bit was `0`.
421    fn hi() -> V {
422        bigint_any(-59_497)
423    }
424
425    fn write(host: &mut Host, stream: &str) -> Result<V, V> {
426        host.perform(string_any("write"), array([string_any(stream), hi()]))
427    }
428
429    /// The console writes both streams and reads input a byte at a time, each
430    /// answered `['ok', ...]`.
431    #[test]
432    fn console() {
433        let mut h = Native::buffered(b"a");
434        assert_eq!(ok(write(&mut h, "stdout")), Nullish::Undefined.to_any());
435        assert_eq!(ok(write(&mut h, "stderr")), Nullish::Undefined.to_any());
436        assert_eq!(h.stdout().as_slice(), b"hi");
437        assert_eq!(h.stderr().as_slice(), b"hi");
438        // the streams are told apart: one write reaches one stream only
439        ok(write(&mut h, "stdout"));
440        assert_eq!(h.stdout().as_slice(), b"hihi");
441        assert_eq!(h.stderr().as_slice(), b"hi");
442        h.stdout.clear();
443        h.perform(
444            string_any("write"),
445            array([string_any("stdout"), bigint_any(0)]),
446        )
447        .unwrap();
448        assert!(h.stdout().is_empty());
449        let read = |h: &mut Host| ok(h.perform(string_any("read"), array([string_any("stdin")])));
450        assert_eq!(read(&mut h), Number::from(97.0).to_any());
451        assert_eq!(read(&mut h), Nullish::Null.to_any());
452    }
453
454    /// A trailing partial byte is zero-padded in its low bits, as `fromVec` does.
455    #[test]
456    fn partial_byte() {
457        let mut h = Native::buffered(b"");
458        // `vec(1n)(1n)`: the one bit `1`
459        ok(h.perform(
460            string_any("write"),
461            array([string_any("stdout"), bigint_any(1)]),
462        ));
463        // `vec(9n)(0b1_0000_0001n)` shifted: nine bits, `1 0000 0001`
464        ok(h.perform(
465            string_any("write"),
466            array([string_any("stdout"), bigint_any(0b1_0000_0001)]),
467        ));
468        assert_eq!(h.stdout().as_slice(), [0x80, 0x80, 0x80]);
469    }
470
471    /// A vector of more than one word is read across its words, whole bytes or not.
472    #[test]
473    fn many_words() {
474        let mut h = Native::buffered(b"");
475        let mut put = |v: V| ok(h.perform(string_any("write"), array([string_any("stdout"), v])));
476        // nine bytes `01 02 .. 09`: the first bit is `0`, so the value is negative
477        put(bigint_any_words(true, &[0x0203_0405_0607_0809, 0x81]));
478        // 65 one bits: the last byte is padded to `80`
479        put(bigint_any_words(false, &[u64::MAX, 1]));
480        let mut expected: Vec<u8> = (1..=9).collect();
481        expected.extend([0xff; 8]);
482        expected.push(0x80);
483        assert_eq!(h.stdout().as_slice(), expected);
484    }
485
486    /// `inflate` answers the bytes of one zlib stream, and a stream it refuses
487    /// as the error zlib gives, through the continuation; a payload that is not
488    /// bytes is thrown.
489    #[test]
490    fn inflate() {
491        let stored = encode_bytes::<Naive>(vec![
492            0x78, 0x01, 0x01, 0x03, 0x00, 0xfc, 0xff, 0x61, 0x62, 0x63, 0x02, 0x4d, 0x01, 0x27,
493        ]);
494        let answer = Native::buffered(b"").perform(string_any("inflate"), array([stored]));
495        assert_eq!(decode_bytes(ok(answer)).unwrap(), b"abc");
496        let bad = encode_bytes::<Naive>(vec![0x78, 0x9d]);
497        assert_eq!(
498            Native::buffered(b"").perform(string_any("inflate"), array([bad])).unwrap().to_json(),
499            Ok("[\"error\",[\"ioError\",{\"code\":\"Z_DATA_ERROR\",\"message\":\"incorrect header check\"}]]".into())
500        );
501        let thrown = Native::buffered(b"")
502            .perform(string_any("inflate"), array([string_any("x")]))
503            .unwrap_err();
504        assert_eq!(decode_string(thrown).unwrap(), "not a bit vector");
505    }
506
507    /// A command the host lacks is answered through the continuation as
508    /// `['error', ['notImplemented', command]]`.
509    #[test]
510    fn a_command_the_host_lacks() {
511        let result = Native::buffered(b"").perform(string_any("fetch"), array([]));
512        assert_eq!(
513            result.unwrap().to_json(),
514            Ok("[\"error\",[\"notImplemented\",\"fetch\"]]".into())
515        );
516    }
517
518    /// A request that is not one the operation takes is thrown, naming why.
519    #[test]
520    fn malformed_requests_throw() {
521        let throws = |command: V, payload: V| {
522            let thrown = Native::buffered(b"").perform(command, payload).unwrap_err();
523            decode_string(thrown).unwrap()
524        };
525        let w = || string_any("write");
526        assert_eq!(throws(Nullish::Null.to_any(), array([])), "not a string");
527        assert_eq!(
528            throws(w(), Nullish::Null.to_any()),
529            "a payload that is not an array"
530        );
531        assert_eq!(
532            throws(w(), array([string_any("stdout")])),
533            "missing argument 1, `data`"
534        );
535        assert_eq!(
536            throws(w(), array([string_any("stdout"), hi(), hi()])),
537            "3 arguments where at most 2 are taken"
538        );
539        assert_eq!(
540            throws(w(), array([string_any("net"), hi()])),
541            "`net` is not one of [\"stdout\", \"stderr\"]"
542        );
543        assert_eq!(
544            throws(w(), array([string_any("stdout"), string_any("hi")])),
545            "not a bit vector"
546        );
547        assert_eq!(
548            throws(string_any("wirte"), array([])),
549            "`wirte` is not a command"
550        );
551        assert_eq!(
552            throws(string_any("read"), array([string_any("stdout")])),
553            "not `stdin`"
554        );
555    }
556
557    /// An interrupted read is tried again.
558    #[test]
559    fn interrupted_read() {
560        struct Once(bool);
561        impl Read for Once {
562            fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
563                if std::mem::replace(&mut self.0, true) {
564                    buf[0] = 7;
565                    Ok(1)
566                } else {
567                    Err(ErrorKind::Interrupted.into())
568                }
569            }
570        }
571        let mut h = Native::new(Once(false), Vec::new(), Vec::new());
572        assert_eq!(h.read(), Some(7.0));
573    }
574
575    #[test]
576    #[should_panic(expected = "a console stream refused a write")]
577    fn refused_write() {
578        struct Full;
579        impl Write for Full {
580            fn write(&mut self, _: &[u8]) -> io::Result<usize> {
581                Err(ErrorKind::BrokenPipe.into())
582            }
583            fn flush(&mut self) -> io::Result<()> {
584                Ok(())
585            }
586        }
587        Native::new(Cursor::new(Vec::new()), Full, Vec::new()).write(false, &[1]);
588    }
589
590    #[test]
591    #[should_panic(expected = "console input failed")]
592    fn failed_read() {
593        struct Broken;
594        impl Read for Broken {
595            fn read(&mut self, _: &mut [u8]) -> io::Result<usize> {
596                Err(ErrorKind::BrokenPipe.into())
597            }
598        }
599        Native::new(Broken, Vec::new(), Vec::new()).read();
600    }
601
602    fn object(members: impl IntoIterator<Item = (&'static str, V)>) -> V {
603        members
604            .into_iter()
605            .map(|(key, value)| (string_key::<Naive>(key), value))
606            .collect::<Vec<_>>()
607            .to_object()
608            .to_any()
609    }
610
611    fn recursive(value: bool) -> V {
612        object([("recursive", value.to_any())])
613    }
614
615    fn perform<const N: usize>(command: &str, payload: [V; N]) -> Result<V, V> {
616        Native::buffered(b"").perform(string_any(command), array(payload))
617    }
618
619    fn thrown<const N: usize>(command: &str, payload: [V; N]) -> String {
620        decode_string(perform(command, payload).unwrap_err()).unwrap()
621    }
622
623    /// A request that is not one the operation takes is thrown, naming why.
624    #[test]
625    fn malformed_file_requests_throw() {
626        let path = || string_any("p");
627        assert_eq!(thrown("mkdir", [path(), recursive(false)]), "not `true`");
628        assert_eq!(thrown("mkdir", [path(), string_any("x")]), "not an object");
629        assert_eq!(
630            thrown("mkdir", [path(), object([("x", hi())])]),
631            "unexpected member `x`"
632        );
633        assert_eq!(
634            thrown("mkdir", [path(), object([])]),
635            "missing member `recursive`"
636        );
637        assert_eq!(thrown("readdir", [path()]), "missing argument 1, `options`");
638        assert_eq!(
639            thrown("readdir", [path(), recursive(false)]),
640            "a flag that is not `true`"
641        );
642        assert_eq!(
643            thrown("writeBytes", [path(), string_any("0"), hi()]),
644            "not a number"
645        );
646        assert_eq!(thrown("rm", [Nullish::Null.to_any()]), "not a string");
647    }
648
649    /// The operations through `perform`, against a real directory, which a
650    /// WebAssembly host does not give a test.
651    #[cfg(not(target_family = "wasm"))]
652    #[test]
653    fn files_through_perform() {
654        let dir = Scratch::new("perform");
655        let at = |name: &str| string_any(&dir.at(name));
656        let undefined = || Nullish::Undefined.to_any();
657
658        assert_eq!(ok(perform("mkdir", [at("a"), undefined()])), undefined());
659        assert_eq!(
660            ok(perform("mkdir", [at("b/c"), recursive(true)])),
661            undefined()
662        );
663        assert_eq!(ok(perform("writeFile", [at("f"), hi()])), undefined());
664        assert_eq!(ok(perform("readFile", [at("f")])), hi());
665        let windows: Vec<V> = Array::try_from(ok(perform("readWhole", [at("f")])))
666            .unwrap()
667            .into_iter()
668            .collect();
669        assert_eq!(windows, [hi()]);
670        assert_eq!(
671            ok(perform(
672                "writeBytes",
673                [at("f"), Number::from(1.0).to_any(), hi()]
674            )),
675            undefined()
676        );
677        let names = |entries: V| -> Vec<String> {
678            Array::try_from(entries)
679                .unwrap()
680                .into_iter()
681                .map(|entry| decode_string(member_of(entry, "name")).unwrap())
682                .collect()
683        };
684        let root = string_any(&dir.path().to_string_lossy());
685        assert_eq!(
686            names(ok(perform("readdir", [root.clone(), object([])]))),
687            ["a", "b", "f"]
688        );
689        assert_eq!(
690            names(ok(perform("readdir", [root, recursive(true)]))),
691            ["a", "b", "f", "c"]
692        );
693        assert_eq!(ok(perform("rm", [at("f")])), undefined());
694
695        let missing = perform("readFile", [at("f")]).unwrap();
696        assert_eq!(error_code(missing), string_any("ENOENT"));
697    }
698
699    /// `stat`, `access`, `rename` and `rmdir` through `perform`.
700    #[cfg(not(target_family = "wasm"))]
701    #[test]
702    fn metadata_through_perform() {
703        let dir = Scratch::new("metadata");
704        let at = |name: &str| string_any(&dir.at(name));
705        let undefined = || Nullish::Undefined.to_any();
706
707        ok(perform("writeFile", [at("a"), hi()]));
708        let stat = ok(perform("stat", [at("a")]));
709        assert_eq!(member_of(stat.clone(), "size"), Number::from(2.0).to_any());
710        assert_eq!(member_of(stat.clone(), "isFile"), true.to_any());
711        assert_eq!(member_of(stat, "isDirectory"), false.to_any());
712        assert_eq!(ok(perform("access", [at("a")])), undefined());
713        assert_eq!(ok(perform("rename", [at("a"), at("b")])), undefined());
714        assert_eq!(ok(perform("rm", [at("b")])), undefined());
715        assert_eq!(ok(perform("mkdir", [at("d"), undefined()])), undefined());
716        assert_eq!(ok(perform("rmdir", [at("d")])), undefined());
717
718        let missing = perform("stat", [at("d")]).unwrap();
719        assert_eq!(error_code(missing), string_any("ENOENT"));
720    }
721
722    /// `readBytes`, `createExclusive` and `writeExclusive` through `perform`.
723    #[cfg(not(target_family = "wasm"))]
724    #[test]
725    fn exclusive_and_windows_through_perform() {
726        let dir = Scratch::new("exclusive");
727        let at = |name: &str| string_any(&dir.at(name));
728        let undefined = || Nullish::Undefined.to_any();
729        let number = |n: f64| Number::from(n).to_any();
730
731        assert_eq!(ok(perform("createExclusive", [at("a")])), undefined());
732        assert_eq!(
733            ok(perform("writeExclusive", [at("b"), array([hi(), hi()])])),
734            undefined()
735        );
736        assert_eq!(std::fs::read(dir.at("b")).unwrap(), b"hihi");
737        let window = ok(perform("readBytes", [at("b"), number(1.0), number(2.0)]));
738        assert_eq!(decode_bytes(window).unwrap(), b"ih");
739
740        // A second create of the same name is EEXIST, answered as an error.
741        let again = perform("createExclusive", [at("a")]).unwrap();
742        assert_eq!(error_code(again), string_any("EEXIST"));
743        assert_eq!(thrown("writeExclusive", [at("c"), hi()]), "not an array");
744    }
745
746    /// The `{code?, message}` of an `['error', ['ioError', info]]` answer.
747    #[cfg(not(target_family = "wasm"))]
748    fn error_info(answer: V) -> V {
749        let [tag, error]: [V; 2] = Array::try_from(answer).unwrap().try_into().unwrap();
750        assert_eq!(tag, string_any("error"));
751        let [kind, info]: [V; 2] = Array::try_from(error).unwrap().try_into().unwrap();
752        assert_eq!(kind, string_any("ioError"));
753        info
754    }
755
756    /// The code of an `['error', ['ioError', {code}]]` answer.
757    #[cfg(not(target_family = "wasm"))]
758    fn error_code(answer: V) -> V {
759        member_of(error_info(answer), "code")
760    }
761
762    /// `open`, `fstat`, `pread` and `close` through one host, which keeps the
763    /// handles: an index, never reused, and `EBADF` once closed.
764    #[cfg(not(target_family = "wasm"))]
765    #[test]
766    fn handles_through_perform() {
767        let dir = Scratch::new("handles");
768        let at = |name: &str| string_any(&dir.at(name));
769        let undefined = || Nullish::Undefined.to_any();
770        let number = |n: f64| Number::from(n).to_any();
771        ok(perform("writeFile", [at("a"), hi()]));
772        let mut h = Native::buffered(b"");
773        let mut run = |command: &str, payload: Vec<V>| {
774            h.perform(string_any(command), payload.to_array().to_any())
775        };
776
777        let a = ok(run("open", vec![at("a")]));
778        assert_eq!(a, number(0.0));
779        let stat = ok(run("fstat", vec![a.clone()]));
780        assert_eq!(member_of(stat.clone(), "size"), number(2.0));
781        assert_eq!(member_of(stat, "isFile"), true.to_any());
782        let window = |at: f64, size: f64| vec![number(0.0), number(at), number(size)];
783        let bytes = |answer: Result<V, V>| decode_bytes(ok(answer)).unwrap();
784        assert_eq!(bytes(run("pread", window(0.0, 2.0))), b"hi");
785        assert_eq!(bytes(run("pread", window(1.0, 10.0))), b"i");
786        assert_eq!(bytes(run("pread", window(5.0, 1.0))), b"");
787
788        // The path is gone, and the handle still reads what it opened.
789        std::fs::remove_file(dir.at("a")).unwrap();
790        assert_eq!(bytes(run("pread", window(0.0, 2.0))), b"hi");
791
792        // A name that is not there answers `ENOENT` and mints no handle.
793        let missing = run("open", vec![at("none")]).unwrap();
794        assert_eq!(error_code(missing), string_any("ENOENT"));
795        #[cfg(unix)]
796        {
797            let directory = ok(run("open", vec![string_any(&dir.path().to_string_lossy())]));
798            assert_eq!(directory, number(1.0));
799            let stat = ok(run("fstat", vec![directory.clone()]));
800            assert_eq!(member_of(stat, "isDirectory"), true.to_any());
801            let read = run("pread", vec![directory.clone(), number(0.0), number(1.0)]).unwrap();
802            assert_eq!(error_code(read), string_any("EISDIR"));
803            assert_eq!(ok(run("close", vec![directory])), undefined());
804        }
805
806        assert_eq!(ok(run("close", vec![a.clone()])), undefined());
807        assert_eq!(ok(run("close", vec![a.clone()])), undefined());
808        let closed = run("fstat", vec![a.clone()]).unwrap();
809        assert_eq!(error_code(closed), string_any("EBADF"));
810        let closed = run("pread", window(0.0, 1.0)).unwrap();
811        assert_eq!(error_code(closed), string_any("EBADF"));
812        // An empty window reads nothing, so it needs no file.
813        assert_eq!(bytes(run("pread", window(0.0, 0.0))), b"");
814
815        // The window is checked before the handle, as the Node runner does.
816        let refused = run("pread", vec![a.clone(), number(-1.0), number(1.0)]).unwrap();
817        assert_eq!(
818            member_of(error_info(refused), "message"),
819            string_any("Offset -1 is negative")
820        );
821
822        let thrown = |result: Result<V, V>| decode_string(result.unwrap_err()).unwrap();
823        assert_eq!(
824            thrown(run("fstat", vec![number(99.0)])),
825            "99 is not a file handle"
826        );
827        assert_eq!(
828            thrown(run("close", vec![number(0.5)])),
829            "0.5 is not a file handle"
830        );
831        assert_eq!(
832            thrown(run("pread", vec![a, number(0.0)])),
833            "missing argument 2, `size`"
834        );
835        let refused = run("pread", vec![number(99.0), number(-1.0), number(1.0)]).unwrap();
836        assert_eq!(
837            member_of(error_info(refused), "message"),
838            string_any("Offset -1 is negative")
839        );
840
841        // Closing frees the file: nothing is kept for a handle that is closed.
842        assert_eq!(h.files.len(), 0);
843    }
844
845    fn thunk(code: nanvm_lib::vm::StaticCode<Naive>) -> V {
846        use nanvm_lib::vm::IVm;
847        Naive::static_function(code, 0, [].to_array(), None).to_any()
848    }
849
850    /// The member `name` of an object that has it.
851    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
852    fn member_of(value: V, name: &str) -> V {
853        member(&nanvm_lib::vm::Object::try_from(value).unwrap(), name).unwrap()
854    }
855
856    /// `catch` answers what the thunk did: `['ok', ['ok', value]]`, and a
857    /// throw as `['ok', ['error', thrown]]`, neither ending the run.
858    #[test]
859    fn catch_answers_what_the_thunk_did() {
860        let returned = ok(perform("catch", [thunk(|_, _| Ok(string_any("fine")))]));
861        assert_eq!(returned.to_json(), Ok("[\"ok\",\"fine\"]".into()));
862        let threw = ok(perform("catch", [thunk(|_, _| Err(string_any("boom")))]));
863        assert_eq!(threw.to_json(), Ok("[\"error\",\"boom\"]".into()));
864        // A value that is no function throws when it is called, which `catch` catches.
865        let not_callable = ok(perform("catch", [string_any("x")]));
866        let [tag, _]: [V; 2] = Array::try_from(not_callable).unwrap().try_into().unwrap();
867        assert_eq!(tag, string_any("error"));
868    }
869
870    /// `sandbox` answers `{result, duration}`, the duration a number of
871    /// milliseconds that is not negative.
872    /// Needs a clock, which `wasm32-unknown-unknown` does not have: `std` panics
873    /// on `Instant::now` and `SystemTime::now` there, WASI has them.
874    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
875    #[test]
876    fn sandbox_answers_the_result_and_a_duration() {
877        for (code, expected) in [
878            (
879                (|_, _| Ok(string_any("fine"))) as nanvm_lib::vm::StaticCode<Naive>,
880                "[\"ok\",\"fine\"]",
881            ),
882            (|_, _| Err(string_any("boom")), "[\"error\",\"boom\"]"),
883        ] {
884            let answer = ok(perform("sandbox", [thunk(code)]));
885            let [result, duration] =
886                ["result", "duration"].map(|name| member_of(answer.clone(), name));
887            assert_eq!(result.to_json(), Ok(expected.into()));
888            let milliseconds = f64::from(Number::try_from(duration).unwrap());
889            assert!((0.0..60_000.0).contains(&milliseconds));
890        }
891    }
892
893    /// `now` is whole milliseconds since the epoch, within the test's own bounds.
894    /// Needs a clock, which `wasm32-unknown-unknown` does not have: `std` panics
895    /// on `Instant::now` and `SystemTime::now` there, WASI has them.
896    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
897    #[test]
898    fn now_is_the_epoch_in_milliseconds() {
899        let epoch = || {
900            std::time::SystemTime::now()
901                .duration_since(std::time::UNIX_EPOCH)
902                .unwrap()
903                .as_millis() as f64
904        };
905        let before = epoch();
906        let answer = f64::from(Number::try_from(ok(perform("now", []))).unwrap());
907        let after = epoch();
908        assert!(before <= answer && answer <= after && answer.fract() == 0.0);
909    }
910
911    /// `randomInt` is a whole number below 2^32. Needs a seed, which
912    /// `wasm32-unknown-unknown` does not have.
913    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
914    #[test]
915    fn random_int_is_below_2_to_the_32() {
916        for _ in 0..16 {
917            let n = f64::from(Number::try_from(ok(perform("randomInt", []))).unwrap());
918            assert!((0.0..0x1_0000_0000_u64 as f64).contains(&n) && n.fract() == 0.0);
919        }
920    }
921
922    #[test]
923    fn the_thunk_operations_take_exactly_their_arguments() {
924        assert_eq!(thrown("catch", []), "missing argument 0, `f`");
925    }
926
927    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
928    #[test]
929    fn the_clock_operations_take_exactly_their_arguments() {
930        assert_eq!(
931            thrown("sandbox", [hi(), hi()]),
932            "2 arguments where at most 1 are taken"
933        );
934        assert_eq!(
935            thrown("now", [hi()]),
936            "1 arguments where at most 0 are taken"
937        );
938        assert_eq!(
939            thrown("randomInt", [hi()]),
940            "1 arguments where at most 0 are taken"
941        );
942    }
943
944    /// Without a clock the operations are refused, not trapped.
945    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
946    #[test]
947    fn the_clock_operations_are_not_implemented_without_a_clock() {
948        for command in ["sandbox", "now", "randomInt"] {
949            assert_eq!(
950                perform(command, []).unwrap().to_json(),
951                Ok(format!("[\"error\",[\"notImplemented\",\"{command}\"]]"))
952            );
953        }
954    }
955
956    /// `resolveFileModule` through `perform`: the working directory is where
957    /// a test runs, a crate's own directory.
958    #[cfg(not(target_family = "wasm"))]
959    #[test]
960    fn resolving_a_module_through_perform() {
961        let module = ok(perform(
962            "resolveFileModule",
963            [string_any("Cargo.toml"), Nullish::Null.to_any()],
964        ));
965        let text = |name: &str| decode_string(member_of(module.clone(), name)).unwrap();
966        assert!(text("path").replace('\\', "/").ends_with("/Cargo.toml"));
967        assert_eq!(text("id"), crate::resolve::path_to_file_url(&text("path")));
968        let sibling = ok(perform(
969            "resolveFileModule",
970            [string_any("./src/lib.rs"), string_any(&text("id"))],
971        ));
972        assert!(
973            decode_string(member_of(sibling, "path"))
974                .unwrap()
975                .replace('\\', "/")
976                .ends_with("/nanvm/effects-node/src/lib.rs")
977        );
978    }
979
980    #[test]
981    fn a_parent_that_is_not_a_string_throws() {
982        assert_eq!(
983            thrown("resolveFileModule", [string_any("a"), hi()]),
984            "not a string"
985        );
986    }
987}