avm2: Implement FileReference browse

This commit is contained in:
Tom Schuster 2023-11-25 16:50:55 +01:00
parent e7b8b75d07
commit fff841a22e
9 changed files with 341 additions and 54 deletions

View File

@ -325,6 +325,34 @@ pub fn make_error_2008<'gc>(activation: &mut Activation<'_, 'gc>, param_name: &s
} }
} }
#[inline(never)]
#[cold]
pub fn make_error_2037<'gc>(activation: &mut Activation<'_, 'gc>) -> Error<'gc> {
let err = error(
activation,
"Error #2037: Functions called in incorrect sequence, or earlier call was unsuccessful.",
2037,
);
match err {
Ok(err) => Error::AvmError(err),
Err(err) => err,
}
}
#[inline(never)]
#[cold]
pub fn make_error_2097<'gc>(activation: &mut Activation<'_, 'gc>) -> Error<'gc> {
let err = argument_error(
activation,
"Error #2097: The FileFilter Array is not in the correct format.",
2097,
);
match err {
Ok(err) => Error::AvmError(err),
Err(err) => err,
}
}
#[inline(never)] #[inline(never)]
#[cold] #[cold]
pub fn make_error_2126<'gc>(activation: &mut Activation<'_, 'gc>) -> Error<'gc> { pub fn make_error_2126<'gc>(activation: &mut Activation<'_, 'gc>) -> Error<'gc> {

View File

@ -168,6 +168,7 @@ pub struct SystemClasses<'gc> {
pub statusevent: ClassObject<'gc>, pub statusevent: ClassObject<'gc>,
pub contextmenuevent: ClassObject<'gc>, pub contextmenuevent: ClassObject<'gc>,
pub filereference: ClassObject<'gc>, pub filereference: ClassObject<'gc>,
pub filefilter: ClassObject<'gc>,
pub font: ClassObject<'gc>, pub font: ClassObject<'gc>,
pub textline: ClassObject<'gc>, pub textline: ClassObject<'gc>,
pub sampledataevent: ClassObject<'gc>, pub sampledataevent: ClassObject<'gc>,
@ -298,6 +299,7 @@ impl<'gc> SystemClasses<'gc> {
statusevent: object, statusevent: object,
contextmenuevent: object, contextmenuevent: object,
filereference: object, filereference: object,
filefilter: object,
font: object, font: object,
textline: object, textline: object,
sampledataevent: object, sampledataevent: object,
@ -809,6 +811,7 @@ fn load_playerglobal<'gc>(
("flash.media", "Video", video), ("flash.media", "Video", video),
("flash.net", "URLVariables", urlvariables), ("flash.net", "URLVariables", urlvariables),
("flash.net", "FileReference", filereference), ("flash.net", "FileReference", filereference),
("flash.net", "FileFilter", filefilter),
("flash.utils", "ByteArray", bytearray), ("flash.utils", "ByteArray", bytearray),
("flash.system", "ApplicationDomain", application_domain), ("flash.system", "ApplicationDomain", application_domain),
("flash.text", "Font", font), ("flash.text", "Font", font),

View File

@ -4,8 +4,8 @@ use crate::avm2::error::type_error;
use crate::avm2::object::TObject; use crate::avm2::object::TObject;
use crate::avm2::{Activation, Error, Object, Value}; use crate::avm2::{Activation, Error, Object, Value};
pub mod local_connection;
pub mod file_reference; pub mod file_reference;
pub mod local_connection;
pub mod net_connection; pub mod net_connection;
pub mod net_stream; pub mod net_stream;
pub mod object_encoding; pub mod object_encoding;

View File

@ -9,7 +9,6 @@ package flash.net
{ {
private var _creationDate: Date; private var _creationDate: Date;
private var _creator: String; private var _creator: String;
private var _data: ByteArray;
private var _extension: String; private var _extension: String;
private var _modificationDate: Date; private var _modificationDate: Date;
private var _name: String; private var _name: String;
@ -29,9 +28,7 @@ package flash.net
return this._creator; return this._creator;
} }
public function get data(): ByteArray { public native function get data(): ByteArray;
return this._data;
}
public function get extension(): String { public function get extension(): String {
return this._extension; return this._extension;
@ -57,9 +54,7 @@ package flash.net
return this._type; return this._type;
} }
public function browse(typeFilter:Array = null):Boolean { public native function browse(typeFilter:Array = null): Boolean;
return false;
}
public function cancel():void { public function cancel():void {
stub_method("flash.net.FileReference", "cancel"); stub_method("flash.net.FileReference", "cancel");
@ -69,9 +64,7 @@ package flash.net
stub_method("flash.net.FileReference", "download"); stub_method("flash.net.FileReference", "download");
} }
public function load():void { public native function load():void;
stub_method("flash.net.FileReference", "load");
}
public function requestPermission():void { public function requestPermission():void {
stub_method("flash.net.FileReference", "requestPermission"); stub_method("flash.net.FileReference", "requestPermission");

View File

@ -1 +1,132 @@
use crate::avm2::bytearray::ByteArrayStorage;
use crate::avm2::error::{make_error_2037, make_error_2097};
pub use crate::avm2::object::file_reference_allocator; pub use crate::avm2::object::file_reference_allocator;
use crate::avm2::object::{ByteArrayObject, FileReference};
use crate::avm2::{Activation, Avm2, Error, EventObject, Object, TObject, Value};
use crate::backend::ui::FileFilter;
pub fn get_data<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let this = this.as_file_reference().unwrap();
let bytearray = match *this.file_reference() {
FileReference::FileDialogResult(ref dialog_result) if this.loaded() => {
let bytes = dialog_result.contents();
let storage = ByteArrayStorage::from_vec(bytes.to_vec());
ByteArrayObject::from_storage(activation, storage)?
}
// Contrary to other getters `data` will return null instead of throwing.
_ => return Ok(Value::Null),
};
Ok(bytearray.into())
}
pub fn browse<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let this = this.as_file_reference().unwrap();
let mut filters = Vec::new();
if let Value::Object(obj) = args[0] {
if let Some(array_storage) = obj.as_array_storage() {
for filter in array_storage.iter() {
if let Some(Value::Object(obj)) = filter {
let filefilter = activation
.avm2()
.classes()
.filefilter
.inner_class_definition();
if !obj.is_of_type(filefilter, &mut activation.context) {
return Err(make_error_2097(activation));
}
let description = obj.get_public_property("description", activation)?;
let extension = obj.get_public_property("extension", activation)?;
let mac_type = obj.get_public_property("macType", activation)?;
// The description and extension must be non-empty strings.
match (description, extension) {
(Value::String(description), Value::String(extension))
if !description.is_empty() && !extension.is_empty() =>
{
let mac_type = match mac_type {
Value::String(mac_type) if !mac_type.is_empty() => {
Some(mac_type.to_string())
}
_ => None,
};
filters.push(FileFilter {
description: description.to_string(),
extensions: extension.to_string(),
mac_type,
});
}
_ => return Err(make_error_2097(activation)),
}
} else {
return Err(make_error_2097(activation));
}
}
}
}
let dialog = activation.context.ui.display_file_open_dialog(filters);
let result = match dialog {
Some(dialog) => {
let process = activation.context.load_manager.select_file_dialog_avm2(
activation.context.player.clone(),
this,
dialog,
);
activation.context.navigator.spawn_future(process);
true
}
None => false,
};
Ok(result.into())
}
pub fn load<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let this = this.as_file_reference().unwrap();
// Somewhat unexpectedly, we don't need to load anything here, because
// that already happened during browse() or save().
let size = match *this.file_reference() {
FileReference::None => return Err(make_error_2037(activation)),
FileReference::FileDialogResult(ref dialog_result) => dialog_result.size().unwrap_or(0),
};
let open_evt = EventObject::bare_default_event(&mut activation.context, "open");
Avm2::dispatch_event(&mut activation.context, open_evt, this.into());
let progress_evt = EventObject::progress_event(activation, "progress", 0, size, false, false);
Avm2::dispatch_event(&mut activation.context, progress_evt, this.into());
let open_evt2 = EventObject::bare_default_event(&mut activation.context, "open");
Avm2::dispatch_event(&mut activation.context, open_evt2, this.into());
let progress_evt2 =
EventObject::progress_event(activation, "progress", size, size, false, false);
Avm2::dispatch_event(&mut activation.context, progress_evt2, this.into());
this.set_loaded(true);
let complete_evt = EventObject::bare_default_event(&mut activation.context, "complete");
Avm2::dispatch_event(&mut activation.context, complete_evt, this.into());
Ok(Value::Undefined)
}

View File

@ -88,7 +88,9 @@ pub use crate::avm2::object::domain_object::{
}; };
pub use crate::avm2::object::error_object::{error_allocator, ErrorObject, ErrorObjectWeak}; pub use crate::avm2::object::error_object::{error_allocator, ErrorObject, ErrorObjectWeak};
pub use crate::avm2::object::event_object::{event_allocator, EventObject, EventObjectWeak}; pub use crate::avm2::object::event_object::{event_allocator, EventObject, EventObjectWeak};
pub use crate::avm2::object::file_reference_object::{file_reference_allocator, FileReferenceObject, FileReferenceObjectWeak}; pub use crate::avm2::object::file_reference_object::{
file_reference_allocator, FileReference, FileReferenceObject, FileReferenceObjectWeak,
};
pub use crate::avm2::object::font_object::{font_allocator, FontObject, FontObjectWeak}; pub use crate::avm2::object::font_object::{font_allocator, FontObject, FontObjectWeak};
pub use crate::avm2::object::function_object::{ pub use crate::avm2::object::function_object::{
function_allocator, FunctionObject, FunctionObjectWeak, function_allocator, FunctionObject, FunctionObjectWeak,

View File

@ -211,6 +211,38 @@ impl<'gc> EventObject<'gc> {
) )
.unwrap() // we don't expect to break here .unwrap() // we don't expect to break here
} }
pub fn progress_event<S>(
activation: &mut Activation<'_, 'gc>,
event_type: S,
bytes_loaded: u64,
bytes_total: u64,
bubbles: bool,
cancelable: bool,
) -> Object<'gc>
where
S: Into<AvmString<'gc>>,
{
let event_type: AvmString<'gc> = event_type.into();
let progress_event_cls = activation.avm2().classes().progressevent;
progress_event_cls
.construct(
activation,
&[
event_type.into(),
// bubbles
bubbles.into(),
// cancelable
cancelable.into(),
// bytesLoaded
(bytes_loaded as f64).into(),
// bytesToal
(bytes_total as f64).into(),
],
)
.unwrap() // we don't expect to break here
}
} }
impl<'gc> TObject<'gc> for EventObject<'gc> { impl<'gc> TObject<'gc> for EventObject<'gc> {

View File

@ -2,6 +2,7 @@ use crate::avm2::object::script_object::ScriptObjectData;
use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject}; use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject};
use crate::avm2::value::Value; use crate::avm2::value::Value;
use crate::avm2::{Activation, Error}; use crate::avm2::{Activation, Error};
use crate::backend::ui::FileDialogResult;
use gc_arena::barrier::unlock; use gc_arena::barrier::unlock;
use gc_arena::{lock::RefLock, Collect, Gc}; use gc_arena::{lock::RefLock, Collect, Gc};
use gc_arena::{GcWeak, Mutation}; use gc_arena::{GcWeak, Mutation};
@ -18,12 +19,13 @@ pub fn file_reference_allocator<'gc>(
activation.context.gc(), activation.context.gc(),
FileReferenceObjectData { FileReferenceObjectData {
base, base,
reference: RefCell::new(FileReference::None),
loaded: Cell::new(false),
}, },
)) ))
.into()) .into())
} }
#[derive(Clone, Collect, Copy)] #[derive(Clone, Collect, Copy)]
#[collect(no_drop)] #[collect(no_drop)]
pub struct FileReferenceObject<'gc>(pub Gc<'gc, FileReferenceObjectData<'gc>>); pub struct FileReferenceObject<'gc>(pub Gc<'gc, FileReferenceObjectData<'gc>>);
@ -54,11 +56,40 @@ impl<'gc> TObject<'gc> for FileReferenceObject<'gc> {
} }
} }
impl<'gc> FileReferenceObject<'gc> {
pub fn init_from_dialog_result(&self, result: Box<dyn FileDialogResult>) -> FileReference {
self.0
.reference
.replace(FileReference::FileDialogResult(result))
}
pub fn file_reference(&self) -> Ref<'_, FileReference> {
self.0.reference.borrow()
}
pub fn set_loaded(&self, value: bool) {
self.0.loaded.set(value)
}
pub fn loaded(&self) -> bool {
self.0.loaded.get()
}
}
pub enum FileReference {
None,
FileDialogResult(Box<dyn FileDialogResult>),
}
#[derive(Collect)] #[derive(Collect)]
#[collect(no_drop)] #[collect(no_drop)]
pub struct FileReferenceObjectData<'gc> { pub struct FileReferenceObjectData<'gc> {
/// Base script object /// Base script object
base: RefLock<ScriptObjectData<'gc>>, base: RefLock<ScriptObjectData<'gc>>,
reference: RefCell<FileReference>,
loaded: Cell<bool>,
} }
impl fmt::Debug for FileReferenceObject<'_> { impl fmt::Debug for FileReferenceObject<'_> {

View File

@ -6,7 +6,8 @@ use crate::avm1::{ExecutionReason, NativeObject};
use crate::avm1::{Object, SoundObject, TObject, Value}; use crate::avm1::{Object, SoundObject, TObject, Value};
use crate::avm2::bytearray::ByteArrayStorage; use crate::avm2::bytearray::ByteArrayStorage;
use crate::avm2::object::{ use crate::avm2::object::{
ByteArrayObject, EventObject as Avm2EventObject, LoaderStream, TObject as _, ByteArrayObject, EventObject as Avm2EventObject, FileReferenceObject, LoaderStream,
TObject as _,
}; };
use crate::avm2::{ use crate::avm2::{
Activation as Avm2Activation, Avm2, Domain as Avm2Domain, Object as Avm2Object, Activation as Avm2Activation, Avm2, Domain as Avm2Domain, Object as Avm2Object,
@ -248,6 +249,7 @@ impl<'gc> LoadManager<'gc> {
| Loader::SoundAvm2 { self_handle, .. } | Loader::SoundAvm2 { self_handle, .. }
| Loader::NetStream { self_handle, .. } | Loader::NetStream { self_handle, .. }
| Loader::FileDialog { self_handle, .. } | Loader::FileDialog { self_handle, .. }
| Loader::FileDialogAvm2 { self_handle, .. }
| Loader::DownloadFileDialog { self_handle, .. } | Loader::DownloadFileDialog { self_handle, .. }
| Loader::UploadFile { self_handle, .. } | Loader::UploadFile { self_handle, .. }
| Loader::MovieUnloader { self_handle, .. } => *self_handle = Some(handle), | Loader::MovieUnloader { self_handle, .. } => *self_handle = Some(handle),
@ -504,6 +506,22 @@ impl<'gc> LoadManager<'gc> {
loader.file_dialog_loader(player, dialog) loader.file_dialog_loader(player, dialog)
} }
#[must_use]
pub fn select_file_dialog_avm2(
&mut self,
player: Weak<Mutex<Player>>,
target_object: FileReferenceObject<'gc>,
dialog: DialogResultFuture,
) -> OwnedFuture<(), Error> {
let loader = Loader::FileDialogAvm2 {
self_handle: None,
target_object,
};
let handle = self.add_loader(loader);
let loader = self.get_loader_mut(handle).unwrap();
loader.file_dialog_loader(player, dialog)
}
/// Display a dialog allowing a user to download a file /// Display a dialog allowing a user to download a file
/// ///
/// Returns a future that will be resolved when a file is selected and the download has completed /// Returns a future that will be resolved when a file is selected and the download has completed
@ -708,6 +726,16 @@ pub enum Loader<'gc> {
target_object: Object<'gc>, target_object: Object<'gc>,
}, },
/// Loader that is choosing a file from an AVM2 scope.
FileDialogAvm2 {
/// The handle to refer to this loader instance.
#[collect(require_static)]
self_handle: Option<Handle>,
/// The target AVM2 object to set to the selected file path.
target_object: FileReferenceObject<'gc>,
},
/// Loader that is downloading a file from an AVM1 object scope. /// Loader that is downloading a file from an AVM1 object scope.
DownloadFileDialog { DownloadFileDialog {
/// The handle to refer to this loader instance. /// The handle to refer to this loader instance.
@ -2399,7 +2427,7 @@ impl<'gc> Loader<'gc> {
dialog: DialogResultFuture, dialog: DialogResultFuture,
) -> OwnedFuture<(), Error> { ) -> OwnedFuture<(), Error> {
let handle = match self { let handle = match self {
Loader::FileDialog { self_handle, .. } => { Loader::FileDialog { self_handle, .. } | Loader::FileDialogAvm2 { self_handle, .. } => {
self_handle.expect("Loader not self-introduced") self_handle.expect("Loader not self-introduced")
} }
_ => return Box::pin(async { Err(Error::NotFileDialogLoader) }), _ => return Box::pin(async { Err(Error::NotFileDialogLoader) }),
@ -2418,12 +2446,8 @@ impl<'gc> Loader<'gc> {
// Fire the load handler. // Fire the load handler.
player.lock().unwrap().update(|uc| -> Result<(), Error> { player.lock().unwrap().update(|uc| -> Result<(), Error> {
let loader = uc.load_manager.get_loader(handle); let loader = uc.load_manager.get_loader(handle);
let target_object = match loader { match loader {
Some(&Loader::FileDialog { target_object, .. }) => target_object, Some(&Loader::FileDialog { target_object, .. }) => {
None => return Err(Error::Cancelled),
_ => return Err(Error::NotFileDialogLoader),
};
let file_ref = match target_object.native() { let file_ref = match target_object.native() {
NativeObject::FileReference(fr) => fr, NativeObject::FileReference(fr) => fr,
_ => panic!("NativeObject must be FileReference"), _ => panic!("NativeObject must be FileReference"),
@ -2439,8 +2463,10 @@ impl<'gc> Loader<'gc> {
use crate::avm1::globals::as_broadcaster; use crate::avm1::globals::as_broadcaster;
if !dialog_result.is_cancelled() { if !dialog_result.is_cancelled() {
file_ref file_ref.init_from_dialog_result(
.init_from_dialog_result(&mut activation, dialog_result.borrow()); &mut activation,
dialog_result.borrow(),
);
as_broadcaster::broadcast_internal( as_broadcaster::broadcast_internal(
&mut activation, &mut activation,
target_object, target_object,
@ -2460,8 +2486,49 @@ impl<'gc> Loader<'gc> {
tracing::warn!("Error on file dialog: {:?}", err); tracing::warn!("Error on file dialog: {:?}", err);
} }
} }
Ok(())
}
Some(&Loader::FileDialogAvm2 { target_object, .. }) => {
match dialog_result {
Ok(dialog_result) => {
if !dialog_result.is_cancelled() {
target_object.init_from_dialog_result(dialog_result);
let mut activation =
Avm2Activation::from_nothing(uc.reborrow());
let select_event = Avm2EventObject::bare_default_event(
&mut activation.context,
"select",
);
Avm2::dispatch_event(
&mut activation.context,
select_event,
target_object.into(),
);
} else {
let mut activation =
Avm2Activation::from_nothing(uc.reborrow());
let cancel_event = Avm2EventObject::bare_default_event(
&mut activation.context,
"cancel",
);
Avm2::dispatch_event(
&mut activation.context,
cancel_event,
target_object.into(),
);
}
}
Err(err) => {
tracing::warn!("Error on file dialog: {:?}", err);
}
}
Ok(()) Ok(())
}
None => Err(Error::Cancelled),
_ => Err(Error::NotFileDialogLoader),
}
}) })
}) })
} }