From afa66afa55ae1cb786667dc735b2f74ab20d8cf3 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Mon, 13 May 2024 13:08:36 +0200 Subject: [PATCH 1/6] feat(io): implement `Read` and `Write` for streams Signed-off-by: Roman Volosatovs --- examples/cli-command.rs | 7 +++- examples/hello-world.rs | 7 +++- examples/http-proxy.rs | 7 +++- src/{errors.rs => ext/mod.rs} | 6 +-- src/ext/std.rs | 69 +++++++++++++++++++++++++++++++++++ src/lib.rs | 2 +- 6 files changed, 88 insertions(+), 10 deletions(-) rename src/{errors.rs => ext/mod.rs} (79%) create mode 100644 src/ext/std.rs diff --git a/examples/cli-command.rs b/examples/cli-command.rs index 809d15a..e3932ab 100644 --- a/examples/cli-command.rs +++ b/examples/cli-command.rs @@ -1,11 +1,14 @@ +use std::io::Write as _; + wasi::cli::command::export!(Example); struct Example; impl wasi::exports::cli::run::Guest for Example { fn run() -> Result<(), ()> { - let stdout = wasi::cli::stdout::get_stdout(); - stdout.blocking_write_and_flush(b"Hello, WASI!").unwrap(); + let mut stdout = wasi::cli::stdout::get_stdout(); + stdout.write_all(b"Hello, WASI!").unwrap(); + stdout.flush().unwrap(); Ok(()) } } diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 026e1bc..62d1210 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -1,4 +1,7 @@ +use std::io::Write as _; + fn main() { - let stdout = wasi::cli::stdout::get_stdout(); - stdout.blocking_write_and_flush(b"Hello, world!\n").unwrap(); + let mut stdout = wasi::cli::stdout::get_stdout(); + stdout.write_all(b"Hello, world!\n").unwrap(); + stdout.flush().unwrap(); } diff --git a/examples/http-proxy.rs b/examples/http-proxy.rs index 9a2e54a..8226e8b 100644 --- a/examples/http-proxy.rs +++ b/examples/http-proxy.rs @@ -1,3 +1,5 @@ +use std::io::Write as _; + use wasi::http::types::{ Fields, IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam, }; @@ -13,8 +15,9 @@ impl wasi::exports::http::incoming_handler::Guest for Example { ResponseOutparam::set(response_out, Ok(resp)); - let out = body.write().unwrap(); - out.blocking_write_and_flush(b"Hello, WASI!").unwrap(); + let mut out = body.write().unwrap(); + out.write_all(b"Hello, WASI!").unwrap(); + out.flush().unwrap(); drop(out); OutgoingBody::finish(body, None).unwrap(); diff --git a/src/errors.rs b/src/ext/mod.rs similarity index 79% rename from src/errors.rs rename to src/ext/mod.rs index 467e6fa..b581f22 100644 --- a/src/errors.rs +++ b/src/ext/mod.rs @@ -1,8 +1,8 @@ +#[cfg(feature = "std")] +mod std; + impl core::fmt::Display for crate::io::error::Error { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str(&self.to_debug_string()) } } - -#[cfg(feature = "std")] -impl std::error::Error for crate::io::error::Error {} diff --git a/src/ext/std.rs b/src/ext/std.rs new file mode 100644 index 0000000..bd0178d --- /dev/null +++ b/src/ext/std.rs @@ -0,0 +1,69 @@ +use std::error::Error; +use std::io; +use std::num::NonZeroU64; + +use crate::io::streams::StreamError; + +impl Error for crate::io::error::Error {} + +impl io::Read for crate::io::streams::InputStream { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let n = buf + .len() + .try_into() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + match self.blocking_read(n) { + Ok(chunk) => { + let n = chunk.len(); + if n > buf.len() { + return Err(io::Error::new( + io::ErrorKind::Other, + "more bytes read than requested", + )); + } + buf[..n].copy_from_slice(&chunk); + Ok(n) + } + Err(StreamError::Closed) => Ok(0), + Err(StreamError::LastOperationFailed(e)) => { + Err(io::Error::new(io::ErrorKind::Other, e.to_debug_string())) + } + } + } +} + +impl io::Write for crate::io::streams::OutputStream { + fn write(&mut self, buf: &[u8]) -> io::Result { + let n = loop { + match self.check_write().map(NonZeroU64::new) { + Ok(Some(n)) => { + break n; + } + Ok(None) => { + self.subscribe().block(); + } + Err(StreamError::Closed) => return Ok(0), + Err(StreamError::LastOperationFailed(e)) => { + return Err(io::Error::new(io::ErrorKind::Other, e.to_debug_string())) + } + }; + }; + let n = n + .get() + .try_into() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let n = buf.len().min(n); + crate::io::streams::OutputStream::write(self, &buf[..n]).map_err(|e| match e { + StreamError::Closed => io::ErrorKind::UnexpectedEof.into(), + StreamError::LastOperationFailed(e) => { + io::Error::new(io::ErrorKind::Other, e.to_debug_string()) + } + })?; + Ok(n) + } + + fn flush(&mut self) -> io::Result<()> { + self.blocking_flush() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) + } +} diff --git a/src/lib.rs b/src/lib.rs index ad02f10..fa6bace 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -179,7 +179,7 @@ #[cfg(feature = "std")] extern crate std; -mod errors; +mod ext; // These modules are all auto-generated by `./ci/regenerate.sh` mod bindings; From afe9cb6082961740187fc4eca466777123c02f44 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Mon, 13 May 2024 13:33:21 +0200 Subject: [PATCH 2/6] feat(rand): add `rand` crate integration Signed-off-by: Roman Volosatovs --- Cargo.toml | 7 +++++ examples/rand.rs | 20 ++++++++++++++ src/ext/mod.rs | 3 ++ src/ext/rand.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 +- 5 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 examples/rand.rs create mode 100644 src/ext/rand.rs diff --git a/Cargo.toml b/Cargo.toml index d3d7dbc..cc1c127 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,9 @@ compiler_builtins = { version = "0.1", optional = true } core = { version = "1.0", optional = true, package = "rustc-std-workspace-core" } rustc-std-workspace-alloc = { version = "1.0", optional = true } +# When built with `rand` crate integration +rand = { version = "0.8.5", default-features = false, optional = true } + [features] default = ["std"] std = [] @@ -32,3 +35,7 @@ crate-type = ["cdylib"] [[example]] name = "http-proxy" crate-type = ["cdylib"] + +[[example]] +name = "rand" +required-features = ["rand"] diff --git a/examples/rand.rs b/examples/rand.rs new file mode 100644 index 0000000..63cc85b --- /dev/null +++ b/examples/rand.rs @@ -0,0 +1,20 @@ +use std::io::Write as _; + +use wasi::ext::rand::rand::Rng as _; +use wasi::ext::rand::{HostInsecureRng, HostRng}; + +fn main() { + let mut stdout = wasi::cli::stdout::get_stdout(); + + let r: u64 = HostRng.gen(); + stdout + .write_all(format!("Cryptographically-secure random u64 is {r}\n").as_bytes()) + .unwrap(); + + let r: u64 = HostInsecureRng.gen(); + stdout + .write_all(format!("Pseudo-random u64 is {r}\n").as_bytes()) + .unwrap(); + + stdout.flush().unwrap(); +} diff --git a/src/ext/mod.rs b/src/ext/mod.rs index b581f22..c982618 100644 --- a/src/ext/mod.rs +++ b/src/ext/mod.rs @@ -1,6 +1,9 @@ #[cfg(feature = "std")] mod std; +#[cfg(feature = "rand")] +pub mod rand; + impl core::fmt::Display for crate::io::error::Error { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str(&self.to_debug_string()) diff --git a/src/ext/rand.rs b/src/ext/rand.rs new file mode 100644 index 0000000..1c97fb1 --- /dev/null +++ b/src/ext/rand.rs @@ -0,0 +1,71 @@ +pub use rand; + +use rand::{CryptoRng, RngCore}; + +/// The secure interface for cryptographically-secure random numbers +pub struct HostRng; + +impl CryptoRng for HostRng {} + +impl RngCore for HostRng { + #[inline] + fn next_u32(&mut self) -> u32 { + crate::random::random::get_random_u64() as _ + } + + #[inline] + fn next_u64(&mut self) -> u64 { + crate::random::random::get_random_u64() + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + let n = dest.len(); + if usize::BITS <= u64::BITS || n <= u64::MAX as _ { + dest.copy_from_slice(&crate::random::random::get_random_bytes(n as _)); + } else { + let (head, tail) = dest.split_at_mut(u64::MAX as _); + head.copy_from_slice(&crate::random::random::get_random_bytes(u64::MAX)); + self.fill_bytes(tail); + } + } + + #[inline] + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> { + self.fill_bytes(dest); + Ok(()) + } +} + +/// The insecure interface for insecure pseudo-random numbers +pub struct HostInsecureRng; + +impl RngCore for HostInsecureRng { + #[inline] + fn next_u32(&mut self) -> u32 { + crate::random::insecure::get_insecure_random_u64() as _ + } + + #[inline] + fn next_u64(&mut self) -> u64 { + crate::random::insecure::get_insecure_random_u64() + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + let n = dest.len(); + if usize::BITS <= u64::BITS || n <= u64::MAX as _ { + dest.copy_from_slice(&crate::random::insecure::get_insecure_random_bytes(n as _)); + } else { + let (head, tail) = dest.split_at_mut(u64::MAX as _); + head.copy_from_slice(&crate::random::insecure::get_insecure_random_bytes( + u64::MAX, + )); + self.fill_bytes(tail); + } + } + + #[inline] + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> { + self.fill_bytes(dest); + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index fa6bace..7b829c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -179,7 +179,7 @@ #[cfg(feature = "std")] extern crate std; -mod ext; +pub mod ext; // These modules are all auto-generated by `./ci/regenerate.sh` mod bindings; From 5595cb2e26d51401136d6f363b792180dc55ee99 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Mon, 13 May 2024 13:50:22 +0200 Subject: [PATCH 3/6] chore: split examples into `std` and `no_std` Signed-off-by: Roman Volosatovs --- Cargo.toml | 20 +++++++++++++++++++- examples/cli-command-no_std.rs | 11 +++++++++++ examples/hello-world-no_std.rs | 4 ++++ examples/http-proxy-no_std.rs | 22 ++++++++++++++++++++++ examples/rand-no_std.rs | 19 +++++++++++++++++++ 5 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 examples/cli-command-no_std.rs create mode 100644 examples/hello-world-no_std.rs create mode 100644 examples/http-proxy-no_std.rs create mode 100644 examples/rand-no_std.rs diff --git a/Cargo.toml b/Cargo.toml index cc1c127..64e6dc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,14 +28,32 @@ std = [] # Unstable feature to support being a libstd dependency rustc-dep-of-std = ["compiler_builtins", "core", "rustc-std-workspace-alloc"] +[[example]] +name = "cli-command-no_std" +crate-type = ["cdylib"] + [[example]] name = "cli-command" crate-type = ["cdylib"] +required-features = ["std"] + +[[example]] +name = "hello-world" +required-features = ["std"] + +[[example]] +name = "http-proxy-no_std" +crate-type = ["cdylib"] [[example]] name = "http-proxy" crate-type = ["cdylib"] +required-features = ["std"] [[example]] -name = "rand" +name = "rand-no_std" required-features = ["rand"] + +[[example]] +name = "rand" +required-features = ["std", "rand"] diff --git a/examples/cli-command-no_std.rs b/examples/cli-command-no_std.rs new file mode 100644 index 0000000..809d15a --- /dev/null +++ b/examples/cli-command-no_std.rs @@ -0,0 +1,11 @@ +wasi::cli::command::export!(Example); + +struct Example; + +impl wasi::exports::cli::run::Guest for Example { + fn run() -> Result<(), ()> { + let stdout = wasi::cli::stdout::get_stdout(); + stdout.blocking_write_and_flush(b"Hello, WASI!").unwrap(); + Ok(()) + } +} diff --git a/examples/hello-world-no_std.rs b/examples/hello-world-no_std.rs new file mode 100644 index 0000000..026e1bc --- /dev/null +++ b/examples/hello-world-no_std.rs @@ -0,0 +1,4 @@ +fn main() { + let stdout = wasi::cli::stdout::get_stdout(); + stdout.blocking_write_and_flush(b"Hello, world!\n").unwrap(); +} diff --git a/examples/http-proxy-no_std.rs b/examples/http-proxy-no_std.rs new file mode 100644 index 0000000..9a2e54a --- /dev/null +++ b/examples/http-proxy-no_std.rs @@ -0,0 +1,22 @@ +use wasi::http::types::{ + Fields, IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam, +}; + +wasi::http::proxy::export!(Example); + +struct Example; + +impl wasi::exports::http::incoming_handler::Guest for Example { + fn handle(_request: IncomingRequest, response_out: ResponseOutparam) { + let resp = OutgoingResponse::new(Fields::new()); + let body = resp.body().unwrap(); + + ResponseOutparam::set(response_out, Ok(resp)); + + let out = body.write().unwrap(); + out.blocking_write_and_flush(b"Hello, WASI!").unwrap(); + drop(out); + + OutgoingBody::finish(body, None).unwrap(); + } +} diff --git a/examples/rand-no_std.rs b/examples/rand-no_std.rs new file mode 100644 index 0000000..5781754 --- /dev/null +++ b/examples/rand-no_std.rs @@ -0,0 +1,19 @@ +use wasi::ext::rand::rand::Rng as _; +use wasi::ext::rand::{HostInsecureRng, HostRng}; + +fn main() { + let stdout = wasi::cli::stdout::get_stdout(); + + let r: u64 = HostRng.gen(); + + stdout + .blocking_write_and_flush( + format!("Cryptographically-secure random u64 is {r}\n").as_bytes(), + ) + .unwrap(); + + let r: u64 = HostInsecureRng.gen(); + stdout + .blocking_write_and_flush(format!("Pseudo-random u64 is {r}\n").as_bytes()) + .unwrap(); +} From 457e7c1ba273f9fc6e64ea6938d1336dc25da3dd Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Mon, 13 May 2024 14:03:06 +0200 Subject: [PATCH 4/6] ci: build all std and no_std examples Signed-off-by: Roman Volosatovs --- .github/workflows/main.yml | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 40268fb..df3f87b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,16 +26,43 @@ jobs: uses: bytecodealliance/actions/wasm-tools/setup@v1 with: version: "1.202.0" - - run: cargo build --examples --target wasm32-wasi - run: curl -LO https://github.com/bytecodealliance/wasmtime/releases/download/v19.0.0/wasi_snapshot_preview1.command.wasm + + - run: cargo build --examples --target wasm32-wasi --no-default-features + + - run: wasm-tools component new ./target/wasm32-wasi/debug/examples/hello-world-no_std.wasm --adapt ./wasi_snapshot_preview1.command.wasm -o component.wasm + - run: wasmtime run component.wasm + + - run: cargo build --examples --target wasm32-unknown-unknown --no-default-features + + - run: wasm-tools component new ./target/wasm32-unknown-unknown/debug/examples/cli_command_no_std.wasm -o component.wasm + - run: wasmtime run component.wasm + + - run: wasm-tools component new ./target/wasm32-unknown-unknown/debug/examples/http_proxy_no_std.wasm -o component.wasm + - run: wasm-tools component targets wit component.wasm -w wasi:http/proxy + + - run: cargo build --examples --target wasm32-wasi + - run: wasm-tools component new ./target/wasm32-wasi/debug/examples/hello-world.wasm --adapt ./wasi_snapshot_preview1.command.wasm -o component.wasm - run: wasmtime run component.wasm + - run: cargo build --examples --target wasm32-unknown-unknown + - run: wasm-tools component new ./target/wasm32-unknown-unknown/debug/examples/cli_command.wasm -o component.wasm - run: wasmtime run component.wasm + - run: wasm-tools component new ./target/wasm32-unknown-unknown/debug/examples/http_proxy.wasm -o component.wasm - run: wasm-tools component targets wit component.wasm -w wasi:http/proxy + - run: cargo build --examples --target wasm32-wasi --no-default-features --features rand + + - run: wasm-tools component new ./target/wasm32-wasi/debug/examples/rand-no_std.wasm --adapt ./wasi_snapshot_preview1.command.wasm -o component.wasm + - run: wasmtime run component.wasm + + - run: cargo build --examples --target wasm32-wasi --features rand + + - run: wasm-tools component new ./target/wasm32-wasi/debug/examples/rand.wasm --adapt ./wasi_snapshot_preview1.command.wasm -o component.wasm + - run: wasmtime run component.wasm rustfmt: name: Rustfmt From 606318f2a43dd48f6d44607a9b41403d10ce3e7a Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Mon, 13 May 2024 16:24:14 +0200 Subject: [PATCH 5/6] refactor(rand): use `writeln` to write to stdout Signed-off-by: Roman Volosatovs --- examples/rand.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/rand.rs b/examples/rand.rs index 63cc85b..09fc75b 100644 --- a/examples/rand.rs +++ b/examples/rand.rs @@ -7,14 +7,10 @@ fn main() { let mut stdout = wasi::cli::stdout::get_stdout(); let r: u64 = HostRng.gen(); - stdout - .write_all(format!("Cryptographically-secure random u64 is {r}\n").as_bytes()) - .unwrap(); + writeln!(stdout, "Cryptographically-secure random u64 is {r}").unwrap(); let r: u64 = HostInsecureRng.gen(); - stdout - .write_all(format!("Pseudo-random u64 is {r}\n").as_bytes()) - .unwrap(); + writeln!(stdout, "Pseudo-random u64 is {r}").unwrap(); stdout.flush().unwrap(); } From a1963fa379ac4039275021b22796f2873cb66720 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Mon, 13 May 2024 16:09:49 +0200 Subject: [PATCH 6/6] feat(http): add `http` crate integration Signed-off-by: Roman Volosatovs --- .github/workflows/main.yml | 8 ++- Cargo.toml | 6 +- examples/http-proxy.rs | 27 +++++++-- src/ext/http.rs | 119 +++++++++++++++++++++++++++++++++++++ src/ext/mod.rs | 3 + 5 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 src/ext/http.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index df3f87b..bdbe2e8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -51,9 +51,6 @@ jobs: - run: wasm-tools component new ./target/wasm32-unknown-unknown/debug/examples/cli_command.wasm -o component.wasm - run: wasmtime run component.wasm - - run: wasm-tools component new ./target/wasm32-unknown-unknown/debug/examples/http_proxy.wasm -o component.wasm - - run: wasm-tools component targets wit component.wasm -w wasi:http/proxy - - run: cargo build --examples --target wasm32-wasi --no-default-features --features rand - run: wasm-tools component new ./target/wasm32-wasi/debug/examples/rand-no_std.wasm --adapt ./wasi_snapshot_preview1.command.wasm -o component.wasm @@ -64,6 +61,11 @@ jobs: - run: wasm-tools component new ./target/wasm32-wasi/debug/examples/rand.wasm --adapt ./wasi_snapshot_preview1.command.wasm -o component.wasm - run: wasmtime run component.wasm + - run: cargo build --examples --target wasm32-unknown-unknown --features http + + - run: wasm-tools component new ./target/wasm32-unknown-unknown/debug/examples/http_proxy.wasm -o component.wasm + - run: wasm-tools component targets wit component.wasm -w wasi:http/proxy + rustfmt: name: Rustfmt runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 64e6dc7..f439e6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,12 +19,16 @@ compiler_builtins = { version = "0.1", optional = true } core = { version = "1.0", optional = true, package = "rustc-std-workspace-core" } rustc-std-workspace-alloc = { version = "1.0", optional = true } +# When built with `http` crate integration +http = { version = "1.1", default-features = false, optional = true } + # When built with `rand` crate integration rand = { version = "0.8.5", default-features = false, optional = true } [features] default = ["std"] std = [] +http = ["dep:http", "http/std", "std"] # Unstable feature to support being a libstd dependency rustc-dep-of-std = ["compiler_builtins", "core", "rustc-std-workspace-alloc"] @@ -48,7 +52,7 @@ crate-type = ["cdylib"] [[example]] name = "http-proxy" crate-type = ["cdylib"] -required-features = ["std"] +required-features = ["http", "std"] [[example]] name = "rand-no_std" diff --git a/examples/http-proxy.rs b/examples/http-proxy.rs index 8226e8b..48c559a 100644 --- a/examples/http-proxy.rs +++ b/examples/http-proxy.rs @@ -1,21 +1,38 @@ use std::io::Write as _; -use wasi::http::types::{ - Fields, IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam, -}; +use wasi::http::types::{IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam}; wasi::http::proxy::export!(Example); struct Example; impl wasi::exports::http::incoming_handler::Guest for Example { - fn handle(_request: IncomingRequest, response_out: ResponseOutparam) { - let resp = OutgoingResponse::new(Fields::new()); + fn handle(request: IncomingRequest, response_out: ResponseOutparam) { + let req_headers = + http::HeaderMap::try_from(request.headers()).expect("failed to parse headers"); + let mut resp_headers = http::HeaderMap::new(); + for (name, value) in req_headers.iter() { + // Append `-orig` to all request headers and send them back to the client + resp_headers.append( + http::HeaderName::try_from(format!("{name}-orig")).unwrap(), + value.clone(), + ); + } + let resp = OutgoingResponse::new(resp_headers.into()); let body = resp.body().unwrap(); ResponseOutparam::set(response_out, Ok(resp)); let mut out = body.write().unwrap(); + + let method = http::Method::try_from(request.method()).unwrap(); + writeln!(out, "method: {method}").unwrap(); + + if let Some(scheme) = request.scheme() { + let scheme = http::uri::Scheme::try_from(scheme).unwrap(); + writeln!(out, "scheme: {scheme}").unwrap(); + } + out.write_all(b"Hello, WASI!").unwrap(); out.flush().unwrap(); drop(out); diff --git a/src/ext/http.rs b/src/ext/http.rs new file mode 100644 index 0000000..91b6f7c --- /dev/null +++ b/src/ext/http.rs @@ -0,0 +1,119 @@ +pub use http; + +use core::fmt::Display; + +impl From for crate::http::types::Method { + fn from(method: http::Method) -> Self { + use std::string::ToString; + + match method.as_str() { + "GET" => Self::Get, + "HEAD" => Self::Head, + "POST" => Self::Post, + "PUT" => Self::Put, + "DELETE" => Self::Delete, + "CONNECT" => Self::Connect, + "OPTIONS" => Self::Options, + "TRACE" => Self::Trace, + "PATCH" => Self::Patch, + _ => Self::Other(method.to_string()), + } + } +} + +impl TryFrom for http::Method { + type Error = http::method::InvalidMethod; + + fn try_from(method: crate::http::types::Method) -> Result { + match method { + crate::http::types::Method::Get => Ok(Self::GET), + crate::http::types::Method::Head => Ok(Self::HEAD), + crate::http::types::Method::Post => Ok(Self::POST), + crate::http::types::Method::Put => Ok(Self::PUT), + crate::http::types::Method::Delete => Ok(Self::DELETE), + crate::http::types::Method::Connect => Ok(Self::CONNECT), + crate::http::types::Method::Options => Ok(Self::OPTIONS), + crate::http::types::Method::Trace => Ok(Self::TRACE), + crate::http::types::Method::Patch => Ok(Self::PATCH), + crate::http::types::Method::Other(method) => method.parse(), + } + } +} + +impl From for crate::http::types::Scheme { + fn from(scheme: http::uri::Scheme) -> Self { + use std::string::ToString; + + match scheme.as_str() { + "http" => Self::Http, + "https" => Self::Https, + _ => Self::Other(scheme.to_string()), + } + } +} + +impl TryFrom for http::uri::Scheme { + type Error = http::uri::InvalidUri; + + fn try_from(scheme: crate::http::types::Scheme) -> Result { + match scheme { + crate::http::types::Scheme::Http => Ok(Self::HTTP), + crate::http::types::Scheme::Https => Ok(Self::HTTPS), + crate::http::types::Scheme::Other(scheme) => scheme.parse(), + } + } +} + +#[derive(Debug)] +pub enum FieldsToHeaderMapError { + InvalidHeaderName(http::header::InvalidHeaderName), + InvalidHeaderValue(http::header::InvalidHeaderValue), +} + +impl Display for FieldsToHeaderMapError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + FieldsToHeaderMapError::InvalidHeaderName(e) => write!(f, "invalid header name: {e}"), + FieldsToHeaderMapError::InvalidHeaderValue(e) => write!(f, "invalid header value: {e}"), + } + } +} + +impl std::error::Error for FieldsToHeaderMapError {} + +impl TryFrom for http::HeaderMap { + type Error = FieldsToHeaderMapError; + + fn try_from(fields: crate::http::types::Fields) -> Result { + let mut headers = http::HeaderMap::new(); + for (name, value) in fields.entries() { + let name = http::HeaderName::try_from(name) + .map_err(FieldsToHeaderMapError::InvalidHeaderName)?; + let value = http::HeaderValue::try_from(value) + .map_err(FieldsToHeaderMapError::InvalidHeaderValue)?; + match headers.entry(name) { + http::header::Entry::Vacant(entry) => { + entry.insert(value); + } + http::header::Entry::Occupied(mut entry) => { + entry.append(value); + } + }; + } + Ok(headers) + } +} + +impl From for crate::http::types::Fields { + fn from(headers: http::HeaderMap) -> Self { + use std::string::ToString; + + let fields = crate::http::types::Fields::new(); + for (name, value) in headers.iter() { + fields + .append(&name.to_string(), &value.as_bytes().to_vec()) + .expect("failed to append header") + } + fields + } +} diff --git a/src/ext/mod.rs b/src/ext/mod.rs index c982618..f632a13 100644 --- a/src/ext/mod.rs +++ b/src/ext/mod.rs @@ -1,6 +1,9 @@ #[cfg(feature = "std")] mod std; +#[cfg(feature = "http")] +pub mod http; + #[cfg(feature = "rand")] pub mod rand;