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:
Aaron Hill 2022-04-05 21:27:39 -04:00 committed by Mike Welsh
parent 7130c6c1c1
commit 8d8a7600d8
17 changed files with 583 additions and 10 deletions

View File

@ -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;

View File

@ -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> {

View File

@ -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),

View File

@ -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
}

View File

@ -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;

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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();

View File

@ -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);

View File

@ -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),

View File

@ -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.

View File

@ -0,0 +1 @@
Fetched from disk!

View File

@ -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.