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, 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#[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(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 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, 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 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 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 #[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 #[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 #[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 #[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 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 #[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 #[cfg(not(target_family = "wasm"))]
758 fn error_code(answer: V) -> V {
759 member_of(error_info(answer), "code")
760 }
761
762 #[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 std::fs::remove_file(dir.at("a")).unwrap();
790 assert_eq!(bytes(run("pread", window(0.0, 2.0))), b"hi");
791
792 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 assert_eq!(bytes(run("pread", window(0.0, 0.0))), b"");
814
815 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 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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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}