diff --git a/Cargo.lock b/Cargo.lock index 3b993cb..9e8de65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,6 +303,7 @@ dependencies = [ "rand", "serde", "serde_json", + "thiserror", ] [[package]] @@ -737,6 +738,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "toml_datetime" version = "0.6.8" diff --git a/Cargo.toml b/Cargo.toml index 4afdb36..21768f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ javy-plugin-api = { version = "3.0.0", features = ["json"] } rand = "0.8.5" serde_json = "1.0.120" serde = { version = "1.0.215", features = ["derive"] } +thiserror = "2.0.12" [profile.release] lto = true @@ -29,4 +30,5 @@ opt-level = 3 crypto = [] fetch = [] llm = [] -default = ["crypto", "fetch"] +wasip1 = [] +default = ["crypto", "fetch", "wasip1"] diff --git a/src/lib.rs b/src/lib.rs index 7de945f..a5eeeaf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,11 +14,14 @@ pub mod crypto; pub mod fetch; #[cfg(feature = "llm")] pub mod llm; +#[cfg(feature = "wasip1")] +pub mod wasi; #[cfg(feature = "crypto")] use crypto::bless_get_random_values; #[cfg(feature = "fetch")] use fetch::bless_fetch_request; + #[cfg(feature = "llm")] use llm::bless_llm_plugin; @@ -61,6 +64,36 @@ pub extern "C" fn initialize_runtime() { )?, )?; + #[cfg(feature = "wasip1")] + { + macro_rules! bind { + (function, $l: ident) => { + let name = concat!("__javy_", stringify!($l)); + ctx.globals().set( + name, + Function::new( + ctx.clone(), + MutFn::new(move |cx, args| { + let (cx, args) = hold_and_release!(cx, args); + wasi::$l(hold!(cx.clone(), args)) + .map_err(|e| to_js_error(cx, e)) + }), + )?, + )?; + }; + } + bind!(function, wasi_preview1_open); + bind!(function, wasi_preview1_fd_prestat_dir_name); + bind!(function, wasi_preview1_path_create_directory); + bind!(function, wasi_preview1_path_remove_directory); + bind!(function, wasi_preview1_path_unlink_file); + bind!(function, wasi_preview1_close); + bind!(function, wasi_preview1_path_symlink); + bind!(function, wasi_preview1_path_link); + bind!(function, wasi_preview1_path_rename); + bind!(function, wasi_preview1_path_filestat_get); + } + #[cfg(feature = "llm")] ctx.globals().set( "BlessLLM", @@ -87,6 +120,8 @@ pub extern "C" fn initialize_runtime() { ctx.eval::<(), _>(include_str!("crypto/crypto.js"))?; #[cfg(feature = "fetch")] ctx.eval::<(), _>(include_str!("fetch/fetch.js"))?; + #[cfg(feature = "wasip1")] + ctx.eval::<(), _>(include_str!("wasi/preview_1.js"))?; Ok::<_, anyhow::Error>(()) }) .unwrap(); diff --git a/src/wasi/close.rs b/src/wasi/close.rs new file mode 100644 index 0000000..6a03362 --- /dev/null +++ b/src/wasi/close.rs @@ -0,0 +1,21 @@ +use anyhow::{anyhow, bail, Result}; +use javy_plugin_api::javy::{quickjs::Value, Args}; + +use super::{preview_1, process_error}; + +/// This function is used to close a file descriptor. +/// `fd`: The file descriptor to close. +pub fn wasi_preview1_close<'a>(args: Args<'a>) -> Result> { + let (cx, args) = args.release(); + let args_pat: &[Value<'_>] = &args.0; + let [fd, ..] = args_pat else { + bail!( + "close expects 1 parameter: the fd, Got: {} parameters.", + args.len() + ); + }; + let fd = fd.as_int().ok_or_else(|| anyhow!("fd must be a number"))?; + let rs = unsafe { preview_1::fd_close(fd) }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx.clone(), rs)) +} diff --git a/src/wasi/descriptor.rs b/src/wasi/descriptor.rs new file mode 100644 index 0000000..dc6b549 --- /dev/null +++ b/src/wasi/descriptor.rs @@ -0,0 +1,521 @@ +use anyhow::{anyhow, bail, Ok, Result}; +use javy_plugin_api::javy::{ + quickjs::{ + prelude::{MutFn, Rest}, + Array, BigInt, Ctx, FromIteratorJs, Function, Object as JObject, String as JString, + TypedArray, Value, + }, + to_js_error, +}; +use std::{sync::Arc, vec}; + +use super::{preview_1, process_error, stat::filestate_to_jsobject, Filestat, Fstflags}; + +pub struct Descriptor(i32); + +/// This struct is used to represent an I/O vector. +#[allow(dead_code)] +pub struct Iovec { + pub buf: i32, + pub buf_len: u32, +} + +impl Descriptor { + /// Create a new file descriptor object. + /// This function creates a new file descriptor object with the given file descriptor. + /// The file descriptor is used to perform operations on the file. + pub fn new<'js>(cx: Ctx<'js>, fd: i32) -> Result> { + let descriptor = Arc::new(Descriptor(fd)); + let desc = JObject::new(cx.clone())?; + desc.set("rawfd", fd)?; + macro_rules! bind_method { + ($name:ident) => { + bind_method!(stringify!($name), $name); + }; + ($name: expr, $method: ident) => { + let descriptor_clone = descriptor.clone(); + desc.set( + $name, + Function::new( + cx.clone(), + MutFn::new(move |cx: Ctx<'js>, args: Rest>| { + descriptor_clone + .clone() + .$method(cx.clone(), args) + .map_err(|e| to_js_error(cx.clone(), e)) + }), + )?, + )?; + }; + } + // Set the read method + bind_method!(read); + // Set the write method + bind_method!(write); + // Set the close method + bind_method!(close); + // Set the fsync method + bind_method!(fsync); + // Set the fdatsync method + bind_method!(fdatasync); + // Set the seek method + bind_method!(seek); + // Set the advise method + bind_method!(advise); + // Set the stat method + bind_method!(stat); + // Set the ftruncate method + bind_method!(ftruncate); + // Set the allocate method + bind_method!(allocate); + // Set the tell method + bind_method!(tell); + // Set the touch method + bind_method!(touch); + // Set the set_flags method + bind_method!("setFlags", set_flags); + // Set the set_all method + bind_method!("readAll", read_all); + // Set the set_string method + bind_method!("readString", read_string); + // Set the fatime method + bind_method!(fatime); + // Set the fmtime method + bind_method!(fmtime); + // Set the readdir method + bind_method!("readdir", read_dir); + Ok(Value::from_object(desc)) + } + + /// todo: implement the readdir method + /// This method reads the directory entries from the file descriptor. + /// It returns an array of strings representing the names of the entries in the directory. + fn read_dir<'js>(self: Arc, cx: Ctx<'js>, _args: Rest>) -> Result> { + let mut dir_buff = vec![]; + let mut r_buff = vec![0u8; 1024 * 4]; // Buffer to read directory entries into + let mut readn: i64 = 0; + let mut rs; + loop { + rs = unsafe { + preview_1::fd_readdir( + self.0, + r_buff.as_mut_ptr() as i32, + r_buff.len() as i32, + 0, + &mut readn as *mut i64 as i32, + ) + }; + if rs != 0 { + process_error(cx.clone(), rs)?; + return Ok(Value::new_null(cx.clone())); + } + + if readn > 0 { + dir_buff.extend_from_slice(&r_buff[0..readn as usize]); + } + if readn < r_buff.len() as i64 { + break; // No more entries to read + } + } + let mut off = 0; + let mut name_jsarray = vec![]; + while off < dir_buff.len() { + off += 16; + let len: i32 = unsafe { *(dir_buff.as_ptr().wrapping_add(off) as *const i32) }; + off += 8; // Move past the length field + if off + len as usize > dir_buff.len() { + return Ok(Value::new_null(cx.clone())); + } + let name_str = + unsafe { std::str::from_utf8_unchecked(&dir_buff[off..off + len as usize]) }; + off += len as usize; // Move past the name + name_jsarray.push(Value::from_string(JString::from_str(cx.clone(), name_str)?)); + } + let name_jsarray = Array::from_iter_js(&cx, name_jsarray.iter())?; + return Ok(Value::from_array(name_jsarray)); + } + + /// The read method + /// Uint8Array as the buffer the first parameter + /// size as the second parameter, it's optional, default is the length of the buffer + fn read<'js>(self: Arc, cx: Ctx<'js>, args: Rest>) -> Result> { + if args.0.len() < 1 { + bail!( + "read expects 1 parameters: the buffer and size[option], Got: {} parameters.", + args.len() + ); + } + let buffer = &args.0[0]; + let null = Value::new_null(cx.clone()); + let mut size = &null; + if args.0.len() > 1 { + size = &args.0[1]; + } + + let mut readn: i32 = 0; + let readn_ptr: i32 = &mut readn as *mut i32 as i32; + let array = buffer + .as_object() + .ok_or_else(|| anyhow!("buffer must be a array"))?; + let array: &TypedArray<'_, u8> = array + .as_typed_array() + .ok_or_else(|| anyhow!("buffer must be a typed array"))?; + let mut array_raw = array + .as_raw() + .ok_or_else(|| anyhow!("buffer get raw ptr error."))?; + let size = size.as_int(); + let size = if let Some(size) = size { + if size > array_raw.len as i32 { + array_raw.len as u32 + } else { + size as u32 + } + } else { + array_raw.len as u32 + }; + let ioslice = vec![Iovec { + buf: unsafe { array_raw.ptr.as_mut() as *mut u8 as i32 }, + buf_len: size, + }]; + let rs = unsafe { + preview_1::fd_read( + self.0, + ioslice.as_ptr() as i32, + ioslice.len() as i32, + readn_ptr, + ) + }; + process_error(cx.clone(), rs)?; + if rs != 0 { + readn = -rs; + } + Ok(Value::new_int(cx, readn)) + } + + /// The write method + /// Uint8Array as the buffer the first parameter + /// size as the second parameter, it's optional, default is the length of the buffer + fn write<'js>(self: Arc, cx: Ctx<'js>, args: Rest>) -> Result> { + if args.0.len() < 1 { + bail!( + "write expects 1 parameters: the buffer and size[option], Got: {} parameters.", + args.len() + ); + } + let buffer = &args.0[0]; + let null = Value::new_null(cx.clone()); + let mut size = &null; + if args.0.len() > 1 { + size = &args.0[1]; + } + + let mut writen: i32 = 0; + let writen_ptr: i32 = &mut writen as *mut i32 as i32; + let array = buffer + .as_object() + .ok_or_else(|| anyhow!("buffer must be a array"))?; + let array: &TypedArray<'_, u8> = array + .as_typed_array() + .ok_or_else(|| anyhow!("buffer must be a typed array"))?; + let mut array_raw = array + .as_raw() + .ok_or_else(|| anyhow!("buffer get raw ptr error."))?; + let size = size.as_int(); + let size = if let Some(size) = size { + if size > array_raw.len as i32 { + array_raw.len as u32 + } else { + size as u32 + } + } else { + array_raw.len as u32 + }; + let ioslice = vec![Iovec { + buf: unsafe { array_raw.ptr.as_mut() as *mut u8 as i32 }, + buf_len: size, + }]; + let rs = unsafe { + preview_1::fd_write( + self.0, + ioslice.as_ptr() as i32, + ioslice.len() as i32, + writen_ptr, + ) + }; + process_error(cx.clone(), rs)?; + if rs != 0 { + writen = -rs; + } + Ok(Value::new_int(cx, writen)) + } + + fn read_all_data<'js>(cx: Ctx<'js>, fd: i32) -> Result> { + let mut data = Vec::new(); + let mut readn: i32 = 0; + let mut rs; + let mut buf = vec![0u8; 1024 * 4]; // Buffer to read data into + loop { + let mut ioslice = vec![Iovec { + buf: buf.as_mut_ptr() as *const u8 as i32, // This will be set to the actual buffer later + buf_len: 1024 * 4, // Read in chunks of 1024*4 bytes + }]; + rs = unsafe { + preview_1::fd_read( + fd, + ioslice.as_mut_ptr() as i32, + ioslice.len() as i32, + &mut readn as *mut i32 as i32, + ) + }; + if rs != 0 || readn <= 0 { + break; // Stop reading on error or no more data + } + // Extend the data with the newly read bytes + data.extend_from_slice(&buf[0..readn as usize]); + } + process_error(cx.clone(), rs)?; + Ok(data) + } + + /// The read_all method + /// This method reads all data from the file descriptor and returns it as a Uint8Array. + fn read_all<'js>(self: Arc, cx: Ctx<'js>, _args: Rest>) -> Result> { + let data = Self::read_all_data(cx.clone(), self.0)?; + let arr: TypedArray<'js, u8> = TypedArray::new(cx.clone(), data)?; + Ok(Value::from_object(arr.into_object())) + } + + /// The read_string method + /// This method reads all data from the file descriptor and returns it as a string. + fn read_string<'js>( + self: Arc, + cx: Ctx<'js>, + _args: Rest>, + ) -> Result> { + let data = Self::read_all_data(cx.clone(), self.0)?; + let string: JString<'js> = JString::from_str(cx.clone(), &String::from_utf8(data)?)?; + Ok(Value::from_string(string)) + } + + /// The advise method + /// This method is used to give advice to the file descriptor. + /// The first parameter is the offset, the second parameter is the length, + /// and the third parameter is the advice. + /// The advice can be one of the following values: + /// - `0`: Normal access. + /// - `1`: Random access. + /// - `2`: Sequential access. + /// - `3`: Will need to read the data. + /// - `4`: Will need to write the data. + /// The offset is the number of bytes to offset from the beginning of the file, + /// and the length is the number of bytes to advise. + fn advise<'js>(self: Arc, cx: Ctx<'js>, args: Rest>) -> Result> { + let args_pat: &[Value<'_>] = &args.0; + let [offset, len, advice, ..] = args_pat else { + bail!( + "advice expects 3 parameters: the offset, len and advice, Got: {} parameters.", + args.len() + ); + }; + let offset: u64 = jsvalue2int64!(offset); + let len: u64 = jsvalue2int64!(len); + let advice: i32 = advice + .as_int() + .ok_or_else(|| anyhow!("advice must be a int"))?; + let rs = unsafe { preview_1::fd_advise(self.0, offset, len, advice) }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx, rs)) + } + + /// The seek method + /// This method is used to change the current position of the file descriptor. + /// The first parameter is the offset, the second parameter is the whence. + /// The whence can be one of the following values: + /// - `0`: Seek from the beginning of the file. + /// - `1`: Seek from the current position of the file. + /// - `2`: Seek from the end of the file. + /// The offset is the number of bytes to seek. + fn seek<'js>(self: Arc, cx: Ctx<'js>, args: Rest>) -> Result> { + let args_pat: &[Value<'_>] = &args.0; + let [offset, whence, ..] = args_pat else { + bail!( + "advice expects 2 parameters: the offset and whence, Got: {} parameters.", + args.len() + ); + }; + let offset: u64 = jsvalue2int64!(offset); + + let whence: i32 = whence + .as_int() + .ok_or_else(|| anyhow!("advice must be a int"))?; + let mut fsize: i64 = 0; + let fsize_ptr: i32 = &mut fsize as *mut i64 as i32; + let rs = unsafe { preview_1::fd_seek(self.0, offset, whence, fsize_ptr) }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx, rs)) + } + + /// The close method + /// Uint8Array as the buffer the first parameter + /// size as the second parameter, it's optional, default is the length of the buffer + fn close<'js>(self: Arc, cx: Ctx<'js>, _: Rest>) -> Result> { + let rs = unsafe { preview_1::fd_close(self.0) }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx, rs)) + } + + /// The fsync method + /// Wait for the data and metadata to be written + fn fsync<'js>(self: Arc, cx: Ctx<'js>, _: Rest>) -> Result> { + let rs = unsafe { preview_1::fd_sync(self.0) }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx, rs)) + } + + /// The fdatasync method + /// Wait for the data to be written + fn fdatasync<'js>(self: Arc, cx: Ctx<'js>, _: Rest>) -> Result> { + let rs = unsafe { preview_1::fd_datasync(self.0) }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx, rs)) + } + + /// The stat method + /// This method is used to get the file status of the file descriptor. + /// It returns a JavaScript object with the following properties: + /// - `filetype`: The type of the file. + /// - `filetype_desc`: The description of the file type. + /// - `filesize`: The size of the file in bytes. + /// - `atime`: The last access time of the file. + /// - `mtime`: The last modification time of the file. + /// - `ctime`: The last change time of the file. + fn stat<'js>(self: Arc, cx: Ctx<'js>, _: Rest>) -> Result> { + let mut fd_stat: Filestat = Default::default(); + let fd_stat_ptr = &mut fd_stat as *mut _ as i32; + let rs = unsafe { preview_1::fd_filestat_get(self.0, fd_stat_ptr) }; + if rs == 0 { + let stat = filestate_to_jsobject(cx.clone(), &fd_stat)?; + Ok(Value::from_object(stat)) + } else { + process_error(cx.clone(), rs)?; + Ok(Value::new_null(cx.clone())) + } + } + + /// The allocate method + /// This method is used to allocate space in the file descriptor. + /// The first parameter is the offset, the second parameter is the length. + /// The offset is the number of bytes to offset from the beginning of the file, + /// and the length is the number of bytes to allocate. + fn allocate<'js>(self: Arc, cx: Ctx<'js>, args: Rest>) -> Result> { + let args_pat: &[Value<'_>] = &args.0; + let [offset, len, ..] = args_pat else { + bail!( + "allocate expects 2 parameters: the offset and length, Got: {} parameters.", + args.len() + ); + }; + let offset: u64 = jsvalue2int64!(offset); + let len: u64 = jsvalue2int64!(len); + let rs = unsafe { preview_1::fd_allocate(self.0, offset, len) }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx, rs)) + } + + /// The fatime method + /// This method is used to set the access time of the file descriptor. + /// The first parameter is the timestamp to set the access time to. + /// The timestamp is a BigInt representing the number of milliseconds since the epoch. + fn fatime<'js>(self: Arc, cx: Ctx<'js>, args: Rest>) -> Result> { + let args_pat: &[Value<'_>] = &args.0; + let [ts, ..] = args_pat else { + bail!( + "fatime expects 1 parameters: the ts, Got: {} parameters.", + args.len() + ); + }; + let ts: i64 = jsvalue2int64!(ts); + let rs = unsafe { preview_1::fd_filestat_set_times(self.0, ts, 0, Fstflags::Atm as u16) }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx, rs)) + } + + /// The fmtime method + /// This method is used to set the modification time of the file descriptor. + /// The first parameter is the timestamp to set the modification time to. + /// The timestamp is a BigInt representing the number of milliseconds since the epoch. + fn fmtime<'js>(self: Arc, cx: Ctx<'js>, args: Rest>) -> Result> { + let args_pat: &[Value<'_>] = &args.0; + let [ts, ..] = args_pat else { + bail!( + "fmtime expects 1 parameters: the ts, Got: {} parameters.", + args.len() + ); + }; + let ts: i64 = jsvalue2int64!(ts); + let rs = unsafe { preview_1::fd_filestat_set_times(self.0, 0, ts, Fstflags::Mtim as u16) }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx, rs)) + } + + /// The ftruncate method + /// This method is used to truncate the file descriptor to the given length. + /// The first parameter is the length to truncate the file to. + fn ftruncate<'js>(self: Arc, cx: Ctx<'js>, args: Rest>) -> Result> { + let args_pat: &[Value<'_>] = &args.0; + let [len, ..] = args_pat else { + bail!( + "ftruncate expects 1 parameters: the offset and whence, Got: {} parameters.", + args.len() + ); + }; + let len = jsvalue2int64!(len); + let rs = unsafe { preview_1::fd_filestat_set_size(self.0, len) }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx, rs)) + } + + /// The tell method + /// This method is used to get the current position of the file descriptor. + /// It returns a BigInt representing the current position in the file. + fn tell<'js>(self: Arc, cx: Ctx<'js>, _: Rest>) -> Result> { + let mut pos: u64 = 0; + let pos_ptr: i32 = &mut pos as *mut u64 as i32; + let rs = unsafe { preview_1::fd_tell(self.0, pos_ptr) }; + process_error(cx.clone(), rs)?; + Ok(Value::from_big_int(BigInt::from_u64(cx, pos)?)) + } + + /// The touch method + /// This method is used to update the access and modification times of the file descriptor. + fn touch<'js>(self: Arc, cx: Ctx<'js>, _args: Rest>) -> Result> { + let rs = unsafe { + preview_1::fd_filestat_set_times( + self.0, + 0, + 0, + Fstflags::AtmNow as u16 | Fstflags::MtimNow as u16, + ) + }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx, rs)) + } + + /// The fd_fdstat_set_flags method + /// This method is used to set the flags of the file descriptor. + fn set_flags<'js>(self: Arc, cx: Ctx<'js>, args: Rest>) -> Result> { + let args_pat: &[Value<'_>] = &args.0; + let [flags, ..] = args_pat else { + bail!( + "set_flags expects 1 parameters: the fd_flags, Got: {} parameters.", + args.len() + ); + }; + let fd_flags = flags + .as_int() + .ok_or_else(|| anyhow!("fd_flags must be a int"))?; + let rs = unsafe { preview_1::fd_fdstat_set_flags(self.0, fd_flags as u16) }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx, rs)) + } +} diff --git a/src/wasi/error.rs b/src/wasi/error.rs new file mode 100644 index 0000000..062f34d --- /dev/null +++ b/src/wasi/error.rs @@ -0,0 +1,241 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum WasiError { + #[error("Argument list too long.")] + TooBig = 1, + #[error("Permission denied.")] + Access, + #[error("Address not available.")] + AddrNotUse, + #[error("Address not available.")] + AddrNotAvail, + #[error("Address family not supported..")] + AfNoSupport, + #[error("Resource unavailable, or operation would block.")] + Again, + #[error("Connection already in progress.")] + Already, + #[error("Bad file descriptor.")] + Badf, + #[error("Bad message.")] + Badmsg, + #[error("Device or resource busy.")] + Busy, + #[error("Operation canceled.")] + Canceled, + #[error("No child processes.")] + Child, + #[error("Connection aborted.")] + Connaborted, + #[error("Connection refused.")] + ConnRefused, + #[error("Connection reset.")] + ConnReset, + #[error("Resource deadlock would occur.")] + Deadlk, + #[error("Destination address required.")] + Destaddrreq, + #[error("Mathematics argument out of domain of function.")] + Dom, + #[error("Reserved.")] + Dquot, + #[error("File exists.")] + Exist, + #[error("Bad address.")] + Fault, + #[error("File too large.")] + Fbig, + #[error("Host is unreachable.")] + Hostunreach, + #[error("Identifier removed.")] + Idrm, + #[error("Illegal byte sequence.")] + Ilseq, + #[error("Operation in progress.")] + Inprogress, + #[error("Interrupted function.")] + Intr, + #[error("Invalid argument.")] + Inval, + #[error("I/O error.")] + Io, + #[error("Socket is connected.")] + Isconn, + #[error("Is a directory.")] + Isdir, + #[error("Too many levels of symbolic links.")] + Loop, + #[error("File descriptor value too large.")] + Mfile, + #[error("Too many links.")] + Mlink, + #[error("Message too large.")] + Msgsize, + #[error("Reserved.")] + Multihop, + #[error("Filename too long.")] + Nametoolong, + #[error("Network is down.")] + Netdown, + #[error("Connection aborted by network.")] + Netreset, + #[error("Network unreachable.")] + Netunreach, + #[error("Too many files open in system.")] + Nfile, + #[error("No buffer space available.")] + Nobufs, + #[error("No such device.")] + Nodev, + #[error("No such file or directory.")] + Noent, + #[error("Executable file format error.")] + Noexec, + #[error("No locks available.")] + Nolck, + #[error("Reserved.")] + Nolink, + #[error("Not enough space.")] + Nomem, + #[error("No message of the desired type.")] + Nomsg, + #[error("Protocol not available.")] + Noprotoopt, + #[error("No space left on device.")] + Nospc, + #[error("Function not supported.")] + Nosys, + #[error("The socket is not connected.")] + Notconn, + #[error("Not a directory or a symbolic link to a directory.")] + Notdir, + #[error("Directory not empty.")] + Notempty, + #[error("State not recoverable.")] + Notrecoverable, + #[error("Not a socket.")] + Notsock, + #[error("Not supported, or operation not supported on socket.")] + Notsup, + #[error("Inappropriate I/O control operation.")] + Notty, + #[error("No such device or address.")] + Nxio, + #[error("Value too large to be stored in data type.")] + Overflow, + #[error("Previous owner died.")] + Ownerdead, + #[error("Operation not permitted.")] + Perm, + #[error("Broken pipe.")] + Pipe, + #[error("Protocol error.")] + Proto, + #[error("Protocol not supported.")] + Protonosupport, + #[error("Protocol wrong type for socket.")] + Prototype, + #[error("Result too large.")] + Range, + #[error("Read-only file system.")] + Rofs, + #[error("Invalid seek.")] + Spipe, + #[error("No such process.")] + Srch, + #[error("Reserved.")] + Stale, + #[error("Connection timed out.")] + Timedout, + #[error("Text file busy.")] + Txtbsy, + #[error("Cross-device link.")] + Xdev, + #[error("Extension: Capabilities insufficient.")] + Notcapable, +} + +impl From for WasiError { + fn from(code: i32) -> Self { + match code { + 1 => WasiError::TooBig, + 2 => WasiError::Access, + 3 => WasiError::AddrNotUse, + 4 => WasiError::AddrNotAvail, + 5 => WasiError::AfNoSupport, + 6 => WasiError::Again, + 7 => WasiError::Already, + 8 => WasiError::Badf, + 9 => WasiError::Badmsg, + 10 => WasiError::Busy, + 11 => WasiError::Canceled, + 12 => WasiError::Child, + 13 => WasiError::Connaborted, + 14 => WasiError::ConnRefused, + 15 => WasiError::ConnReset, + 16 => WasiError::Deadlk, + 17 => WasiError::Destaddrreq, + 18 => WasiError::Dom, + 19 => WasiError::Dquot, + 20 => WasiError::Exist, + 21 => WasiError::Fault, + 22 => WasiError::Fbig, + 23 => WasiError::Hostunreach, + 24 => WasiError::Idrm, + 25 => WasiError::Ilseq, + 26 => WasiError::Inprogress, + 27 => WasiError::Intr, + 28 => WasiError::Inval, + 29 => WasiError::Io, + 30 => WasiError::Isconn, + 31 => WasiError::Isdir, + 32 => WasiError::Loop, + 33 => WasiError::Mfile, + 34 => WasiError::Mlink, + 35 => WasiError::Msgsize, + 36 => WasiError::Multihop, + 37 => WasiError::Nametoolong, + 38 => WasiError::Netdown, + 39 => WasiError::Netreset, + 40 => WasiError::Netunreach, + 41 => WasiError::Nfile, + 42 => WasiError::Nobufs, + 43 => WasiError::Nodev, + 44 => WasiError::Noent, + 45 => WasiError::Noexec, + 46 => WasiError::Nolck, + 47 => WasiError::Nolink, + 48 => WasiError::Nomem, + 49 => WasiError::Nomsg, + 50 => WasiError::Noprotoopt, + 51 => WasiError::Nospc, + 52 => WasiError::Nosys, + 53 => WasiError::Notconn, + 54 => WasiError::Notdir, + 55 => WasiError::Notempty, + 56 => WasiError::Notrecoverable, + 57 => WasiError::Notsock, + 58 => WasiError::Notsup, + 59 => WasiError::Notty, + 60 => WasiError::Nxio, + 61 => WasiError::Overflow, + 62 => WasiError::Ownerdead, + 63 => WasiError::Perm, + 64 => WasiError::Pipe, + 65 => WasiError::Proto, + 66 => WasiError::Protonosupport, + 67 => WasiError::Prototype, + 68 => WasiError::Range, + 69 => WasiError::Rofs, + 70 => WasiError::Spipe, + 71 => WasiError::Srch, + 72 => WasiError::Stale, + 73 => WasiError::Timedout, + 74 => WasiError::Txtbsy, + 75 => WasiError::Xdev, + 76 => WasiError::Notcapable, + _ => unimplemented!("WasiError code: {}", code), + } + } +} diff --git a/src/wasi/link.rs b/src/wasi/link.rs new file mode 100644 index 0000000..b0cd378 --- /dev/null +++ b/src/wasi/link.rs @@ -0,0 +1,58 @@ +use anyhow::{anyhow, bail, Result}; +use javy_plugin_api::javy::{quickjs::Value, Args}; + +use super::{preview_1, process_error}; + +/// This function is used to link a file at the given path to a new path. +/// It is used to create a hard link from one file to another. +/// - `old_dirfd`: The directory file descriptor of the old file. +/// - `fd_lookup_flags`: Flags for looking up the file descriptor. +/// - `old_path`: The path of the old file. +/// - `new_dirfd`: The directory file descriptor of the new file. +/// - `new_path`: The path of the new file. +pub fn wasi_preview1_path_link(args: Args<'_>) -> Result> { + let (cx, args) = args.release(); + let args_pat: &[Value<'_>] = &args.0; + let [old_dirfd, fd_lookup_flags, old_path, new_dirfd, new_path, ..] = args_pat else { + bail!( + "path_link expects 5 parameters: old_dirfd, fd_lookup_flags, old_path, new_dirfd, new_path. Got: {} parameters.", + args.len() + ); + }; + let dirfd = old_dirfd + .as_int() + .ok_or_else(|| anyhow!("old_dirfd must be a number"))?; + let fd_lookup_flags = fd_lookup_flags + .as_int() + .ok_or_else(|| anyhow!("fd_lookup_flags must be a number"))?; + let old_path = old_path + .as_string() + .ok_or_else(|| anyhow!("old_path must be a string"))? + .to_string() + .map_err(|_| anyhow!("invalid UTF-8 in path"))?; + let new_dirfd = new_dirfd + .as_int() + .ok_or_else(|| anyhow!("new_dirfd must be a number"))?; + let new_path = new_path + .as_string() + .ok_or_else(|| anyhow!("new_path must be a string"))? + .to_string() + .map_err(|_| anyhow!("invalid UTF-8 in path"))?; + let new_path_ptr = new_path.as_ptr() as i32; + let new_path_len = new_path.len() as i32; + let old_path_ptr = old_path.as_ptr() as i32; + let old_path_len = old_path.len() as i32; + let rs = unsafe { + preview_1::path_link( + dirfd, + fd_lookup_flags, + old_path_ptr, + old_path_len, + new_dirfd, + new_path_ptr, + new_path_len, + ) + }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx.clone(), rs)) +} diff --git a/src/wasi/macros.rs b/src/wasi/macros.rs new file mode 100644 index 0000000..8944368 --- /dev/null +++ b/src/wasi/macros.rs @@ -0,0 +1,13 @@ +macro_rules! jsvalue2int64 { + ($i: ident) => { + if $i.is_int() { + $i.as_int() + .ok_or_else(|| anyhow!("{} must be a int", stringify!($i)))? as _ + } else { + $i.as_big_int() + .map(|o| o.clone()) + .ok_or_else(|| anyhow!("{} must be a int", stringify!($i)))? + .to_i64()? as _ + } + }; +} diff --git a/src/wasi/mkdir.rs b/src/wasi/mkdir.rs new file mode 100644 index 0000000..ebdc82f --- /dev/null +++ b/src/wasi/mkdir.rs @@ -0,0 +1,31 @@ +use anyhow::{anyhow, bail, Result}; +use javy_plugin_api::javy::{quickjs::Value, Args}; + +use super::{preview_1, process_error}; + +/// This function is used to create a directory at the given path. +/// It is used to create a directory at the given path. +/// The directory must not exist. +pub fn wasi_preview1_path_create_directory(args: Args<'_>) -> Result> { + let (cx, args) = args.release(); + let args_pat: &[Value<'_>] = &args.0; + let [dirfd, path, ..] = args_pat else { + bail!( + "path_create_directory expects 2 parameters: the dirfd and path, Got: {} parameters.", + args.len() + ); + }; + let dirfd = dirfd + .as_int() + .ok_or_else(|| anyhow!("dirfd must be a number"))?; + let path = path + .as_string() + .ok_or_else(|| anyhow!("path must be a string"))? + .to_string() + .map_err(|_| anyhow!("invalid UTF-8 in path"))?; + let path_ptr = path.as_ptr() as i32; + let path_len = path.len() as i32; + let rs = unsafe { preview_1::path_create_directory(dirfd, path_ptr, path_len) }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx.clone(), rs)) +} diff --git a/src/wasi/mod.rs b/src/wasi/mod.rs new file mode 100644 index 0000000..74bb659 --- /dev/null +++ b/src/wasi/mod.rs @@ -0,0 +1,123 @@ +use anyhow::{anyhow, bail, Result}; +use javy_plugin_api::javy::{ + quickjs::{Ctx, Object as JObject, Value}, + Args, +}; + +#[macro_use] +mod macros; +mod close; +mod descriptor; +mod error; +mod link; +mod mkdir; +mod open; +mod preview_1; +mod rename; +mod rmdir; +mod stat; +mod symlink; +mod unlink; +pub(crate) use close::wasi_preview1_close; +pub use error::WasiError; +pub(crate) use link::wasi_preview1_path_link; +pub(crate) use mkdir::wasi_preview1_path_create_directory; +pub(crate) use open::wasi_preview1_open; +pub(crate) use rename::wasi_preview1_path_rename; +pub(crate) use rmdir::wasi_preview1_path_remove_directory; +pub(crate) use stat::wasi_preview1_path_filestat_get; +pub(crate) use symlink::wasi_preview1_path_symlink; +pub(crate) use unlink::wasi_preview1_path_unlink_file; + +#[inline] +pub fn process_error(ctx: Ctx<'_>, rs: i32) -> Result<()> { + let obj = JObject::new(ctx.clone())?; + let error_messgae = if rs != 0 { + let error: WasiError = rs.into(); + error.to_string() + } else { + "Success".to_string() + }; + obj.set("errno", rs)?; + obj.set("error", error_messgae)?; + ctx.globals().set("lastErr", obj)?; + Ok(()) +} + +/// This function is used to get the directory name of a file descriptor. +/// It is used to get the directory name of a file descriptor. +/// The file descriptor must be a directory. +pub fn wasi_preview1_fd_prestat_dir_name(args: Args<'_>) -> Result> { + let (cx, args) = args.release(); + let args_pat: &[Value<'_>] = &args.0; + let [fd, ..] = args_pat else { + bail!( + "fd_prestat_dir_name expects 1 parameters: the fd, path_ptr and path_len, Got: {} parameters.", + args.len() + ); + }; + let mut path_len_buf = [0u8; 8]; + let fd = fd.as_int().ok_or_else(|| anyhow!("fd must be a number"))?; + let path_len_ptr: i32 = path_len_buf.as_mut_ptr() as i32; + let rs = unsafe { preview_1::fd_prestat_get(fd, path_len_ptr) }; + let path_len_buf: [u8; 4] = path_len_buf[4..].try_into()?; + let path_len = i32::from_le_bytes(path_len_buf); + let obj = JObject::new(cx.clone())?; + if rs != 0 { + process_error(cx.clone(), rs)?; + return Ok(Value::from_object(obj)); + } + let mut path_buf = vec![0u8; path_len as usize]; + let rs = unsafe { + preview_1::fd_prestat_dir_name( + fd, + path_buf.as_mut_ptr() as *const i32 as i32, + path_len as _, + ) + }; + if rs == 0 { + let path = String::from_utf8(path_buf)?; + obj.set("dir_name", path)?; + } + obj.set("code", rs)?; + process_error(cx.clone(), rs)?; + Ok(Value::from_object(obj)) +} + +#[derive(Default, Debug)] +#[repr(C)] +pub struct Filestat { + pub device_id: u64, + pub inode: u64, + pub filetype: u8, + pub nlink: u64, + pub size: u64, // this is a read field, the rest are file fields + pub atim: u64, + pub mtim: u64, + pub ctim: u64, +} + +pub struct FileType(u8); + +impl Into<&str> for FileType { + fn into(self) -> &'static str { + match self.0 { + 0 => "unknown", + 1 => "block device", + 2 => "character device", + 3 => "directory", + 4 => "regular file", + 5 => "socket dgram", + 6 => "socket stream", + 7 => "symbolic link", + _ => unimplemented!("FileType not implemented"), + } + } +} + +pub enum Fstflags { + Atm = 1 << 0, + AtmNow = 1 << 1, + Mtim = 1 << 2, + MtimNow = 1 << 3, +} diff --git a/src/wasi/open.rs b/src/wasi/open.rs new file mode 100644 index 0000000..d85c2c4 --- /dev/null +++ b/src/wasi/open.rs @@ -0,0 +1,62 @@ +use anyhow::{anyhow, bail, Result}; +use javy_plugin_api::javy::{quickjs::Value, Args}; + +use super::descriptor::Descriptor; +use super::{preview_1, process_error}; + +/// This function is used to open a file at the given path. +/// It is used to open a file at the given path. +pub fn wasi_preview1_open<'a>(args: Args<'a>) -> Result> { + let (cx, args) = args.release(); + let args_pat: &[Value<'_>] = &args.0; + let mut opened_fd: i32 = 0; + let [dirfd, fd_lookup_flags, path, fd_oflags, fd_rights, fd_rights_inherited, fd_flags, ..] = + args_pat + else { + bail!( + "open expects 7 parameters: the path and the dirfd, fd_lookup_flags, path, fd_oflags, fd_rights ... Got: {} parameters.", + args.len() + ); + }; + let dirfd = dirfd + .as_int() + .ok_or_else(|| anyhow!("dirfd must be a number"))?; + let fd_lookup_flags = fd_lookup_flags + .as_int() + .ok_or_else(|| anyhow!("fd_lookup_flags must be a number"))?; + let oflags = fd_oflags + .as_int() + .ok_or_else(|| anyhow!("oflags must be a number"))?; + let fs_rights_base = jsvalue2int64!(fd_rights); + let fd_rights_inherited = jsvalue2int64!(fd_rights_inherited); + let fdflags = fd_flags + .as_int() + .ok_or_else(|| anyhow!("fdflags must be a number"))?; + let opened_fd_ptr = (&mut opened_fd as *mut i32) as i32; + let path = path + .as_string() + .ok_or_else(|| anyhow!("path must be a string"))? + .to_string() + .map_err(|_| anyhow!("invalid UTF-8 in path"))?; + let path_ptr = path.as_ptr() as i32; + let path_len = path.len() as i32; + let rs = unsafe { + preview_1::path_open( + dirfd, + fd_lookup_flags, + path_ptr, + path_len, + oflags, + fs_rights_base, + fd_rights_inherited, + fdflags, + opened_fd_ptr, + ) + }; + if rs == 0 { + Ok(Descriptor::new(cx.clone(), opened_fd)?) + } else { + process_error(cx.clone(), rs)?; + Ok(Value::new_null(cx.clone())) + } +} diff --git a/src/wasi/preview_1.js b/src/wasi/preview_1.js new file mode 100644 index 0000000..4d0e140 --- /dev/null +++ b/src/wasi/preview_1.js @@ -0,0 +1,352 @@ + +// Wrap everything in an anonymous function to avoid leaking local variables into the global scope. +(function () { + + let lastErr = { + errno: 0, + error: "", + } + globalThis.lastErr = lastErr; + // Get a reference to the function before we delete it from `globalThis`. + const __javy_wasi_preview1_open = globalThis.__javy_wasi_preview1_open; + const __javy_wasi_preview1_fd_prestat_dir_name = globalThis.__javy_wasi_preview1_fd_prestat_dir_name; + const __javy_wasi_preview1_path_create_directory = globalThis.__javy_wasi_preview1_path_create_directory; + const __javy_wasi_preview1_path_remove_directory = globalThis.__javy_wasi_preview1_path_remove_directory; + const __javy_wasi_preview1_path_unlink_file = globalThis.__javy_wasi_preview1_path_unlink_file; + const __javy_wasi_preview1_path_symlink = globalThis.__javy_wasi_preview1_path_symlink; + const __javy_wasi_preview1_path_link = globalThis.__javy_wasi_preview1_path_link; + const __javy_wasi_preview1_path_rename = globalThis.__javy_wasi_preview1_path_rename; + const __javy_wasi_preview1_path_filestat_get = globalThis.__javy_wasi_preview1_path_filestat_get; + + const InvalParameter = 0x1C + const Rights = { + FD_DATASYNC: 0x1, + FD_READ: 0x2, + FD_SEEK: 0x4, + FD_FDSTAT_SET_FLAGS: 0x8, + FD_SYNC: 0x10, + FD_TELL: 0x20, + FD_WRITE: 0x40, + FD_ADVISE: 0x80, + FD_ALLOCATE: 0x100, + PATH_CREATE_DIRECTORY: 0x200, + PATH_CREATE_FILE: 0x400 , + PATH_LINK_SOURCE: 0x800, + PATH_LINK_TARGET: 0x1000, + PATH_OPEN: 0x2000, + FD_READDIR: 0x4000, + PATH_READLINK: 0x8000, + PATH_RENAME_SOURCE: 0x10000, + PATH_RENAME_TARGET: 0x20000, + PATH_FILESTAT_GET: 0x40000, + PATH_FILESTAT_SET_SIZE: 0x80000, + PATH_FILESTAT_SET_TIMES: 0x100000, + FD_FILESTAT_GET: 0x200000, + FD_FILESTAT_SET_SIZE: 0x400000, + FD_FILESTAT_SET_TIMES: 0x800000, + PATH_SYMLINK: 0x1000000, + PATH_REMOVE_DIRECTORY: 0x2000000, + PATH_UNLINK_FILE: 0x4000000, + POLL_FD_READWRITE: 0x8000000, + SOCK_SHUTDOWN: 0x10000000, + SOCK_ACCEPT: 0x20000000, + } + + const Advise = { + Normal: 0x0, + Sequential: 0x1, + Random: 0x2, + Willneed: 0x3, + Dontneed: 0x4, + } + + const Whence = { + SeekSet: 0x0, + SeekCur: 0x1, + SeekEnd: 0x2, + } + + const Lookupflags = { + SYMLINK_FOLLOW: 0x1, + } + + const Oflags = { + CREAT: 0x1, + DIRECTORY: 0x2, + EXCL: 0x4, + TRUNC: 0x8, + } + + // This function is used to open a file with the specified path and flags. + // It first checks if the path is valid and then determines the directory file descriptor (dirfd) for the path. + // It sets the appropriate flags and rights based on the specified mode (read, write, etc.). + function open(path, flags = "r") { + if (path == null) { + lastErr.errno = InvalParameter; + lastErr.error = "Path is required"; + return null; + } + const dirpathObj = dirfdForPath(path); + if (dirpathObj == null) { + return false; + } + const {dirpath, dirfd} = dirpathObj; + let fd_lookup_flags = Lookupflags.SYMLINK_FOLLOW;; + let fd_oflags = 0; + let fd_rights = 0; + if (flags == "r") { + fd_rights = + Rights.FD_READ | Rights.FD_SEEK | Rights.FD_TELL | Rights.FD_FILESTAT_GET | + Rights.FD_READDIR; + } else if (flags == "r+") { + fd_rights = + Rights.FD_WRITE | + Rights.FD_READ | Rights.FD_SEEK | Rights.FD_TELL | Rights.FD_FILESTAT_GET | + Rights.PATH_CREATE_FILE; + } else if (flags == "w") { + fd_oflags = Oflags.CREAT | Oflags.TRUNC; + fd_rights = + Rights.FD_WRITE | Rights.FD_SEEK | Rights.FD_TELL | Rights.FD_FILESTAT_GET | + Rights.PATH_CREATE_FILE; + } else if (flags == "wx") { + fd_oflags = Oflags.CREAT | Oflags.TRUNC | Oflags.EXCL; + fd_rights = + Rights.FD_WRITE | Rights.FD_SEEK | Rights.FD_TELL | Rights.FD_FILESTAT_GET | + Rights.PATH_CREATE_FILE; + } else if (flags == "w+") { + fd_oflags = Oflags.CREAT | Oflags.TRUNC; + fd_rights = + Rights.FD_WRITE | + Rights.FD_READ | Rights.FD_SEEK | Rights.FD_TELL | Rights.FD_FILESTAT_GET | + Rights.PATH_CREATE_FILE; + } else if (flags == "xw+") { + fd_oflags = Oflags.CREAT | Oflags.TRUNC | Oflags.EXCL; + fd_rights = + Rights.FD_WRITE | + Rights.FD_READ | Rights.FD_SEEK | Rights.FD_TELL | Rights.FD_FILESTAT_GET | + Rights.PATH_CREATE_FILE; + } else { + return null; + } + path = path.substring(dirpath.length, path.length); + let fd_rights_inherited = fd_rights; + let fd_flags = 0; + const file = __javy_wasi_preview1_open( + dirfd, + fd_lookup_flags, + path, + fd_oflags, + fd_rights, + fd_rights_inherited, + fd_flags, + ) + return file; + } + + // This function is used to create a new directory with the specified path. + // It first checks if the path is valid and then determines the directory file descriptor (dirfd) for the path. + function mkdir(path) { + if (path == null) { + lastErr.errno = InvalParameter; + lastErr.error = "Path is required"; + return false; + } + const dirpathObj = dirfdForPath(path); + if (dirpathObj == null) { + return false; + } + const {dirpath, dirfd} = dirpathObj; + path = path.substring(dirpath.length, path.length); + let rs = __javy_wasi_preview1_path_create_directory(dirfd, path) + if (rs != 0) { + return false; + } + return true; + } + + // This function is used to remove a directory with the specified path. + // It first checks if the path is valid and then determines the directory file descriptor (dirfd) for the path. + function rmdir(path) { + if (path == null) { + lastErr.errno = InvalParameter; + lastErr.error = "Path is required"; + return false; + } + const dirpathObj = dirfdForPath(path); + if (dirpathObj == null) { + return false; + } + const {dirpath, dirfd} = dirpathObj; + path = path.substring(dirpath.length, path.length); + let rs = __javy_wasi_preview1_path_remove_directory(dirfd, path) + if (rs != 0) { + return false; + } + return true; + } + + // This function is used to unlink (delete) a file with the specified path. + // It first checks if the path is valid and then determines the directory file descriptor (dirfd) for the path. + function unlink(path) { + if (path == null) { + lastErr.errno = InvalParameter; + lastErr.error = "Path is required"; + return false; + } + const dirpathObj = dirfdForPath(path); + if (dirpathObj == null) { + return false; + } + const {dirpath, dirfd} = dirpathObj; + path = path.substring(dirpath.length, path.length); + let rs = __javy_wasi_preview1_path_unlink_file(dirfd, path) + if (rs != 0) { + return false; + } + return true; + } + + // This function is used to create a symbolic link from oldpath to newpath. + // It first checks if the newpath is valid and then determines the directory file descriptor (dirfd) for the newpath. + function symlink(oldpath, newpath) { + if (oldpath == null || newpath == null) { + lastErr.errno = InvalParameter; + lastErr.error = "Path is required"; + return false; + } + const dirpathObj = dirfdForPath(newpath); + if (dirpathObj == null) { + return false; + } + const {dirpath, dirfd} = dirpathObj; + newpath = newpath.substring(dirpath.length, newpath.length); + let rs = __javy_wasi_preview1_path_symlink(oldpath, dirfd, newpath) + if (rs != 0) { + return false; + } + return true; + } + + // This function is used to create a hard link from oldpath to newpath. + function link(oldpath, newpath) { + if (oldpath == null || newpath == null) { + lastErr.errno = InvalParameter; + lastErr.error = "Path is required"; + return false; + } + const old_dirpath_rs = dirfdForPath(oldpath); + if (old_dirpath_rs == null) { + return false; + } + + const new_dirpath_rs = dirfdForPath(newpath); + if (new_dirpath_rs == null) { + return false; + } + const old_dirpath = old_dirpath_rs.dirpath; + const old_dirfd = old_dirpath_rs.dirfd; + + const new_dirpath = new_dirpath_rs.dirpath; + const new_dirfd = new_dirpath_rs.dirfd; + newpath = newpath.substring(new_dirpath.length, newpath.length); + oldpath = oldpath.substring(old_dirpath.length, oldpath.length); + let rs = __javy_wasi_preview1_path_link(old_dirfd, 0, oldpath, new_dirfd, newpath); + if (rs != 0) { + return false; + } + return true; + } + + function rename(oldpath, newpath) { + if (oldpath == null || newpath == null) { + lastErr.errno = InvalParameter; + lastErr.error = "Path is required"; + return false; + } + const old_dirpath_rs = dirfdForPath(oldpath); + if (old_dirpath_rs == null) { + return false; + } + + const new_dirpath_rs = dirfdForPath(newpath); + if (new_dirpath_rs == null) { + return false; + } + const old_dirpath = old_dirpath_rs.dirpath; + const old_dirfd = old_dirpath_rs.dirfd; + + const new_dirpath = new_dirpath_rs.dirpath; + const new_dirfd = new_dirpath_rs.dirfd; + newpath = newpath.substring(new_dirpath.length, newpath.length); + oldpath = oldpath.substring(old_dirpath.length, oldpath.length); + let rs = __javy_wasi_preview1_path_rename(old_dirfd, oldpath, new_dirfd, newpath); + if (rs != 0) { + return false; + } + return true; + } + + function stat(path) { + if (path == null) { + lastErr.errno = InvalParameter; + lastErr.error = "Path is required"; + return false; + } + const dirpath_rs = dirfdForPath(path); + if (dirpath_rs == null) { + return false; + } + + + const dirpath = dirpath_rs.dirpath; + const dirfd = dirpath_rs.dirfd; + + path = path.substring(dirpath.length, path.length); + let stat = __javy_wasi_preview1_path_filestat_get(dirfd, Lookupflags.SYMLINK_FOLLOW, path); + return stat; + } + + // This function is used to get the directory name for a given file descriptor. + // It recursively calls itself with an incremented file descriptor until it finds a valid directory name. + function dirfdForPath(path, fd = 3) { + let rs = __javy_wasi_preview1_fd_prestat_dir_name(fd); + if (rs.code == 0) { + if (path.startsWith(rs.dir_name)) { + rs.fd = fd; + return {dirpath: rs.dir_name, dirfd: fd}; + } else { + return dirfdForPath(path, fd + 1); + } + } else { + return null; + } + } + + globalThis.wasi_fs = function () { + return { + open, + mkdir, + rmdir, + unlink, + symlink, + link, + rename, + stat, + Advise, + Whence, + errno: () => globalThis.lastErr.errno, + error: () => globalThis.lastErr.error, + }; + }(); + + // Delete the function from `globalThis` so it doesn't leak. + Reflect.deleteProperty(globalThis, "__javy_wasi_preview1_open"); + Reflect.deleteProperty(globalThis, "__javy_wasi_preview1_fd_prestat_dir_name"); + Reflect.deleteProperty(globalThis, "__javy_wasi_preview1_path_create_directory"); + Reflect.deleteProperty(globalThis, "__javy_wasi_preview1_path_remove_directory"); + Reflect.deleteProperty(globalThis, "__javy_wasi_preview1_path_unlink_file"); + Reflect.deleteProperty(globalThis, "__javy_wasi_preview1_path_symlink"); + Reflect.deleteProperty(globalThis, "__javy_wasi_preview1_path_link"); + Reflect.deleteProperty(globalThis, "__javy_wasi_preview1_path_rename"); + Reflect.deleteProperty(globalThis, "__javy_wasi_preview1_path_filestat_get"); +})(); \ No newline at end of file diff --git a/src/wasi/preview_1.rs b/src/wasi/preview_1.rs new file mode 100644 index 0000000..5b9bbbf --- /dev/null +++ b/src/wasi/preview_1.rs @@ -0,0 +1,114 @@ +#[link(wasm_import_module = "wasi_snapshot_preview1")] +unsafe extern "C" { + #[link_name = "path_open"] + pub unsafe fn path_open( + dirfd: i32, + dirflags: i32, + path: i32, + path_len: i32, + oflags: i32, + fs_rights_base: i64, + _fs_rights_inheriting: i64, + fdflags: i32, + fd_ptr: i32, + ) -> i32; + + #[link_name = "path_create_directory"] + pub unsafe fn path_create_directory(dirfd: i32, path_ptr: i32, path_len: i32) -> i32; + + #[link_name = "path_remove_directory"] + pub unsafe fn path_remove_directory(dirfd: i32, path_ptr: i32, path_len: i32) -> i32; + + #[link_name = "path_unlink_file"] + pub unsafe fn path_unlink_file(dirfd: i32, path_ptr: i32, path_len: i32) -> i32; + + #[link_name = "path_symlink"] + pub unsafe fn path_symlink( + old_path_ptr: i32, + old_path_len: i32, + dirfd: i32, + new_path_ptr: i32, + new_path_len: i32, + ) -> i32; + + #[link_name = "fd_close"] + pub unsafe fn fd_close(fd: i32) -> i32; + + #[link_name = "fd_sync"] + pub unsafe fn fd_sync(fd: i32) -> i32; + + #[link_name = "fd_datasync"] + pub unsafe fn fd_datasync(fd: i32) -> i32; + + #[link_name = "fd_prestat_get"] + pub unsafe fn fd_prestat_get(fd: i32, path_len_ptr: i32) -> i32; + + #[link_name = "fd_read"] + pub unsafe fn fd_read(fd: i32, iovec_slice: i32, iovec_len: i32, readn_ptr: i32) -> i32; + + #[link_name = "fd_write"] + pub unsafe fn fd_write(fd: i32, iovec_slice: i32, iovec_len: i32, writen_ptr: i32) -> i32; + + #[link_name = "fd_prestat_dir_name"] + pub unsafe fn fd_prestat_dir_name(fd: i32, path_ptr: i32, path_len: i32) -> i32; + + #[link_name = "path_link"] + pub unsafe fn path_link( + old_dirfd: i32, + fd_lookup_flags: i32, + old_path: i32, + old_path_len: i32, + new_dirfd: i32, + new_path: i32, + new_path_len: i32, + ) -> i32; + + #[link_name = "path_rename"] + pub unsafe fn path_rename( + old_dirfd: i32, + old_path: i32, + old_path_len: i32, + new_dirfd: i32, + new_path: i32, + new_path_len: i32, + ) -> i32; + + #[link_name = "path_filestat_get"] + pub unsafe fn path_filestat_get( + dirfd: i32, + flags: i32, + path: i32, + path_len: i32, + stat_ptr: i32, + ) -> i32; + + #[link_name = "fd_advise"] + pub unsafe fn fd_advise(fd: i32, offset: u64, len: u64, advice: i32) -> i32; + + #[link_name = "fd_seek"] + pub unsafe fn fd_seek(fd: i32, offset: u64, whence: i32, fsize: i32) -> i32; + + #[link_name = "fd_allocate"] + pub unsafe fn fd_allocate(fd: i32, offset: u64, len: u64) -> i32; + + #[link_name = "fd_filestat_get"] + pub unsafe fn fd_filestat_get(fd: i32, stat_ptr: i32) -> i32; + + #[link_name = "fd_filestat_set_size"] + pub unsafe fn fd_filestat_set_size(fd: i32, stat: u64) -> i32; + + #[link_name = "fd_tell"] + pub unsafe fn fd_tell(fd: i32, pos_ptr: i32) -> i32; + + #[link_name = "fd_filestat_set_times"] + pub unsafe fn fd_filestat_set_times(fd: i32, atim: i64, mtim: i64, fst_flags: u16) -> i32; + + #[link_name = "fd_fdstat_set_flags"] + pub unsafe fn fd_fdstat_set_flags(fd: i32, fd_flags: u16) -> i32; + + /// Reads directory entries from a file descriptor + #[allow(dead_code)] + #[link_name = "fd_readdir"] + pub unsafe fn fd_readdir(fd: i32, buf: i32, buf_len: i32, cookie: u64, readn_ptr: i32) -> i32; + +} diff --git a/src/wasi/rename.rs b/src/wasi/rename.rs new file mode 100644 index 0000000..bf28c06 --- /dev/null +++ b/src/wasi/rename.rs @@ -0,0 +1,52 @@ +use anyhow::{anyhow, bail, Result}; +use javy_plugin_api::javy::{quickjs::Value, Args}; + +use super::{preview_1, process_error}; + +/// This function is used to rename a file or directory from one path to another. +/// - `old_dirfd`: The directory file descriptor of the old file. +/// - `old_path`: The path of the old file. +/// - `new_dirfd`: The directory file descriptor of the new file. +/// - `new_path`: The path of the new file. +pub fn wasi_preview1_path_rename(args: Args<'_>) -> Result> { + let (cx, args) = args.release(); + let args_pat: &[Value<'_>] = &args.0; + let [old_dirfd, old_path, new_dirfd, new_path, ..] = args_pat else { + bail!( + "path_rename expects 4 parameters: old_dirfd, old_path, new_dirfd, new_path. Got: {} parameters.", + args.len() + ); + }; + let dirfd = old_dirfd + .as_int() + .ok_or_else(|| anyhow!("old_dirfd must be a number"))?; + let old_path = old_path + .as_string() + .ok_or_else(|| anyhow!("old_path must be a string"))? + .to_string() + .map_err(|_| anyhow!("invalid UTF-8 in path"))?; + let new_dirfd = new_dirfd + .as_int() + .ok_or_else(|| anyhow!("new_dirfd must be a number"))?; + let new_path = new_path + .as_string() + .ok_or_else(|| anyhow!("new_path must be a string"))? + .to_string() + .map_err(|_| anyhow!("invalid UTF-8 in path"))?; + let new_path_ptr = new_path.as_ptr() as i32; + let new_path_len = new_path.len() as i32; + let old_path_ptr = old_path.as_ptr() as i32; + let old_path_len = old_path.len() as i32; + let rs = unsafe { + preview_1::path_rename( + dirfd, + old_path_ptr, + old_path_len, + new_dirfd, + new_path_ptr, + new_path_len, + ) + }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx.clone(), rs)) +} diff --git a/src/wasi/rmdir.rs b/src/wasi/rmdir.rs new file mode 100644 index 0000000..48c55aa --- /dev/null +++ b/src/wasi/rmdir.rs @@ -0,0 +1,33 @@ +use anyhow::{anyhow, bail, Result}; +use javy_plugin_api::javy::{quickjs::Value, Args}; + +use super::{preview_1, process_error}; + +/// Remove a directory at the given path. +/// This function is used to remove a directory at the given path. +/// It is used to remove a directory at the given path. +/// - `old_dirfd`: The directory file descriptor of the old directory. +/// - `path`: The path of the directory to remove. +pub fn wasi_preview1_path_remove_directory(args: Args<'_>) -> Result> { + let (cx, args) = args.release(); + let args_pat: &[Value<'_>] = &args.0; + let [dirfd, path, ..] = args_pat else { + bail!( + "path_remove_directory expects 2 parameters: the dirfd and path, Got: {} parameters.", + args.len() + ); + }; + let dirfd = dirfd + .as_int() + .ok_or_else(|| anyhow!("dirfd must be a number"))?; + let path = path + .as_string() + .ok_or_else(|| anyhow!("path must be a string"))? + .to_string() + .map_err(|_| anyhow!("invalid UTF-8 in path"))?; + let path_ptr = path.as_ptr() as i32; + let path_len = path.len() as i32; + let rs = unsafe { preview_1::path_remove_directory(dirfd, path_ptr, path_len) }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx.clone(), rs)) +} diff --git a/src/wasi/stat.rs b/src/wasi/stat.rs new file mode 100644 index 0000000..ab5cc5d --- /dev/null +++ b/src/wasi/stat.rs @@ -0,0 +1,60 @@ +use anyhow::{anyhow, bail, Result}; +use javy_plugin_api::javy::{ + quickjs::{Ctx, Object as JObject, Value}, + Args, +}; + +use super::{preview_1, process_error, FileType, Filestat}; + +/// Get the file status of a file at the given path. +/// This function is used to get the file status of a file at the given path. +/// - `dirfd`: The directory file descriptor of the file. +/// - `lookup_flags`: Flags for looking up the file descriptor. +/// - `path`: The path of the file to get the status of. +pub fn wasi_preview1_path_filestat_get(args: Args<'_>) -> Result> { + let (cx, args) = args.release(); + let args_pat: &[Value<'_>] = &args.0; + let [dirfd, lookup_flags, path, ..] = args_pat else { + bail!( + "wasi_preview1_path_filestat_get expects 3 parameters: the dirfd and path, Got: {} parameters.", + args.len() + ); + }; + let dirfd = dirfd + .as_int() + .ok_or_else(|| anyhow!("dirfd must be a number"))?; + let lookup_flags = lookup_flags + .as_int() + .ok_or_else(|| anyhow!("lookup_flags must be a number"))?; + let path = path + .as_string() + .ok_or_else(|| anyhow!("path must be a string"))? + .to_string() + .map_err(|_| anyhow!("invalid UTF-8 in path"))?; + let path_ptr = path.as_ptr() as i32; + let path_len = path.len() as i32; + let mut fd_stat: Filestat = Default::default(); + let fd_stat_ptr = &mut fd_stat as *mut _ as i32; + let rs = unsafe { + preview_1::path_filestat_get(dirfd, lookup_flags, path_ptr, path_len, fd_stat_ptr) + }; + if rs == 0 { + let stat = filestate_to_jsobject(cx.clone(), &fd_stat)?; + Ok(Value::from_object(stat)) + } else { + process_error(cx.clone(), rs)?; + Ok(Value::new_null(cx.clone())) + } +} + +pub fn filestate_to_jsobject<'js>(cx: Ctx<'js>, fd_stat: &Filestat) -> Result> { + let stat = JObject::new(cx.clone())?; + stat.set("filetype", fd_stat.filetype)?; + let filetype: &str = FileType(fd_stat.filetype).into(); + stat.set("filetype_desc", filetype)?; + stat.set("filesize", fd_stat.size)?; + stat.set("access_time", fd_stat.atim)?; + stat.set("modification_time", fd_stat.mtim)?; + stat.set("creation_time", fd_stat.ctim)?; + Ok(stat) +} diff --git a/src/wasi/symlink.rs b/src/wasi/symlink.rs new file mode 100644 index 0000000..4926ac3 --- /dev/null +++ b/src/wasi/symlink.rs @@ -0,0 +1,48 @@ +use anyhow::{anyhow, bail, Result}; +use javy_plugin_api::javy::{quickjs::Value, Args}; + +use super::{preview_1, process_error}; + +/// This function creates a symbolic link to a file or directory. +/// It takes the following parameters: +/// - `old_path`: The path to the file or directory to be linked. +/// - `dirfd`: The file descriptor of the directory where the link will be created. +/// - `new_path`: The name of the new symbolic link. +pub fn wasi_preview1_path_symlink(args: Args<'_>) -> Result> { + let (cx, args) = args.release(); + let args_pat: &[Value<'_>] = &args.0; + let [old_path, dirfd, new_path, ..] = args_pat else { + bail!( + "path_symlink expects 3 parameters: the old_path, fd and new_path, Got: {} parameters.", + args.len() + ); + }; + let old_path = old_path + .as_string() + .ok_or_else(|| anyhow!("old_path must be a string"))? + .to_string() + .map_err(|_| anyhow!("invalid UTF-8 in old_path"))?; + let dirfd = dirfd + .as_int() + .ok_or_else(|| anyhow!("fd must be a number"))?; + let new_path = new_path + .as_string() + .ok_or_else(|| anyhow!("new_path must be a string"))? + .to_string() + .map_err(|_| anyhow!("invalid UTF-8 in new_path"))?; + let old_path_ptr = old_path.as_ptr() as i32; + let old_path_len = old_path.len() as i32; + let new_path_ptr = new_path.as_ptr() as i32; + let new_path_len = new_path.len() as i32; + let rs = unsafe { + preview_1::path_symlink( + old_path_ptr, + old_path_len, + dirfd, + new_path_ptr, + new_path_len, + ) + }; + process_error(cx.clone(), rs)?; + Ok(Value::new_null(cx.clone())) +} diff --git a/src/wasi/unlink.rs b/src/wasi/unlink.rs new file mode 100644 index 0000000..4c0bcd0 --- /dev/null +++ b/src/wasi/unlink.rs @@ -0,0 +1,34 @@ +use anyhow::{anyhow, bail, Result}; +use javy_plugin_api::javy::{quickjs::Value, Args}; + +use super::{preview_1, process_error}; + +/// Unlink a file at the given path. +/// - `dirfd`: The directory file descriptor of the directory containing the file. +/// - `path`: The path of the file to unlink. +pub fn wasi_preview1_path_unlink_file(args: Args<'_>) -> Result> { + let (cx, args) = args.release(); + let args_pat: &[Value<'_>] = &args.0; + let [dirfd, path, ..] = args_pat else { + bail!( + "path_unlink_file expects 2 parameters: the dirfd and path, Got: {} parameters.", + args.len() + ); + }; + + // dirfd is the file descriptor of the directory + let dirfd = dirfd + .as_int() + .ok_or_else(|| anyhow!("dirfd must be a number"))?; + // path is the path to the file + let path = path + .as_string() + .ok_or_else(|| anyhow!("path must be a string"))? + .to_string() + .map_err(|_| anyhow!("invalid UTF-8 in path"))?; + let path_ptr = path.as_ptr() as i32; + let path_len = path.len() as i32; + let rs = unsafe { preview_1::path_unlink_file(dirfd, path_ptr, path_len) }; + process_error(cx.clone(), rs)?; + Ok(Value::new_int(cx.clone(), rs)) +}