diff --git a/.gitignore b/.gitignore index 6d6048b..626c574 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ run-data repo -.DS_Store \ No newline at end of file +.DS_Store +**/.DS_Store diff --git a/cli-programs/.gitignore b/cli-programs/.gitignore new file mode 100644 index 0000000..9f97022 --- /dev/null +++ b/cli-programs/.gitignore @@ -0,0 +1 @@ +target/ \ No newline at end of file diff --git a/cli-programs/Cargo.lock b/cli-programs/Cargo.lock new file mode 100644 index 0000000..a184a30 --- /dev/null +++ b/cli-programs/Cargo.lock @@ -0,0 +1,348 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wget" +version = "0.1.0" +dependencies = [ + "url", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/cli-programs/Cargo.toml b/cli-programs/Cargo.toml new file mode 100644 index 0000000..217a29d --- /dev/null +++ b/cli-programs/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] +members = ["bin/*"] +resolver = "3" + +[workspace.dependencies] +rustedcomputer = { path = "../rustedcomputer-rs" } + +[profile.release] +opt-level = 'z' # Optimize for size +lto = true # Enable link-time optimization +codegen-units = 1 # Reduce number of codegen units to increase optimizations +panic = 'abort' # Abort on panic +strip = true # Strip symbols from binary* +incremental = false # Disable incremental compilation for release diff --git a/rustedcomputer-rs/Cargo.lock b/rustedcomputer-rs/Cargo.lock index dc12c3f..8e586c7 100644 --- a/rustedcomputer-rs/Cargo.lock +++ b/rustedcomputer-rs/Cargo.lock @@ -2,6 +2,35 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "proc-macro2" version = "1.0.101" @@ -24,6 +53,7 @@ dependencies = [ name = "rustedcomputer" version = "0.1.0" dependencies = [ + "http", "thiserror", ] diff --git a/rustedcomputer-rs/Cargo.toml b/rustedcomputer-rs/Cargo.toml index d7a65f6..cded664 100644 --- a/rustedcomputer-rs/Cargo.toml +++ b/rustedcomputer-rs/Cargo.toml @@ -9,4 +9,5 @@ repository = "https://github.com/TheBlckbird/rustedcomputer" readme = "Readme.md" [dependencies] -thiserror = "2.0.16" +http = "1.3" +thiserror = "2.0" diff --git a/rustedcomputer-rs/src/error.rs b/rustedcomputer-rs/src/error.rs index af2a4c5..2888c95 100644 --- a/rustedcomputer-rs/src/error.rs +++ b/rustedcomputer-rs/src/error.rs @@ -6,6 +6,22 @@ pub enum RustedComputerError { FutureFailed, #[error("Future had a timeout")] FutureTimeout, + #[error("HTTP Error: {0}")] + HttpError(http::Error), + #[error("HTTP Connection Issue")] + Connect, + #[error("Java returned an IOException")] + IO, + #[error("Java returned an InterruptedException")] + Interrupted, + #[error("Java returned a SecurityException")] + Security, +} + +impl From for RustedComputerError { + fn from(value: http::Error) -> Self { + Self::HttpError(value) + } } pub type Result = std::result::Result; \ No newline at end of file diff --git a/rustedcomputer-rs/src/http/deserialize_response.rs b/rustedcomputer-rs/src/http/deserialize_response.rs new file mode 100644 index 0000000..7539be2 --- /dev/null +++ b/rustedcomputer-rs/src/http/deserialize_response.rs @@ -0,0 +1,65 @@ +use crate::http::headers::deserialize_headers::deserialize_headers; +use http::Response; +use std::slice; +use crate::error::RustedComputerError; + +pub fn deserialize_response(raw_response: &[u8]) -> crate::Result> { + let mut raw_response = raw_response.iter(); + + let is_success = char::from(*raw_response.next().unwrap()) == 'S'; + + if !is_success { + let error_identifier = str::from_utf8(raw_response.as_slice()).expect("This result shouldn't be sent from Java."); + + return Err(match error_identifier { + "IO" => RustedComputerError::IO, + "C" => RustedComputerError::Connect, + "I" => RustedComputerError::Interrupted, + "S" => RustedComputerError::Security, + _ => unreachable!() + }); + } + + let status_code = + ((*raw_response.next().unwrap() as u16) << 8) | *raw_response.next().unwrap() as u16; + + let http_version = match raw_response.next().unwrap() { + 11 => http::Version::HTTP_11, + 2 => http::Version::HTTP_2, + _ => unreachable!(), + }; + + let headers_ptr = take_next_u32(&mut raw_response) as *const u8; + let headers_len = take_next_u32(&mut raw_response) as usize; + let raw_headers = unsafe { slice::from_raw_parts(headers_ptr, headers_len) }; + let headers = deserialize_headers(raw_headers); + + let body = str::from_utf8(raw_response.copied().collect::>().as_slice()) + .unwrap() + .to_owned(); + + let mut response_builder = Response::builder() + .status(status_code) + .version(http_version); + + for (name, value) in headers { + // Pseudo headers are for some reason not accepted by the `http`-crate + if !name.starts_with(':') { + response_builder = response_builder.header(name, value); + } + } + + response_builder.body(body).map_err(RustedComputerError::from) +} + +fn take_next_u32(raw_response: &mut slice::Iter) -> u32 { + u32::from_be_bytes( + raw_response + .by_ref() + .take(4) + .copied() + .collect::>() + .try_into() + .unwrap(), + ) +} diff --git a/rustedcomputer-rs/src/http/headers/deserialize_headers.rs b/rustedcomputer-rs/src/http/headers/deserialize_headers.rs new file mode 100644 index 0000000..b6d2e32 --- /dev/null +++ b/rustedcomputer-rs/src/http/headers/deserialize_headers.rs @@ -0,0 +1,34 @@ +use std::collections::HashMap; +use std::slice; + +/// Retrieves the [`HeaderMap`] from the addresses and lengths returned by Java. +pub fn deserialize_headers(raw_headers: &[u8]) -> HashMap { + let mut headers = HashMap::new(); + + let mut next_ptr = None; + let mut next_header_name: Option = None; + + for single_ptr_or_len in raw_headers.chunks_exact(4) { + let ptr_or_len = u32::from_be_bytes(single_ptr_or_len.try_into().unwrap()); + + if next_ptr.is_none() { + next_ptr = Some(ptr_or_len as *mut u8); + } else { + let slice = + unsafe { slice::from_raw_parts_mut(next_ptr.unwrap(), ptr_or_len as usize) }; + let boxed_slice = unsafe { Box::from_raw(slice) }; + let header_name_or_value = str::from_utf8(&boxed_slice).unwrap().to_owned(); + + if next_header_name.is_none() { + next_header_name = Some(header_name_or_value); + } else { + headers.insert(next_header_name.unwrap(), header_name_or_value); + next_header_name = None; + } + + next_ptr = None; + } + } + + headers +} diff --git a/rustedcomputer-rs/src/http/headers/mod.rs b/rustedcomputer-rs/src/http/headers/mod.rs new file mode 100644 index 0000000..960697b --- /dev/null +++ b/rustedcomputer-rs/src/http/headers/mod.rs @@ -0,0 +1,2 @@ +pub mod serialize_headers; +pub mod deserialize_headers; \ No newline at end of file diff --git a/rustedcomputer-rs/src/http/headers/serialize_headers.rs b/rustedcomputer-rs/src/http/headers/serialize_headers.rs new file mode 100644 index 0000000..7d16191 --- /dev/null +++ b/rustedcomputer-rs/src/http/headers/serialize_headers.rs @@ -0,0 +1,26 @@ +use http::HeaderMap; + +/// Collects the addresses and lengths of the header names and values and puts them into a [`Vec`]. +/// +/// This is needed for interfacing with Java. +/// +/// The caller has to make sure that the [`HeaderMap`] isn't deallocated when the return value of +/// this function is used. +pub fn serialize_headers(headers: &HeaderMap) -> Vec { + let mut serialized = Vec::new(); + + for (header_name, header_value) in headers { + let name_length = header_name.as_str().len(); + let name_pointer = header_name.as_str().as_ptr(); + + let value_length = header_value.as_bytes().len(); + let value_pointer = header_value.as_bytes().as_ptr(); + + serialized.push(name_pointer as usize); + serialized.push(name_length); + serialized.push(value_pointer as usize); + serialized.push(value_length); + } + + serialized +} diff --git a/rustedcomputer-rs/src/http/method_to_int.rs b/rustedcomputer-rs/src/http/method_to_int.rs new file mode 100644 index 0000000..3dbff19 --- /dev/null +++ b/rustedcomputer-rs/src/http/method_to_int.rs @@ -0,0 +1,27 @@ +use http::Method; + +macro_rules! match_method { + {match ($method:expr) {$($name:ident => $return_expr:expr),*$(,)?}} => { + $(if $method == http::Method::$name { + return $return_expr; + })* + + return -1 + }; +} + +pub fn method_to_int(method: &Method) -> i32 { + match_method! { + match (method) { + GET => 0, + HEAD => 1, + POST => 2, + PUT => 3, + DELETE => 4, + CONNECT => 5, + OPTIONS => 6, + TRACE => 7, + PATCH => 8, + } + } +} diff --git a/rustedcomputer-rs/src/http/mod.rs b/rustedcomputer-rs/src/http/mod.rs new file mode 100644 index 0000000..6c403ad --- /dev/null +++ b/rustedcomputer-rs/src/http/mod.rs @@ -0,0 +1,66 @@ +use crate::http::deserialize_response::deserialize_response; +use crate::wasm_helpers::PtrLen; +use crate::{http::method_to_int::method_to_int, wasm_helpers::GetPtrLen}; +use headers::serialize_headers::serialize_headers; +use std::slice; + +pub use http::*; + +mod deserialize_response; +mod headers; +mod method_to_int; + +pub fn fetch(request: Request) -> crate::Result> { + let extension = request.extensions(); + if !extension.is_empty() { + panic!("Request extensions aren't supported!"); + } + + let method = method_to_int(request.method()); + + // A temporary variable has to be created in order to retrieve a valid pointer. + let uri = request.uri().to_string(); + let uri_ptr_len = uri.get_ptr_len(); + + let headers_serialized = serialize_headers(request.headers()); + let headers_ptr_len = headers_serialized.get_ptr_len(); + let body_ptr_len = request.body().get_ptr_len(); + + println!( + "{}; {}", + headers_ptr_len.as_wasm_ptr(), + headers_ptr_len.as_wasm_len() + ); + + let raw_response = unsafe { + let raw_response_ptr_len: PtrLen = PtrLen::from(extern_fns::fetch( + method, + uri_ptr_len.as_wasm_ptr(), + uri_ptr_len.as_wasm_len(), + body_ptr_len.as_wasm_ptr(), + body_ptr_len.as_wasm_len(), + headers_ptr_len.as_wasm_ptr(), + headers_ptr_len.as_wasm_len(), + )); + + slice::from_raw_parts(raw_response_ptr_len.ptr, raw_response_ptr_len.len) + }; + + deserialize_response(raw_response) +} + +mod extern_fns { + #[link(wasm_import_module = "http")] + unsafe extern "C" { + #[link_name = "fetch"] + pub fn fetch( + method: i32, + uri_address: i32, + uri_length: i32, + body_address: i32, + body_length: i32, + headers_address: i32, + headers_length: i32, + ) -> i64; + } +} diff --git a/rustedcomputer-rs/src/lib.rs b/rustedcomputer-rs/src/lib.rs index dbc995c..248850b 100644 --- a/rustedcomputer-rs/src/lib.rs +++ b/rustedcomputer-rs/src/lib.rs @@ -1,7 +1,11 @@ +pub use crate::error::*; + pub mod async_runtime; pub mod side; pub mod functions; pub mod error; +pub mod http; +mod wasm_helpers; #[unsafe(no_mangle)] pub extern "C" fn alloc(length: i32) -> i32 { diff --git a/rustedcomputer-rs/src/wasm_helpers/mod.rs b/rustedcomputer-rs/src/wasm_helpers/mod.rs new file mode 100644 index 0000000..6932c89 --- /dev/null +++ b/rustedcomputer-rs/src/wasm_helpers/mod.rs @@ -0,0 +1,59 @@ +pub struct PtrLen { + pub ptr: *const T, + pub len: usize, +} + +impl PtrLen { + pub fn new(ptr: *const T, len: usize) -> Self { + Self { + ptr, + len, + } + } + + /// Returns the pointer as an `i32` to pass it to WASM. + /// + /// This cast works, because pointers in WASM are never longer than 32 bits. + pub fn as_wasm_ptr(&self) -> i32 { + self.ptr as i32 + } + + /// Returns the length as an `i32` to pass it to WASM. + /// + /// A string probably shouldn't be longer than 32 bits. + pub fn as_wasm_len(&self) -> i32 { + self.len as i32 + } +} + +impl From for PtrLen { + fn from(value: i64) -> Self { + let length = (value & 0xFFFFFFFF) as usize; + let pointer = (value >> 32) as *mut T; + + PtrLen::new(pointer, length) + } +} + +pub trait GetPtrLen { + type PointerType; + + /// Gets the pointer and length to this type and combines it in a struct. + fn get_ptr_len(&self) -> PtrLen; +} + +impl GetPtrLen for String { + type PointerType = u8; + + fn get_ptr_len(&self) -> PtrLen { + PtrLen::new(self.as_ptr(), self.len()) + } +} + +impl GetPtrLen for Vec { + type PointerType = T; + + fn get_ptr_len(&self) -> PtrLen { + PtrLen::new(self.as_ptr(), self.len() * size_of::()) + } +} \ No newline at end of file diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index f520e8a..0000000 Binary files a/src/.DS_Store and /dev/null differ diff --git a/src/main/.DS_Store b/src/main/.DS_Store deleted file mode 100644 index 0bee39b..0000000 Binary files a/src/main/.DS_Store and /dev/null differ diff --git a/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/block/ComputerBlockEntity.kt b/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/block/ComputerBlockEntity.kt index bfeeaad..3d87602 100644 --- a/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/block/ComputerBlockEntity.kt +++ b/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/block/ComputerBlockEntity.kt @@ -14,6 +14,7 @@ import dev.theblckbird.rustedcomputer.RustedComputer import dev.theblckbird.rustedcomputer.computer.ComputerObservations import dev.theblckbird.rustedcomputer.computer.ComputerScreenHolder import dev.theblckbird.rustedcomputer.computer.MinecraftTimeClock +import dev.theblckbird.rustedcomputer.computer.hostfunctions.http.HttpFunctions import dev.theblckbird.rustedcomputer.computer.hostfunctions.infrastructure.FutureFunctions import dev.theblckbird.rustedcomputer.computer.hostfunctions.redstone.RedstoneFunctions import dev.theblckbird.rustedcomputer.computer.networking.toclient.stdout.StdoutData @@ -111,11 +112,13 @@ class ComputerBlockEntity(position: BlockPos, state: BlockState) : val redstoneFunctions = RedstoneFunctions(level, blockPos) val futureFunctions = FutureFunctions() + val httpFunctions = HttpFunctions() val store = Store() .addFunction(*wasi.toHostFunctions()) .addFunction(*redstoneFunctions.toHostFunctions()) .addFunction(*futureFunctions.toHostFunctions()) + .addFunction(*httpFunctions.toHostFunctions()) val resourceManager = level.server.resourceManager val romFileLocation = ResourceLocation.fromNamespaceAndPath(RustedComputer.MODID, "rom/$fileName") diff --git a/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/HostFunctionsHelpers.kt b/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/HostFunctionsHelpers.kt index 65ad25d..00f87c0 100644 --- a/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/HostFunctionsHelpers.kt +++ b/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/HostFunctionsHelpers.kt @@ -1,8 +1,7 @@ package dev.theblckbird.rustedcomputer.computer.hostfunctions -import com.dylibso.chicory.runtime.ExportFunction +import com.dylibso.chicory.runtime.Instance import com.dylibso.chicory.runtime.Memory -import dev.theblckbird.rustedcomputer.RustedComputer object HostFunctionsHelpers { /** @@ -15,14 +14,24 @@ object HostFunctionsHelpers { } /** - * Allocates a string in WASM memory and returns the combined pointer and length. + * Writes a string to WASM memory and returns the combined pointer and length. * * @return The pointer and length combined to a singular `Long` */ - fun allocateString(content: String, alloc: ExportFunction, memory: Memory): Long { - val length = content.toByteArray().count() + fun allocateString(content: String, instance: Instance, memory: Memory): Long { + return allocateByteArray(content.toByteArray(), instance, memory) + } + + /** + * Writes a `ByteArray` to WASM memory and returns the combined pointer and length. + * + * @return The pointer and length combined to a singular `Long` + */ + fun allocateByteArray(content: ByteArray, instance: Instance, memory: Memory): Long { + val alloc = instance.export("alloc") + val length = content.count() val pointer = alloc.apply(length.toLong())[0].toInt() - memory.writeString(pointer, content) + memory.write(pointer, content) return combinePointerAndLength(pointer, length) } diff --git a/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/http/HttpFunctions.kt b/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/http/HttpFunctions.kt new file mode 100644 index 0000000..8fd8a10 --- /dev/null +++ b/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/http/HttpFunctions.kt @@ -0,0 +1,76 @@ +package dev.theblckbird.rustedcomputer.computer.hostfunctions.http + +import com.dylibso.chicory.annotations.HostModule +import com.dylibso.chicory.annotations.WasmExport +import com.dylibso.chicory.runtime.HostFunction +import com.dylibso.chicory.runtime.Instance +import com.dylibso.chicory.runtime.Memory +import dev.theblckbird.rustedcomputer.computer.hostfunctions.HostFunctionsHelpers +import java.io.IOException +import java.net.ConnectException +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse + +@HostModule("http") +class HttpFunctions { + private val httpClient by lazy { + HttpClient.newBuilder().build() + } + + @WasmExport + fun fetch( + instance: Instance, + memory: Memory, + method: Int, + uriAddress: Int, + uriLength: Int, + bodyAddress: Int, + bodyLength: Int, + headersAddress: Int, + headersLength: Int, + ): Long { + val rawUri = memory.readString(uriAddress, uriLength) + val uri = URI(rawUri) + + val body = memory.readString(bodyAddress, bodyLength) + val method = Method.fromInt(method) + + val headersAddresses = memory.readBytes(headersAddress, headersLength) + val headers = deserializeHeaders(headersAddresses, memory) + + var requestBuilder = HttpRequest.newBuilder() + .uri(uri) + .method(method.toString(), HttpRequest.BodyPublishers.ofString(body)) + + if (headers.isNotEmpty()) { + requestBuilder = requestBuilder.headers(*headers) + } + + val request = requestBuilder.build() + + try { + val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()) + val responseSerialized = serializeResponse(response, memory, instance) + + return responseSerialized + } catch (exception: IOException) { + val returnAddress = HostFunctionsHelpers.allocateString("FIO", instance, memory) + return returnAddress + } catch (exception: ConnectException) { + val returnAddress = HostFunctionsHelpers.allocateString("FC", instance, memory) + return returnAddress + } catch (exception: InterruptedException) { + val returnAddress = HostFunctionsHelpers.allocateString("FI", instance, memory) + return returnAddress + } catch (exception: SecurityException) { + val returnAddress = HostFunctionsHelpers.allocateString("FS", instance, memory) + return returnAddress + } + } + + fun toHostFunctions(): Array { + return HttpFunctions_ModuleFactory.toHostFunctions(this) + } +} diff --git a/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/http/Method.kt b/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/http/Method.kt new file mode 100644 index 0000000..7646306 --- /dev/null +++ b/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/http/Method.kt @@ -0,0 +1,24 @@ +package dev.theblckbird.rustedcomputer.computer.hostfunctions.http + +enum class Method { + GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH; + + companion object { + fun fromInt(number: Int): Method? { + return when (number) { + 0 -> GET + 1 -> HEAD + 2 -> POST + 3 -> PUT + 4 -> DELETE + 5 -> CONNECT + 6 -> OPTIONS + 7 -> TRACE + 8 -> PATCH + else -> null + } + } + } +} + +// TODO: add script to automatically generate enums diff --git a/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/http/deserializeHeaders.kt b/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/http/deserializeHeaders.kt new file mode 100644 index 0000000..6d14236 --- /dev/null +++ b/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/http/deserializeHeaders.kt @@ -0,0 +1,59 @@ +package dev.theblckbird.rustedcomputer.computer.hostfunctions.http + +import com.dylibso.chicory.runtime.Memory +import dev.theblckbird.rustedcomputer.RustedComputer +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * Deserializes the headers as specified in the documentation. + * + * It returns an array of strings, because that's the way, the HTTP client expects it: + * The header name, then the header value repeating. + */ +fun deserializeHeaders(rawHeaders: ByteArray, memory: Memory): Array { + val nextInt = ByteArray(4) + val headersAddressesLengths = mutableListOf() + + val headersDeserialized = Array(rawHeaders.count() / 4 / 2) { "" } + + for ((index, byte) in rawHeaders.withIndex()) { + val nextIntIndex = index % 4 + + nextInt[nextIntIndex] = byte + + if (nextIntIndex == 3) { + headersAddressesLengths.add(byteArrayToInt(nextInt)) + } + } + + var nextAddress: Int? = null + var nextLength: Int? = null + var nextIndex = 0 + + for ((index, header) in headersAddressesLengths.withIndex()) { + if (index % 2 == 0) { + nextAddress = header + } else { + nextLength = header + } + + if (nextAddress != null && nextLength != null) { + val value = memory.readString(nextAddress, nextLength) + headersDeserialized[nextIndex] = value + + nextAddress = null + nextLength = null + nextIndex += 1 + } + } + + return headersDeserialized +} + +fun byteArrayToInt(byteArray: ByteArray): Int { + val result = ByteBuffer.wrap(byteArray) + result.order(ByteOrder.LITTLE_ENDIAN) + + return result.getInt() +} diff --git a/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/http/serializeResponse.kt b/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/http/serializeResponse.kt new file mode 100644 index 0000000..2f6682a --- /dev/null +++ b/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/http/serializeResponse.kt @@ -0,0 +1,63 @@ +package dev.theblckbird.rustedcomputer.computer.hostfunctions.http + +import com.dylibso.chicory.runtime.Instance +import com.dylibso.chicory.runtime.Memory +import dev.theblckbird.rustedcomputer.computer.hostfunctions.HostFunctionsHelpers +import java.net.http.HttpClient +import java.net.http.HttpHeaders +import java.net.http.HttpResponse +import java.nio.ByteBuffer + +/** + * Serializes the response per the documentation. + * + * Returns the combined pointer and length of a byte array containing all the response data. + * The caller has to manage deallocation of the memory. + */ +fun serializeResponse(response: HttpResponse, memory: Memory, instance: Instance): Long { + val statusCode = response.statusCode().toUShort() + val headersSerialized = serializeHeaders(response.headers(), instance, memory) + + val version: Byte = when (response.version()) { + HttpClient.Version.HTTP_1_1 -> 11 + HttpClient.Version.HTTP_2 -> 2 + } + + val headersPointerLength = + HostFunctionsHelpers.allocateByteArray(longsToByteArray(headersSerialized), instance, memory) + val headersPointerLengthAsBytes = ByteBuffer.allocate(8) + headersPointerLengthAsBytes.putLong(headersPointerLength) + + val responseSerialized = byteArrayOf( + ('S'.code.toByte()), + (statusCode.toInt() shr 8).toByte(), + (statusCode.toInt() and 0xFF).toByte(), + version, + ) + headersPointerLengthAsBytes.array() + response.body().toByteArray() + + val responsePointer = HostFunctionsHelpers.allocateByteArray(responseSerialized, instance, memory) + + return responsePointer +} + +private fun serializeHeaders(headers: HttpHeaders, instance: Instance, memory: Memory): List { + val outputAddresses = mutableListOf() + + for ((headerName, headerValues) in headers.map()) { + val headerNamePointer = HostFunctionsHelpers.allocateString(headerName, instance, memory) + + for (headerValue in headerValues) { + val headerValuePointer = HostFunctionsHelpers.allocateString(headerValue, instance, memory) + outputAddresses.add(headerNamePointer) + outputAddresses.add(headerValuePointer) + } + } + + return outputAddresses +} + +private fun longsToByteArray(longs: List): ByteArray { + val buffer = ByteBuffer.allocate(longs.size * 8) + longs.forEach { buffer.putLong(it) } + return buffer.array() +} diff --git a/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/infrastructure/FutureFunctions.kt b/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/infrastructure/FutureFunctions.kt index 5d37067..3116bc0 100644 --- a/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/infrastructure/FutureFunctions.kt +++ b/src/main/kotlin/dev/theblckbird/rustedcomputer/computer/hostfunctions/infrastructure/FutureFunctions.kt @@ -24,8 +24,7 @@ class FutureFunctions { fun poll(memory: Memory, instance: Instance, futureId: FutureId): Long { val future = Commands.getFuture(futureId) val stringifiedFuture = future.toString() - val alloc = instance.export("alloc") - val address = HostFunctionsHelpers.allocateString(stringifiedFuture, alloc, memory) + val address = HostFunctionsHelpers.allocateString(stringifiedFuture, instance, memory) return address }