core: `Request` is now a trait. Body download is deferred to a second async method.

This commit is contained in:
David Wendt 2023-03-29 20:40:54 -04:00 committed by kmeisthax
parent 36210c9f20
commit 06eb2e1ee8
6 changed files with 237 additions and 101 deletions

View File

@ -6,6 +6,7 @@ use crate::string::WStr;
use async_channel::Receiver;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::fmt;
use std::fmt::Display;
use std::future::Future;
@ -186,18 +187,20 @@ impl Request {
}
/// A response to a successful fetch request.
pub struct SuccessResponse {
pub trait SuccessResponse {
/// The final URL obtained after any redirects.
pub url: String,
fn url(&self) -> Cow<str>;
/// The contents of the response body.
pub body: Vec<u8>,
/// Retrieve the contents of the response body.
///
/// This method consumes the response.
fn body(self: Box<Self>) -> OwnedFuture<Vec<u8>, Error>;
/// The status code of the response.
pub status: u16,
fn status(&self) -> u16;
/// The field to indicate if the request has been redirected.
pub redirected: bool,
/// Indicates if the request has been redirected.
fn redirected(&self) -> bool;
}
/// A response to a non-successful fetch request.
@ -245,7 +248,7 @@ pub trait NavigatorBackend {
);
/// Fetch data and return it some time in the future.
fn fetch(&self, request: Request) -> OwnedFuture<SuccessResponse, ErrorResponse>;
fn fetch(&self, request: Request) -> OwnedFuture<Box<dyn SuccessResponse>, ErrorResponse>;
/// Take a URL string and resolve it to the actual URL from which a file
/// can be fetched. This includes handling of relative links and pre-processing.
@ -402,7 +405,7 @@ impl NavigatorBackend for NullNavigatorBackend {
) {
}
fn fetch(&self, request: Request) -> OwnedFuture<SuccessResponse, ErrorResponse> {
fn fetch(&self, request: Request) -> OwnedFuture<Box<dyn SuccessResponse>, ErrorResponse> {
fetch_path(self, "NullNavigatorBackend", request.url(), None)
}
@ -449,7 +452,7 @@ pub fn async_return<SuccessType: 'static, ErrorType: 'static>(
pub fn create_fetch_error<ErrorType: Display>(
url: &str,
error: ErrorType,
) -> Result<SuccessResponse, ErrorResponse> {
) -> Result<Box<dyn SuccessResponse>, ErrorResponse> {
create_specific_fetch_error("Invalid URL", url, error)
}
@ -459,7 +462,7 @@ pub fn create_specific_fetch_error<ErrorType: Display>(
reason: &str,
url: &str,
error: ErrorType,
) -> Result<SuccessResponse, ErrorResponse> {
) -> Result<Box<dyn SuccessResponse>, ErrorResponse> {
let message = if error.to_string() == "" {
format!("{reason} {url}")
} else {
@ -543,7 +546,34 @@ pub fn fetch_path<NavigatorType: NavigatorBackend>(
navigator_name: &str,
url: &str,
base_path: Option<&Path>,
) -> OwnedFuture<SuccessResponse, ErrorResponse> {
) -> OwnedFuture<Box<dyn SuccessResponse>, ErrorResponse> {
struct LocalResponse {
url: String,
path: PathBuf,
status: u16,
redirected: bool,
}
impl SuccessResponse for LocalResponse {
fn url(&self) -> Cow<str> {
Cow::Borrowed(&self.url)
}
fn body(self: Box<Self>) -> OwnedFuture<Vec<u8>, Error> {
Box::pin(async move {
std::fs::read(self.path).map_err(|e| Error::FetchError(e.to_string()))
})
}
fn status(&self) -> u16 {
self.status
}
fn redirected(&self) -> bool {
self.redirected
}
}
let url = match navigator.resolve_url(url) {
Ok(url) => url,
Err(e) => return async_return(create_fetch_error(url, e)),
@ -584,15 +614,13 @@ pub fn fetch_path<NavigatorType: NavigatorBackend>(
};
Box::pin(async move {
let body = match std::fs::read(path) {
Ok(body) => body,
Err(e) => return create_specific_fetch_error("Can't open file", url.as_str(), e),
};
Ok(SuccessResponse {
let response: Box<dyn SuccessResponse> = Box::new(LocalResponse {
url: url.to_string(),
body,
path,
status: 0,
redirected: false,
})
});
Ok(response)
})
}

View File

@ -13,7 +13,7 @@ use crate::avm2::{
Activation as Avm2Activation, Avm2, BitmapDataObject, Domain as Avm2Domain,
Object as Avm2Object, Value as Avm2Value,
};
use crate::backend::navigator::{OwnedFuture, Request};
use crate::backend::navigator::{ErrorResponse, OwnedFuture, Request, SuccessResponse};
use crate::backend::ui::DialogResultFuture;
use crate::bitmap::bitmap_data::Color;
use crate::bitmap::bitmap_data::{BitmapData, BitmapDataWrapper};
@ -854,6 +854,21 @@ impl<'gc> Loader<'gc> {
Ok(did_finish)
}
async fn wait_for_full_response(
response: OwnedFuture<Box<dyn SuccessResponse>, ErrorResponse>,
) -> Result<(Vec<u8>, String, u16, bool), ErrorResponse> {
let response = response.await?;
let url = response.url().to_string();
let status = response.status();
let redirected = response.redirected();
let body = response.body().await;
match body {
Ok(body) => Ok((body, url, status, redirected)),
Err(error) => Err(ErrorResponse { url, error }),
}
}
/// Construct a future for the root movie loader.
fn root_movie_loader(
&mut self,
@ -875,7 +890,6 @@ impl<'gc> Loader<'gc> {
Box::pin(async move {
let fetch = player.lock().unwrap().navigator().fetch(request);
let response = fetch.await.map_err(|error| {
player
.lock()
@ -884,13 +898,22 @@ impl<'gc> Loader<'gc> {
.display_root_movie_download_failed_message(false);
error.error
})?;
let url = response.url().into_owned();
let body = response.body().await.map_err(|error| {
player
.lock()
.unwrap()
.ui()
.display_root_movie_download_failed_message(true);
error
})?;
// The spoofed root movie URL takes precedence over the actual URL.
let swf_url = player
.lock()
.unwrap()
.compatibility_rules()
.rewrite_swf_url(response.url);
.rewrite_swf_url(url);
let spoofed_or_swf_url = player
.lock()
.unwrap()
@ -899,7 +922,7 @@ impl<'gc> Loader<'gc> {
.unwrap_or(swf_url);
let mut movie =
SwfMovie::from_data(&response.body, spoofed_or_swf_url, None).map_err(|error| {
SwfMovie::from_data(&body, spoofed_or_swf_url, None).map_err(|error| {
player
.lock()
.unwrap()
@ -970,25 +993,25 @@ impl<'gc> Loader<'gc> {
Loader::movie_loader_start(handle, uc)
})?;
match fetch.await {
Ok(response) if replacing_root_movie => {
ContentType::sniff(&response.body).expect(ContentType::Swf)?;
match Self::wait_for_full_response(fetch).await {
Ok((body, url, _status, _redirected)) if replacing_root_movie => {
ContentType::sniff(&body).expect(ContentType::Swf)?;
let movie = SwfMovie::from_data(&response.body, response.url, loader_url)?;
let movie = SwfMovie::from_data(&body, url.to_string(), loader_url)?;
player.lock().unwrap().mutate_with_update_context(|uc| {
uc.set_root_movie(movie);
});
return Ok(());
}
Ok(response) => {
Ok((body, url, status, redirected)) => {
player.lock().unwrap().mutate_with_update_context(|uc| {
Loader::movie_loader_data(
handle,
uc,
&response.body,
response.url,
response.status,
response.redirected,
&body,
url.to_string(),
status,
redirected,
loader_url,
)
})?;
@ -1084,6 +1107,7 @@ impl<'gc> Loader<'gc> {
let fetch = player.lock().unwrap().navigator().fetch(request);
let response = fetch.await.map_err(|e| e.error)?;
let body = response.body().await?;
// Fire the load handler.
player.lock().unwrap().update(|uc| {
@ -1099,7 +1123,7 @@ impl<'gc> Loader<'gc> {
ActivationIdentifier::root("[Form Loader]"),
);
for (k, v) in form_urlencoded::parse(&response.body) {
for (k, v) in form_urlencoded::parse(&body) {
let k = AvmString::new_utf8(activation.context.gc_context, k);
let v = AvmString::new_utf8(activation.context.gc_context, v);
that.set(k, v.into(), &mut activation)?;
@ -1145,8 +1169,7 @@ impl<'gc> Loader<'gc> {
Box::pin(async move {
let fetch = player.lock().unwrap().navigator().fetch(request);
let data = fetch.await;
let response = Self::wait_for_full_response(fetch).await;
// Fire the load handler.
player.lock().unwrap().update(|uc| {
@ -1160,9 +1183,9 @@ impl<'gc> Loader<'gc> {
let mut activation =
Activation::from_stub(uc.reborrow(), ActivationIdentifier::root("[Loader]"));
match data {
Ok(response) => {
let length = response.body.len();
match response {
Ok((body, _, status, _)) => {
let length = body.len();
// Set the properties used by the getBytesTotal and getBytesLoaded methods.
that.set("_bytesTotal", length.into(), &mut activation)?;
@ -1172,7 +1195,7 @@ impl<'gc> Loader<'gc> {
let _ = that.call_method(
"onHTTPStatus".into(),
&[response.status.into()],
&[status.into()],
&mut activation,
ExecutionReason::Special,
);
@ -1184,7 +1207,7 @@ impl<'gc> Loader<'gc> {
} else {
AvmString::new_utf8(
activation.context.gc_context,
UTF_8.decode(&response.body).0,
UTF_8.decode(&body).0,
)
.into()
};
@ -1247,7 +1270,7 @@ impl<'gc> Loader<'gc> {
Box::pin(async move {
let fetch = player.lock().unwrap().navigator().fetch(request);
let response = fetch.await;
let response = Self::wait_for_full_response(fetch).await;
player.lock().unwrap().update(|uc| {
let loader = uc.load_manager.get_loader(handle);
@ -1299,8 +1322,8 @@ impl<'gc> Loader<'gc> {
}
match response {
Ok(response) => {
let total_len = response.body.len();
Ok((body, _, status, redirected)) => {
let total_len = body.len();
// FIXME - the "open" event should be fired earlier, just before
// we start to fetch the data.
@ -1314,7 +1337,7 @@ impl<'gc> Loader<'gc> {
let open_evt =
Avm2EventObject::bare_default_event(&mut activation.context, "open");
Avm2::dispatch_event(&mut activation.context, open_evt, target);
set_data(response.body, &mut activation, target, data_format);
set_data(body, &mut activation, target, data_format);
// FIXME - we should fire "progress" events as we receive data, not
// just at the end
@ -1346,8 +1369,8 @@ impl<'gc> Loader<'gc> {
"httpStatus".into(),
false.into(),
false.into(),
response.status.into(),
response.redirected.into(),
status.into(),
redirected.into(),
],
)
.map_err(|e| Error::Avm2Error(e.to_string()))?;
@ -1436,7 +1459,7 @@ impl<'gc> Loader<'gc> {
Box::pin(async move {
let fetch = player.lock().unwrap().navigator().fetch(request);
let data = fetch.await;
let response = Self::wait_for_full_response(fetch).await;
// Fire the load handler.
player.lock().unwrap().update(|uc| {
@ -1447,10 +1470,10 @@ impl<'gc> Loader<'gc> {
_ => return Err(Error::NotSoundLoader),
};
let success = data
let success = response
.map_err(|e| e.error)
.and_then(|data| {
let handle = uc.audio.register_mp3(&data.body)?;
.and_then(|(body, _, _, _)| {
let handle = uc.audio.register_mp3(&body)?;
sound_object.set_sound(uc.gc_context, Some(handle));
let duration = uc
.audio
@ -1499,7 +1522,7 @@ impl<'gc> Loader<'gc> {
Box::pin(async move {
let fetch = player.lock().unwrap().navigator().fetch(request);
let response = fetch.await;
let response = Self::wait_for_full_response(fetch).await;
player.lock().unwrap().update(|uc| {
let loader = uc.load_manager.get_loader(handle);
@ -1510,8 +1533,8 @@ impl<'gc> Loader<'gc> {
};
match response {
Ok(response) => {
let handle = uc.audio.register_mp3(&response.body)?;
Ok((body, _, _, _)) => {
let handle = uc.audio.register_mp3(&body)?;
if let Err(e) = sound_object
.as_sound_object()
.expect("Not a sound object")
@ -1520,7 +1543,7 @@ impl<'gc> Loader<'gc> {
tracing::error!("Encountered AVM2 error when setting sound: {}", e);
}
let total_len = response.body.len();
let total_len = body.len();
// FIXME - the "open" event should be fired earlier, and not fired in case of ioerror.
let mut activation = Avm2Activation::from_nothing(uc.reborrow());
@ -1598,7 +1621,7 @@ impl<'gc> Loader<'gc> {
Box::pin(async move {
let fetch = player.lock().unwrap().navigator().fetch(request);
let response = fetch.await;
let response = Self::wait_for_full_response(fetch).await;
player.lock().unwrap().update(|uc| {
let loader = uc.load_manager.get_loader(handle);
@ -1609,9 +1632,9 @@ impl<'gc> Loader<'gc> {
};
match response {
Ok(mut response) => {
Ok((mut body, _, _, _)) => {
stream.reset_buffer(uc);
stream.load_buffer(uc, &mut response.body);
stream.load_buffer(uc, &mut body);
}
Err(response) => {
stream.report_error(response.error);
@ -2702,7 +2725,7 @@ impl<'gc> Loader<'gc> {
let req = Request::get(url.clone());
// Doing this in two steps to prevent holding the player lock during fetch
let future = player.lock().unwrap().navigator().fetch(req);
let download_res = future.await;
let download_res = Self::wait_for_full_response(future).await;
// Fire the load handler.
player.lock().unwrap().update(|uc| -> Result<(), Error> {
@ -2740,7 +2763,7 @@ impl<'gc> Loader<'gc> {
)?;
match download_res {
Ok(download_res) => {
Ok((body, _, _, _)) => {
as_broadcaster::broadcast_internal(
&mut activation,
target_object,
@ -2753,14 +2776,14 @@ impl<'gc> Loader<'gc> {
// perspective of AS, we want to refresh the file_ref internal data
// before invoking the callbacks
dialog_result.write(&download_res.body);
dialog_result.write(&body);
dialog_result.refresh();
file_ref.init_from_dialog_result(
&mut activation,
dialog_result.borrow(),
);
let total_bytes = download_res.body.len();
let total_bytes = body.len();
as_broadcaster::broadcast_internal(
&mut activation,

View File

@ -2,7 +2,7 @@ use crate::avm2::object::{
NetConnectionObject as Avm2NetConnectionObject, ResponderObject as Avm2ResponderObject,
};
use crate::avm2::{Activation as Avm2Activation, Avm2, EventObject as Avm2EventObject};
use crate::backend::navigator::{NavigatorBackend, OwnedFuture, Request};
use crate::backend::navigator::{ErrorResponse, NavigatorBackend, OwnedFuture, Request};
use crate::context::UpdateContext;
use crate::loader::Error;
use crate::string::AvmString;
@ -455,7 +455,18 @@ impl FlashRemoting {
.expect("Must be able to serialize a packet");
let request = Request::post(url, Some((bytes, "application/x-amf".to_string())));
let fetch = player.lock().unwrap().navigator().fetch(request);
let response = match fetch.await {
let response: Result<_, ErrorResponse> = async {
let response = fetch.await?;
let url = response.url().to_string();
let body = response
.body()
.await
.map_err(|error| ErrorResponse { url, error })?;
Ok(body)
}
.await;
let response = match response {
Ok(response) => response,
Err(response) => {
player.lock().unwrap().update(|uc| {
@ -497,7 +508,7 @@ impl FlashRemoting {
};
// Flash completely ignores invalid responses, it seems
if let Ok(response_packet) = flash_lso::packet::read::parse(&response.body) {
if let Ok(response_packet) = flash_lso::packet::read::parse(&response) {
player.lock().unwrap().update(|uc| {
for message in response_packet.messages {
if let Some(target_uri) = message.target_uri.strip_prefix('/') {

View File

@ -171,7 +171,32 @@ impl NavigatorBackend for ExternalNavigatorBackend {
};
}
fn fetch(&self, request: Request) -> OwnedFuture<SuccessResponse, ErrorResponse> {
fn fetch(&self, request: Request) -> OwnedFuture<Box<dyn SuccessResponse>, ErrorResponse> {
struct DesktopResponse {
url: String,
body: Vec<u8>,
status: u16,
redirected: bool,
}
impl SuccessResponse for DesktopResponse {
fn url(&self) -> std::borrow::Cow<str> {
std::borrow::Cow::Borrowed(&self.url)
}
fn body(self: Box<Self>) -> OwnedFuture<Vec<u8>, Error> {
Box::pin(async { Ok(self.body) })
}
fn status(&self) -> u16 {
self.status
}
fn redirected(&self) -> bool {
self.redirected
}
}
// TODO: honor sandbox type (local-with-filesystem, local-with-network, remote, ...)
let mut processed_url = match self.resolve_url(request.url()) {
Ok(url) => url,
@ -236,12 +261,14 @@ impl NavigatorBackend for ExternalNavigatorBackend {
}
};
Ok(SuccessResponse {
let response: Box<dyn SuccessResponse> = Box::new(DesktopResponse {
url: response_url.to_string(),
body,
status: 0,
redirected: false,
})
});
Ok(response)
}),
_ => Box::pin(async move {
let client = client.ok_or_else(|| ErrorResponse {
@ -321,12 +348,13 @@ impl NavigatorBackend for ExternalNavigatorBackend {
error: Error::FetchError(e.to_string()),
})?;
Ok(SuccessResponse {
let response: Box<dyn SuccessResponse> = Box::new(DesktopResponse {
url,
body,
status,
redirected,
})
});
Ok(response)
}),
}
}

View File

@ -11,11 +11,37 @@ use ruffle_core::indexmap::IndexMap;
use ruffle_core::loader::Error;
use ruffle_core::socket::{ConnectionState, SocketAction, SocketHandle};
use ruffle_socket_format::SocketEvent;
use std::borrow::Cow;
use std::sync::mpsc::Sender;
use std::time::Duration;
use url::{ParseError, Url};
use vfs::VfsPath;
struct TestResponse {
url: String,
body: Vec<u8>,
status: u16,
redirected: bool,
}
impl SuccessResponse for TestResponse {
fn url(&self) -> Cow<str> {
Cow::Borrowed(&self.url)
}
fn body(self: Box<Self>) -> OwnedFuture<Vec<u8>, Error> {
Box::pin(async move { Ok(self.body) })
}
fn status(&self) -> u16 {
self.status
}
fn redirected(&self) -> bool {
self.redirected
}
}
/// A `NavigatorBackend` used by tests that supports logging fetch requests.
///
/// This can be used by tests that fetch data to verify that the request is correct.
@ -71,15 +97,17 @@ impl NavigatorBackend for TestNavigatorBackend {
}
}
fn fetch(&self, request: Request) -> OwnedFuture<SuccessResponse, ErrorResponse> {
fn fetch(&self, request: Request) -> OwnedFuture<Box<dyn SuccessResponse>, ErrorResponse> {
if request.url().contains("?debug-success") {
return Box::pin(async move {
Ok(SuccessResponse {
let response: Box<dyn SuccessResponse> = Box::new(TestResponse {
url: request.url().to_string(),
body: b"Hello, World!".to_vec(),
status: 200,
redirected: false,
})
});
Ok(response)
});
}
@ -184,12 +212,15 @@ impl NavigatorBackend for TestNavigatorBackend {
url: url.to_string(),
error: Error::FetchError(error.to_string()),
})?;
Ok(SuccessResponse {
let response: Box<dyn SuccessResponse> = Box::new(TestResponse {
url: url.to_string(),
body,
status: 0,
redirected: false,
})
});
Ok(response)
})
}

View File

@ -3,7 +3,7 @@ use crate::SocketProxy;
use async_channel::Receiver;
use futures_util::{SinkExt, StreamExt};
use gloo_net::websocket::{futures::WebSocket, Message};
use js_sys::{Array, ArrayBuffer, Uint8Array};
use js_sys::{Array, Uint8Array};
use ruffle_core::backend::navigator::{
async_return, create_fetch_error, create_specific_fetch_error, ErrorResponse, NavigationMethod,
NavigatorBackend, OpenURLMode, OwnedFuture, Request, SuccessResponse,
@ -12,6 +12,7 @@ use ruffle_core::config::NetworkingAccessMode;
use ruffle_core::indexmap::IndexMap;
use ruffle_core::loader::Error;
use ruffle_core::socket::{ConnectionState, SocketAction, SocketHandle};
use std::borrow::Cow;
use std::sync::mpsc::Sender;
use std::sync::Arc;
use std::time::Duration;
@ -223,7 +224,7 @@ impl NavigatorBackend for WebNavigatorBackend {
};
}
fn fetch(&self, request: Request) -> OwnedFuture<SuccessResponse, ErrorResponse> {
fn fetch(&self, request: Request) -> OwnedFuture<Box<dyn SuccessResponse>, ErrorResponse> {
let url = match self.resolve_url(request.url()) {
Ok(url) => {
if url.scheme() == "file" {
@ -326,32 +327,9 @@ impl NavigatorBackend for WebNavigatorBackend {
return Err(ErrorResponse { url, error });
}
let body: ArrayBuffer = JsFuture::from(response.array_buffer().map_err(|_| {
ErrorResponse {
url: url.clone(),
error: Error::FetchError("Got JS error".to_string()),
}
})?)
.await
.map_err(|_| ErrorResponse {
url: url.clone(),
error: Error::FetchError(
"Could not allocate array buffer for response".to_string(),
),
})?
.dyn_into()
.map_err(|_| ErrorResponse {
url: url.clone(),
error: Error::FetchError("array_buffer result wasn't an ArrayBuffer".to_string()),
})?;
let body = Uint8Array::new(&body).to_vec();
let wrapper: Box<dyn SuccessResponse> = Box::new(WebResponseWrapper(response));
Ok(SuccessResponse {
url,
body,
status,
redirected,
})
Ok(wrapper)
})
}
@ -462,3 +440,40 @@ impl NavigatorBackend for WebNavigatorBackend {
}));
}
}
struct WebResponseWrapper(WebResponse);
impl SuccessResponse for WebResponseWrapper {
fn url(&self) -> Cow<str> {
Cow::Owned(self.0.url())
}
fn body(self: Box<Self>) -> OwnedFuture<Vec<u8>, Error> {
Box::pin(async move {
let body = JsFuture::from(
self.0
.array_buffer()
.map_err(|_| Error::FetchError("Got JS error".to_string()))?,
)
.await
.map_err(|_| {
Error::FetchError("Could not allocate array buffer for response".to_string())
})?
.dyn_into()
.map_err(|_| {
Error::FetchError("array_buffer result wasn't an ArrayBuffer".to_string())
})?;
let body = Uint8Array::new(&body).to_vec();
Ok(body)
})
}
fn status(&self) -> u16 {
self.0.status()
}
fn redirected(&self) -> bool {
self.0.redirected()
}
}