Skip to content

Commit

Permalink
Implement multipart/form-data deserialization, refactor FormData to u…
Browse files Browse the repository at this point in the history
…se File instead of Blob
  • Loading branch information
Arshia001 committed Jan 9, 2024
1 parent d05b749 commit 6def586
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 42 deletions.
42 changes: 36 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ sourcemap.workspace = true
url.workspace = true
hyper-multipart-rfc7578 = "0.8.0"
as-any = "0.3.1"
multer = "3.0.0"

[dependencies.async-recursion]
version = "1.0.5"
Expand Down
75 changes: 67 additions & 8 deletions runtime/src/globals/fetch/body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

use std::convert::Infallible;
use std::fmt;
use std::fmt::{Display, Formatter};

use bytes::Bytes;
use futures::StreamExt;
use futures::{StreamExt, stream};
use hyper::Body;
use hyper::body::HttpBody;
use ion::class::NativeObject;
Expand All @@ -24,7 +25,7 @@ use ion::{
};
use ion::conversions::{FromValue, ToValue};

use crate::globals::file::{Blob, buffer_source_to_bytes};
use crate::globals::file::{Blob, buffer_source_to_bytes, File, BlobPart, FileOptions, BlobOptions};
use crate::globals::form_data::{FormData, FormDataEntryValue};
use crate::globals::streams::{NativeStreamSourceCallbacks, NativeStreamSource};
use crate::globals::url::URLSearchParams;
Expand Down Expand Up @@ -190,10 +191,63 @@ impl FetchBody {

Ok(FormData::new_object(&cx, Box::new(form_data)))
} else if content_type.starts_with("multipart/form-data") {
Err(Error::new(
"multipart/form-data deserialization is not supported yet",
ErrorKind::Normal,
))
const MULTIPART_HEADER_WITH_BOUNDARY: &str = "multipart/form-data; boundary=";
if !content_type.starts_with(MULTIPART_HEADER_WITH_BOUNDARY) {
return Err(Error::new(
"multipart/form-data content without a boundary, cannot parse",
ErrorKind::Normal,
));
}

let boundary = &content_type.as_str()[MULTIPART_HEADER_WITH_BOUNDARY.len()..];

// TODO: read asynchronously from stream bodies. This is complicated by the fact
// that multer expects a `Send` stream, but we're running on a single-threaded
// runtime.
let mut parser = multer::Multipart::new(
stream::once(async { std::result::Result::<_, Infallible>::Ok(bytes) }),
boundary,
);

let mut form_data = FormData::constructor();
let mut cx = cx;

while let Some(next) = parser.next_field().await? {
let name = next.name().unwrap_or("").to_string();
let file_name = next.file_name().map(|f| f.to_string());
let content_type = next.content_type().map(|c| c.to_string());

let bytes;
(cx, bytes) = cx.await_native(next.bytes()).await;
let bytes = bytes.map_err(|_| Error::new("Failed to read form data from body", ErrorKind::Normal))?;

match file_name {
Some(file_name) => {
let file = File::constructor(
vec![BlobPart(bytes)],
file_name,
Some(FileOptions {
modified: None,
blob: BlobOptions {
endings: crate::globals::file::Endings::Transparent,
kind: content_type,
},
}),
);
let file = cx.root_object(File::new_object(&cx, Box::new(file))).into();

form_data.append_native_file(name, File::get_private(&file));
}

None => form_data.append_native_string(
name,
String::from_utf8(bytes.into())
.map_err(|_| Error::new("String contains invalid UTF-8 sequence", ErrorKind::Normal))?,
),
}
}

Ok(FormData::new_object(&cx, Box::new(form_data)))
} else {
Err(Error::new(
"Invalid content-type, cannot decide form data format",
Expand Down Expand Up @@ -271,9 +325,14 @@ impl<'cx> FromValue<'cx> for FetchBody {
for kv in form_data.all_pairs() {
match &kv.value {
FormDataEntryValue::String(str) => form.add_text(kv.key.as_str(), str),
FormDataEntryValue::File(bytes, name) => {
FormDataEntryValue::File(file) => {
// TODO: remove to_vec call
form.add_reader_file(kv.key.as_str(), std::io::Cursor::new(bytes.to_vec()), name.as_str())
let file = File::get_private(&file.root(cx).into());
form.add_reader_file(
kv.key.as_str(),
std::io::Cursor::new(file.blob.as_bytes().to_vec()),
&file.name,
)
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions runtime/src/globals/file/blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub fn buffer_source_to_bytes(object: &Object) -> Result<Bytes> {
}

#[derive(Clone, Debug, Default)]
pub struct BlobPart(Bytes);
pub struct BlobPart(pub Bytes);

impl<'cx> FromValue<'cx> for BlobPart {
type Config = ();
Expand Down Expand Up @@ -83,9 +83,9 @@ impl<'cx> FromValue<'cx> for Endings {
#[derive(Debug, Default, FromValue)]
pub struct BlobOptions {
#[ion(name = "type")]
kind: Option<String>,
pub kind: Option<String>,
#[ion(default)]
endings: Endings,
pub endings: Endings,
}

#[js_class]
Expand Down
13 changes: 6 additions & 7 deletions runtime/src/globals/file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
use chrono::{DateTime, TimeZone, Utc};
use mozjs::conversions::ConversionBehavior;

pub use blob::{Blob, buffer_source_to_bytes};
pub use blob::{Blob, buffer_source_to_bytes, BlobPart, BlobOptions, Endings};
use ion::{ClassDefinition, Context, Object};

use crate::globals::file::blob::{BlobOptions, BlobPart};
use crate::globals::file::reader::{FileReader, FileReaderSync};

mod blob;
Expand All @@ -19,17 +18,17 @@ mod reader;
#[derive(Debug, Default, FromValue)]
pub struct FileOptions {
#[ion(inherit)]
blob: BlobOptions,
pub blob: BlobOptions,
#[ion(convert = ConversionBehavior::Default)]
modified: Option<i64>,
pub modified: Option<i64>,
}

#[js_class]
pub struct File {
blob: Blob,
name: String,
pub blob: Blob,
pub name: String,
#[ion(no_trace)]
modified: DateTime<Utc>,
pub modified: DateTime<Utc>,
}

#[js_class]
Expand Down
73 changes: 55 additions & 18 deletions runtime/src/globals/form_data.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
use bytes::Bytes;
use ion::{
Context, Object,
conversions::{FromValue, ToValue},
ClassDefinition, Result, Error, ErrorKind, JSIterator,
symbol::WellKnownSymbolCode,
class::Reflector,
class::{Reflector, NativeObject},
TracedHeap,
};
use mozjs::jsapi::JSObject;

use super::file::Blob;
use super::file::{Blob, File, BlobPart, FileOptions, BlobOptions, Endings};

// TODO: maintain the same File instance instead of Bytes
#[derive(Clone)]
pub enum FormDataEntryValue {
String(String),
File(Bytes, String),
File(TracedHeap<*mut JSObject>),
}

impl FormDataEntryValue {
Expand All @@ -22,17 +23,50 @@ impl FormDataEntryValue {
) -> Result<Self> {
if value.get().is_string() {
let str = String::from_value(cx, value, false, ())?;
match file_name {
None => Ok(Self::String(str)),
Some(file_name) => Ok(Self::File(str.into_bytes().into(), file_name)),
}
Ok(Self::String(str))
} else if value.get().is_object() && Blob::instance_of(cx, &value.to_object(cx), None) {
let obj = value.to_object(cx);
let blob = Blob::get_private(&obj);
Ok(Self::File(
blob.as_bytes().clone(),
file_name.unwrap_or_else(|| "blob".to_string()),
))
let file = if File::instance_of(cx, &obj, None) {
if let Some(name) = file_name {
let file = File::get_private(&obj);
cx.root_object(File::new_object(
cx,
Box::new(File::constructor(
vec![BlobPart(file.blob.as_bytes().clone())],
name,
Some(FileOptions {
modified: Some(file.get_last_modified()),
blob: BlobOptions {
kind: file.blob.kind(),
endings: Endings::Transparent,
},
}),
)),
))
.into()
} else {
obj
}
} else {
let name = file_name.unwrap_or("blob".to_string());
let blob = Blob::get_private(&obj);
cx.root_object(File::new_object(
cx,
Box::new(File::constructor(
vec![BlobPart(blob.as_bytes().clone())],
name,
Some(FileOptions {
modified: None,
blob: BlobOptions {
kind: blob.kind(),
endings: Endings::Transparent,
},
}),
)),
))
.into()
};
Ok(Self::File(TracedHeap::from_local(&file)))
} else {
Err(Error::new("FormData value must be a string or a Blob", ErrorKind::Type))
}
Expand All @@ -43,11 +77,7 @@ impl<'cx> ToValue<'cx> for FormDataEntryValue {
fn to_value(&self, cx: &'cx Context, value: &mut ion::Value) {
match self {
Self::String(s) => s.to_value(cx, value),
// TODO: this should return a file, not a blob
Self::File(bytes, _name) => {
let blob = Blob::new_object(cx, Box::new(Blob::new(bytes.clone())));
value.handle_mut().set(blob.as_value(cx).get());
}
Self::File(obj) => obj.get().to_value(cx, value),
}
}
}
Expand Down Expand Up @@ -77,6 +107,13 @@ impl FormData {
value: FormDataEntryValue::String(value),
});
}

pub fn append_native_file(&mut self, key: String, value: &File) {
self.kv_pairs.push(KvPair {
key,
value: FormDataEntryValue::File(TracedHeap::new(value.reflector().get())),
});
}
}

#[js_class]
Expand Down

0 comments on commit 6def586

Please sign in to comment.