use crate::avm1::activation::Activation; use crate::avm1::{AvmString, Object, ObjectPtr, TObject, Value}; #[allow(dead_code)] pub struct VariableDumper<'a> { objects: Vec<*const ObjectPtr>, depth: u32, output: String, indent: &'a str, } impl<'a> VariableDumper<'a> { pub fn new(indent: &'a str) -> Self { Self { objects: Vec::new(), depth: 0, output: String::new(), indent, } } #[allow(dead_code)] pub fn dump<'gc>( value: &Value<'gc>, indent: &str, activation: &mut Activation<'_, 'gc, '_>, ) -> String { let mut dumper = VariableDumper::new(indent); dumper.print_value(value, activation); dumper.output } pub fn output(&self) -> &str { &self.output } fn object_id(&mut self, object: &Object) -> (usize, bool) { let ptr = object.as_ptr(); for (i, other) in self.objects.iter().enumerate() { if *other == ptr { return (i, false); } } let id = self.objects.len(); self.objects.push(ptr); (id, true) } fn indent(&mut self) { for _ in 0..self.depth { self.output.push_str(self.indent); } } pub fn print_string(&mut self, string: AvmString<'_>) { self.output.push('\"'); for c in string.chars() { let c = c.unwrap_or(char::REPLACEMENT_CHARACTER); let escape = match u8::try_from(c as u32) { Ok(b'"') => "\\\"", Ok(b'\\') => "\\\\", Ok(b'\n') => "\\n", Ok(b'\r') => "\\r", Ok(b'\t') => "\\t", Ok(0x08) => "\\b", Ok(0x0C) => "\\f", _ => { self.output.push(c); continue; } }; self.output.push_str(escape); } self.output.push('\"'); } pub fn print_object<'gc>( &mut self, object: &Object<'gc>, activation: &mut Activation<'_, 'gc, '_>, ) { let (id, new) = self.object_id(object); self.output.push_str("[object #"); self.output.push_str(&id.to_string()); self.output.push(']'); if new { self.print_properties(object, activation); } } pub fn print_property<'gc>( &mut self, object: &Object<'gc>, key: AvmString<'gc>, activation: &mut Activation<'_, 'gc, '_>, ) { match object.get(key, activation) { Ok(value) => { self.print_value(&value, activation); } Err(e) => { self.output.push_str("Error: \""); self.output.push_str(&e.to_string()); self.output.push('\"'); } } } pub fn print_properties<'gc>( &mut self, object: &Object<'gc>, activation: &mut Activation<'_, 'gc, '_>, ) { let keys = object.get_keys(activation); if keys.is_empty() { self.output.push_str(" {}"); } else { self.output.push_str(" {\n"); self.depth += 1; for key in keys.into_iter() { self.indent(); self.output.push_str(&key.to_utf8_lossy()); self.output.push_str(": "); self.print_property(object, key, activation); self.output.push('\n'); } self.depth -= 1; self.indent(); self.output.push('}'); } } pub fn print_value<'gc>( &mut self, value: &Value<'gc>, activation: &mut Activation<'_, 'gc, '_>, ) { match value { Value::Undefined => self.output.push_str("undefined"), Value::Null => self.output.push_str("null"), Value::Bool(value) => self.output.push_str(&value.to_string()), Value::Number(value) => self.output.push_str(&value.to_string()), Value::String(value) => { self.print_string(*value); } Value::Object(object) => { self.print_object(object, activation); } } } pub fn print_variables<'gc>( &mut self, header: &str, name: &str, object: &Object<'gc>, activation: &mut Activation<'_, 'gc, '_>, ) { let keys = object.get_keys(activation); if keys.is_empty() { return; } self.output.push_str(header); self.output.push('\n'); self.depth += 1; for key in keys.into_iter() { self.output.push_str(&format!("{}.{}", name, key)); self.output.push_str(" = "); self.print_property(object, key, activation); self.output.push('\n'); } self.depth -= 1; self.output.push('\n'); } } #[cfg(test)] mod tests { use super::*; use crate::avm1::error::Error; use crate::avm1::test_utils::with_avm; use crate::avm1::ScriptObject; #[test] fn dump_undefined() { with_avm(19, |activation, _root| -> Result<(), Error> { assert_eq!( VariableDumper::dump(&Value::Undefined, " ", activation), "undefined" ); Ok(()) }) } #[test] fn dump_null() { with_avm(19, |activation, _root| -> Result<(), Error> { assert_eq!(VariableDumper::dump(&Value::Null, " ", activation), "null"); Ok(()) }) } #[test] fn dump_bool() { with_avm(19, |activation, _root| -> Result<(), Error> { assert_eq!(VariableDumper::dump(&true.into(), " ", activation), "true"); assert_eq!( VariableDumper::dump(&false.into(), " ", activation), "false" ); Ok(()) }) } #[test] fn dump_number() { with_avm(19, |activation, _root| -> Result<(), Error> { assert_eq!(VariableDumper::dump(&1000.into(), " ", activation), "1000"); assert_eq!( VariableDumper::dump(&(-0.05).into(), " ", activation), "-0.05" ); Ok(()) }) } #[test] fn dump_string() { with_avm(19, |activation, _root| -> Result<(), Error> { assert_eq!(VariableDumper::dump(&"".into(), " ", activation), "\"\""); assert_eq!( VariableDumper::dump(&"HELLO WORLD".into(), " ", activation), "\"HELLO WORLD\"" ); assert_eq!( VariableDumper::dump( &"Escape \"this\" string\nplease! \u{0008}\u{000C}\n\r\t\"\\".into(), " ", activation, ), "\"Escape \\\"this\\\" string\\nplease! \\b\\f\\n\\r\\t\\\"\\\\\"" ); Ok(()) }) } #[test] fn dump_empty_object() { with_avm(19, |activation, _root| -> Result<(), Error> { let object = ScriptObject::object(activation.context.gc_context, None); assert_eq!( VariableDumper::dump(&object.into(), " ", activation), "[object #0] {}" ); Ok(()) }) } #[test] fn dump_object() { with_avm(19, |activation, _root| -> Result<(), Error> { let object = ScriptObject::object(activation.context.gc_context, None); let child = ScriptObject::object(activation.context.gc_context, None); object.set("self", object.into(), activation)?; object.set("test", "value".into(), activation)?; object.set("child", child.into(), activation)?; child.set("parent", object.into(), activation)?; child.set("age", 6.into(), activation)?; assert_eq!( VariableDumper::dump(&object.into(), " ", activation), "[object #0] {\n child: [object #1] {\n age: 6\n parent: [object #0]\n }\n test: \"value\"\n self: [object #0]\n}", ); Ok(()) }) } #[test] fn dump_variables() { with_avm(19, |activation, _root| -> Result<(), Error> { let object = ScriptObject::object(activation.context.gc_context, None); let child = ScriptObject::object(activation.context.gc_context, None); object.set("self", object.into(), activation)?; object.set("test", "value".into(), activation)?; object.set("child", child.into(), activation)?; child.set("parent", object.into(), activation)?; child.set("age", 6.into(), activation)?; let mut dumper = VariableDumper::new(" "); dumper.print_variables("Variables:", "object", &object.into(), activation); assert_eq!( dumper.output, "Variables:\nobject.child = [object #0] {\n age: 6\n parent: [object #1] {\n child: [object #0]\n test: \"value\"\n self: [object #1]\n }\n }\nobject.test = \"value\"\nobject.self = [object #1]\n\n" ); Ok(()) }) } }