From 84f788c2d2440b63d089c161a681e27d3f95fcb9 Mon Sep 17 00:00:00 2001 From: Aaron Hill Date: Sat, 28 Oct 2023 16:44:01 -0400 Subject: [PATCH] avm2: Use RawTable to implement 'public index' iteration (#12470) * avm2: Use RawTable to implement 'public index' iteration This makes our implementation more closely aligned with avmplus. In particular, it's now possible to delete keys from an object while iterating without disturbing the iteration order (as long as those keys were already produced by the iterator). This is based on @Bale001's work on RawTable-based iteration A few tests had their output changed (they depend on the exact object iteration order, and don't neccessarily match Flash Player exactly). * Use Cell to store index fields * Remove outdated comment --- Cargo.lock | 1 + core/src/avm2.rs | 1 + core/src/avm2/dynamic_map.rs | 209 ++++++++++++++++++ core/src/avm2/object/array_object.rs | 18 +- core/src/avm2/object/dictionary_object.rs | 101 ++++----- core/src/avm2/object/script_object.rs | 96 ++++---- core/src/avm2/object/xml_list_object.rs | 4 +- ruffle_gc_arena/Cargo.toml | 2 +- .../swfs/avm2/dictionary_iter_modify/Test.as | 65 ++++++ .../avm2/dictionary_iter_modify/output.txt | 8 + .../swfs/avm2/dictionary_iter_modify/test.fla | Bin 0 -> 3814 bytes .../swfs/avm2/dictionary_iter_modify/test.swf | Bin 0 -> 1160 bytes .../avm2/dictionary_iter_modify/test.toml | 1 + tests/tests/swfs/avm2/loader_load/output.txt | 4 +- .../swfs/avm2/netstream_seek_flv/output.txt | 14 +- .../swfs/avm2/netstream_seek_flv/test.fla | Bin 4405 -> 4514 bytes .../swfs/avm2/netstream_seek_flv/test.swf | Bin 1655 -> 1751 bytes .../swfs/avm2/object_enumeration/Test.as | 6 +- .../swfs/avm2/object_enumeration/output.txt | 11 +- .../swfs/avm2/object_enumeration/test.swf | Bin 706 -> 1071 bytes .../ecma3/Expressions/e11_4_1/test.toml | 3 +- 21 files changed, 397 insertions(+), 147 deletions(-) create mode 100644 core/src/avm2/dynamic_map.rs create mode 100755 tests/tests/swfs/avm2/dictionary_iter_modify/Test.as create mode 100644 tests/tests/swfs/avm2/dictionary_iter_modify/output.txt create mode 100755 tests/tests/swfs/avm2/dictionary_iter_modify/test.fla create mode 100755 tests/tests/swfs/avm2/dictionary_iter_modify/test.swf create mode 100644 tests/tests/swfs/avm2/dictionary_iter_modify/test.toml mode change 100644 => 100755 tests/tests/swfs/avm2/netstream_seek_flv/test.fla diff --git a/Cargo.lock b/Cargo.lock index ff37b9b7a19d..1af7232163c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1896,6 +1896,7 @@ version = "0.3.3" source = "git+https://github.com/kyren/gc-arena?rev=efd89fc683c6bb456af3e226c33763cb822645e9#efd89fc683c6bb456af3e226c33763cb822645e9" dependencies = [ "gc-arena-derive", + "hashbrown 0.14.2", "sptr", ] diff --git a/core/src/avm2.rs b/core/src/avm2.rs index b5d4aa2fb158..7743ab56f5b5 100644 --- a/core/src/avm2.rs +++ b/core/src/avm2.rs @@ -34,6 +34,7 @@ pub mod bytearray; mod call_stack; mod class; mod domain; +mod dynamic_map; mod e4x; pub mod error; mod events; diff --git a/core/src/avm2/dynamic_map.rs b/core/src/avm2/dynamic_map.rs new file mode 100644 index 000000000000..ea7b29751270 --- /dev/null +++ b/core/src/avm2/dynamic_map.rs @@ -0,0 +1,209 @@ +use fnv::FnvBuildHasher; +use gc_arena::Collect; +use hashbrown::{self, raw::RawTable}; +use std::{cell::Cell, hash::Hash}; + +use super::{string::AvmString, Object}; + +#[derive(Debug, Collect, Copy, Clone)] +#[collect(no_drop)] +pub struct DynamicProperty { + pub value: V, + pub enumerable: bool, +} + +#[derive(Eq, PartialEq, Hash, Copy, Clone, Collect)] +#[collect(no_drop)] +pub enum StringOrObject<'gc> { + String(AvmString<'gc>), + Object(Object<'gc>), +} + +/// A HashMap designed for dynamic properties on an object. +#[derive(Debug, Collect, Clone)] +#[collect(no_drop)] +pub struct DynamicMap { + values: hashbrown::HashMap, FnvBuildHasher>, + // The last index that was given back to flash + public_index: Cell, + // The actual index that represents where an item is in the HashMap + real_index: Cell, +} + +impl Default for DynamicMap { + fn default() -> Self { + Self::new() + } +} + +impl DynamicMap { + pub fn new() -> Self { + Self { + values: hashbrown::HashMap::default(), + public_index: Cell::new(0), + real_index: Cell::new(0), + } + } + + pub fn as_hashmap(&self) -> &hashbrown::HashMap, FnvBuildHasher> { + &self.values + } + + pub fn entry( + &mut self, + key: K, + ) -> hashbrown::hash_map::Entry, FnvBuildHasher> { + self.values.entry(key) + } + + /// Gets the real index from the current public index, returns false if real index is out of bounds + fn public_to_real_index(&self, index: usize) -> Option { + let mut count = 0; + let raw = self.raw(); + if raw.is_empty() { + return None; + } + for i in 0..raw.buckets() { + unsafe { + // SAFETY: It is impossible for i to be greater than the total buckets. + if raw.is_bucket_full(i) { + // SAFETY: We know that this bucket is safe to access because we just checked + // that it is full. + let bucket = raw.bucket(i).as_ref(); + if bucket.1.enumerable { + count += 1; + if count >= index { + return Some(i); + } + } + } + } + } + None + } + + fn raw(&self) -> &RawTable<(K, DynamicProperty)> { + self.values.raw_table() + } + + pub fn remove(&mut self, key: &K) -> Option> { + self.values.remove(key) + } + + pub fn next(&self, index: usize) -> Option { + // If the public index is zero than this is the first time this is being enumerated, + // if index != self.public_index, then we are enumerating twice OR we mutated while enumerating. + // + // Regardless of the reason, we just need to sync the supplied index to the real index. + if self.public_index.get() == 0 || index != self.public_index.get() { + if let Some(real) = self.public_to_real_index(index) { + self.real_index.set(real); + self.public_index.set(index + 1); + return Some(self.public_index.get()); + } else { + self.public_index.set(0); + self.real_index.set(0); + return None; + } + } + + let real = self.real_index.get() + 1; + let raw = self.raw(); + let total_buckets = raw.buckets(); + if !raw.is_empty() && real < total_buckets { + for i in real..total_buckets { + unsafe { + // SAFETY: It is impossible for i to be greater than the total buckets. + if raw.is_bucket_full(i) { + // SAFETY: We know that this bucket is safe to access because we just checked + // that it is full. + let bucket = raw.bucket(i).as_ref(); + if bucket.1.enumerable { + self.real_index.set(i); + self.public_index.set(self.public_index.get() + 1); + return Some(self.public_index.get()); + } + } + } + } + } + None + } + + pub fn pair_at(&self, index: usize) -> Option<&(K, DynamicProperty)> { + let real_index = if self.public_index.get() == 0 || self.public_index.get() != index { + self.public_to_real_index(index)? + } else { + self.real_index.get() + }; + if !self.values.is_empty() && real_index < self.raw().buckets() { + unsafe { + let bucket = self.raw().bucket(real_index); + return Some(bucket.as_ref()); + } + } + None + } + + pub fn key_at(&self, index: usize) -> Option<&K> { + self.pair_at(index).map(|p| &p.0) + } + + pub fn value_at(&self, index: usize) -> Option<&V> { + self.pair_at(index).map(|p| &p.1.value) + } +} + +impl DynamicMap +where + K: Eq + Hash, +{ + pub fn insert(&mut self, key: K, value: V) { + self.values.insert( + key, + DynamicProperty { + value, + enumerable: true, + }, + ); + } + pub fn insert_no_enum(&mut self, key: K, value: V) { + self.values.insert( + key, + DynamicProperty { + value, + enumerable: false, + }, + ); + } +} + +#[cfg(test)] +mod tests { + + use super::DynamicMap; + + #[test] + fn test_dynamic_map() { + let mut map: DynamicMap<&'static str, i32> = DynamicMap::new(); + map.insert("a", 0); + map.insert("b", 0); + map.insert("c", 0); + map.insert("d", 0); + let mut current = 0; + while let Some(next) = map.next(current) { + if current == 2 { + map.insert("e", 0); + map.insert("f", 0); + } + println!("{}", map.key_at(current).unwrap()); + current = next; + } + println!("next"); + current = 0; + while let Some(next) = map.next(current) { + println!("{}", map.key_at(current).unwrap()); + current = next; + } + } +} diff --git a/core/src/avm2/object/array_object.rs b/core/src/avm2/object/array_object.rs index 22df7041e1ea..69b33ed36d7e 100644 --- a/core/src/avm2/object/array_object.rs +++ b/core/src/avm2/object/array_object.rs @@ -207,10 +207,9 @@ impl<'gc> TObject<'gc> for ArrayObject<'gc> { fn get_next_enumerant( self, mut last_index: u32, - _activation: &mut Activation<'_, 'gc>, + activation: &mut Activation<'_, 'gc>, ) -> Result, Error<'gc>> { let read = self.0.read(); - let num_enumerants = read.base.num_enumerants(); let array_length = read.array.length() as u32; // Array enumeration skips over holes. @@ -221,11 +220,18 @@ impl<'gc> TObject<'gc> for ArrayObject<'gc> { last_index += 1; } + drop(read); + // After enumerating all of the 'normal' array entries, // we enumerate all of the local properties stored on the // ScriptObject. - if last_index < num_enumerants + array_length { - return Ok(Some(last_index + 1)); + if let Some(index) = self + .0 + .write(activation.context.gc_context) + .base + .get_next_enumerant(last_index - array_length) + { + return Ok(Some(index + array_length)); } Ok(None) } @@ -233,7 +239,7 @@ impl<'gc> TObject<'gc> for ArrayObject<'gc> { fn get_enumerant_name( self, index: u32, - _activation: &mut Activation<'_, 'gc>, + activation: &mut Activation<'_, 'gc>, ) -> Result, Error<'gc>> { let arr_len = self.0.read().array.length() as u32; if arr_len >= index { @@ -243,7 +249,7 @@ impl<'gc> TObject<'gc> for ArrayObject<'gc> { .unwrap_or(Value::Undefined)) } else { Ok(self - .base() + .base_mut(activation.context.gc_context) .get_enumerant_name(index - arr_len) .unwrap_or(Value::Undefined)) } diff --git a/core/src/avm2/object/dictionary_object.rs b/core/src/avm2/object/dictionary_object.rs index 5ed3b80b8834..c0859f176103 100644 --- a/core/src/avm2/object/dictionary_object.rs +++ b/core/src/avm2/object/dictionary_object.rs @@ -1,13 +1,13 @@ //! Object representation for `flash.utils.Dictionary` use crate::avm2::activation::Activation; +use crate::avm2::dynamic_map::StringOrObject; use crate::avm2::object::script_object::ScriptObjectData; use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject}; use crate::avm2::value::Value; use crate::avm2::Error; use crate::string::AvmString; use core::fmt; -use fnv::FnvHashMap; use gc_arena::{Collect, GcCell, GcWeakCell, Mutation}; use std::cell::{Ref, RefMut}; @@ -20,10 +20,7 @@ pub fn dictionary_allocator<'gc>( Ok(DictionaryObject(GcCell::new( activation.context.gc_context, - DictionaryObjectData { - base, - object_space: Default::default(), - }, + DictionaryObjectData { base }, )) .into()) } @@ -54,9 +51,6 @@ impl fmt::Debug for DictionaryObject<'_> { pub struct DictionaryObjectData<'gc> { /// Base script object base: ScriptObjectData<'gc>, - - /// Object key storage - object_space: FnvHashMap, Value<'gc>>, } impl<'gc> DictionaryObject<'gc> { @@ -64,24 +58,41 @@ impl<'gc> DictionaryObject<'gc> { pub fn get_property_by_object(self, name: Object<'gc>) -> Value<'gc> { self.0 .read() - .object_space - .get(&name) + .base + .values + .as_hashmap() + .get(&StringOrObject::Object(name)) .cloned() + .map(|v| v.value) .unwrap_or(Value::Undefined) } /// Set a value in the dictionary's object space. pub fn set_property_by_object(self, name: Object<'gc>, value: Value<'gc>, mc: &Mutation<'gc>) { - self.0.write(mc).object_space.insert(name, value); + self.0 + .write(mc) + .base + .values + .insert(StringOrObject::Object(name), value); } /// Delete a value from the dictionary's object space. pub fn delete_property_by_object(self, name: Object<'gc>, mc: &Mutation<'gc>) { - self.0.write(mc).object_space.remove(&name); + self.0 + .write(mc) + .base + .values + .remove(&StringOrObject::Object(name)); } pub fn has_property_by_object(self, name: Object<'gc>) -> bool { - self.0.read().object_space.get(&name).is_some() + self.0 + .read() + .base + .values + .as_hashmap() + .get(&StringOrObject::Object(name)) + .is_some() } } @@ -106,56 +117,6 @@ impl<'gc> TObject<'gc> for DictionaryObject<'gc> { Some(self) } - fn get_next_enumerant( - self, - last_index: u32, - _activation: &mut Activation<'_, 'gc>, - ) -> Result, Error<'gc>> { - let read = self.0.read(); - let num_enumerants = read.base.num_enumerants(); - let object_space_length = read.object_space.keys().len() as u32; - - if last_index < num_enumerants + object_space_length { - Ok(Some(last_index.saturating_add(1))) - } else { - Ok(None) - } - } - - fn get_enumerant_name( - self, - index: u32, - _activation: &mut Activation<'_, 'gc>, - ) -> Result, Error<'gc>> { - let read = self.0.read(); - let object_space_len = read.object_space.keys().len() as u32; - if object_space_len >= index { - Ok(index - .checked_sub(1) - .and_then(|index| read.object_space.keys().nth(index as usize).cloned()) - .map(|v| v.into()) - .unwrap_or(Value::Undefined)) - } else { - Ok(read - .base - .get_enumerant_name(index - object_space_len) - .unwrap_or(Value::Undefined)) - } - } - - fn get_enumerant_value( - self, - index: u32, - activation: &mut Activation<'_, 'gc>, - ) -> Result, Error<'gc>> { - let name_value = self.get_enumerant_name(index, activation)?; - if !name_value.is_primitive() { - Ok(self.get_property_by_object(name_value.as_object().unwrap())) - } else { - self.get_public_property(name_value.coerce_to_string(activation)?, activation) - } - } - // Calling `setPropertyIsEnumerable` on a `Dictionary` has no effect - // stringified properties are always enumerable. fn set_local_property_is_enumerable( @@ -165,4 +126,18 @@ impl<'gc> TObject<'gc> for DictionaryObject<'gc> { _is_enumerable: bool, ) { } + + fn get_enumerant_value( + self, + index: u32, + activation: &mut Activation<'_, 'gc>, + ) -> Result, Error<'gc>> { + Ok(*self + .0 + .write(activation.context.gc_context) + .base + .values + .value_at(index as usize) + .unwrap_or(&Value::Undefined)) + } } diff --git a/core/src/avm2/object/script_object.rs b/core/src/avm2/object/script_object.rs index ed26bf48bf38..9315fa11f82c 100644 --- a/core/src/avm2/object/script_object.rs +++ b/core/src/avm2/object/script_object.rs @@ -1,6 +1,7 @@ //! Default AVM2 object impl use crate::avm2::activation::Activation; +use crate::avm2::dynamic_map::{DynamicMap, StringOrObject}; use crate::avm2::error; use crate::avm2::object::{ClassObject, FunctionObject, Object, ObjectPtr, TObject}; use crate::avm2::value::Value; @@ -8,10 +9,8 @@ use crate::avm2::vtable::VTable; use crate::avm2::Multiname; use crate::avm2::{Error, QName}; use crate::string::AvmString; -use fnv::FnvHashMap; use gc_arena::{Collect, GcCell, GcWeakCell, Mutation}; use std::cell::{Ref, RefMut}; -use std::collections::hash_map::Entry; use std::fmt::Debug; /// A class instance allocator that allocates `ScriptObject`s. @@ -42,7 +41,7 @@ pub struct ScriptObjectWeak<'gc>(pub GcWeakCell<'gc, ScriptObjectData<'gc>>); #[collect(no_drop)] pub struct ScriptObjectData<'gc> { /// Values stored on this object. - values: FnvHashMap, Value<'gc>>, + pub values: DynamicMap, Value<'gc>>, /// Slots stored on this object. slots: Vec>, @@ -59,9 +58,6 @@ pub struct ScriptObjectData<'gc> { /// The table used for non-dynamic property lookups. vtable: Option>, - - /// Enumeratable property names. - enumerants: Vec>, } impl<'gc> TObject<'gc> for ScriptObject<'gc> { @@ -138,7 +134,6 @@ impl<'gc> ScriptObjectData<'gc> { proto, instance_of, vtable: instance_of.map(|cls| cls.instance_vtable()), - enumerants: Vec::new(), } } @@ -166,18 +161,24 @@ impl<'gc> ScriptObjectData<'gc> { )); }; - let value = self.values.get(&local_name); + let value = self + .values + .as_hashmap() + .get(&StringOrObject::String(local_name)); if let Some(value) = value { - return Ok(*value); + return Ok(value.value); } // follow the prototype chain let mut proto = self.proto(); while let Some(obj) = proto { let obj = obj.base(); - let value = obj.values.get(&local_name); + let value = obj + .values + .as_hashmap() + .get(&StringOrObject::String(local_name)); if let Some(value) = value { - return Ok(*value); + return Ok(value.value); } proto = obj.proto(); } @@ -221,16 +222,8 @@ impl<'gc> ScriptObjectData<'gc> { )); }; - match self.values.entry(local_name) { - Entry::Occupied(mut o) => { - o.insert(value); - } - Entry::Vacant(v) => { - //TODO: Not all classes are dynamic like this - self.enumerants.push(local_name); - v.insert(value); - } - }; + self.values + .insert(StringOrObject::String(local_name), value); Ok(()) } @@ -248,8 +241,7 @@ impl<'gc> ScriptObjectData<'gc> { return false; } if let Some(name) = multiname.local_name() { - self.set_local_property_is_enumerable(name, false); - self.values.remove(&name); + self.values.remove(&StringOrObject::String(name)); true } else { false @@ -338,7 +330,11 @@ impl<'gc> ScriptObjectData<'gc> { pub fn has_own_dynamic_property(&self, name: &Multiname<'gc>) -> bool { if name.contains_public_namespace() { if let Some(name) = name.local_name() { - return self.values.get(&name).is_some(); + return self + .values + .as_hashmap() + .get(&StringOrObject::String(name)) + .is_some(); } } false @@ -357,51 +353,39 @@ impl<'gc> ScriptObjectData<'gc> { } pub fn get_next_enumerant(&self, last_index: u32) -> Option { - if last_index < self.enumerants.len() as u32 { - Some(last_index.saturating_add(1)) - } else { - None - } + self.values.next(last_index as usize).map(|val| val as u32) } pub fn get_enumerant_name(&self, index: u32) -> Option> { - // NOTE: AVM2 object enumeration is one of the weakest parts of an - // otherwise well-designed VM. Notably, because of the way they - // implemented `hasnext` and `hasnext2`, all enumerants start from ONE. - // Hence why we have to `checked_sub` here in case some miscompiled - // code doesn't check for the zero index, which is actually a failure - // sentinel. - let true_index = (index as usize).checked_sub(1)?; - - self.enumerants.get(true_index).cloned().map(|q| q.into()) + self.values.key_at(index as usize).map(|key| match key { + StringOrObject::String(name) => Value::String(*name), + StringOrObject::Object(obj) => Value::Object(*obj), + }) } pub fn property_is_enumerable(&self, name: AvmString<'gc>) -> bool { - self.enumerants.contains(&name) + self.values + .as_hashmap() + .get(&StringOrObject::String(name)) + .map_or(false, |prop| prop.enumerable) } pub fn set_local_property_is_enumerable(&mut self, name: AvmString<'gc>, is_enumerable: bool) { - if is_enumerable && self.values.contains_key(&name) && !self.enumerants.contains(&name) { - self.enumerants.push(name); - } else if !is_enumerable && self.enumerants.contains(&name) { - let mut index = None; - for (i, other_name) in self.enumerants.iter().enumerate() { - if *other_name == name { - index = Some(i); - } - } - - if let Some(index) = index { - self.enumerants.remove(index); + if let Some(val) = self + .values + .as_hashmap() + .get(&StringOrObject::String(name)) + .copied() + { + if is_enumerable { + self.values.insert(StringOrObject::String(name), val.value) + } else { + self.values + .insert_no_enum(StringOrObject::String(name), val.value) } } } - /// Gets the number of (standard) enumerants. - pub fn num_enumerants(&self) -> u32 { - self.enumerants.len() as u32 - } - /// Install a method into the object. pub fn install_bound_method(&mut self, disp_id: u32, function: FunctionObject<'gc>) { if self.bound_methods.len() <= disp_id as usize { diff --git a/core/src/avm2/object/xml_list_object.rs b/core/src/avm2/object/xml_list_object.rs index 0236150fa873..ee228308af6a 100644 --- a/core/src/avm2/object/xml_list_object.rs +++ b/core/src/avm2/object/xml_list_object.rs @@ -1001,7 +1001,7 @@ impl<'gc> TObject<'gc> for XmlListObject<'gc> { fn get_enumerant_name( self, index: u32, - _activation: &mut Activation<'_, 'gc>, + activation: &mut Activation<'_, 'gc>, ) -> Result, Error<'gc>> { let children_len = self.0.read().children.len() as u32; if children_len >= index { @@ -1011,7 +1011,7 @@ impl<'gc> TObject<'gc> for XmlListObject<'gc> { .unwrap_or(Value::Undefined)) } else { Ok(self - .base() + .base_mut(activation.context.gc_context) .get_enumerant_name(index - children_len) .unwrap_or(Value::Undefined)) } diff --git a/ruffle_gc_arena/Cargo.toml b/ruffle_gc_arena/Cargo.toml index aa08f8dd49f8..3a8719af18f1 100644 --- a/ruffle_gc_arena/Cargo.toml +++ b/ruffle_gc_arena/Cargo.toml @@ -9,4 +9,4 @@ license.workspace = true repository.workspace = true [dependencies] -gc-arena = { git = "https://github.com/kyren/gc-arena", rev = "efd89fc683c6bb456af3e226c33763cb822645e9" } +gc-arena = { git = "https://github.com/kyren/gc-arena", rev = "efd89fc683c6bb456af3e226c33763cb822645e9", features = ["hashbrown"] } diff --git a/tests/tests/swfs/avm2/dictionary_iter_modify/Test.as b/tests/tests/swfs/avm2/dictionary_iter_modify/Test.as new file mode 100755 index 000000000000..ee765f211028 --- /dev/null +++ b/tests/tests/swfs/avm2/dictionary_iter_modify/Test.as @@ -0,0 +1,65 @@ +package { + import flash.utils.Dictionary; + + public class Test { + public function Test() { + runTest(new Object()); + + var array = new Array(); + array.push("First normal entry"); + array.push("Second normal entry"); + //runTest(array); + + var dict = new Dictionary(); + //dict[new Object()] = "First distinct object key"; + //dict[new Object()] = "Second distinct object key"; + runTest(dict); + } + + private function runTest(obj: Object) { + trace("Initial object: " + obj); + for (var i = 0; i < 100; i++) { + obj["Key " + i] = i + }; + + var toDelete = []; + + var seen = []; + for (var key in obj) { + seen.push("Key: '" + key + "' Value: " + obj[key]); + + if (seen.length < 94) { + toDelete.push(key); + } + + if (seen.length == 95) { + for (var j = 0; j < toDelete.length; j++) { + var newKey = toDelete[j]; + if (j % 2 == 0) { + delete obj[newKey]; + } else { + //obj.setPropertyIsEnumerable(newKey, false); + } + } + } + } + // The order of entries pushed to 'seen' depends on the internal + // hash table iteration order, so sort the output to make it comparable + // between Flash Player and Ruffle + seen.sort(); + trace("Seen during iteration:"); + trace(seen); + + var seenAfter = []; + for (var newKey in obj) { + seenAfter.push("Key: '" + newKey + "' Value: " + obj[newKey]); + } + seenAfter.sort(); + // We delete keys after seeing them during iteration. Since + // the iteration order will be different between Flash Player and Ruffle, + // we'll delete different keys. Therefore, we only check the *number* of + // keys remaining. + trace("Seen during iteration after deletion: " + seenAfter.length); + } + } +} \ No newline at end of file diff --git a/tests/tests/swfs/avm2/dictionary_iter_modify/output.txt b/tests/tests/swfs/avm2/dictionary_iter_modify/output.txt new file mode 100644 index 000000000000..c70fce2ab17e --- /dev/null +++ b/tests/tests/swfs/avm2/dictionary_iter_modify/output.txt @@ -0,0 +1,8 @@ +Initial object: [object Object] +Seen during iteration: +Key: 'Key 0' Value: 0,Key: 'Key 1' Value: 1,Key: 'Key 10' Value: 10,Key: 'Key 11' Value: 11,Key: 'Key 12' Value: 12,Key: 'Key 13' Value: 13,Key: 'Key 14' Value: 14,Key: 'Key 15' Value: 15,Key: 'Key 16' Value: 16,Key: 'Key 17' Value: 17,Key: 'Key 18' Value: 18,Key: 'Key 19' Value: 19,Key: 'Key 2' Value: 2,Key: 'Key 20' Value: 20,Key: 'Key 21' Value: 21,Key: 'Key 22' Value: 22,Key: 'Key 23' Value: 23,Key: 'Key 24' Value: 24,Key: 'Key 25' Value: 25,Key: 'Key 26' Value: 26,Key: 'Key 27' Value: 27,Key: 'Key 28' Value: 28,Key: 'Key 29' Value: 29,Key: 'Key 3' Value: 3,Key: 'Key 30' Value: 30,Key: 'Key 31' Value: 31,Key: 'Key 32' Value: 32,Key: 'Key 33' Value: 33,Key: 'Key 34' Value: 34,Key: 'Key 35' Value: 35,Key: 'Key 36' Value: 36,Key: 'Key 37' Value: 37,Key: 'Key 38' Value: 38,Key: 'Key 39' Value: 39,Key: 'Key 4' Value: 4,Key: 'Key 40' Value: 40,Key: 'Key 41' Value: 41,Key: 'Key 42' Value: 42,Key: 'Key 43' Value: 43,Key: 'Key 44' Value: 44,Key: 'Key 45' Value: 45,Key: 'Key 46' Value: 46,Key: 'Key 47' Value: 47,Key: 'Key 48' Value: 48,Key: 'Key 49' Value: 49,Key: 'Key 5' Value: 5,Key: 'Key 50' Value: 50,Key: 'Key 51' Value: 51,Key: 'Key 52' Value: 52,Key: 'Key 53' Value: 53,Key: 'Key 54' Value: 54,Key: 'Key 55' Value: 55,Key: 'Key 56' Value: 56,Key: 'Key 57' Value: 57,Key: 'Key 58' Value: 58,Key: 'Key 59' Value: 59,Key: 'Key 6' Value: 6,Key: 'Key 60' Value: 60,Key: 'Key 61' Value: 61,Key: 'Key 62' Value: 62,Key: 'Key 63' Value: 63,Key: 'Key 64' Value: 64,Key: 'Key 65' Value: 65,Key: 'Key 66' Value: 66,Key: 'Key 67' Value: 67,Key: 'Key 68' Value: 68,Key: 'Key 69' Value: 69,Key: 'Key 7' Value: 7,Key: 'Key 70' Value: 70,Key: 'Key 71' Value: 71,Key: 'Key 72' Value: 72,Key: 'Key 73' Value: 73,Key: 'Key 74' Value: 74,Key: 'Key 75' Value: 75,Key: 'Key 76' Value: 76,Key: 'Key 77' Value: 77,Key: 'Key 78' Value: 78,Key: 'Key 79' Value: 79,Key: 'Key 8' Value: 8,Key: 'Key 80' Value: 80,Key: 'Key 81' Value: 81,Key: 'Key 82' Value: 82,Key: 'Key 83' Value: 83,Key: 'Key 84' Value: 84,Key: 'Key 85' Value: 85,Key: 'Key 86' Value: 86,Key: 'Key 87' Value: 87,Key: 'Key 88' Value: 88,Key: 'Key 89' Value: 89,Key: 'Key 9' Value: 9,Key: 'Key 90' Value: 90,Key: 'Key 91' Value: 91,Key: 'Key 92' Value: 92,Key: 'Key 93' Value: 93,Key: 'Key 94' Value: 94,Key: 'Key 95' Value: 95,Key: 'Key 96' Value: 96,Key: 'Key 97' Value: 97,Key: 'Key 98' Value: 98,Key: 'Key 99' Value: 99 +Seen during iteration after deletion: 53 +Initial object: [object Dictionary] +Seen during iteration: +Key: 'Key 0' Value: 0,Key: 'Key 1' Value: 1,Key: 'Key 10' Value: 10,Key: 'Key 11' Value: 11,Key: 'Key 12' Value: 12,Key: 'Key 13' Value: 13,Key: 'Key 14' Value: 14,Key: 'Key 15' Value: 15,Key: 'Key 16' Value: 16,Key: 'Key 17' Value: 17,Key: 'Key 18' Value: 18,Key: 'Key 19' Value: 19,Key: 'Key 2' Value: 2,Key: 'Key 20' Value: 20,Key: 'Key 21' Value: 21,Key: 'Key 22' Value: 22,Key: 'Key 23' Value: 23,Key: 'Key 24' Value: 24,Key: 'Key 25' Value: 25,Key: 'Key 26' Value: 26,Key: 'Key 27' Value: 27,Key: 'Key 28' Value: 28,Key: 'Key 29' Value: 29,Key: 'Key 3' Value: 3,Key: 'Key 30' Value: 30,Key: 'Key 31' Value: 31,Key: 'Key 32' Value: 32,Key: 'Key 33' Value: 33,Key: 'Key 34' Value: 34,Key: 'Key 35' Value: 35,Key: 'Key 36' Value: 36,Key: 'Key 37' Value: 37,Key: 'Key 38' Value: 38,Key: 'Key 39' Value: 39,Key: 'Key 4' Value: 4,Key: 'Key 40' Value: 40,Key: 'Key 41' Value: 41,Key: 'Key 42' Value: 42,Key: 'Key 43' Value: 43,Key: 'Key 44' Value: 44,Key: 'Key 45' Value: 45,Key: 'Key 46' Value: 46,Key: 'Key 47' Value: 47,Key: 'Key 48' Value: 48,Key: 'Key 49' Value: 49,Key: 'Key 5' Value: 5,Key: 'Key 50' Value: 50,Key: 'Key 51' Value: 51,Key: 'Key 52' Value: 52,Key: 'Key 53' Value: 53,Key: 'Key 54' Value: 54,Key: 'Key 55' Value: 55,Key: 'Key 56' Value: 56,Key: 'Key 57' Value: 57,Key: 'Key 58' Value: 58,Key: 'Key 59' Value: 59,Key: 'Key 6' Value: 6,Key: 'Key 60' Value: 60,Key: 'Key 61' Value: 61,Key: 'Key 62' Value: 62,Key: 'Key 63' Value: 63,Key: 'Key 64' Value: 64,Key: 'Key 65' Value: 65,Key: 'Key 66' Value: 66,Key: 'Key 67' Value: 67,Key: 'Key 68' Value: 68,Key: 'Key 69' Value: 69,Key: 'Key 7' Value: 7,Key: 'Key 70' Value: 70,Key: 'Key 71' Value: 71,Key: 'Key 72' Value: 72,Key: 'Key 73' Value: 73,Key: 'Key 74' Value: 74,Key: 'Key 75' Value: 75,Key: 'Key 76' Value: 76,Key: 'Key 77' Value: 77,Key: 'Key 78' Value: 78,Key: 'Key 79' Value: 79,Key: 'Key 8' Value: 8,Key: 'Key 80' Value: 80,Key: 'Key 81' Value: 81,Key: 'Key 82' Value: 82,Key: 'Key 83' Value: 83,Key: 'Key 84' Value: 84,Key: 'Key 85' Value: 85,Key: 'Key 86' Value: 86,Key: 'Key 87' Value: 87,Key: 'Key 88' Value: 88,Key: 'Key 89' Value: 89,Key: 'Key 9' Value: 9,Key: 'Key 90' Value: 90,Key: 'Key 91' Value: 91,Key: 'Key 92' Value: 92,Key: 'Key 93' Value: 93,Key: 'Key 94' Value: 94,Key: 'Key 95' Value: 95,Key: 'Key 96' Value: 96,Key: 'Key 97' Value: 97,Key: 'Key 98' Value: 98,Key: 'Key 99' Value: 99 +Seen during iteration after deletion: 53 diff --git a/tests/tests/swfs/avm2/dictionary_iter_modify/test.fla b/tests/tests/swfs/avm2/dictionary_iter_modify/test.fla new file mode 100755 index 0000000000000000000000000000000000000000..b9aa92fd95d95bce87b4c068b816b215e41aafdc GIT binary patch literal 3814 zcmbVPc|26>AD*m}eUL08vXrbt3E9Wkx5;D~M249mhOtiz*%Pu8Dnv!bmWT*Z7?F#V zB3sB(_H608zcbe@w?1|M_+AP7%EH_REu(`{{1FZyz84j4VhFd;wm{4M9z~tlRQHs&jG>v{f50FBjOdT?aPxKb z!xD%dPQXU~fzyD&fB#ZuIvuhK4G6?Z4+60RaD=%j!pqs8fb|4+Oz=oMH5fkt;|MD2 zyF?tiwzdb#Svp8}#Dl`Kx7MILw$j$}V5YPumukgL`t63Us}-1Q0V@-ooteHqILu(( zX_8te>I~D|%ZPSz{cW~k$+Xjuiov-7Rq~4#T6xxdonYw?WFoU$cb-X6NMwjqAEe1e zHF|?#;GlWCh8fel#53F<;I8qCrU!4aK;QLHls6gR7F|$}EW*=1EDcRdOI*vlCeI%5 zqqw`Fy$CdCVMc6U)5ww74KvJVnOl4g5PY%_lus-$qi{$28?bT7 zO>uK2VZYo$r*dmD2XCi{%=OmF&QuY$>dan)M}2K7Q<>r8!b!w*I|g=PN24zy22HFT z>&~VoT*%8To()}Uj2E)U&ZS%#nKiu|Odhwms< z#v^l4NnrrDLr)uOBS@V48_eRXp zEbTS4&)7NmtHE~X$K{b_Qi0neuUSjcbz=S0=oH_DDM_ZR2bFaY#AC?%1Mq&N@+&uBVR6+}y;3cBO zu|4bx1=Ii%;&#W z0$y!=DXxB|3zI5y0Huc)Yeg@j`e}^ZI~Cw53D||(wzA@`S2WYNNKsbua@X?qIV+C| zb=)W+s%+dlH5+ebF5k{W`#e25f!9=ioDMI3Be#IpoH zK$s$0f|?)&pF+j7t1g^fil=bsrbmyaQNrY$Z=ts(UCnTCo39e1#xYYgNsYF?B9Nv0 z5D!tAXd!xJC3nci5#^qd6W6v-&jL>S;-LHk{u|v0ysH2MK8$&?$$J9oBA}q(6}hZc z%WS$9t!*zT&)6P5AFX2GB*l>UmUCQr{P0`c-D=Uc!1dYde zTuk@iQM$Z`&mfW0gUKL@y7vU)GoeXt#-0#U_vsmgPHR3!lu=+b3YI+hp3=nj#s@?R z@}A>~)w`mVwkX%t-0f8<6+FEzxoTB{R`IeLObe)ctG2Md^xV+fvG zFWTr|B4-ueTcikA$fV=&)LN_%)D&Mn`AW-BpEo4}c8KQb* zsgH#)(oMBD3aOGc>wN7}&IyO0=S!8fugLn1@DE#Zv_c=yd0#YWDGjlBU&M3xi@Zf{ zsl`^wf@CTxDRgy0Ctz|wgTU!-?=rc)N3lKfPQ(0+gUt()FscE}&x$NIpm%gqa9qrF zptpb7Dy49(nq7Cg>QDsUhmedMWxaC`T=CViJ43Wp-!*OQUZQM;tuXw#Apk+@HNc}! zN)u!c_Q4l4lr__CAmTS<3nTurzm>7rpF6Csc$INhKfe*ASrO_P}FvFHLFC?h=vg;P8ycAWwMt&F@GZV;N>O6;0DIn#af(oh-da;h~dOKb|KFG8fjnOER%;Keg#4YDLR3 z8|=_@HFH0j_#Sfm#Kyffhu+{hx;~Zh+FD9?yr`aqXJ6=tpz>iHw|c%x0|{cMd8W2> zL0e`C@;+9YGRaN0hONnbrYlxO$`;0Rea%_QU7rac25pW#9X$kfRF7H@w9e6R9B~N# zI+?R?QZQ*WX4W96CG%WG@dsZ@ECiS6YHvwVFO-U}@X&dCOoB0aInOT=4K2{-rQ$4n*NlI!t9Y$8`!ecw#EU6A)>siF{H)LVF%*TCTLwYfK66pR z-g}_!WE^i4Za^bNK^J!I6W25;cb~x#mks+T?cR*-EhZ{26;#~K?qn!fd9BMh*On=t zac+po-r|kNjw&J!2WX`*oP3=1zVWhX8jVW9h2xl)tQ-6FAo2IlDmPC!s5z=~UpdFq zS6>wK0As4ruHmnf&5J2h5`#?1pFvCexQ`^xvyESeJeAi)bw*{#O&@y$qf6*fr>#5M z${s#C<#_8+(aM+WmeEGrhO68>i8m$3^u+{ZT}o2FO9x^5P_4z2`lJcJ?CY$-AC4jX&l^~jxWm~-ygGFI5?>H6q?yZF7zpRdIi zE-`>B7)wGJnXD?~-zHngY6hKso^gJrC!<2TP?5hm(J9vZ{$sf)tlx#r{HM!@M%a~o z%}7E8PhQWa9d0%jXx@JZX12fZLRLckQ?HKtypf#lW&N!olu#q~<(|6b6JuI>uyMa5 zB=0l|Ur?T>Tnm>@Iu9}b$xJe+EK534BMDJKw7J;h-=X8)Q61d4*^#x~&E8VWUE#WC zs?I*|;$7N@n!y8I3cU)!`6R$B_?1b@0$@<|JD-oJ7|!G0e0!5#-7Y1)7n` z*(Gzzz&5Snwzc)7ka>J<(O+WD+Z>N*U78xd*@hkG{@rE(15T>b@WQxxVE^qJestA_ z=%5h5VSuPjwHX*UPdU^%0s>3K0{-6F$=MZ)2Odu`G5}XPz#_|olz?}mRY6AT|Mi~Fd+C}Zejh)XNz|9lr-8Aug7Bx@o6qS_<^mm7b z{Ex+``C_N2v^t=Fkv4W#{axPphY#Flf&M9dd|Uf>HgcDYr2W^He>9apuK%;G+?^sE znBo^E^S|~$71(aoGXGj`KmP4M3vQP`3-Es-zi%7(T{hjFK#65HsUnT(sfqE7#) literal 0 HcmV?d00001 diff --git a/tests/tests/swfs/avm2/dictionary_iter_modify/test.swf b/tests/tests/swfs/avm2/dictionary_iter_modify/test.swf new file mode 100755 index 0000000000000000000000000000000000000000..26390745b9d49821aadcf89b5e09b17c4bf29a4e GIT binary patch literal 1160 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 diff --git a/tests/tests/swfs/avm2/netstream_seek_flv/test.swf b/tests/tests/swfs/avm2/netstream_seek_flv/test.swf index bdc649e2eaffd04851bef6adc1bd8bc6bf500abc..c19db3f0daa9777b78813f34c13c8286e58aa535 100644 GIT binary patch literal 1751 zcmV;|1}OPMS5qqr4FCXmoQ;-SSKHVX$Ip=^%d#zBxX8o=FhLMV7+aW2I?i+gH6fEs zz-gIeZZKHbM_7q1c_f($ed@~U*XUaL0`nQBA3;f0JFoNDw}`jC(S0OiOhXt9(m8vd z|K9s|F0%X*aeD|QUn9hY(Rw<95Gpe#CnpoB&NX>KzEhUBzIzq&d8PHd^K|XO zk6Vw|3h0;N7-%utt`URe77+Sn=#8ns$io>}gH4w5jUG zS9*&yb%PXoUd>WlWZ_Dx;+0g3S5hrqNws_>)ynPPQT2Nxr`~wKR|JRM$k%nd-Bgc+ zP4iGE<)+>qzAy)4)EL7SoCeuD)a%3y2a6aq#h_UXnx&vw4w@B?91_DBJI$K!&sys4 z1?O05jl@;^$f(`7EYm9GI6_-SRwf&c(=HVXYTeu?`I^}(tZgqAii)yQ*zf2~M>k@@ zP{+}m_Fyn8wN@jxt?%ni-8nj)DY0KTW;-5OetP@Vpon&&YrJ%bWvI<=m}5t(&~~pz?nGf=3zPHIlsG#uDhVojVEJv3nL)S9Col0Z0mNs{aRn^C$?8#=B!wFa>S-RzzW zmd-Eqf*i4M*+R#t6Aen&1z6^7M|Cs5V5;mK!1zll_hFC;xMbD0IO0Cy@_QyKx z7Ay(hC6AiDTF^Z$5w10L5GVZS!+*5$TJw;ltHM2_W>R$=wbtmBia`EyLvPkEeWZ%4 z1CD5+VAy%uyPNd8HX$!&mHhJDU6yuZ2c~1L8TBVn?b39Mec=l<{8IIj=&Rdm$0oDC zqh02F*`=Bl)`kZ+0TbV#=621}+s@QSBKoaxaIlj9cb3F+$1eTBA(bvZ+u`6jSp2TE zV|6sGNp>iL-M!mCY4(nK*edS8L9+d=+N0|Qh=)5jB|(afh!V~SoXA~|j7j6tgp`ve zr5nd(rsy8`b5f01xb+>q@uJaEoH^*a&{$qC;Nx&r`ZxS6J~kL zMTBUKiHnJ3Dm^ebG%RICu4PBBkBv`+a+5cvNB!uFE|Awa;UxdOKg?Wtl2u2YBBO!`WG=>rUPQW6@EJg{46rv$lc>t~u6N_cu z;|vNPk9hoUd_Ma|6JJ;2*o(k0(;JS$a14g;zR?o?a2$q1Y&aJC2d3kR_xO)E`5u4H zq(PaiCSU#AOM!lsYSYiqt6#kV8P(V~TIyAeOC=E!_PKp_9Arc9-d57s8z^#Y`adTp z5iY`d(!0t4@-j5y9o66L>Zd?I0y*Ap4B~+SguLu-{O$bx*VLsA`Cu8E$L;1B14{Oc zGDj&VDdiMUPWqHLfO3NdPWhDbM<{0kO1YnM)~B4ikn&bP<@^VfbN!SBpybXd7bsAGR7*`ABDiZcscQ*t5M;Y8;J0z10O zP+O$-D%gqD5VgmtUGXN`;L5GC^G`g5b{AH;r;K{dE7Re>RI=DxcXvmp|H({y_|YiS z;Jgi2qqQp6vG>4*eq{`MUjQ}F;AreyS2_L~ST$A~mr>$AOqhX1{g%!%v>W&Rw9kR_ zJK70@tAHyL@F<+>gg#x&mDx%TdmamRrFjX*A!B9mA6j7>Bd`0zG?J9hbjY&j}3o?fxE`}ja1gIK7Rc-(cIO8HX z3$EfmM7vYRnulfNYGE0uc^S$^_2)Q?y4Zf$)g)bts+V_Xh{{U|;?)P>gZd(8V literal 1655 zcmV--28j7XS5qrM3;+OloQ+o7TH96@-dmO|%d&jHAi$gxaY(}0;@H%g8OT7A32ov` zDNWmfVj*u~HL~PSl4)-^|?38${gA9kaG%95bO5f%aa9f31J* zz1K#(M%*Dnxi<)LNwi%^A%wP=^Yinmd}mZyQa;&J^xY@hd)CU=ZENS{dd1!P-ap#= zOhrEqiGamuS0^@^UP9={!GDO<08bv3VN9Ut`Oii?|99gRn)Qk29~zcceWscA=Vpso zrcEk6tKn)bvUDTV@{Ld{H$tu62(@-2)cU>OK=oTw7uHO_RRV+8EH_NAZE2^%Gv~x4 zTb9`#x-ti2>x}IQev=%Ym<{42<4ugG<#<|&r`32`i>GytoDkcexX4=W&sy!C1?Rh( zPBNNzYU__&*Kw=U9HBF#s54F9Z&xc7t>GMzvhK7h4|i88%c{CwIqH~}Z`xw)>iDMR z72{QDx=uXLJTfiQKfPEf@m~2(I}>YBdhbG^C3@Me^4ce^ty$eL12~B5rDN@wIIS+n zvE>|TmifKrn~p8Uwiaoanh?u2@ui+rZn&*({oA zITbo~gBZZt5MYOQea-K9tZ5rgwyAlw@9Y<@(|KPsy9oX|ztP!^mnug9YBoQAkWYo*`!YF=wJqGCTa;do#V_-?6br^f~21w~S~ zOaN!23lZI=vSFRDbQ`!YsG8G!U(=gCsubkkYMNH#+B>w&`d~;`Dz;aq(t}(idW5`M zP|Is~9euX&_PeOCCOE9I-3IT<(p{ioIaEvG^D-Hu^cWS`dIbw}?z z!`s(RTFd*;O%Iw?%kJCJp;n^(D=6E*Ga|NCo;JG5t4f5O&&&fPHDvV*_ z_n2q@C~RWkAs)kg@v-m(#y<-?Sa^=; z7(8>#Gfkc`v-Fz`UuFEaUmX!*nD}$e!pp&%gqMdm6@B6n#!M=KGD7r4r}i2fHKVa#Hbg-9Wt2-GKFN-&~0{0(K58fKt$ejUqg5^Z9PTkpe_zyS-m|Q*g!@t5jAU7zgprC)hHS#~`Y<>cTRcD7-yCPqC>)I08Iuk7tbXj04Yj z#4`arTQp?yN}lO{o?9RAO!o840uR^YnWsDpz_SqX+yS0#8glnap2dEi@&`P3`+1gt zC&@9Q^miPnFYo?SeTN?*1WY(MDDD(dGZ8X@k(jOx;*bpvhCtpK!c8{h0)t(A3c%zh zuE|U&Q2i6o)0+&{C8}?Lp507PeU$2-hNU)`#x~i-XW<+b=Qp`MMjH-SsJm7h!QpCf zFgp7aGaKTaQKrd->w!VJY9$1XBgqCZM4%a6%tW3>3CDi9-npQj}l?2)^zSlroecP#D1Eiy*TX zdAVL*9zq2gS_)L~{`G7>#1z3$1Va%CK%^-`jSvTC-*gdCA&;jJRj$aKgg6mHoCHPH zp{Uw4R0GD01jb6B22at!%$Xiy1qDV@fvR~0Xj4ieFa(7SjI;vT#M(?I3X>SPdG`X9 zp!zMfep{_8YJFC%&#Co!wa&ormuMn>n)vM7sP#9BrTDg^EB~6K&AuOm{s)ENue?I5 BHTwVn diff --git a/tests/tests/swfs/avm2/object_enumeration/Test.as b/tests/tests/swfs/avm2/object_enumeration/Test.as index f2ddb939f317..bc4613450807 100644 --- a/tests/tests/swfs/avm2/object_enumeration/Test.as +++ b/tests/tests/swfs/avm2/object_enumeration/Test.as @@ -11,10 +11,12 @@ package { function enumerate(x) { trace("enumerating: " + x) + var out = []; for (var name in x) { - trace(name); - trace(x[name]); + out.push(name + " = " + x[name]) } + out.sort(); + trace(out); } var x = {"key": "value", "key2": "value2"}; diff --git a/tests/tests/swfs/avm2/object_enumeration/output.txt b/tests/tests/swfs/avm2/object_enumeration/output.txt index ece71298a52a..8ba9c74f9960 100644 --- a/tests/tests/swfs/avm2/object_enumeration/output.txt +++ b/tests/tests/swfs/avm2/object_enumeration/output.txt @@ -1,12 +1,11 @@ enumerating: [object Object] -key2 -value2 -key -value +key = value,key2 = value2 Delete key2 enumerating: [object Object] -key -value +key = value enumerating: [object Object] + enumerating: null + enumerating: undefined + diff --git a/tests/tests/swfs/avm2/object_enumeration/test.swf b/tests/tests/swfs/avm2/object_enumeration/test.swf index 711058fc9423597d1e6989562aedbd334aa7d6a3..aa277a5f74b2b5dd030b17e3327ccd23248aaeb4 100644 GIT binary patch literal 1071 zcmV+~1kn3KS5qbt1^@tf0fkddZ`(!?-Q}k!o01$m{)p|AZrmD1C6SiWv{r1vhE=r* z(7K4~wkSXd6t%P%QzStyCHd6CxBd$Q1#;?t=&6U^YzJ+Q0eZ?U%Be?pNXd56O9^l> zeDmJyEa&Y2aepI}d4|wDMzyI0gwT)EuP{PeLEEZ4snwN1*KxxNd~L6FDD^9<+VA&^ z{c_O@4%Lm#%}rG+sijf@7=`fIrN*G(hWFO0Nr4&(&A|4l?YT-^&}exP-CkQ8S8bcu zMg1snl9FvxB@XElmxk&_aRUmq&5Gp(U4vE)-*;@&h|8&iLfG-lqrTB21I_eP$OM~s-;zUi|%KBWbUqLHYRz7oaTyjbT)EyoT!B)HnA z;aRkA1hAnOx$UIWM0i7tWoAGO>iw_iiWW;8!##|QLsBJf^PrK`yoQn!7?e~WM~+h7 z$SWnSR37ykqg!hH>)#s17=h_lQ6?8He6?TrdbiQ|ZvROQon^rS?>x2KD|QR*F_)K@ zKcrbm3Gi~cqd&@E6NKJ;{LimHL-c$48xm6V+srS(i8IM(4i#h4+GDljb%|;i0gNsn zz89)NWWhgHDX7Bn8uqPFHG18W>b0H{lQxMPbxDx?#)l8%f<+_50~Sogxki`pG%!qp z2=lJMp-Rs+5@!}*$%Z`&X2+l7kUAm8E*ly z%2iFm#h?@~YP`sa*@|sD^c~|^`qJy!WY4jEVLxd(`&kdHTZ61Y!Fa*sh)!ZOlw%xJ zBkI&6m%`ypYWK(pnSCd}=fQ2VT@nZlKd>p0u3KV{$T8nDoQQA`m4qZLP1NAxP@=?; z1nwgih}%oe=AqZ5Ueg4}H51T|FHo~X9G`_jup8wEb62GQ2lj)>oPPu4~M*)bhZFce(5Q*KD=|2 pUex6Sln|G6SxJIrUDh*J2I7-H&%a2)2&C!jOS%I?_!qT=!&_Hy6CnTq literal 706 zcmV;z0zLg&S5qtQ0{{TA0ssJA001BW06YKujv4yj=Vl0?JwAo?b#KsqVKo~Xixy4a z1v37mOh!q!duw(1D_l1z3z0VgHxB9Y;ryOt+H4UaGwq-bc5tB0m@amJ@Zeci1_!QZnT84W^-kDu!b~qNjlRVSVp3_xhTH3JNC+2)1J*>=-p>|kv2*_BK)@TdzxW~s5 zRX~U5S-?);A+G44YL3M0*f=<)^i{biPBGOIR$lYO&{zPAusrYQ+MNi=aD#T4!$ zPkS*y6Mr~&jb2>K)qny&rD&(ESY3$E?lK{?1zUms&{I3Vb0^tWjDS<@YzD{(Tfzzd8?YKxpBneJTbq2W*S$SCp0u zKECrnLGB#iQdpmS&VOvArRBr|*UA36WzbH;Ifxp1^7%tGR*YDe0s#BZnL8fjz#?Yu z2J>C?*rWJ_MU@_4-d>b)SLppKIKRFi{`KF!0+KsAlFl_ zyXw5U*fijE^HQ%e#uboMf{sc@&WQEw&oD+10Q>1Ja|wI<9}I<3pEeo;JUpWp5B|?N zEeA{-_IMsgMh3x9pH@oFGnw_67|1#r;k!Ufc?E>e4^mtK>F0!ZqYlxK9=>SQqUN@I oH`bJ-rGe{B*IyKU|KfV6$$RBvqv0F^7xFfm2v5QU|M0-L0Cy8vd;kCd diff --git a/tests/tests/swfs/from_avmplus/ecma3/Expressions/e11_4_1/test.toml b/tests/tests/swfs/from_avmplus/ecma3/Expressions/e11_4_1/test.toml index 29f3cef79022..67f15e8639d2 100644 --- a/tests/tests/swfs/from_avmplus/ecma3/Expressions/e11_4_1/test.toml +++ b/tests/tests/swfs/from_avmplus/ecma3/Expressions/e11_4_1/test.toml @@ -1,2 +1 @@ -num_ticks = 1 -known_failure = true +num_ticks = 1 \ No newline at end of file