,
+}
+
+impl Form {
+ pub(crate) fn is_empty(&self) -> bool {
+ self.inner.fields.is_empty()
+ }
+}
+
+/// A field in a multipart form.
+pub struct Part {
+ meta: PartMetadata,
+ value: Body,
+}
+
+pub(crate) struct FormParts {
+ pub(crate) fields: Vec<(Cow<'static, str>, P)>,
+}
+
+pub(crate) struct PartMetadata {
+ mime: Option,
+ file_name: Option>,
+ pub(crate) headers: HeaderMap,
+}
+
+pub(crate) trait PartProps {
+ fn metadata(&self) -> &PartMetadata;
+}
+
+// ===== impl Form =====
+
+impl Default for Form {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Form {
+ /// Creates a new async Form without any content.
+ pub fn new() -> Form {
+ Form {
+ inner: FormParts::new(),
+ }
+ }
+
+ /// Add a data field with supplied name and value.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// let form = reqwest::multipart::Form::new()
+ /// .text("username", "seanmonstar")
+ /// .text("password", "secret");
+ /// ```
+ pub fn text(self, name: T, value: U) -> Form
+ where
+ T: Into>,
+ U: Into>,
+ {
+ self.part(name, Part::text(value))
+ }
+
+ /// Adds a customized Part.
+ pub fn part(self, name: T, part: Part) -> Form
+ where
+ T: Into>,
+ {
+ self.with_inner(move |inner| inner.part(name, part))
+ }
+
+ fn with_inner(self, func: F) -> Self
+ where
+ F: FnOnce(FormParts) -> FormParts,
+ {
+ Form {
+ inner: func(self.inner),
+ }
+ }
+
+ pub(crate) fn to_form_data(&self) -> crate::Result {
+ let form = FormData::new()
+ .map_err(crate::error::wasm)
+ .map_err(crate::error::builder)?;
+
+ for (name, part) in self.inner.fields.iter() {
+ part.append_to_form(name, &form)
+ .map_err(crate::error::wasm)
+ .map_err(crate::error::builder)?;
+ }
+ Ok(form)
+ }
+}
+
+impl fmt::Debug for Form {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.inner.fmt_fields("Form", f)
+ }
+}
+
+// ===== impl Part =====
+
+impl Part {
+ /// Makes a text parameter.
+ pub fn text(value: T) -> Part
+ where
+ T: Into>,
+ {
+ let body = match value.into() {
+ Cow::Borrowed(slice) => Body::from(slice),
+ Cow::Owned(string) => Body::from(string),
+ };
+ Part::new(body)
+ }
+
+ /// Makes a new parameter from arbitrary bytes.
+ pub fn bytes(value: T) -> Part
+ where
+ T: Into>,
+ {
+ let body = match value.into() {
+ Cow::Borrowed(slice) => Body::from(slice),
+ Cow::Owned(vec) => Body::from(vec),
+ };
+ Part::new(body)
+ }
+
+ /// Makes a new parameter from an arbitrary stream.
+ pub fn stream>(value: T) -> Part {
+ Part::new(value.into())
+ }
+
+ fn new(value: Body) -> Part {
+ Part {
+ meta: PartMetadata::new(),
+ value: value.into_part(),
+ }
+ }
+
+ /// Tries to set the mime of this part.
+ pub fn mime_str(self, mime: &str) -> crate::Result {
+ Ok(self.mime(mime.parse().map_err(crate::error::builder)?))
+ }
+
+ // Re-export when mime 0.4 is available, with split MediaType/MediaRange.
+ fn mime(self, mime: Mime) -> Part {
+ self.with_inner(move |inner| inner.mime(mime))
+ }
+
+ /// Sets the filename, builder style.
+ pub fn file_name(self, filename: T) -> Part
+ where
+ T: Into>,
+ {
+ self.with_inner(move |inner| inner.file_name(filename))
+ }
+
+ /// Sets custom headers for the part.
+ pub fn headers(self, headers: HeaderMap) -> Part {
+ self.with_inner(move |inner| inner.headers(headers))
+ }
+
+ fn with_inner(self, func: F) -> Self
+ where
+ F: FnOnce(PartMetadata) -> PartMetadata,
+ {
+ Part {
+ meta: func(self.meta),
+ value: self.value,
+ }
+ }
+
+ fn append_to_form(
+ &self,
+ name: &str,
+ form: &web_sys::FormData,
+ ) -> Result<(), wasm_bindgen::JsValue> {
+ let single = self
+ .value
+ .as_single()
+ .expect("A part's body can't be multipart itself");
+
+ let mut mime_type = self.metadata().mime.as_ref();
+
+ // The JS fetch API doesn't support file names and mime types for strings. So we do our best
+ // effort to use `append_with_str` and fallback to `append_with_blob_*` if that's not
+ // possible.
+ if let super::body::Single::Text(text) = single {
+ if mime_type.is_none() || mime_type == Some(&mime_guess::mime::TEXT_PLAIN) {
+ if self.metadata().file_name.is_none() {
+ return form.append_with_str(name, text);
+ }
+ } else {
+ mime_type = Some(&mime_guess::mime::TEXT_PLAIN);
+ }
+ }
+
+ let blob = self.blob(mime_type)?;
+
+ if let Some(file_name) = &self.metadata().file_name {
+ form.append_with_blob_and_filename(name, &blob, file_name)
+ } else {
+ form.append_with_blob(name, &blob)
+ }
+ }
+
+ fn blob(&self, mime_type: Option<&Mime>) -> crate::Result {
+ use web_sys::Blob;
+ use web_sys::BlobPropertyBag;
+ let mut properties = BlobPropertyBag::new();
+ if let Some(mime) = mime_type {
+ properties.type_(mime.as_ref());
+ }
+
+ let js_value = self
+ .value
+ .as_single()
+ .expect("A part's body can't be set to a multipart body")
+ .to_js_value();
+
+ let body_array = js_sys::Array::new();
+ body_array.push(&js_value);
+
+ Blob::new_with_u8_array_sequence_and_options(body_array.as_ref(), &properties)
+ .map_err(crate::error::wasm)
+ .map_err(crate::error::builder)
+ }
+}
+
+impl fmt::Debug for Part {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let mut dbg = f.debug_struct("Part");
+ dbg.field("value", &self.value);
+ self.meta.fmt_fields(&mut dbg);
+ dbg.finish()
+ }
+}
+
+impl PartProps for Part {
+ fn metadata(&self) -> &PartMetadata {
+ &self.meta
+ }
+}
+
+// ===== impl FormParts =====
+
+impl FormParts {
+ pub(crate) fn new() -> Self {
+ FormParts { fields: Vec::new() }
+ }
+
+ /// Adds a customized Part.
+ pub(crate) fn part(mut self, name: T, part: P) -> Self
+ where
+ T: Into>,
+ {
+ self.fields.push((name.into(), part));
+ self
+ }
+}
+
+impl FormParts {
+ pub(crate) fn fmt_fields(&self, ty_name: &'static str, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct(ty_name)
+ .field("parts", &self.fields)
+ .finish()
+ }
+}
+
+// ===== impl PartMetadata =====
+
+impl PartMetadata {
+ pub(crate) fn new() -> Self {
+ PartMetadata {
+ mime: None,
+ file_name: None,
+ headers: HeaderMap::default(),
+ }
+ }
+
+ pub(crate) fn mime(mut self, mime: Mime) -> Self {
+ self.mime = Some(mime);
+ self
+ }
+
+ pub(crate) fn file_name(mut self, filename: T) -> Self
+ where
+ T: Into>,
+ {
+ self.file_name = Some(filename.into());
+ self
+ }
+
+ pub(crate) fn headers(mut self, headers: T) -> Self
+ where
+ T: Into,
+ {
+ self.headers = headers.into();
+ self
+ }
+}
+
+impl PartMetadata {
+ pub(crate) fn fmt_fields<'f, 'fa, 'fb>(
+ &self,
+ debug_struct: &'f mut fmt::DebugStruct<'fa, 'fb>,
+ ) -> &'f mut fmt::DebugStruct<'fa, 'fb> {
+ debug_struct
+ .field("mime", &self.mime)
+ .field("file_name", &self.file_name)
+ .field("headers", &self.headers)
+ }
+}
+
+#[cfg(test)]
+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 string_name = "string";
+ let string_content = "CONTENT";
+ let string_part = Part::text(string_content);
+
+ let text_name = "text part";
+ let binary_name = "binary part";
+ let form = Form::new()
+ .part(text_name, text_part)
+ .part(binary_name, binary_part)
+ .part(string_name, string_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);
+
+ // check string part
+ let string = form_data
+ .get(string_name)
+ .as_string()
+ .expect("content is not a string");
+ assert_eq!(string, string_content);
+
+ 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);
+ }
+}
diff --git a/src/wasm/component/request.rs b/src/wasm/component/request.rs
new file mode 100644
index 000000000..e6f51ebc1
--- /dev/null
+++ b/src/wasm/component/request.rs
@@ -0,0 +1,496 @@
+use std::convert::TryFrom;
+use std::fmt;
+
+use bytes::Bytes;
+use http::{request::Parts, Method, Request as HttpRequest};
+use serde::Serialize;
+#[cfg(feature = "json")]
+use serde_json;
+use url::Url;
+use web_sys::RequestCredentials;
+
+use super::{Body, Client, Response};
+use crate::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_TYPE};
+
+/// A request which can be executed with `Client::execute()`.
+pub struct Request {
+ method: Method,
+ url: Url,
+ headers: HeaderMap,
+ body: Option,
+ pub(super) cors: bool,
+ pub(super) credentials: Option,
+}
+
+/// A builder to construct the properties of a `Request`.
+pub struct RequestBuilder {
+ client: Client,
+ request: crate::Result,
+}
+
+impl Request {
+ /// Constructs a new request.
+ #[inline]
+ pub fn new(method: Method, url: Url) -> Self {
+ Request {
+ method,
+ url,
+ headers: HeaderMap::new(),
+ body: None,
+ cors: true,
+ credentials: None,
+ }
+ }
+
+ /// Get the method.
+ #[inline]
+ pub fn method(&self) -> &Method {
+ &self.method
+ }
+
+ /// Get a mutable reference to the method.
+ #[inline]
+ pub fn method_mut(&mut self) -> &mut Method {
+ &mut self.method
+ }
+
+ /// Get the url.
+ #[inline]
+ pub fn url(&self) -> &Url {
+ &self.url
+ }
+
+ /// Get a mutable reference to the url.
+ #[inline]
+ pub fn url_mut(&mut self) -> &mut Url {
+ &mut self.url
+ }
+
+ /// Get the headers.
+ #[inline]
+ pub fn headers(&self) -> &HeaderMap {
+ &self.headers
+ }
+
+ /// Get a mutable reference to the headers.
+ #[inline]
+ pub fn headers_mut(&mut self) -> &mut HeaderMap {
+ &mut self.headers
+ }
+
+ /// Get the body.
+ #[inline]
+ pub fn body(&self) -> Option<&Body> {
+ self.body.as_ref()
+ }
+
+ /// Get a mutable reference to the body.
+ #[inline]
+ pub fn body_mut(&mut self) -> &mut Option {
+ &mut self.body
+ }
+
+ /// Attempts to clone the `Request`.
+ ///
+ /// None is returned if a body is which can not be cloned.
+ pub fn try_clone(&self) -> Option {
+ let body = match self.body.as_ref() {
+ Some(body) => Some(body.try_clone()?),
+ None => None,
+ };
+
+ Some(Self {
+ method: self.method.clone(),
+ url: self.url.clone(),
+ headers: self.headers.clone(),
+ body,
+ cors: self.cors,
+ credentials: self.credentials,
+ })
+ }
+}
+
+impl RequestBuilder {
+ pub(super) fn new(client: Client, request: crate::Result) -> RequestBuilder {
+ RequestBuilder { client, request }
+ }
+
+ /// Assemble a builder starting from an existing `Client` and a `Request`.
+ pub fn from_parts(client: crate::Client, request: crate::Request) -> crate::RequestBuilder {
+ crate::RequestBuilder {
+ client,
+ request: crate::Result::Ok(request),
+ }
+ }
+
+ /// Modify the query string of the URL.
+ ///
+ /// Modifies the URL of this request, adding the parameters provided.
+ /// This method appends and does not overwrite. This means that it can
+ /// be called multiple times and that existing query parameters are not
+ /// overwritten if the same key is used. The key will simply show up
+ /// twice in the query string.
+ /// Calling `.query([("foo", "a"), ("foo", "b")])` gives `"foo=a&foo=b"`.
+ ///
+ /// # Note
+ /// This method does not support serializing a single key-value
+ /// pair. Instead of using `.query(("key", "val"))`, use a sequence, such
+ /// as `.query(&[("key", "val")])`. It's also possible to serialize structs
+ /// and maps into a key-value pair.
+ ///
+ /// # Errors
+ /// This method will fail if the object you provide cannot be serialized
+ /// into a query string.
+ pub fn query(mut self, query: &T) -> RequestBuilder {
+ let mut error = None;
+ if let Ok(ref mut req) = self.request {
+ let url = req.url_mut();
+ let mut pairs = url.query_pairs_mut();
+ let serializer = serde_urlencoded::Serializer::new(&mut pairs);
+
+ if let Err(err) = query.serialize(serializer) {
+ error = Some(crate::error::builder(err));
+ }
+ }
+ if let Ok(ref mut req) = self.request {
+ if let Some("") = req.url().query() {
+ req.url_mut().set_query(None);
+ }
+ }
+ if let Some(err) = error {
+ self.request = Err(err);
+ }
+ self
+ }
+
+ /// Send a form body.
+ ///
+ /// Sets the body to the url encoded serialization of the passed value,
+ /// and also sets the `Content-Type: application/x-www-form-urlencoded`
+ /// header.
+ ///
+ /// # Errors
+ ///
+ /// This method fails if the passed value cannot be serialized into
+ /// url encoded format
+ pub fn form(mut self, form: &T) -> RequestBuilder {
+ let mut error = None;
+ if let Ok(ref mut req) = self.request {
+ match serde_urlencoded::to_string(form) {
+ Ok(body) => {
+ req.headers_mut().insert(
+ CONTENT_TYPE,
+ HeaderValue::from_static("application/x-www-form-urlencoded"),
+ );
+ *req.body_mut() = Some(body.into());
+ }
+ Err(err) => error = Some(crate::error::builder(err)),
+ }
+ }
+ if let Some(err) = error {
+ self.request = Err(err);
+ }
+ self
+ }
+
+ #[cfg(feature = "json")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
+ /// Set the request json
+ pub fn json(mut self, json: &T) -> RequestBuilder {
+ let mut error = None;
+ if let Ok(ref mut req) = self.request {
+ match serde_json::to_vec(json) {
+ Ok(body) => {
+ req.headers_mut()
+ .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
+ *req.body_mut() = Some(body.into());
+ }
+ Err(err) => error = Some(crate::error::builder(err)),
+ }
+ }
+ if let Some(err) = error {
+ self.request = Err(err);
+ }
+ self
+ }
+
+ /// Enable HTTP basic authentication.
+ pub fn basic_auth(self, username: U, password: Option) -> RequestBuilder
+ where
+ U: fmt::Display,
+ P: fmt::Display,
+ {
+ let header_value = crate::util::basic_auth(username, password);
+ self.header(crate::header::AUTHORIZATION, header_value)
+ }
+
+ /// Enable HTTP bearer authentication.
+ pub fn bearer_auth(self, token: T) -> RequestBuilder
+ where
+ T: fmt::Display,
+ {
+ let header_value = format!("Bearer {token}");
+ self.header(crate::header::AUTHORIZATION, header_value)
+ }
+
+ /// Set the request body.
+ pub fn body>(mut self, body: T) -> RequestBuilder {
+ if let Ok(ref mut req) = self.request {
+ req.body = Some(body.into());
+ }
+ self
+ }
+
+ /// TODO
+ #[cfg(feature = "multipart")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "multipart")))]
+ pub fn multipart(mut self, multipart: super::multipart::Form) -> RequestBuilder {
+ if let Ok(ref mut req) = self.request {
+ *req.body_mut() = Some(Body::from_form(multipart))
+ }
+ self
+ }
+
+ /// Add a `Header` to this Request.
+ pub fn header(mut self, key: K, value: V) -> RequestBuilder
+ where
+ HeaderName: TryFrom,
+ >::Error: Into,
+ HeaderValue: TryFrom,
+ >::Error: Into,
+ {
+ let mut error = None;
+ if let Ok(ref mut req) = self.request {
+ match >::try_from(key) {
+ Ok(key) => match >::try_from(value) {
+ Ok(value) => {
+ req.headers_mut().append(key, value);
+ }
+ Err(e) => error = Some(crate::error::builder(e.into())),
+ },
+ Err(e) => error = Some(crate::error::builder(e.into())),
+ };
+ }
+ if let Some(err) = error {
+ self.request = Err(err);
+ }
+ self
+ }
+
+ /// Add a set of Headers to the existing ones on this Request.
+ ///
+ /// The headers will be merged in to any already set.
+ pub fn headers(mut self, headers: crate::header::HeaderMap) -> RequestBuilder {
+ if let Ok(ref mut req) = self.request {
+ crate::util::replace_headers(req.headers_mut(), headers);
+ }
+ self
+ }
+
+ /// Disable CORS on fetching the request.
+ ///
+ /// # WASM
+ ///
+ /// This option is only effective with WebAssembly target.
+ ///
+ /// The [request mode][mdn] will be set to 'no-cors'.
+ ///
+ /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request/mode
+ pub fn fetch_mode_no_cors(mut self) -> RequestBuilder {
+ if let Ok(ref mut req) = self.request {
+ req.cors = false;
+ }
+ self
+ }
+
+ /// Set fetch credentials to 'same-origin'
+ ///
+ /// # WASM
+ ///
+ /// This option is only effective with WebAssembly target.
+ ///
+ /// The [request credentials][mdn] will be set to 'same-origin'.
+ ///
+ /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
+ pub fn fetch_credentials_same_origin(mut self) -> RequestBuilder {
+ if let Ok(ref mut req) = self.request {
+ req.credentials = Some(RequestCredentials::SameOrigin);
+ }
+ self
+ }
+
+ /// Set fetch credentials to 'include'
+ ///
+ /// # WASM
+ ///
+ /// This option is only effective with WebAssembly target.
+ ///
+ /// The [request credentials][mdn] will be set to 'include'.
+ ///
+ /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
+ pub fn fetch_credentials_include(mut self) -> RequestBuilder {
+ if let Ok(ref mut req) = self.request {
+ req.credentials = Some(RequestCredentials::Include);
+ }
+ self
+ }
+
+ /// Set fetch credentials to 'omit'
+ ///
+ /// # WASM
+ ///
+ /// This option is only effective with WebAssembly target.
+ ///
+ /// The [request credentials][mdn] will be set to 'omit'.
+ ///
+ /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
+ pub fn fetch_credentials_omit(mut self) -> RequestBuilder {
+ if let Ok(ref mut req) = self.request {
+ req.credentials = Some(RequestCredentials::Omit);
+ }
+ self
+ }
+
+ /// Build a `Request`, which can be inspected, modified and executed with
+ /// `Client::execute()`.
+ pub fn build(self) -> crate::Result {
+ self.request
+ }
+
+ /// Build a `Request`, which can be inspected, modified and executed with
+ /// `Client::execute()`.
+ ///
+ /// This is similar to [`RequestBuilder::build()`], but also returns the
+ /// embedded `Client`.
+ pub fn build_split(self) -> (Client, crate::Result) {
+ (self.client, self.request)
+ }
+
+ /// Constructs the Request and sends it to the target URL, returning a
+ /// future Response.
+ ///
+ /// # Errors
+ ///
+ /// This method fails if there was an error while sending request.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # use reqwest::Error;
+ /// #
+ /// # async fn run() -> Result<(), Error> {
+ /// let response = reqwest::Client::new()
+ /// .get("https://hyper.rs")
+ /// .send()
+ /// .await?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub async fn send(self) -> crate::Result {
+ let req = self.request?;
+ self.client.execute_request(req).await
+ }
+
+ /// Attempt to clone the RequestBuilder.
+ ///
+ /// `None` is returned if the RequestBuilder can not be cloned.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use reqwest::Error;
+ /// #
+ /// # fn run() -> Result<(), Error> {
+ /// let client = reqwest::Client::new();
+ /// let builder = client.post("http://httpbin.org/post")
+ /// .body("from a &str!");
+ /// let clone = builder.try_clone();
+ /// assert!(clone.is_some());
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn try_clone(&self) -> Option {
+ self.request
+ .as_ref()
+ .ok()
+ .and_then(|req| req.try_clone())
+ .map(|req| RequestBuilder {
+ client: self.client.clone(),
+ request: Ok(req),
+ })
+ }
+}
+
+impl fmt::Debug for Request {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ fmt_request_fields(&mut f.debug_struct("Request"), self).finish()
+ }
+}
+
+impl fmt::Debug for RequestBuilder {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let mut builder = f.debug_struct("RequestBuilder");
+ match self.request {
+ Ok(ref req) => fmt_request_fields(&mut builder, req).finish(),
+ Err(ref err) => builder.field("error", err).finish(),
+ }
+ }
+}
+
+fn fmt_request_fields<'a, 'b>(
+ f: &'a mut fmt::DebugStruct<'a, 'b>,
+ req: &Request,
+) -> &'a mut fmt::DebugStruct<'a, 'b> {
+ f.field("method", &req.method)
+ .field("url", &req.url)
+ .field("headers", &req.headers)
+}
+
+impl TryFrom> for Request
+where
+ T: Into,
+{
+ type Error = crate::Error;
+
+ fn try_from(req: HttpRequest) -> crate::Result {
+ let (parts, body) = req.into_parts();
+ let Parts {
+ method,
+ uri,
+ headers,
+ ..
+ } = parts;
+ let url = Url::parse(&uri.to_string()).map_err(crate::error::builder)?;
+ Ok(Request {
+ method,
+ url,
+ headers,
+ body: Some(body.into()),
+ cors: true,
+ credentials: None,
+ })
+ }
+}
+
+impl TryFrom for HttpRequest {
+ type Error = crate::Error;
+
+ fn try_from(req: Request) -> crate::Result {
+ let Request {
+ method,
+ url,
+ headers,
+ body,
+ ..
+ } = req;
+
+ let mut req = HttpRequest::builder()
+ .method(method)
+ .uri(url.as_str())
+ .body(body.unwrap_or_else(|| Body::from(Bytes::default())))
+ .map_err(crate::error::builder)?;
+
+ *req.headers_mut() = headers;
+ Ok(req)
+ }
+}
diff --git a/src/wasm/component/response.rs b/src/wasm/component/response.rs
new file mode 100644
index 000000000..c703227e0
--- /dev/null
+++ b/src/wasm/component/response.rs
@@ -0,0 +1,236 @@
+use std::{fmt, io::Read as _};
+
+use bytes::Bytes;
+use http::{HeaderMap, StatusCode};
+use url::Url;
+
+#[cfg(feature = "stream")]
+use wasm_bindgen::JsCast;
+
+#[cfg(feature = "stream")]
+use futures_util::stream::StreamExt;
+
+#[cfg(feature = "json")]
+use serde::de::DeserializeOwned;
+
+use crate::wasm::component::bindings::wasi;
+
+// use super::AbortGuard;
+
+/// A Response to a submitted `Request`.
+pub struct Response {
+ http: http::Response,
+ // _abort: AbortGuard,
+ // Boxed to save space (11 words to 1 word), and it's not accessed
+ // frequently internally.
+ url: Box,
+}
+
+impl Response {
+ pub(super) fn new(
+ res: http::Response,
+ url: Url,
+ // abort: AbortGuard,
+ ) -> Response {
+ Response {
+ http: res,
+ url: Box::new(url),
+ // _abort: abort,
+ }
+ }
+
+ /// Get the `StatusCode` of this `Response`.
+ #[inline]
+ pub fn status(&self) -> StatusCode {
+ self.http.status()
+ }
+
+ /// Get the `Headers` of this `Response`.
+ #[inline]
+ pub fn headers(&self) -> &HeaderMap {
+ self.http.headers()
+ }
+
+ /// Get a mutable reference to the `Headers` of this `Response`.
+ #[inline]
+ pub fn headers_mut(&mut self) -> &mut HeaderMap {
+ self.http.headers_mut()
+ }
+
+ /// Get the content-length of this response, if known.
+ ///
+ /// Reasons it may not be known:
+ ///
+ /// - The server didn't send a `content-length` header.
+ /// - The response is compressed and automatically decoded (thus changing
+ /// the actual decoded length).
+ pub fn content_length(&self) -> Option {
+ self.headers()
+ .get(http::header::CONTENT_LENGTH)?
+ .to_str()
+ .ok()?
+ .parse()
+ .ok()
+ }
+
+ /// Get the final `Url` of this `Response`.
+ #[inline]
+ pub fn url(&self) -> &Url {
+ &self.url
+ }
+
+ /* It might not be possible to detect this in JS?
+ /// Get the HTTP `Version` of this `Response`.
+ #[inline]
+ pub fn version(&self) -> Version {
+ self.http.version()
+ }
+ */
+
+ /// Try to deserialize the response body as JSON.
+ #[cfg(feature = "json")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
+ pub async fn json(self) -> crate::Result {
+ let full = self.bytes().await?;
+
+ serde_json::from_slice(&full).map_err(crate::error::decode)
+ }
+
+ /// Get the response text.
+ pub async fn text(self) -> crate::Result {
+ // let p = self
+ // .http
+ // .body()
+ // .text()
+ // .map_err(crate::error::wasm)
+ // .map_err(crate::error::decode)?;
+ // let js_val = super::promise::(p)
+ // .await
+ // .map_err(crate::error::decode)?;
+ // if let Some(s) = js_val.as_string() {
+ // Ok(s)
+ // } else {
+ // Err(crate::error::decode("response.text isn't string"))
+ // }
+ Ok("str_resp".to_string())
+ }
+
+ /// Get the response as bytes
+ pub async fn bytes(self) -> crate::Result {
+ let response_body = self
+ .http
+ .body()
+ .consume()
+ .expect("failed to get incoming request body");
+ let body = {
+ let mut buf = vec![];
+ let mut stream = response_body
+ .stream()
+ .expect("failed to get HTTP request response stream");
+ InputStreamReader::from(&mut stream)
+ .read_to_end(&mut buf)
+ .expect("failed to read value from HTTP request response stream");
+ buf
+ };
+ let _trailers = wasi::http::types::IncomingBody::finish(response_body);
+ Ok(body.into())
+ }
+
+ /// Convert the response into a `Stream` of `Bytes` from the body.
+ #[cfg(feature = "stream")]
+ pub fn bytes_stream(self) -> impl futures_core::Stream- > {
+ let web_response = self.http.into_body();
+ let abort = self._abort;
+ let body = web_response
+ .body()
+ .expect("could not create wasm byte stream");
+ let body = wasm_streams::ReadableStream::from_raw(body.unchecked_into());
+ Box::pin(body.into_stream().map(move |buf_js| {
+ // Keep the abort guard alive as long as this stream is.
+ let _abort = &abort;
+ let buffer = Uint8Array::new(
+ &buf_js
+ .map_err(crate::error::wasm)
+ .map_err(crate::error::decode)?,
+ );
+ let mut bytes = vec![0; buffer.length() as usize];
+ buffer.copy_to(&mut bytes);
+ Ok(bytes.into())
+ }))
+ }
+
+ // util methods
+
+ /// Turn a response into an error if the server returned an error.
+ pub fn error_for_status(self) -> crate::Result {
+ let status = self.status();
+ if status.is_client_error() || status.is_server_error() {
+ Err(crate::error::status_code(*self.url, status))
+ } else {
+ Ok(self)
+ }
+ }
+
+ /// Turn a reference to a response into an error if the server returned an error.
+ pub fn error_for_status_ref(&self) -> crate::Result<&Self> {
+ let status = self.status();
+ if status.is_client_error() || status.is_server_error() {
+ Err(crate::error::status_code(*self.url.clone(), status))
+ } else {
+ Ok(self)
+ }
+ }
+}
+
+impl fmt::Debug for Response {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("Response")
+ //.field("url", self.url())
+ .field("status", &self.status())
+ .field("headers", self.headers())
+ .finish()
+ }
+}
+
+pub struct InputStreamReader<'a> {
+ stream: &'a mut crate::wasm::component::bindings::wasi::io::streams::InputStream,
+}
+
+impl<'a> From<&'a mut crate::wasm::component::bindings::wasi::io::streams::InputStream>
+ for InputStreamReader<'a>
+{
+ fn from(
+ stream: &'a mut crate::wasm::component::bindings::wasi::io::streams::InputStream,
+ ) -> Self {
+ Self { stream }
+ }
+}
+
+impl std::io::Read for InputStreamReader<'_> {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result {
+ use crate::wasm::component::bindings::wasi::io::streams::StreamError;
+ use std::io;
+
+ let n = buf
+ .len()
+ .try_into()
+ .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
+ match self.stream.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()))
+ }
+ }
+ }
+}
diff --git a/src/wasm/component/wit/deps.lock b/src/wasm/component/wit/deps.lock
new file mode 100644
index 000000000..5bd958bd5
--- /dev/null
+++ b/src/wasm/component/wit/deps.lock
@@ -0,0 +1,29 @@
+[cli]
+sha256 = "285865a31d777181b075f39e92bcfe59c89cd6bacce660be1b9a627646956258"
+sha512 = "da2622210a9e3eea82b99f1a5b8a44ce5443d009cb943f7bca0bf9cf4360829b289913d7ee727c011f0f72994ea7dc8e661ebcc0a6b34b587297d80cd9b3f7e8"
+
+[clocks]
+sha256 = "468b4d12892fe926b8eb5d398dbf579d566c93231fa44f415440572c695b7613"
+sha512 = "e6b53a07221f1413953c9797c68f08b815fdaebf66419bbc1ea3e8b7dece73731062693634731f311a03957b268cf9cc509c518bd15e513c318aa04a8459b93a"
+
+[filesystem]
+sha256 = "498c465cfd04587db40f970fff2185daa597d074c20b68a8bcbae558f261499b"
+sha512 = "ead452f9b7bfb88593a502ec00d76d4228003d51c40fd0408aebc32d35c94673551b00230d730873361567cc209ec218c41fb4e95bad194268592c49e7964347"
+
+[http]
+url = "https://github.com/WebAssembly/wasi-http/archive/v0.2.0.tar.gz"
+sha256 = "8f44402bde16c48e28c47dc53eab0b26af5b3b3482a1852cf77673e0880ba1c1"
+sha512 = "760695f9a25c25bf75a25b731cb21c3bda9e288e450edda823324ecbc73d5d798bbb5de2edad999566980836f037463ee9e57d61789d04b3f3e381475b1a9a0f"
+deps = ["cli", "clocks", "filesystem", "io", "random", "sockets"]
+
+[io]
+sha256 = "7210e5653539a15478f894d4da24cc69d61924cbcba21d2804d69314a88e5a4c"
+sha512 = "49184a1b0945a889abd52d25271172ed3dc2db6968fcdddb1bab7ee0081f4a3eeee0977ad2291126a37631c0d86eeea75d822fa8af224c422134500bf9f0f2bb"
+
+[random]
+sha256 = "7371d03c037d924caba2587fb2e7c5773a0d3c5fcecbf7971e0e0ba57973c53d"
+sha512 = "964c4e8925a53078e4d94ba907b54f89a0b7e154f46823a505391471466c17f53c8692682e5c85771712acd88b348686173fc07c53a3cfe3d301b8cd8ddd0de4"
+
+[sockets]
+sha256 = "622bd28bbeb43736375dc02bd003fd3a016ff8ee91e14bab488325c6b38bf966"
+sha512 = "5a63c1f36de0c4548e1d2297bdbededb28721cbad94ef7825c469eae29d7451c97e00b4c1d6730ee1ec0c4a5aac922961a2795762d4a0c3bb54e30a391a84bae"
diff --git a/src/wasm/component/wit/deps.toml b/src/wasm/component/wit/deps.toml
new file mode 100644
index 000000000..1b375eef8
--- /dev/null
+++ b/src/wasm/component/wit/deps.toml
@@ -0,0 +1 @@
+http = "https://github.com/WebAssembly/wasi-http/archive/v0.2.0.tar.gz"
diff --git a/src/wasm/component/wit/deps/cli/command.wit b/src/wasm/component/wit/deps/cli/command.wit
new file mode 100644
index 000000000..d8005bd38
--- /dev/null
+++ b/src/wasm/component/wit/deps/cli/command.wit
@@ -0,0 +1,7 @@
+package wasi:cli@0.2.0;
+
+world command {
+ include imports;
+
+ export run;
+}
diff --git a/src/wasm/component/wit/deps/cli/environment.wit b/src/wasm/component/wit/deps/cli/environment.wit
new file mode 100644
index 000000000..70065233e
--- /dev/null
+++ b/src/wasm/component/wit/deps/cli/environment.wit
@@ -0,0 +1,18 @@
+interface environment {
+ /// Get the POSIX-style environment variables.
+ ///
+ /// Each environment variable is provided as a pair of string variable names
+ /// and string value.
+ ///
+ /// Morally, these are a value import, but until value imports are available
+ /// in the component model, this import function should return the same
+ /// values each time it is called.
+ get-environment: func() -> list>;
+
+ /// Get the POSIX-style arguments to the program.
+ get-arguments: func() -> list;
+
+ /// Return a path that programs should use as their initial current working
+ /// directory, interpreting `.` as shorthand for this.
+ initial-cwd: func() -> option;
+}
diff --git a/src/wasm/component/wit/deps/cli/exit.wit b/src/wasm/component/wit/deps/cli/exit.wit
new file mode 100644
index 000000000..d0c2b82ae
--- /dev/null
+++ b/src/wasm/component/wit/deps/cli/exit.wit
@@ -0,0 +1,4 @@
+interface exit {
+ /// Exit the current instance and any linked instances.
+ exit: func(status: result);
+}
diff --git a/src/wasm/component/wit/deps/cli/imports.wit b/src/wasm/component/wit/deps/cli/imports.wit
new file mode 100644
index 000000000..083b84a03
--- /dev/null
+++ b/src/wasm/component/wit/deps/cli/imports.wit
@@ -0,0 +1,20 @@
+package wasi:cli@0.2.0;
+
+world imports {
+ include wasi:clocks/imports@0.2.0;
+ include wasi:filesystem/imports@0.2.0;
+ include wasi:sockets/imports@0.2.0;
+ include wasi:random/imports@0.2.0;
+ include wasi:io/imports@0.2.0;
+
+ import environment;
+ import exit;
+ import stdin;
+ import stdout;
+ import stderr;
+ import terminal-input;
+ import terminal-output;
+ import terminal-stdin;
+ import terminal-stdout;
+ import terminal-stderr;
+}
diff --git a/src/wasm/component/wit/deps/cli/run.wit b/src/wasm/component/wit/deps/cli/run.wit
new file mode 100644
index 000000000..a70ee8c03
--- /dev/null
+++ b/src/wasm/component/wit/deps/cli/run.wit
@@ -0,0 +1,4 @@
+interface run {
+ /// Run the program.
+ run: func() -> result;
+}
diff --git a/src/wasm/component/wit/deps/cli/stdio.wit b/src/wasm/component/wit/deps/cli/stdio.wit
new file mode 100644
index 000000000..31ef35b5a
--- /dev/null
+++ b/src/wasm/component/wit/deps/cli/stdio.wit
@@ -0,0 +1,17 @@
+interface stdin {
+ use wasi:io/streams@0.2.0.{input-stream};
+
+ get-stdin: func() -> input-stream;
+}
+
+interface stdout {
+ use wasi:io/streams@0.2.0.{output-stream};
+
+ get-stdout: func() -> output-stream;
+}
+
+interface stderr {
+ use wasi:io/streams@0.2.0.{output-stream};
+
+ get-stderr: func() -> output-stream;
+}
diff --git a/src/wasm/component/wit/deps/cli/terminal.wit b/src/wasm/component/wit/deps/cli/terminal.wit
new file mode 100644
index 000000000..38c724efc
--- /dev/null
+++ b/src/wasm/component/wit/deps/cli/terminal.wit
@@ -0,0 +1,49 @@
+/// Terminal input.
+///
+/// In the future, this may include functions for disabling echoing,
+/// disabling input buffering so that keyboard events are sent through
+/// immediately, querying supported features, and so on.
+interface terminal-input {
+ /// The input side of a terminal.
+ resource terminal-input;
+}
+
+/// Terminal output.
+///
+/// In the future, this may include functions for querying the terminal
+/// size, being notified of terminal size changes, querying supported
+/// features, and so on.
+interface terminal-output {
+ /// The output side of a terminal.
+ resource terminal-output;
+}
+
+/// An interface providing an optional `terminal-input` for stdin as a
+/// link-time authority.
+interface terminal-stdin {
+ use terminal-input.{terminal-input};
+
+ /// If stdin is connected to a terminal, return a `terminal-input` handle
+ /// allowing further interaction with it.
+ get-terminal-stdin: func() -> option;
+}
+
+/// An interface providing an optional `terminal-output` for stdout as a
+/// link-time authority.
+interface terminal-stdout {
+ use terminal-output.{terminal-output};
+
+ /// If stdout is connected to a terminal, return a `terminal-output` handle
+ /// allowing further interaction with it.
+ get-terminal-stdout: func() -> option;
+}
+
+/// An interface providing an optional `terminal-output` for stderr as a
+/// link-time authority.
+interface terminal-stderr {
+ use terminal-output.{terminal-output};
+
+ /// If stderr is connected to a terminal, return a `terminal-output` handle
+ /// allowing further interaction with it.
+ get-terminal-stderr: func() -> option;
+}
diff --git a/src/wasm/component/wit/deps/clocks/monotonic-clock.wit b/src/wasm/component/wit/deps/clocks/monotonic-clock.wit
new file mode 100644
index 000000000..4e4dc3a19
--- /dev/null
+++ b/src/wasm/component/wit/deps/clocks/monotonic-clock.wit
@@ -0,0 +1,45 @@
+package wasi:clocks@0.2.0;
+/// WASI Monotonic Clock is a clock API intended to let users measure elapsed
+/// time.
+///
+/// It is intended to be portable at least between Unix-family platforms and
+/// Windows.
+///
+/// A monotonic clock is a clock which has an unspecified initial value, and
+/// successive reads of the clock will produce non-decreasing values.
+///
+/// It is intended for measuring elapsed time.
+interface monotonic-clock {
+ use wasi:io/poll@0.2.0.{pollable};
+
+ /// An instant in time, in nanoseconds. An instant is relative to an
+ /// unspecified initial value, and can only be compared to instances from
+ /// the same monotonic-clock.
+ type instant = u64;
+
+ /// A duration of time, in nanoseconds.
+ type duration = u64;
+
+ /// Read the current value of the clock.
+ ///
+ /// The clock is monotonic, therefore calling this function repeatedly will
+ /// produce a sequence of non-decreasing values.
+ now: func() -> instant;
+
+ /// Query the resolution of the clock. Returns the duration of time
+ /// corresponding to a clock tick.
+ resolution: func() -> duration;
+
+ /// Create a `pollable` which will resolve once the specified instant
+ /// occured.
+ subscribe-instant: func(
+ when: instant,
+ ) -> pollable;
+
+ /// Create a `pollable` which will resolve once the given duration has
+ /// elapsed, starting at the time at which this function was called.
+ /// occured.
+ subscribe-duration: func(
+ when: duration,
+ ) -> pollable;
+}
diff --git a/src/wasm/component/wit/deps/clocks/wall-clock.wit b/src/wasm/component/wit/deps/clocks/wall-clock.wit
new file mode 100644
index 000000000..440ca0f33
--- /dev/null
+++ b/src/wasm/component/wit/deps/clocks/wall-clock.wit
@@ -0,0 +1,42 @@
+package wasi:clocks@0.2.0;
+/// WASI Wall Clock is a clock API intended to let users query the current
+/// time. The name "wall" makes an analogy to a "clock on the wall", which
+/// is not necessarily monotonic as it may be reset.
+///
+/// It is intended to be portable at least between Unix-family platforms and
+/// Windows.
+///
+/// A wall clock is a clock which measures the date and time according to
+/// some external reference.
+///
+/// External references may be reset, so this clock is not necessarily
+/// monotonic, making it unsuitable for measuring elapsed time.
+///
+/// It is intended for reporting the current date and time for humans.
+interface wall-clock {
+ /// A time and date in seconds plus nanoseconds.
+ record datetime {
+ seconds: u64,
+ nanoseconds: u32,
+ }
+
+ /// Read the current value of the clock.
+ ///
+ /// This clock is not monotonic, therefore calling this function repeatedly
+ /// will not necessarily produce a sequence of non-decreasing values.
+ ///
+ /// The returned timestamps represent the number of seconds since
+ /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch],
+ /// also known as [Unix Time].
+ ///
+ /// The nanoseconds field of the output is always less than 1000000000.
+ ///
+ /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16
+ /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time
+ now: func() -> datetime;
+
+ /// Query the resolution of the clock.
+ ///
+ /// The nanoseconds field of the output is always less than 1000000000.
+ resolution: func() -> datetime;
+}
diff --git a/src/wasm/component/wit/deps/clocks/world.wit b/src/wasm/component/wit/deps/clocks/world.wit
new file mode 100644
index 000000000..c0224572a
--- /dev/null
+++ b/src/wasm/component/wit/deps/clocks/world.wit
@@ -0,0 +1,6 @@
+package wasi:clocks@0.2.0;
+
+world imports {
+ import monotonic-clock;
+ import wall-clock;
+}
diff --git a/src/wasm/component/wit/deps/filesystem/preopens.wit b/src/wasm/component/wit/deps/filesystem/preopens.wit
new file mode 100644
index 000000000..da801f6d6
--- /dev/null
+++ b/src/wasm/component/wit/deps/filesystem/preopens.wit
@@ -0,0 +1,8 @@
+package wasi:filesystem@0.2.0;
+
+interface preopens {
+ use types.{descriptor};
+
+ /// Return the set of preopened directories, and their path.
+ get-directories: func() -> list>;
+}
diff --git a/src/wasm/component/wit/deps/filesystem/types.wit b/src/wasm/component/wit/deps/filesystem/types.wit
new file mode 100644
index 000000000..11108fcda
--- /dev/null
+++ b/src/wasm/component/wit/deps/filesystem/types.wit
@@ -0,0 +1,634 @@
+package wasi:filesystem@0.2.0;
+/// WASI filesystem is a filesystem API primarily intended to let users run WASI
+/// programs that access their files on their existing filesystems, without
+/// significant overhead.
+///
+/// It is intended to be roughly portable between Unix-family platforms and
+/// Windows, though it does not hide many of the major differences.
+///
+/// Paths are passed as interface-type `string`s, meaning they must consist of
+/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain
+/// paths which are not accessible by this API.
+///
+/// The directory separator in WASI is always the forward-slash (`/`).
+///
+/// All paths in WASI are relative paths, and are interpreted relative to a
+/// `descriptor` referring to a base directory. If a `path` argument to any WASI
+/// function starts with `/`, or if any step of resolving a `path`, including
+/// `..` and symbolic link steps, reaches a directory outside of the base
+/// directory, or reaches a symlink to an absolute or rooted path in the
+/// underlying filesystem, the function fails with `error-code::not-permitted`.
+///
+/// For more information about WASI path resolution and sandboxing, see
+/// [WASI filesystem path resolution].
+///
+/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md
+interface types {
+ use wasi:io/streams@0.2.0.{input-stream, output-stream, error};
+ use wasi:clocks/wall-clock@0.2.0.{datetime};
+
+ /// File size or length of a region within a file.
+ type filesize = u64;
+
+ /// The type of a filesystem object referenced by a descriptor.
+ ///
+ /// Note: This was called `filetype` in earlier versions of WASI.
+ enum descriptor-type {
+ /// The type of the descriptor or file is unknown or is different from
+ /// any of the other types specified.
+ unknown,
+ /// The descriptor refers to a block device inode.
+ block-device,
+ /// The descriptor refers to a character device inode.
+ character-device,
+ /// The descriptor refers to a directory inode.
+ directory,
+ /// The descriptor refers to a named pipe.
+ fifo,
+ /// The file refers to a symbolic link inode.
+ symbolic-link,
+ /// The descriptor refers to a regular file inode.
+ regular-file,
+ /// The descriptor refers to a socket.
+ socket,
+ }
+
+ /// Descriptor flags.
+ ///
+ /// Note: This was called `fdflags` in earlier versions of WASI.
+ flags descriptor-flags {
+ /// Read mode: Data can be read.
+ read,
+ /// Write mode: Data can be written to.
+ write,
+ /// Request that writes be performed according to synchronized I/O file
+ /// integrity completion. The data stored in the file and the file's
+ /// metadata are synchronized. This is similar to `O_SYNC` in POSIX.
+ ///
+ /// The precise semantics of this operation have not yet been defined for
+ /// WASI. At this time, it should be interpreted as a request, and not a
+ /// requirement.
+ file-integrity-sync,
+ /// Request that writes be performed according to synchronized I/O data
+ /// integrity completion. Only the data stored in the file is
+ /// synchronized. This is similar to `O_DSYNC` in POSIX.
+ ///
+ /// The precise semantics of this operation have not yet been defined for
+ /// WASI. At this time, it should be interpreted as a request, and not a
+ /// requirement.
+ data-integrity-sync,
+ /// Requests that reads be performed at the same level of integrety
+ /// requested for writes. This is similar to `O_RSYNC` in POSIX.
+ ///
+ /// The precise semantics of this operation have not yet been defined for
+ /// WASI. At this time, it should be interpreted as a request, and not a
+ /// requirement.
+ requested-write-sync,
+ /// Mutating directories mode: Directory contents may be mutated.
+ ///
+ /// When this flag is unset on a descriptor, operations using the
+ /// descriptor which would create, rename, delete, modify the data or
+ /// metadata of filesystem objects, or obtain another handle which
+ /// would permit any of those, shall fail with `error-code::read-only` if
+ /// they would otherwise succeed.
+ ///
+ /// This may only be set on directories.
+ mutate-directory,
+ }
+
+ /// File attributes.
+ ///
+ /// Note: This was called `filestat` in earlier versions of WASI.
+ record descriptor-stat {
+ /// File type.
+ %type: descriptor-type,
+ /// Number of hard links to the file.
+ link-count: link-count,
+ /// For regular files, the file size in bytes. For symbolic links, the
+ /// length in bytes of the pathname contained in the symbolic link.
+ size: filesize,
+ /// Last data access timestamp.
+ ///
+ /// If the `option` is none, the platform doesn't maintain an access
+ /// timestamp for this file.
+ data-access-timestamp: option,
+ /// Last data modification timestamp.
+ ///
+ /// If the `option` is none, the platform doesn't maintain a
+ /// modification timestamp for this file.
+ data-modification-timestamp: option,
+ /// Last file status-change timestamp.
+ ///
+ /// If the `option` is none, the platform doesn't maintain a
+ /// status-change timestamp for this file.
+ status-change-timestamp: option,
+ }
+
+ /// Flags determining the method of how paths are resolved.
+ flags path-flags {
+ /// As long as the resolved path corresponds to a symbolic link, it is
+ /// expanded.
+ symlink-follow,
+ }
+
+ /// Open flags used by `open-at`.
+ flags open-flags {
+ /// Create file if it does not exist, similar to `O_CREAT` in POSIX.
+ create,
+ /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX.
+ directory,
+ /// Fail if file already exists, similar to `O_EXCL` in POSIX.
+ exclusive,
+ /// Truncate file to size 0, similar to `O_TRUNC` in POSIX.
+ truncate,
+ }
+
+ /// Number of hard links to an inode.
+ type link-count = u64;
+
+ /// When setting a timestamp, this gives the value to set it to.
+ variant new-timestamp {
+ /// Leave the timestamp set to its previous value.
+ no-change,
+ /// Set the timestamp to the current time of the system clock associated
+ /// with the filesystem.
+ now,
+ /// Set the timestamp to the given value.
+ timestamp(datetime),
+ }
+
+ /// A directory entry.
+ record directory-entry {
+ /// The type of the file referred to by this directory entry.
+ %type: descriptor-type,
+
+ /// The name of the object.
+ name: string,
+ }
+
+ /// Error codes returned by functions, similar to `errno` in POSIX.
+ /// Not all of these error codes are returned by the functions provided by this
+ /// API; some are used in higher-level library layers, and others are provided
+ /// merely for alignment with POSIX.
+ enum error-code {
+ /// Permission denied, similar to `EACCES` in POSIX.
+ access,
+ /// Resource unavailable, or operation would block, similar to `EAGAIN` and `EWOULDBLOCK` in POSIX.
+ would-block,
+ /// Connection already in progress, similar to `EALREADY` in POSIX.
+ already,
+ /// Bad descriptor, similar to `EBADF` in POSIX.
+ bad-descriptor,
+ /// Device or resource busy, similar to `EBUSY` in POSIX.
+ busy,
+ /// Resource deadlock would occur, similar to `EDEADLK` in POSIX.
+ deadlock,
+ /// Storage quota exceeded, similar to `EDQUOT` in POSIX.
+ quota,
+ /// File exists, similar to `EEXIST` in POSIX.
+ exist,
+ /// File too large, similar to `EFBIG` in POSIX.
+ file-too-large,
+ /// Illegal byte sequence, similar to `EILSEQ` in POSIX.
+ illegal-byte-sequence,
+ /// Operation in progress, similar to `EINPROGRESS` in POSIX.
+ in-progress,
+ /// Interrupted function, similar to `EINTR` in POSIX.
+ interrupted,
+ /// Invalid argument, similar to `EINVAL` in POSIX.
+ invalid,
+ /// I/O error, similar to `EIO` in POSIX.
+ io,
+ /// Is a directory, similar to `EISDIR` in POSIX.
+ is-directory,
+ /// Too many levels of symbolic links, similar to `ELOOP` in POSIX.
+ loop,
+ /// Too many links, similar to `EMLINK` in POSIX.
+ too-many-links,
+ /// Message too large, similar to `EMSGSIZE` in POSIX.
+ message-size,
+ /// Filename too long, similar to `ENAMETOOLONG` in POSIX.
+ name-too-long,
+ /// No such device, similar to `ENODEV` in POSIX.
+ no-device,
+ /// No such file or directory, similar to `ENOENT` in POSIX.
+ no-entry,
+ /// No locks available, similar to `ENOLCK` in POSIX.
+ no-lock,
+ /// Not enough space, similar to `ENOMEM` in POSIX.
+ insufficient-memory,
+ /// No space left on device, similar to `ENOSPC` in POSIX.
+ insufficient-space,
+ /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX.
+ not-directory,
+ /// Directory not empty, similar to `ENOTEMPTY` in POSIX.
+ not-empty,
+ /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX.
+ not-recoverable,
+ /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX.
+ unsupported,
+ /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX.
+ no-tty,
+ /// No such device or address, similar to `ENXIO` in POSIX.
+ no-such-device,
+ /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX.
+ overflow,
+ /// Operation not permitted, similar to `EPERM` in POSIX.
+ not-permitted,
+ /// Broken pipe, similar to `EPIPE` in POSIX.
+ pipe,
+ /// Read-only file system, similar to `EROFS` in POSIX.
+ read-only,
+ /// Invalid seek, similar to `ESPIPE` in POSIX.
+ invalid-seek,
+ /// Text file busy, similar to `ETXTBSY` in POSIX.
+ text-file-busy,
+ /// Cross-device link, similar to `EXDEV` in POSIX.
+ cross-device,
+ }
+
+ /// File or memory access pattern advisory information.
+ enum advice {
+ /// The application has no advice to give on its behavior with respect
+ /// to the specified data.
+ normal,
+ /// The application expects to access the specified data sequentially
+ /// from lower offsets to higher offsets.
+ sequential,
+ /// The application expects to access the specified data in a random
+ /// order.
+ random,
+ /// The application expects to access the specified data in the near
+ /// future.
+ will-need,
+ /// The application expects that it will not access the specified data
+ /// in the near future.
+ dont-need,
+ /// The application expects to access the specified data once and then
+ /// not reuse it thereafter.
+ no-reuse,
+ }
+
+ /// A 128-bit hash value, split into parts because wasm doesn't have a
+ /// 128-bit integer type.
+ record metadata-hash-value {
+ /// 64 bits of a 128-bit hash value.
+ lower: u64,
+ /// Another 64 bits of a 128-bit hash value.
+ upper: u64,
+ }
+
+ /// A descriptor is a reference to a filesystem object, which may be a file,
+ /// directory, named pipe, special file, or other object on which filesystem
+ /// calls may be made.
+ resource descriptor {
+ /// Return a stream for reading from a file, if available.
+ ///
+ /// May fail with an error-code describing why the file cannot be read.
+ ///
+ /// Multiple read, write, and append streams may be active on the same open
+ /// file and they do not interfere with each other.
+ ///
+ /// Note: This allows using `read-stream`, which is similar to `read` in POSIX.
+ read-via-stream: func(
+ /// The offset within the file at which to start reading.
+ offset: filesize,
+ ) -> result;
+
+ /// Return a stream for writing to a file, if available.
+ ///
+ /// May fail with an error-code describing why the file cannot be written.
+ ///
+ /// Note: This allows using `write-stream`, which is similar to `write` in
+ /// POSIX.
+ write-via-stream: func(
+ /// The offset within the file at which to start writing.
+ offset: filesize,
+ ) -> result;
+
+ /// Return a stream for appending to a file, if available.
+ ///
+ /// May fail with an error-code describing why the file cannot be appended.
+ ///
+ /// Note: This allows using `write-stream`, which is similar to `write` with
+ /// `O_APPEND` in in POSIX.
+ append-via-stream: func() -> result;
+
+ /// Provide file advisory information on a descriptor.
+ ///
+ /// This is similar to `posix_fadvise` in POSIX.
+ advise: func(
+ /// The offset within the file to which the advisory applies.
+ offset: filesize,
+ /// The length of the region to which the advisory applies.
+ length: filesize,
+ /// The advice.
+ advice: advice
+ ) -> result<_, error-code>;
+
+ /// Synchronize the data of a file to disk.
+ ///
+ /// This function succeeds with no effect if the file descriptor is not
+ /// opened for writing.
+ ///
+ /// Note: This is similar to `fdatasync` in POSIX.
+ sync-data: func() -> result<_, error-code>;
+
+ /// Get flags associated with a descriptor.
+ ///
+ /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX.
+ ///
+ /// Note: This returns the value that was the `fs_flags` value returned
+ /// from `fdstat_get` in earlier versions of WASI.
+ get-flags: func() -> result;
+
+ /// Get the dynamic type of a descriptor.
+ ///
+ /// Note: This returns the same value as the `type` field of the `fd-stat`
+ /// returned by `stat`, `stat-at` and similar.
+ ///
+ /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided
+ /// by `fstat` in POSIX.
+ ///
+ /// Note: This returns the value that was the `fs_filetype` value returned
+ /// from `fdstat_get` in earlier versions of WASI.
+ get-type: func() -> result;
+
+ /// Adjust the size of an open file. If this increases the file's size, the
+ /// extra bytes are filled with zeros.
+ ///
+ /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI.
+ set-size: func(size: filesize) -> result<_, error-code>;
+
+ /// Adjust the timestamps of an open file or directory.
+ ///
+ /// Note: This is similar to `futimens` in POSIX.
+ ///
+ /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI.
+ set-times: func(
+ /// The desired values of the data access timestamp.
+ data-access-timestamp: new-timestamp,
+ /// The desired values of the data modification timestamp.
+ data-modification-timestamp: new-timestamp,
+ ) -> result<_, error-code>;
+
+ /// Read from a descriptor, without using and updating the descriptor's offset.
+ ///
+ /// This function returns a list of bytes containing the data that was
+ /// read, along with a bool which, when true, indicates that the end of the
+ /// file was reached. The returned list will contain up to `length` bytes; it
+ /// may return fewer than requested, if the end of the file is reached or
+ /// if the I/O operation is interrupted.
+ ///
+ /// In the future, this may change to return a `stream`.
+ ///
+ /// Note: This is similar to `pread` in POSIX.
+ read: func(
+ /// The maximum number of bytes to read.
+ length: filesize,
+ /// The offset within the file at which to read.
+ offset: filesize,
+ ) -> result, bool>, error-code>;
+
+ /// Write to a descriptor, without using and updating the descriptor's offset.
+ ///
+ /// It is valid to write past the end of a file; the file is extended to the
+ /// extent of the write, with bytes between the previous end and the start of
+ /// the write set to zero.
+ ///
+ /// In the future, this may change to take a `stream`.
+ ///
+ /// Note: This is similar to `pwrite` in POSIX.
+ write: func(
+ /// Data to write
+ buffer: list,
+ /// The offset within the file at which to write.
+ offset: filesize,
+ ) -> result;
+
+ /// Read directory entries from a directory.
+ ///
+ /// On filesystems where directories contain entries referring to themselves
+ /// and their parents, often named `.` and `..` respectively, these entries
+ /// are omitted.
+ ///
+ /// This always returns a new stream which starts at the beginning of the
+ /// directory. Multiple streams may be active on the same directory, and they
+ /// do not interfere with each other.
+ read-directory: func() -> result;
+
+ /// Synchronize the data and metadata of a file to disk.
+ ///
+ /// This function succeeds with no effect if the file descriptor is not
+ /// opened for writing.
+ ///
+ /// Note: This is similar to `fsync` in POSIX.
+ sync: func() -> result<_, error-code>;
+
+ /// Create a directory.
+ ///
+ /// Note: This is similar to `mkdirat` in POSIX.
+ create-directory-at: func(
+ /// The relative path at which to create the directory.
+ path: string,
+ ) -> result<_, error-code>;
+
+ /// Return the attributes of an open file or directory.
+ ///
+ /// Note: This is similar to `fstat` in POSIX, except that it does not return
+ /// device and inode information. For testing whether two descriptors refer to
+ /// the same underlying filesystem object, use `is-same-object`. To obtain
+ /// additional data that can be used do determine whether a file has been
+ /// modified, use `metadata-hash`.
+ ///
+ /// Note: This was called `fd_filestat_get` in earlier versions of WASI.
+ stat: func() -> result;
+
+ /// Return the attributes of a file or directory.
+ ///
+ /// Note: This is similar to `fstatat` in POSIX, except that it does not
+ /// return device and inode information. See the `stat` description for a
+ /// discussion of alternatives.
+ ///
+ /// Note: This was called `path_filestat_get` in earlier versions of WASI.
+ stat-at: func(
+ /// Flags determining the method of how the path is resolved.
+ path-flags: path-flags,
+ /// The relative path of the file or directory to inspect.
+ path: string,
+ ) -> result;
+
+ /// Adjust the timestamps of a file or directory.
+ ///
+ /// Note: This is similar to `utimensat` in POSIX.
+ ///
+ /// Note: This was called `path_filestat_set_times` in earlier versions of
+ /// WASI.
+ set-times-at: func(
+ /// Flags determining the method of how the path is resolved.
+ path-flags: path-flags,
+ /// The relative path of the file or directory to operate on.
+ path: string,
+ /// The desired values of the data access timestamp.
+ data-access-timestamp: new-timestamp,
+ /// The desired values of the data modification timestamp.
+ data-modification-timestamp: new-timestamp,
+ ) -> result<_, error-code>;
+
+ /// Create a hard link.
+ ///
+ /// Note: This is similar to `linkat` in POSIX.
+ link-at: func(
+ /// Flags determining the method of how the path is resolved.
+ old-path-flags: path-flags,
+ /// The relative source path from which to link.
+ old-path: string,
+ /// The base directory for `new-path`.
+ new-descriptor: borrow,
+ /// The relative destination path at which to create the hard link.
+ new-path: string,
+ ) -> result<_, error-code>;
+
+ /// Open a file or directory.
+ ///
+ /// The returned descriptor is not guaranteed to be the lowest-numbered
+ /// descriptor not currently open/ it is randomized to prevent applications
+ /// from depending on making assumptions about indexes, since this is
+ /// error-prone in multi-threaded contexts. The returned descriptor is
+ /// guaranteed to be less than 2**31.
+ ///
+ /// If `flags` contains `descriptor-flags::mutate-directory`, and the base
+ /// descriptor doesn't have `descriptor-flags::mutate-directory` set,
+ /// `open-at` fails with `error-code::read-only`.
+ ///
+ /// If `flags` contains `write` or `mutate-directory`, or `open-flags`
+ /// contains `truncate` or `create`, and the base descriptor doesn't have
+ /// `descriptor-flags::mutate-directory` set, `open-at` fails with
+ /// `error-code::read-only`.
+ ///
+ /// Note: This is similar to `openat` in POSIX.
+ open-at: func(
+ /// Flags determining the method of how the path is resolved.
+ path-flags: path-flags,
+ /// The relative path of the object to open.
+ path: string,
+ /// The method by which to open the file.
+ open-flags: open-flags,
+ /// Flags to use for the resulting descriptor.
+ %flags: descriptor-flags,
+ ) -> result;
+
+ /// Read the contents of a symbolic link.
+ ///
+ /// If the contents contain an absolute or rooted path in the underlying
+ /// filesystem, this function fails with `error-code::not-permitted`.
+ ///
+ /// Note: This is similar to `readlinkat` in POSIX.
+ readlink-at: func(
+ /// The relative path of the symbolic link from which to read.
+ path: string,
+ ) -> result;
+
+ /// Remove a directory.
+ ///
+ /// Return `error-code::not-empty` if the directory is not empty.
+ ///
+ /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX.
+ remove-directory-at: func(
+ /// The relative path to a directory to remove.
+ path: string,
+ ) -> result<_, error-code>;
+
+ /// Rename a filesystem object.
+ ///
+ /// Note: This is similar to `renameat` in POSIX.
+ rename-at: func(
+ /// The relative source path of the file or directory to rename.
+ old-path: string,
+ /// The base directory for `new-path`.
+ new-descriptor: borrow,
+ /// The relative destination path to which to rename the file or directory.
+ new-path: string,
+ ) -> result<_, error-code>;
+
+ /// Create a symbolic link (also known as a "symlink").
+ ///
+ /// If `old-path` starts with `/`, the function fails with
+ /// `error-code::not-permitted`.
+ ///
+ /// Note: This is similar to `symlinkat` in POSIX.
+ symlink-at: func(
+ /// The contents of the symbolic link.
+ old-path: string,
+ /// The relative destination path at which to create the symbolic link.
+ new-path: string,
+ ) -> result<_, error-code>;
+
+ /// Unlink a filesystem object that is not a directory.
+ ///
+ /// Return `error-code::is-directory` if the path refers to a directory.
+ /// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX.
+ unlink-file-at: func(
+ /// The relative path to a file to unlink.
+ path: string,
+ ) -> result<_, error-code>;
+
+ /// Test whether two descriptors refer to the same filesystem object.
+ ///
+ /// In POSIX, this corresponds to testing whether the two descriptors have the
+ /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers.
+ /// wasi-filesystem does not expose device and inode numbers, so this function
+ /// may be used instead.
+ is-same-object: func(other: borrow) -> bool;
+
+ /// Return a hash of the metadata associated with a filesystem object referred
+ /// to by a descriptor.
+ ///
+ /// This returns a hash of the last-modification timestamp and file size, and
+ /// may also include the inode number, device number, birth timestamp, and
+ /// other metadata fields that may change when the file is modified or
+ /// replaced. It may also include a secret value chosen by the
+ /// implementation and not otherwise exposed.
+ ///
+ /// Implementations are encourated to provide the following properties:
+ ///
+ /// - If the file is not modified or replaced, the computed hash value should
+ /// usually not change.
+ /// - If the object is modified or replaced, the computed hash value should
+ /// usually change.
+ /// - The inputs to the hash should not be easily computable from the
+ /// computed hash.
+ ///
+ /// However, none of these is required.
+ metadata-hash: func() -> result;
+
+ /// Return a hash of the metadata associated with a filesystem object referred
+ /// to by a directory descriptor and a relative path.
+ ///
+ /// This performs the same hash computation as `metadata-hash`.
+ metadata-hash-at: func(
+ /// Flags determining the method of how the path is resolved.
+ path-flags: path-flags,
+ /// The relative path of the file or directory to inspect.
+ path: string,
+ ) -> result;
+ }
+
+ /// A stream of directory entries.
+ resource directory-entry-stream {
+ /// Read a single directory entry from a `directory-entry-stream`.
+ read-directory-entry: func() -> result