From 0ef1a2ea78eaa5aeb280fd1dbbbabb83abc45c30 Mon Sep 17 00:00:00 2001 From: Niklas Wolber Date: Fri, 19 Nov 2021 01:13:59 +0100 Subject: [PATCH] wasm: fix standalone/multipart body conversion to JsValue (#1364) --- Cargo.toml | 3 +- src/wasm/body.rs | 136 ++++++++++++++++++++++++++++++++++++++---- src/wasm/multipart.rs | 86 +++++++++++++++++++++++++- 3 files changed, 209 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e50bf7a..5431dd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -166,7 +166,8 @@ features = [ "Blob", "BlobPropertyBag", "ServiceWorkerGlobalScope", - "RequestCredentials" + "RequestCredentials", + "File" ] [target.'cfg(target_arch = "wasm32")'.dev-dependencies] diff --git a/src/wasm/body.rs b/src/wasm/body.rs index 9d1ce0d..b485f8b 100644 --- a/src/wasm/body.rs +++ b/src/wasm/body.rs @@ -19,8 +19,12 @@ pub struct Body { enum Inner { Bytes(Bytes), + /// MultipartForm holds a multipart/form-data body. #[cfg(feature = "multipart")] - Multipart(Form), + MultipartForm(Form), + /// MultipartPart holds the body of a multipart/form-data part. + #[cfg(feature = "multipart")] + MultipartPart(Bytes), } impl Body { @@ -32,7 +36,9 @@ impl Body { match &self.inner { Inner::Bytes(bytes) => Some(bytes.as_ref()), #[cfg(feature = "multipart")] - Inner::Multipart(_) => None, + Inner::MultipartForm(_) => None, + #[cfg(feature = "multipart")] + Inner::MultipartPart(bytes) => Some(bytes.as_ref()), } } pub(crate) fn to_js_value(&self) -> crate::Result { @@ -44,11 +50,20 @@ impl Body { Ok(js_value.to_owned()) } #[cfg(feature = "multipart")] - Inner::Multipart(form) => { + Inner::MultipartForm(form) => { let form_data = form.to_form_data()?; let js_value: &JsValue = form_data.as_ref(); Ok(js_value.to_owned()) } + #[cfg(feature = "multipart")] + Inner::MultipartPart(body_bytes) => { + let body_bytes: &[u8] = body_bytes.as_ref(); + let body_uint8_array: Uint8Array = body_bytes.into(); + let body_array = js_sys::Array::new(); + body_array.push(&body_uint8_array); + let js_value: &JsValue = body_array.as_ref(); + Ok(js_value.to_owned()) + } } } @@ -56,7 +71,23 @@ impl Body { #[cfg(feature = "multipart")] pub(crate) fn from_form(f: Form) -> Body { Self { - inner: Inner::Multipart(f), + inner: Inner::MultipartForm(f), + } + } + + /// into_part turns a regular body into the body of a mutlipart/form-data part. + #[cfg(feature = "multipart")] + pub(crate) fn into_part(self) -> Body { + match self.inner { + Inner::Bytes(bytes) => Self { + inner: Inner::MultipartPart(bytes), + }, + Inner::MultipartForm(form) => Self { + inner: Inner::MultipartForm(form), + }, + Inner::MultipartPart(bytes) => Self { + inner: Inner::MultipartPart(bytes), + }, } } @@ -64,7 +95,9 @@ impl Body { match &self.inner { Inner::Bytes(bytes) => bytes.is_empty(), #[cfg(feature = "multipart")] - Inner::Multipart(form) => form.is_empty(), + Inner::MultipartForm(form) => form.is_empty(), + #[cfg(feature = "multipart")] + Inner::MultipartPart(bytes) => bytes.is_empty(), } } @@ -74,7 +107,11 @@ impl Body { inner: Inner::Bytes(bytes.clone()), }), #[cfg(feature = "multipart")] - Inner::Multipart(_) => None, + Inner::MultipartForm(_) => None, + #[cfg(feature = "multipart")] + Inner::MultipartPart(bytes) => Some(Self { + inner: Inner::MultipartPart(bytes.clone()), + }), } } } @@ -130,7 +167,8 @@ impl fmt::Debug for Body { #[cfg(test)] mod tests { - // use js_sys::{Array, Uint8Array}; + use crate::Body; + use js_sys::Uint8Array; use wasm_bindgen::prelude::*; use wasm_bindgen_test::*; @@ -146,16 +184,12 @@ mod tests { #[wasm_bindgen_test] async fn test_body() { - use crate::Body; - let body = Body::from("TEST"); assert_eq!([84, 69, 83, 84], body.as_bytes().unwrap()); } #[wasm_bindgen_test] - async fn test_body_js() { - use crate::Body; - + async fn test_body_js_static_str() { let body_value = "TEST"; let body = Body::from(body_value); @@ -176,4 +210,82 @@ mod tests { assert_eq!(text.as_string().expect("text is not a string"), body_value); } + #[wasm_bindgen_test] + async fn test_body_js_string() { + let body_value = "TEST".to_string(); + let body = Body::from(body_value.clone()); + + let mut init = web_sys::RequestInit::new(); + init.method("POST"); + init.body(Some( + body.to_js_value() + .expect("could not convert body to JsValue") + .as_ref(), + )); + + let js_req = web_sys::Request::new_with_str_and_init("", &init) + .expect("could not create JS request"); + let text_promise = js_req.text().expect("could not get text promise"); + let text = crate::wasm::promise::(text_promise) + .await + .expect("could not get request body as text"); + + assert_eq!(text.as_string().expect("text is not a string"), body_value); + } + + #[wasm_bindgen_test] + async fn test_body_js_static_u8_slice() { + let body_value: &'static [u8] = b"\x00\x42"; + let body = Body::from(body_value); + + let mut init = web_sys::RequestInit::new(); + init.method("POST"); + init.body(Some( + body.to_js_value() + .expect("could not convert body to JsValue") + .as_ref(), + )); + + let js_req = web_sys::Request::new_with_str_and_init("", &init) + .expect("could not create JS request"); + + let array_buffer_promise = js_req + .array_buffer() + .expect("could not get array_buffer promise"); + let array_buffer = crate::wasm::promise::(array_buffer_promise) + .await + .expect("could not get request body as array buffer"); + + let v = Uint8Array::new(&array_buffer).to_vec(); + + assert_eq!(v, body_value); + } + + #[wasm_bindgen_test] + async fn test_body_js_vec_u8() { + let body_value = vec![0u8, 42]; + let body = Body::from(body_value.clone()); + + let mut init = web_sys::RequestInit::new(); + init.method("POST"); + init.body(Some( + body.to_js_value() + .expect("could not convert body to JsValue") + .as_ref(), + )); + + let js_req = web_sys::Request::new_with_str_and_init("", &init) + .expect("could not create JS request"); + + let array_buffer_promise = js_req + .array_buffer() + .expect("could not get array_buffer promise"); + let array_buffer = crate::wasm::promise::(array_buffer_promise) + .await + .expect("could not get request body as array buffer"); + + let v = Uint8Array::new(&array_buffer).to_vec(); + + assert_eq!(v, body_value); + } } diff --git a/src/wasm/multipart.rs b/src/wasm/multipart.rs index 1e6e798..909875c 100644 --- a/src/wasm/multipart.rs +++ b/src/wasm/multipart.rs @@ -150,7 +150,7 @@ impl Part { fn new(value: Body) -> Part { Part { meta: PartMetadata::new(), - value, + value: value.into_part(), } } @@ -191,7 +191,7 @@ impl Part { } // BUG: the return value of to_js_value() is not valid if - // it is a Multipart variant. + // it is a MultipartForm variant. let js_value = self.value.to_js_value()?; Blob::new_with_u8_array_sequence_and_options(&js_value, &properties) .map_err(crate::error::wasm) @@ -277,4 +277,84 @@ impl PartMetadata { } #[cfg(test)] -mod tests {} +mod tests { + + use wasm_bindgen_test::*; + + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + + #[wasm_bindgen_test] + async fn test_multipart_js() { + use super::{Form, Part}; + use js_sys::Uint8Array; + use wasm_bindgen::JsValue; + use web_sys::{File, FormData}; + + let text_file_name = "test.txt"; + let text_file_type = "text/plain"; + let text_content = "TEST"; + let text_part = Part::text(text_content) + .file_name(text_file_name) + .mime_str(text_file_type) + .expect("invalid mime type"); + + let binary_file_name = "binary.bin"; + let binary_file_type = "application/octet-stream"; + let binary_content = vec![0u8, 42]; + let binary_part = Part::bytes(binary_content.clone()) + .file_name(binary_file_name) + .mime_str(binary_file_type) + .expect("invalid mime type"); + + let text_name = "text part"; + let binary_name = "binary part"; + let form = Form::new() + .part(text_name, text_part) + .part(binary_name, binary_part); + + let mut init = web_sys::RequestInit::new(); + init.method("POST"); + init.body(Some( + form.to_form_data() + .expect("could not convert to FormData") + .as_ref(), + )); + + let js_req = web_sys::Request::new_with_str_and_init("", &init) + .expect("could not create JS request"); + + let form_data_promise = js_req.form_data().expect("could not get form_data promise"); + + let form_data = crate::wasm::promise::(form_data_promise) + .await + .expect("could not get body as form data"); + + // check text part + let text_file = File::from(form_data.get(text_name)); + assert_eq!(text_file.name(), text_file_name); + assert_eq!(text_file.type_(), text_file_type); + + let text_promise = text_file.text(); + let text = crate::wasm::promise::(text_promise) + .await + .expect("could not get text body as text"); + assert_eq!( + text.as_string().expect("text is not a string"), + text_content + ); + + // check binary part + let binary_file = File::from(form_data.get(binary_name)); + assert_eq!(binary_file.name(), binary_file_name); + assert_eq!(binary_file.type_(), binary_file_type); + + let binary_array_buffer_promise = binary_file.array_buffer(); + let array_buffer = crate::wasm::promise::(binary_array_buffer_promise) + .await + .expect("could not get request body as array buffer"); + + let binary = Uint8Array::new(&array_buffer).to_vec(); + + assert_eq!(binary, binary_content); + } +}