1use 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, 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#[path = "gen.commands.rs"]
36mod commands;
37use commands::COMMANDS;
38
39#[derive(Debug)]
42pub struct Native<R, O, E> {
43 stdin: R,
44 stdout: O,
45 stderr: E,
46 files: HashMap<u64, File>,
50 opened: u64,
52}
53
54impl Native<io::Stdin, io::Stdout, io::Stderr> {
55 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 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 pub fn stdout(&self) -> &O {
82 &self.stdout
83 }
84
85 pub fn stderr(&self) -> &E {
87 &self.stderr
88 }
89}
90
91fn 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
105fn 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
111fn 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 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(payload.present(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 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 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 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 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 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 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 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,
403 unstable::{bigint_any, bigint_any_words},
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 fn ok(result: Result<V, V>) -> V {
416 decode_result(result.unwrap()).unwrap().unwrap()
417 }
418
419 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 #[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 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 #[test]
456 fn partial_byte() {
457 let mut h = Native::buffered(b"");
458 ok(h.perform(
460 string_any("write"),
461 array([string_any("stdout"), bigint_any(1)]),
462 ));
463 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 #[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 put(bigint_any_words(true, &[0x0203_0405_0607_0809, 0x81]));
478 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 #[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 #[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 #[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 #[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 empty_object() -> V {
603 encode_object::<Naive, 0>([])
604 }
605
606 fn recursive(value: bool) -> V {
607 encode_object([Some(("recursive", value.to_any()))])
608 }
609
610 fn perform<const N: usize>(command: &str, payload: [V; N]) -> Result<V, V> {
611 Native::buffered(b"").perform(string_any(command), array(payload))
612 }
613
614 fn thrown<const N: usize>(command: &str, payload: [V; N]) -> String {
615 decode_string(perform(command, payload).unwrap_err()).unwrap()
616 }
617
618 #[test]
620 fn malformed_file_requests_throw() {
621 let path = || string_any("p");
622 assert_eq!(thrown("mkdir", [path(), recursive(false)]), "not `true`");
623 assert_eq!(thrown("mkdir", [path(), string_any("x")]), "not an object");
624 assert_eq!(
625 thrown("mkdir", [path(), encode_object([Some(("x", hi()))])]),
626 "unexpected member `x`"
627 );
628 assert_eq!(
629 thrown("mkdir", [path(), empty_object()]),
630 "missing member `recursive`"
631 );
632 assert_eq!(thrown("readdir", [path()]), "missing argument 1, `options`");
633 assert_eq!(
634 thrown("readdir", [path(), recursive(false)]),
635 "a flag that is not `true`"
636 );
637 assert_eq!(
638 thrown("writeBytes", [path(), string_any("0"), hi()]),
639 "not a number"
640 );
641 assert_eq!(thrown("rm", [Nullish::Null.to_any()]), "not a string");
642 }
643
644 #[cfg(not(target_family = "wasm"))]
647 #[test]
648 fn files_through_perform() {
649 let dir = Scratch::new("perform");
650 let at = |name: &str| string_any(&dir.at(name));
651 let undefined = || Nullish::Undefined.to_any();
652
653 assert_eq!(ok(perform("mkdir", [at("a"), undefined()])), undefined());
654 assert_eq!(
655 ok(perform("mkdir", [at("b/c"), recursive(true)])),
656 undefined()
657 );
658 assert_eq!(ok(perform("writeFile", [at("f"), hi()])), undefined());
659 assert_eq!(ok(perform("readFile", [at("f")])), hi());
660 let windows: Vec<V> = Array::try_from(ok(perform("readWhole", [at("f")])))
661 .unwrap()
662 .into_iter()
663 .collect();
664 assert_eq!(windows, [hi()]);
665 assert_eq!(
666 ok(perform(
667 "writeBytes",
668 [at("f"), Number::from(1.0).to_any(), hi()]
669 )),
670 undefined()
671 );
672 let names = |entries: V| -> Vec<String> {
673 Array::try_from(entries)
674 .unwrap()
675 .into_iter()
676 .map(|entry| decode_string(member_of(entry, "name")).unwrap())
677 .collect()
678 };
679 let root = string_any(&dir.path().to_string_lossy());
680 assert_eq!(
681 names(ok(perform("readdir", [root.clone(), empty_object()]))),
682 ["a", "b", "f"]
683 );
684 assert_eq!(
685 names(ok(perform("readdir", [root, recursive(true)]))),
686 ["a", "b", "f", "c"]
687 );
688 assert_eq!(ok(perform("rm", [at("f")])), undefined());
689
690 let missing = perform("readFile", [at("f")]).unwrap();
691 assert_eq!(error_code(missing), string_any("ENOENT"));
692 }
693
694 #[cfg(not(target_family = "wasm"))]
696 #[test]
697 fn metadata_through_perform() {
698 let dir = Scratch::new("metadata");
699 let at = |name: &str| string_any(&dir.at(name));
700 let undefined = || Nullish::Undefined.to_any();
701
702 ok(perform("writeFile", [at("a"), hi()]));
703 let stat = ok(perform("stat", [at("a")]));
704 assert_eq!(member_of(stat.clone(), "size"), Number::from(2.0).to_any());
705 assert_eq!(member_of(stat.clone(), "isFile"), true.to_any());
706 assert_eq!(member_of(stat, "isDirectory"), false.to_any());
707 assert_eq!(ok(perform("access", [at("a")])), undefined());
708 assert_eq!(ok(perform("rename", [at("a"), at("b")])), undefined());
709 assert_eq!(ok(perform("rm", [at("b")])), undefined());
710 assert_eq!(ok(perform("mkdir", [at("d"), undefined()])), undefined());
711 assert_eq!(ok(perform("rmdir", [at("d")])), undefined());
712
713 let missing = perform("stat", [at("d")]).unwrap();
714 assert_eq!(error_code(missing), string_any("ENOENT"));
715 }
716
717 #[cfg(not(target_family = "wasm"))]
719 #[test]
720 fn exclusive_and_windows_through_perform() {
721 let dir = Scratch::new("exclusive");
722 let at = |name: &str| string_any(&dir.at(name));
723 let undefined = || Nullish::Undefined.to_any();
724 let number = |n: f64| Number::from(n).to_any();
725
726 assert_eq!(ok(perform("createExclusive", [at("a")])), undefined());
727 assert_eq!(
728 ok(perform("writeExclusive", [at("b"), array([hi(), hi()])])),
729 undefined()
730 );
731 assert_eq!(std::fs::read(dir.at("b")).unwrap(), b"hihi");
732 let window = ok(perform("readBytes", [at("b"), number(1.0), number(2.0)]));
733 assert_eq!(decode_bytes(window).unwrap(), b"ih");
734
735 let again = perform("createExclusive", [at("a")]).unwrap();
737 assert_eq!(error_code(again), string_any("EEXIST"));
738 assert_eq!(thrown("writeExclusive", [at("c"), hi()]), "not an array");
739 }
740
741 #[cfg(not(target_family = "wasm"))]
743 fn error_info(answer: V) -> V {
744 let [tag, error]: [V; 2] = Array::try_from(answer).unwrap().try_into().unwrap();
745 assert_eq!(tag, string_any("error"));
746 let [kind, info]: [V; 2] = Array::try_from(error).unwrap().try_into().unwrap();
747 assert_eq!(kind, string_any("ioError"));
748 info
749 }
750
751 #[cfg(not(target_family = "wasm"))]
753 fn error_code(answer: V) -> V {
754 member_of(error_info(answer), "code")
755 }
756
757 #[cfg(not(target_family = "wasm"))]
760 #[test]
761 fn handles_through_perform() {
762 let dir = Scratch::new("handles");
763 let at = |name: &str| string_any(&dir.at(name));
764 let undefined = || Nullish::Undefined.to_any();
765 let number = |n: f64| Number::from(n).to_any();
766 ok(perform("writeFile", [at("a"), hi()]));
767 let mut h = Native::buffered(b"");
768 let mut run = |command: &str, payload: Vec<V>| {
769 h.perform(string_any(command), payload.to_array().to_any())
770 };
771
772 let a = ok(run("open", vec![at("a")]));
773 assert_eq!(a, number(0.0));
774 let stat = ok(run("fstat", vec![a.clone()]));
775 assert_eq!(member_of(stat.clone(), "size"), number(2.0));
776 assert_eq!(member_of(stat, "isFile"), true.to_any());
777 let window = |at: f64, size: f64| vec![number(0.0), number(at), number(size)];
778 let bytes = |answer: Result<V, V>| decode_bytes(ok(answer)).unwrap();
779 assert_eq!(bytes(run("pread", window(0.0, 2.0))), b"hi");
780 assert_eq!(bytes(run("pread", window(1.0, 10.0))), b"i");
781 assert_eq!(bytes(run("pread", window(5.0, 1.0))), b"");
782
783 std::fs::remove_file(dir.at("a")).unwrap();
785 assert_eq!(bytes(run("pread", window(0.0, 2.0))), b"hi");
786
787 let missing = run("open", vec![at("none")]).unwrap();
789 assert_eq!(error_code(missing), string_any("ENOENT"));
790 #[cfg(unix)]
791 {
792 let directory = ok(run("open", vec![string_any(&dir.path().to_string_lossy())]));
793 assert_eq!(directory, number(1.0));
794 let stat = ok(run("fstat", vec![directory.clone()]));
795 assert_eq!(member_of(stat, "isDirectory"), true.to_any());
796 let read = run("pread", vec![directory.clone(), number(0.0), number(1.0)]).unwrap();
797 assert_eq!(error_code(read), string_any("EISDIR"));
798 assert_eq!(ok(run("close", vec![directory])), undefined());
799 }
800
801 assert_eq!(ok(run("close", vec![a.clone()])), undefined());
802 assert_eq!(ok(run("close", vec![a.clone()])), undefined());
803 let closed = run("fstat", vec![a.clone()]).unwrap();
804 assert_eq!(error_code(closed), string_any("EBADF"));
805 let closed = run("pread", window(0.0, 1.0)).unwrap();
806 assert_eq!(error_code(closed), string_any("EBADF"));
807 assert_eq!(bytes(run("pread", window(0.0, 0.0))), b"");
809
810 let refused = run("pread", vec![a.clone(), number(-1.0), number(1.0)]).unwrap();
812 assert_eq!(
813 member_of(error_info(refused), "message"),
814 string_any("Offset -1 is negative")
815 );
816
817 let thrown = |result: Result<V, V>| decode_string(result.unwrap_err()).unwrap();
818 assert_eq!(
819 thrown(run("fstat", vec![number(99.0)])),
820 "99 is not a file handle"
821 );
822 assert_eq!(
823 thrown(run("close", vec![number(0.5)])),
824 "0.5 is not a file handle"
825 );
826 assert_eq!(
827 thrown(run("pread", vec![a, number(0.0)])),
828 "missing argument 2, `size`"
829 );
830 let refused = run("pread", vec![number(99.0), number(-1.0), number(1.0)]).unwrap();
831 assert_eq!(
832 member_of(error_info(refused), "message"),
833 string_any("Offset -1 is negative")
834 );
835
836 assert_eq!(h.files.len(), 0);
838 }
839
840 fn thunk(code: nanvm_lib::vm::StaticCode<Naive>) -> V {
841 use nanvm_lib::vm::IVm;
842 Naive::static_function(code, 0, [].to_array(), None).to_any()
843 }
844
845 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
847 fn member_of(value: V, name: &str) -> V {
848 member(&nanvm_lib::vm::Object::try_from(value).unwrap(), name).unwrap()
849 }
850
851 #[test]
854 fn catch_answers_what_the_thunk_did() {
855 let returned = ok(perform("catch", [thunk(|_, _| Ok(string_any("fine")))]));
856 assert_eq!(returned.to_json(), Ok("[\"ok\",\"fine\"]".into()));
857 let threw = ok(perform("catch", [thunk(|_, _| Err(string_any("boom")))]));
858 assert_eq!(threw.to_json(), Ok("[\"error\",\"boom\"]".into()));
859 let not_callable = ok(perform("catch", [string_any("x")]));
861 let [tag, _]: [V; 2] = Array::try_from(not_callable).unwrap().try_into().unwrap();
862 assert_eq!(tag, string_any("error"));
863 }
864
865 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
870 #[test]
871 fn sandbox_answers_the_result_and_a_duration() {
872 for (code, expected) in [
873 (
874 (|_, _| Ok(string_any("fine"))) as nanvm_lib::vm::StaticCode<Naive>,
875 "[\"ok\",\"fine\"]",
876 ),
877 (|_, _| Err(string_any("boom")), "[\"error\",\"boom\"]"),
878 ] {
879 let answer = ok(perform("sandbox", [thunk(code)]));
880 let [result, duration] =
881 ["result", "duration"].map(|name| member_of(answer.clone(), name));
882 assert_eq!(result.to_json(), Ok(expected.into()));
883 let milliseconds = f64::from(Number::try_from(duration).unwrap());
884 assert!((0.0..60_000.0).contains(&milliseconds));
885 }
886 }
887
888 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
892 #[test]
893 fn now_is_the_epoch_in_milliseconds() {
894 let epoch = || {
895 std::time::SystemTime::now()
896 .duration_since(std::time::UNIX_EPOCH)
897 .unwrap()
898 .as_millis() as f64
899 };
900 let before = epoch();
901 let answer = f64::from(Number::try_from(ok(perform("now", []))).unwrap());
902 let after = epoch();
903 assert!(before <= answer && answer <= after && answer.fract() == 0.0);
904 }
905
906 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
909 #[test]
910 fn random_int_is_below_2_to_the_32() {
911 for _ in 0..16 {
912 let n = f64::from(Number::try_from(ok(perform("randomInt", []))).unwrap());
913 assert!((0.0..0x1_0000_0000_u64 as f64).contains(&n) && n.fract() == 0.0);
914 }
915 }
916
917 #[test]
918 fn the_thunk_operations_take_exactly_their_arguments() {
919 assert_eq!(thrown("catch", []), "missing argument 0, `f`");
920 }
921
922 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
923 #[test]
924 fn the_clock_operations_take_exactly_their_arguments() {
925 assert_eq!(
926 thrown("sandbox", [hi(), hi()]),
927 "2 arguments where at most 1 are taken"
928 );
929 assert_eq!(
930 thrown("now", [hi()]),
931 "1 arguments where at most 0 are taken"
932 );
933 assert_eq!(
934 thrown("randomInt", [hi()]),
935 "1 arguments where at most 0 are taken"
936 );
937 }
938
939 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
941 #[test]
942 fn the_clock_operations_are_not_implemented_without_a_clock() {
943 for command in ["sandbox", "now", "randomInt"] {
944 assert_eq!(
945 perform(command, []).unwrap().to_json(),
946 Ok(format!("[\"error\",[\"notImplemented\",\"{command}\"]]"))
947 );
948 }
949 }
950
951 #[cfg(not(target_family = "wasm"))]
954 #[test]
955 fn resolving_a_module_through_perform() {
956 let module = ok(perform(
957 "resolveFileModule",
958 [string_any("Cargo.toml"), Nullish::Null.to_any()],
959 ));
960 let text = |name: &str| decode_string(member_of(module.clone(), name)).unwrap();
961 assert!(text("path").replace('\\', "/").ends_with("/Cargo.toml"));
962 assert_eq!(text("id"), crate::resolve::path_to_file_url(&text("path")));
963 let sibling = ok(perform(
964 "resolveFileModule",
965 [string_any("./src/lib.rs"), string_any(&text("id"))],
966 ));
967 assert!(
968 decode_string(member_of(sibling, "path"))
969 .unwrap()
970 .replace('\\', "/")
971 .ends_with("/nanvm/effects-node/src/lib.rs")
972 );
973 }
974
975 #[test]
976 fn a_parent_that_is_not_a_string_throws() {
977 assert_eq!(
978 thrown("resolveFileModule", [string_any("a"), hi()]),
979 "not a string"
980 );
981 }
982}