avm2: Partially implement `URLLoader` and related classes
This PR implements the `URLLoader` class, allowing AVM2 scripts to load data from a URL. This requires several other related classes (`URLLoaderDataFormat`, `URLRequest`, `IOError`) to be implemented as well. Currently implemented: * Fetching from URLs using the 'navigator' backend * The `text` and `binary` data formats (which store data in a `String` or `ByteArray` respectively) * The `open`, `complete`, and `ioError` events * The `bytesLoaded`, `bytesTotal`, and `data` properties Not yet implemented: * The HTTP and security events * All of the properties of `IOError` * The properties on `URLRequest` (besides `url`) * The "variables" data format This should be enough to get some basic uses of `URLLoader` working (e.g. simple GET requests to a particular website). Note that in Flash's `playerglobal`, the `URLLoader` class is just a think wrapper around the more general `URLStream`. However, implementing `URLStream` will require changes to `Navigator`` to support notifications when data arrives in the stream. When that happens, we should be able to re-use a large amount of the code in this PR.
This commit is contained in:
parent
7130c6c1c1
commit
8d8a7600d8
|
@ -23,14 +23,14 @@ macro_rules! avm_debug {
|
|||
|
||||
pub mod activation;
|
||||
mod array;
|
||||
mod bytearray;
|
||||
pub mod bytearray;
|
||||
mod class;
|
||||
mod domain;
|
||||
mod events;
|
||||
mod function;
|
||||
mod globals;
|
||||
mod method;
|
||||
mod names;
|
||||
pub mod names;
|
||||
pub mod object;
|
||||
mod property;
|
||||
mod property_map;
|
||||
|
|
|
@ -101,6 +101,12 @@ pub enum EventData<'gc> {
|
|||
button_down: bool,
|
||||
delta: i32,
|
||||
},
|
||||
// FIXME - define properties from 'ErrorEvent' and 'TextEvent'
|
||||
IOError {
|
||||
// FIXME - this should be inherited in some way from
|
||||
// the (currently not declared) `TextEvent`
|
||||
text: AvmString<'gc>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'gc> EventData<'gc> {
|
||||
|
|
|
@ -88,6 +88,7 @@ pub struct SystemPrototypes<'gc> {
|
|||
pub nativemenu: Object<'gc>,
|
||||
pub contextmenu: Object<'gc>,
|
||||
pub mouseevent: Object<'gc>,
|
||||
pub ioerrorevent: Object<'gc>,
|
||||
}
|
||||
|
||||
impl<'gc> SystemPrototypes<'gc> {
|
||||
|
@ -150,6 +151,7 @@ impl<'gc> SystemPrototypes<'gc> {
|
|||
nativemenu: empty,
|
||||
contextmenu: empty,
|
||||
mouseevent: empty,
|
||||
ioerrorevent: empty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -202,6 +204,7 @@ pub struct SystemClasses<'gc> {
|
|||
pub nativemenu: ClassObject<'gc>,
|
||||
pub contextmenu: ClassObject<'gc>,
|
||||
pub mouseevent: ClassObject<'gc>,
|
||||
pub ioerrorevent: ClassObject<'gc>,
|
||||
}
|
||||
|
||||
impl<'gc> SystemClasses<'gc> {
|
||||
|
@ -264,6 +267,7 @@ impl<'gc> SystemClasses<'gc> {
|
|||
nativemenu: object,
|
||||
contextmenu: object,
|
||||
mouseevent: object,
|
||||
ioerrorevent: object,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -576,11 +580,12 @@ pub fn load_player_globals<'gc>(
|
|||
flash::events::mouseevent::create_class(mc),
|
||||
script
|
||||
);
|
||||
class(
|
||||
avm2_system_class!(
|
||||
ioerrorevent,
|
||||
activation,
|
||||
flash::events::ioerrorevent::create_class(mc),
|
||||
script,
|
||||
)?;
|
||||
script
|
||||
);
|
||||
class(
|
||||
activation,
|
||||
flash::events::contextmenuevent::create_class(mc),
|
||||
|
@ -927,6 +932,12 @@ pub fn load_player_globals<'gc>(
|
|||
flash::net::object_encoding::create_class(mc),
|
||||
script,
|
||||
)?;
|
||||
class(activation, flash::net::url_loader::create_class(mc), script)?;
|
||||
class(
|
||||
activation,
|
||||
flash::net::url_loader_data_format::create_class(mc),
|
||||
script,
|
||||
)?;
|
||||
class(
|
||||
activation,
|
||||
flash::net::url_request::create_class(mc),
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
use crate::avm2::activation::Activation;
|
||||
use crate::avm2::class::{Class, ClassAttributes};
|
||||
use crate::avm2::events::EventData;
|
||||
use crate::avm2::method::Method;
|
||||
use crate::avm2::method::NativeMethodImpl;
|
||||
use crate::avm2::names::{Namespace, QName};
|
||||
use crate::avm2::object::Object;
|
||||
use crate::avm2::object::TObject;
|
||||
use crate::avm2::value::Value;
|
||||
use crate::avm2::Error;
|
||||
use gc_arena::{GcCell, MutationContext};
|
||||
|
@ -28,6 +31,24 @@ pub fn class_init<'gc>(
|
|||
Ok(Value::Undefined)
|
||||
}
|
||||
|
||||
/// Implements `text`'s getter.
|
||||
// FIXME - we should define the ancestor class `TextEvent`
|
||||
// and declare this getter there
|
||||
pub fn text<'gc>(
|
||||
_activation: &mut Activation<'_, 'gc, '_>,
|
||||
this: Option<Object<'gc>>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<Value<'gc>, Error> {
|
||||
if let Some(this) = this {
|
||||
if let Some(evt) = this.as_event() {
|
||||
if let EventData::IOError { text } = evt.event_data() {
|
||||
return Ok(Value::String(*text));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Value::Undefined)
|
||||
}
|
||||
|
||||
/// Construct `IOErrorEvent`'s class.
|
||||
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
|
||||
let class = Class::new(
|
||||
|
@ -47,5 +68,12 @@ pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>
|
|||
|
||||
write.define_public_constant_string_class_traits(CONSTANTS);
|
||||
|
||||
const PUBLIC_INSTANCE_PROPERTIES: &[(
|
||||
&str,
|
||||
Option<NativeMethodImpl>,
|
||||
Option<NativeMethodImpl>,
|
||||
)] = &[("text", Some(text), None)];
|
||||
write.define_public_builtin_instance_properties(mc, PUBLIC_INSTANCE_PROPERTIES);
|
||||
|
||||
class
|
||||
}
|
||||
|
|
|
@ -2,4 +2,6 @@
|
|||
|
||||
pub mod object_encoding;
|
||||
pub mod sharedobject;
|
||||
pub mod url_loader;
|
||||
pub mod url_loader_data_format;
|
||||
pub mod url_request;
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
//! `flash.net.URLLoader` builtin/prototype
|
||||
|
||||
use crate::avm2::activation::Activation;
|
||||
use crate::avm2::class::Class;
|
||||
use crate::avm2::method::{Method, NativeMethodImpl};
|
||||
use crate::avm2::names::{Namespace, QName};
|
||||
use crate::avm2::object::TObject;
|
||||
use crate::avm2::value::Value;
|
||||
use crate::avm2::{Error, Object};
|
||||
use crate::backend::navigator::RequestOptions;
|
||||
use crate::loader::DataFormat;
|
||||
use crate::string::AvmString;
|
||||
use gc_arena::{GcCell, MutationContext};
|
||||
|
||||
/// Implements `flash.net.URLLoader`'s class constructor.
|
||||
pub fn class_init<'gc>(
|
||||
_activation: &mut Activation<'_, 'gc, '_>,
|
||||
_this: Option<Object<'gc>>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<Value<'gc>, Error> {
|
||||
Ok(Value::Undefined)
|
||||
}
|
||||
|
||||
/// Implements `flash.net.URLLoader`'s instance constructor.
|
||||
pub fn instance_init<'gc>(
|
||||
activation: &mut Activation<'_, 'gc, '_>,
|
||||
this: Option<Object<'gc>>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<Value<'gc>, Error> {
|
||||
if let Some(mut this) = this {
|
||||
activation.super_init(this, &[])?;
|
||||
this.set_property(
|
||||
&QName::new(Namespace::public(), "dataFormat").into(),
|
||||
"text".into(),
|
||||
activation,
|
||||
)?;
|
||||
this.set_property(
|
||||
&QName::new(Namespace::public(), "data").into(),
|
||||
Value::Undefined,
|
||||
activation,
|
||||
)?;
|
||||
|
||||
if let Some(request) = args.get(0) {
|
||||
if request != &Value::Null {
|
||||
load(activation, Some(this), args)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Value::Undefined)
|
||||
}
|
||||
|
||||
pub fn bytes_loaded<'gc>(
|
||||
activation: &mut Activation<'_, 'gc, '_>,
|
||||
this: Option<Object<'gc>>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<Value<'gc>, Error> {
|
||||
// For now, just use `bytes_total`. The `bytesLoaded` value
|
||||
// should really update as the download progresses, instead
|
||||
// of jumping at completion from 0 to the total length
|
||||
log::warn!("URLLoader.bytesLoaded - not yet implemented");
|
||||
bytes_total(activation, this, args)
|
||||
}
|
||||
|
||||
pub fn bytes_total<'gc>(
|
||||
activation: &mut Activation<'_, 'gc, '_>,
|
||||
this: Option<Object<'gc>>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<Value<'gc>, Error> {
|
||||
if let Some(this) = this {
|
||||
let data =
|
||||
this.get_property(&QName::new(Namespace::public(), "data").into(), activation)?;
|
||||
|
||||
if let Value::Object(data) = data {
|
||||
// `bytesTotal` should be 0 while the download is in progress
|
||||
// (the `data` property is only set after the download is completed)
|
||||
if let Some(array) = data.as_bytearray() {
|
||||
return Ok(array.len().into());
|
||||
} else {
|
||||
return Err(format!("Unexpected value for `data` property: {:?}", data).into());
|
||||
}
|
||||
} else if let Value::String(data) = data {
|
||||
return Ok(data.len().into());
|
||||
}
|
||||
return Ok(0.into());
|
||||
}
|
||||
Ok(Value::Undefined)
|
||||
}
|
||||
|
||||
pub fn load<'gc>(
|
||||
activation: &mut Activation<'_, 'gc, '_>,
|
||||
this: Option<Object<'gc>>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<Value<'gc>, Error> {
|
||||
if let Some(this) = this {
|
||||
let request = match args.get(0) {
|
||||
Some(Value::Object(request)) => request,
|
||||
// This should never actually happen
|
||||
_ => return Ok(Value::Undefined),
|
||||
};
|
||||
|
||||
let data_format = this
|
||||
.get_property(
|
||||
&QName::new(Namespace::public(), "dataFormat").into(),
|
||||
activation,
|
||||
)?
|
||||
.coerce_to_string(activation)?;
|
||||
|
||||
let data_format = if data_format == AvmString::from("binary") {
|
||||
DataFormat::Binary
|
||||
} else if data_format == AvmString::from("text") {
|
||||
DataFormat::Text
|
||||
} else if data_format == AvmString::from("variables") {
|
||||
DataFormat::Variables
|
||||
} else {
|
||||
return Err(format!("Unknown data format: {}", data_format).into());
|
||||
};
|
||||
|
||||
return spawn_fetch(activation, this, request, data_format);
|
||||
}
|
||||
Ok(Value::Undefined)
|
||||
}
|
||||
|
||||
fn spawn_fetch<'gc>(
|
||||
activation: &mut Activation<'_, 'gc, '_>,
|
||||
loader_object: Object<'gc>,
|
||||
url_request: &Object<'gc>,
|
||||
data_format: DataFormat,
|
||||
) -> Result<Value<'gc>, Error> {
|
||||
let url = url_request
|
||||
.get_property(&QName::new(Namespace::public(), "url").into(), activation)?
|
||||
.coerce_to_string(activation)?;
|
||||
|
||||
let url = url.to_utf8_lossy();
|
||||
|
||||
let future = activation.context.load_manager.load_data_into_url_loader(
|
||||
activation.context.player.clone(),
|
||||
loader_object,
|
||||
&url,
|
||||
// FIXME - get these from the `URLRequest`
|
||||
RequestOptions::get(),
|
||||
data_format,
|
||||
);
|
||||
activation.context.navigator.spawn_future(future);
|
||||
Ok(Value::Undefined)
|
||||
}
|
||||
|
||||
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
|
||||
let class = Class::new(
|
||||
QName::new(Namespace::package("flash.net"), "URLLoader"),
|
||||
Some(QName::new(Namespace::package("flash.events"), "EventDispatcher").into()),
|
||||
Method::from_builtin(instance_init, "<URLLoader instance initializer>", mc),
|
||||
Method::from_builtin(class_init, "<URLLoader class initializer>", mc),
|
||||
mc,
|
||||
);
|
||||
|
||||
let mut write = class.write(mc);
|
||||
|
||||
const PUBLIC_INSTANCE_PROPERTIES: &[(
|
||||
&str,
|
||||
Option<NativeMethodImpl>,
|
||||
Option<NativeMethodImpl>,
|
||||
)] = &[
|
||||
("bytesLoaded", Some(bytes_loaded), None),
|
||||
("bytesTotal", Some(bytes_total), None),
|
||||
];
|
||||
write.define_public_builtin_instance_properties(mc, PUBLIC_INSTANCE_PROPERTIES);
|
||||
|
||||
const PUBLIC_INSTANCE_SLOTS: &[(&str, &str, &str)] = &[("data", "", "Object")];
|
||||
write.define_public_slot_instance_traits(PUBLIC_INSTANCE_SLOTS);
|
||||
|
||||
const PUBLIC_INSTANCE_METHODS: &[(&str, NativeMethodImpl)] = &[("load", load)];
|
||||
write.define_public_builtin_instance_methods(mc, PUBLIC_INSTANCE_METHODS);
|
||||
|
||||
class
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
//! `flash.net.URLLoaderDataFormat` builtin/prototype
|
||||
|
||||
use crate::avm2::activation::Activation;
|
||||
use crate::avm2::class::{Class, ClassAttributes};
|
||||
use crate::avm2::method::Method;
|
||||
use crate::avm2::names::{Namespace, QName};
|
||||
use crate::avm2::object::Object;
|
||||
use crate::avm2::value::Value;
|
||||
use crate::avm2::Error;
|
||||
use gc_arena::{GcCell, MutationContext};
|
||||
|
||||
/// Implements `flash.net.URLLoaderDataFormat`'s instance constructor.
|
||||
pub fn instance_init<'gc>(
|
||||
_activation: &mut Activation<'_, 'gc, '_>,
|
||||
_this: Option<Object<'gc>>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<Value<'gc>, Error> {
|
||||
Ok(Value::Undefined)
|
||||
}
|
||||
|
||||
/// Implements `flash.net.URLLoaderDataFormat`'s class constructor.
|
||||
pub fn class_init<'gc>(
|
||||
_activation: &mut Activation<'_, 'gc, '_>,
|
||||
_this: Option<Object<'gc>>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<Value<'gc>, Error> {
|
||||
Ok(Value::Undefined)
|
||||
}
|
||||
|
||||
/// Construct `URLLoaderDataFormat`'s class.
|
||||
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
|
||||
let class = Class::new(
|
||||
QName::new(Namespace::package("flash.net"), "URLLoaderDataFormat"),
|
||||
Some(QName::new(Namespace::public(), "Object").into()),
|
||||
Method::from_builtin(
|
||||
instance_init,
|
||||
"<URLLoaderDataFormat instance initializer>",
|
||||
mc,
|
||||
),
|
||||
Method::from_builtin(class_init, "<URLLoaderDataFormat class initializer>", mc),
|
||||
mc,
|
||||
);
|
||||
|
||||
let mut write = class.write(mc);
|
||||
|
||||
write.set_attributes(ClassAttributes::SEALED | ClassAttributes::FINAL);
|
||||
|
||||
const CONSTANTS: &[(&str, &str)] = &[
|
||||
("BINARY", "binary"),
|
||||
("TEXT", "text"),
|
||||
("VARIABLES", "variables"),
|
||||
];
|
||||
|
||||
write.define_public_constant_string_class_traits(CONSTANTS);
|
||||
class
|
||||
}
|
|
@ -4,6 +4,7 @@ use crate::avm2::activation::Activation;
|
|||
use crate::avm2::class::{Class, ClassAttributes};
|
||||
use crate::avm2::method::Method;
|
||||
use crate::avm2::names::{Namespace, QName};
|
||||
use crate::avm2::object::TObject;
|
||||
use crate::avm2::value::Value;
|
||||
use crate::avm2::{Error, Object};
|
||||
use gc_arena::{GcCell, MutationContext};
|
||||
|
@ -19,10 +20,19 @@ pub fn class_init<'gc>(
|
|||
|
||||
/// Implements `flash.net.URLRequest`'s instance constructor.
|
||||
pub fn instance_init<'gc>(
|
||||
_activation: &mut Activation<'_, 'gc, '_>,
|
||||
_this: Option<Object<'gc>>,
|
||||
_args: &[Value<'gc>],
|
||||
activation: &mut Activation<'_, 'gc, '_>,
|
||||
this: Option<Object<'gc>>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<Value<'gc>, Error> {
|
||||
if let Some(mut this) = this {
|
||||
if let Some(url) = args.get(0) {
|
||||
this.set_property(
|
||||
&QName::new(Namespace::public(), "url").into(),
|
||||
*url,
|
||||
activation,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(Value::Undefined)
|
||||
}
|
||||
|
||||
|
@ -38,5 +48,10 @@ pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>
|
|||
let mut write = class.write(mc);
|
||||
write.set_attributes(ClassAttributes::FINAL | ClassAttributes::SEALED);
|
||||
|
||||
// NOTE - when implementing properties (e.g. `contentType`, `data`, etc.)
|
||||
// be sure to also check for them in `UrlLoader`
|
||||
const PUBLIC_INSTANCE_SLOTS: &[(&str, &str, &str)] = &[("url", "", "String")];
|
||||
write.define_public_slot_instance_traits(PUBLIC_INSTANCE_SLOTS);
|
||||
|
||||
class
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ impl<'gc> EventObject<'gc> {
|
|||
EventData::Empty => activation.avm2().classes().event,
|
||||
EventData::FullScreen { .. } => activation.avm2().classes().fullscreenevent,
|
||||
EventData::Mouse { .. } => activation.avm2().classes().mouseevent,
|
||||
EventData::IOError { .. } => activation.avm2().classes().ioerrorevent,
|
||||
};
|
||||
|
||||
let proto = class.prototype();
|
||||
|
|
|
@ -3,7 +3,14 @@
|
|||
use crate::avm1::activation::{Activation, ActivationIdentifier};
|
||||
use crate::avm1::function::ExecutionReason;
|
||||
use crate::avm1::{Avm1, Object, TObject, Value};
|
||||
use crate::avm2::{Activation as Avm2Activation, Domain as Avm2Domain};
|
||||
use crate::avm2::bytearray::ByteArrayStorage;
|
||||
use crate::avm2::names::Namespace;
|
||||
use crate::avm2::object::ByteArrayObject;
|
||||
use crate::avm2::object::TObject as _;
|
||||
use crate::avm2::{
|
||||
Activation as Avm2Activation, Avm2, Domain as Avm2Domain, Event as Avm2Event,
|
||||
EventData as Avm2EventData, Object as Avm2Object, QName, Value as Avm2Value,
|
||||
};
|
||||
use crate::backend::navigator::{OwnedFuture, RequestOptions};
|
||||
use crate::backend::render::{determine_jpeg_tag_format, JpegTagFormat};
|
||||
use crate::context::{ActionQueue, ActionType, UpdateContext};
|
||||
|
@ -77,6 +84,14 @@ impl ContentType {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Collect, Copy, Clone)]
|
||||
#[collect(no_drop)]
|
||||
pub enum DataFormat {
|
||||
Binary,
|
||||
Text,
|
||||
Variables,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Load cancelled")]
|
||||
|
@ -94,6 +109,9 @@ pub enum Error {
|
|||
#[error("Non-load vars loader spawned as load vars loader")]
|
||||
NotLoadVarsLoader,
|
||||
|
||||
#[error("Non-data loader spawned as data loader")]
|
||||
NotLoadDataLoader,
|
||||
|
||||
#[error("Could not fetch: {0}")]
|
||||
FetchError(String),
|
||||
|
||||
|
@ -143,7 +161,8 @@ impl<'gc> LoadManager<'gc> {
|
|||
Loader::RootMovie { self_handle, .. }
|
||||
| Loader::Movie { self_handle, .. }
|
||||
| Loader::Form { self_handle, .. }
|
||||
| Loader::LoadVars { self_handle, .. } => *self_handle = Some(handle),
|
||||
| Loader::LoadVars { self_handle, .. }
|
||||
| Loader::LoadURLLoader { self_handle, .. } => *self_handle = Some(handle),
|
||||
}
|
||||
handle
|
||||
}
|
||||
|
@ -255,6 +274,27 @@ impl<'gc> LoadManager<'gc> {
|
|||
let loader = self.get_loader_mut(handle).unwrap();
|
||||
loader.load_vars_loader(player, url.to_owned(), options)
|
||||
}
|
||||
|
||||
/// Kick off a data load into a `URLLoader`, updating
|
||||
/// its `data` property when the load completes.
|
||||
///
|
||||
/// Returns the loader's async process, which you will need to spawn.
|
||||
pub fn load_data_into_url_loader(
|
||||
&mut self,
|
||||
player: Weak<Mutex<Player>>,
|
||||
target_object: Avm2Object<'gc>,
|
||||
url: &str,
|
||||
options: RequestOptions,
|
||||
data_format: DataFormat,
|
||||
) -> OwnedFuture<(), Error> {
|
||||
let loader = Loader::LoadURLLoader {
|
||||
self_handle: None,
|
||||
target_object,
|
||||
};
|
||||
let handle = self.add_loader(loader);
|
||||
let loader = self.get_loader_mut(handle).unwrap();
|
||||
loader.load_url_loader(player, url.to_owned(), options, data_format)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gc> Default for LoadManager<'gc> {
|
||||
|
@ -329,6 +369,17 @@ pub enum Loader<'gc> {
|
|||
/// The target AVM1 object to load form data into.
|
||||
target_object: Object<'gc>,
|
||||
},
|
||||
|
||||
/// Loader that is loading data into a `URLLoader`'s `data` property
|
||||
/// The `data` property is only updated after the data is loaded completely
|
||||
LoadURLLoader {
|
||||
/// The handle to refer to this loader instance.
|
||||
#[collect(require_static)]
|
||||
self_handle: Option<Handle>,
|
||||
|
||||
/// The target `URLLoader` to load data into.
|
||||
target_object: Avm2Object<'gc>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'gc> Loader<'gc> {
|
||||
|
@ -615,6 +666,144 @@ impl<'gc> Loader<'gc> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Creates a future for a LoadURLLoader load call.
|
||||
fn load_url_loader(
|
||||
&mut self,
|
||||
player: Weak<Mutex<Player>>,
|
||||
url: String,
|
||||
options: RequestOptions,
|
||||
data_format: DataFormat,
|
||||
) -> OwnedFuture<(), Error> {
|
||||
let handle = match self {
|
||||
Loader::LoadURLLoader { self_handle, .. } => {
|
||||
self_handle.expect("Loader not self-introduced")
|
||||
}
|
||||
_ => return Box::pin(async { Err(Error::NotLoadDataLoader) }),
|
||||
};
|
||||
|
||||
let player = player
|
||||
.upgrade()
|
||||
.expect("Could not upgrade weak reference to player");
|
||||
|
||||
Box::pin(async move {
|
||||
let fetch = player.lock().unwrap().navigator().fetch(&url, options);
|
||||
let response = fetch.await;
|
||||
|
||||
player.lock().unwrap().update(|uc| {
|
||||
let loader = uc.load_manager.get_loader(handle);
|
||||
let target = match loader {
|
||||
Some(&Loader::LoadURLLoader { target_object, .. }) => target_object,
|
||||
// We would have already returned after the previous 'update' call
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let mut activation = Avm2Activation::from_nothing(uc.reborrow());
|
||||
|
||||
fn set_data<'a, 'gc: 'a, 'gc_context: 'a>(
|
||||
body: Vec<u8>,
|
||||
activation: &mut Avm2Activation<'a, 'gc, 'gc_context>,
|
||||
mut target: Avm2Object<'gc>,
|
||||
data_format: DataFormat,
|
||||
) {
|
||||
let data_object = match data_format {
|
||||
DataFormat::Binary => {
|
||||
let storage = ByteArrayStorage::from_vec(body);
|
||||
let bytearray =
|
||||
ByteArrayObject::from_storage(activation, storage).unwrap();
|
||||
bytearray.into()
|
||||
}
|
||||
DataFormat::Text => {
|
||||
// FIXME - what do we do if the data is not UTF-8?
|
||||
Avm2Value::String(
|
||||
AvmString::new_utf8_bytes(activation.context.gc_context, body)
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
DataFormat::Variables => {
|
||||
log::warn!(
|
||||
"Support for URLLoaderDataFormat.VARIABLES not yet implemented"
|
||||
);
|
||||
Avm2Value::Undefined
|
||||
}
|
||||
};
|
||||
|
||||
target
|
||||
.set_property(
|
||||
&QName::new(Namespace::public(), "data").into(),
|
||||
data_object,
|
||||
activation,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
match response {
|
||||
Ok(response) => {
|
||||
// FIXME - the "open" event should be fired earlier, just before
|
||||
// we start to fetch the data.
|
||||
// However, the "open" event should not be fired if an IO error
|
||||
// occurs opening the connection (e.g. if a file does not exist on disk).
|
||||
// We currently have no way of detecting this, so we settle for firing
|
||||
// the event after the entire fetch is complete. This causes there
|
||||
// to a longer delay between the initial load triggered by the script
|
||||
// and the "load" event firing, but it ensures that we match
|
||||
// the Flash behavior w.r.t when an event is fired vs not fired.
|
||||
let mut open_evt = Avm2Event::new("open", Avm2EventData::Empty);
|
||||
open_evt.set_bubbles(false);
|
||||
open_evt.set_cancelable(false);
|
||||
|
||||
if let Err(e) =
|
||||
Avm2::dispatch_event(&mut activation.context, open_evt, target)
|
||||
{
|
||||
log::error!(
|
||||
"Encountered AVM2 error when broadcasting `open` event: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
set_data(response.body, &mut activation, target, data_format);
|
||||
|
||||
let mut complete_evt = Avm2Event::new("complete", Avm2EventData::Empty);
|
||||
complete_evt.set_bubbles(false);
|
||||
complete_evt.set_cancelable(false);
|
||||
|
||||
if let Err(e) = Avm2::dispatch_event(uc, complete_evt, target) {
|
||||
log::error!(
|
||||
"Encountered AVM2 error when broadcasting `complete` event: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// Testing with Flash shoes that the 'data' property is cleared
|
||||
// when an error occurs
|
||||
|
||||
set_data(Vec::new(), &mut activation, target, data_format);
|
||||
let mut io_error_evt = Avm2Event::new(
|
||||
"ioError",
|
||||
Avm2EventData::IOError {
|
||||
text: AvmString::new_utf8(
|
||||
activation.context.gc_context,
|
||||
format!("Ruffle: Failed to fetch url '{:?}' : {:?}", url, err),
|
||||
),
|
||||
},
|
||||
);
|
||||
io_error_evt.set_bubbles(false);
|
||||
io_error_evt.set_cancelable(false);
|
||||
|
||||
if let Err(e) = Avm2::dispatch_event(uc, io_error_evt, target) {
|
||||
log::error!(
|
||||
"Encountered AVM2 error when broadcasting `ioError` event: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Report a movie loader start event to script code.
|
||||
fn movie_loader_start(handle: Index, uc: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> {
|
||||
let me = uc.load_manager.get_loader_mut(handle);
|
||||
|
|
|
@ -460,6 +460,7 @@ swf_tests! {
|
|||
#[ignore] (as3_uint_toprecision, "avm2/uint_toprecision", 1), //Ignored because Flash Player has a print routine that adds extraneous zeros to things
|
||||
(as3_uint_tostring, "avm2/uint_tostring", 1),
|
||||
(as3_unchecked_function, "avm2/unchecked_function", 1),
|
||||
(as3_url_loader, "avm2/url_loader", 1),
|
||||
(as3_urshift, "avm2/urshift", 1),
|
||||
(as3_vector_coercion, "avm2/vector_coercion", 1),
|
||||
(as3_vector_concat, "avm2/vector_concat", 1),
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
package {
|
||||
public class Test {
|
||||
}
|
||||
}
|
||||
|
||||
import flash.net.URLLoader;
|
||||
import flash.net.URLRequest;
|
||||
import flash.net.URLLoaderDataFormat;
|
||||
import flash.events.IOErrorEvent;
|
||||
import flash.events.Event;
|
||||
import flash.utils.setInterval;
|
||||
import flash.utils.clearInterval;
|
||||
|
||||
var txtRequest:URLRequest = new URLRequest("data.txt");
|
||||
var binRequest:URLRequest = new URLRequest("data.bin");
|
||||
var missingRequest:URLRequest = new URLRequest("missingFile.bin");
|
||||
var urlLoader:URLLoader = new URLLoader();
|
||||
urlLoader.addEventListener(Event.OPEN, on_open);
|
||||
urlLoader.addEventListener(Event.COMPLETE, on_complete);
|
||||
urlLoader.addEventListener(IOErrorEvent.IO_ERROR, on_error);
|
||||
urlLoader.load(txtRequest);
|
||||
|
||||
var state = "first";
|
||||
|
||||
function on_open(evt: Event):void {
|
||||
trace("Event.OPEN with: ", evt.target)
|
||||
trace("Got data: " + evt.target.data);
|
||||
}
|
||||
|
||||
function on_complete(evt:Event):void {
|
||||
trace("Event.COMPLETE with: " + evt.target);
|
||||
trace("bytesTotal: " + evt.target.bytesTotal);
|
||||
if (state == "first") {
|
||||
trace("Loaded text: " + evt.target.data)
|
||||
state = "second";
|
||||
urlLoader.dataFormat = URLLoaderDataFormat.BINARY;
|
||||
urlLoader.load(binRequest);
|
||||
} else if (state == "second") {
|
||||
trace("Loaded binary with length: " + evt.target.data.bytesAvailable);
|
||||
while (evt.target.data.bytesAvailable != 0) {
|
||||
trace(evt.target.data.readByte());
|
||||
}
|
||||
|
||||
state = "third";
|
||||
urlLoader.load(missingRequest);
|
||||
} else if (state == "third") {
|
||||
trace("ERROR: expected `missingRequest` to fail");
|
||||
}
|
||||
}
|
||||
|
||||
function on_error(evt:IOErrorEvent):void {
|
||||
trace("IOErrorEvent.IO_ERROR: " + evt.target);
|
||||
// FIXME - this needs to be implemented in Ruffle
|
||||
trace("IOErrorEvent text: " + evt.text);
|
||||
trace("Old data: " + evt.target.data);
|
||||
|
||||
// Now, perform a load that's started by the constructor
|
||||
var loader = new URLLoader(txtRequest);
|
||||
// FIXME - setInterval is not currently implemented,
|
||||
// so the rest of this test does not work under Ruffle
|
||||
/*var interval = setInterval(checkData, 100);
|
||||
|
||||
function checkData() {
|
||||
if (loader.data != null) {
|
||||
trace("Loaded using constructor: " + loader.data);
|
||||
clearInterval(interval);
|
||||
}
|
||||
}*/
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
Fetched from disk!
|
|
@ -0,0 +1,19 @@
|
|||
Event.OPEN with: [object URLLoader]
|
||||
Got data: undefined
|
||||
Event.COMPLETE with: [object URLLoader]
|
||||
bytesTotal: 19
|
||||
Loaded text: Fetched from disk!
|
||||
|
||||
Event.OPEN with: [object URLLoader]
|
||||
Got data: Fetched from disk!
|
||||
|
||||
Event.COMPLETE with: [object URLLoader]
|
||||
bytesTotal: 4
|
||||
Loaded binary with length: 4
|
||||
0
|
||||
1
|
||||
2
|
||||
3
|
||||
IOErrorEvent.IO_ERROR: [object URLLoader]
|
||||
IOErrorEvent text: Ruffle: Failed to fetch url '"missingFile.bin"' : FetchError("No such file or directory (os error 2)")
|
||||
Old data:
|
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue