diff --git a/core/src/avm2/error.rs b/core/src/avm2/error.rs index 15ab20826..4fe666929 100644 --- a/core/src/avm2/error.rs +++ b/core/src/avm2/error.rs @@ -210,6 +210,17 @@ pub fn eof_error<'gc>( error_constructor(activation, class, message, code) } +#[inline(never)] +#[cold] +pub fn uri_error<'gc>( + activation: &mut Activation<'_, 'gc>, + message: &str, + code: u32, +) -> Result, Error<'gc>> { + let class = activation.avm2().classes().urierror; + error_constructor(activation, class, message, code) +} + #[inline(never)] #[cold] pub fn error<'gc>( diff --git a/core/src/avm2/globals.rs b/core/src/avm2/globals.rs index ee5f5bd68..20a817a26 100644 --- a/core/src/avm2/globals.rs +++ b/core/src/avm2/globals.rs @@ -111,6 +111,7 @@ pub struct SystemClasses<'gc> { pub verifyerror: ClassObject<'gc>, pub ioerror: ClassObject<'gc>, pub eoferror: ClassObject<'gc>, + pub urierror: ClassObject<'gc>, pub error: ClassObject<'gc>, pub uncaughterrorevents: ClassObject<'gc>, pub statictext: ClassObject<'gc>, @@ -219,6 +220,7 @@ impl<'gc> SystemClasses<'gc> { verifyerror: object, ioerror: object, eoferror: object, + urierror: object, error: object, uncaughterrorevents: object, statictext: object, @@ -520,6 +522,14 @@ pub fn load_player_globals<'gc>( toplevel::encode_uri_component, script, )?; + function(activation, "", "decodeURI", toplevel::decode_uri, script)?; + function( + activation, + "", + "decodeURIComponent", + toplevel::decode_uri_component, + script, + )?; function(activation, "", "unescape", toplevel::unescape, script)?; avm2_system_class!(vector, activation, vector::create_class(activation), script); @@ -606,6 +616,7 @@ fn load_playerglobal<'gc>( ("", "RegExp", regexp), ("", "ReferenceError", referenceerror), ("", "TypeError", typeerror), + ("", "URIError", urierror), ("", "VerifyError", verifyerror), ("", "XML", xml), ("", "XMLList", xml_list), diff --git a/core/src/avm2/globals/toplevel.rs b/core/src/avm2/globals/toplevel.rs index 893cf90c2..7985f897c 100644 --- a/core/src/avm2/globals/toplevel.rs +++ b/core/src/avm2/globals/toplevel.rs @@ -3,9 +3,9 @@ use ruffle_wstr::Units; use crate::avm2::activation::Activation; +use crate::avm2::error::{uri_error, Error}; use crate::avm2::object::Object; use crate::avm2::value::Value; -use crate::avm2::Error; use crate::string::{AvmString, WStr, WString}; use crate::stub::Stub; use ruffle_wstr::Integer; @@ -379,3 +379,134 @@ fn encode_utf8_with_exclusions<'gc>( Ok(AvmString::new_utf8(activation.context.gc_context, output).into()) } + +pub fn decode_uri<'gc>( + activation: &mut Activation<'_, 'gc>, + _this: Option>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + decode( + activation, + args, + // Characters that are reserved, sourced from as3 docs + "#$&+,/:;=?@", + "decodeURI", + ) +} + +pub fn decode_uri_component<'gc>( + activation: &mut Activation<'_, 'gc>, + _this: Option>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + decode(activation, args, "", "decodeURIComponent") +} + +fn handle_percent(chars: &mut I) -> Option +where + I: Iterator>, +{ + let high = chars.next()?.ok()?.to_digit(16)?; + let low = chars.next()?.ok()?.to_digit(16)?; + Some(low as u8 | ((high as u8) << 4)) +} + +// code derived from flash.utils.unescapeMultiByte +// FIXME: support bugzilla #538107 +fn decode<'gc>( + activation: &mut Activation<'_, 'gc>, + args: &[Value<'gc>], + reserved_set: &str, + func_name: &str, +) -> Result, Error<'gc>> { + let value = match args.first() { + None => return Ok("undefined".into()), + Some(Value::Undefined) => return Ok("null".into()), + Some(value) => value.coerce_to_string(activation)?, + }; + + let mut output = WString::new(); + let mut chars = value.chars(); + let mut bytes = Vec::with_capacity(4); + + while let Some(c) = chars.next() { + let Ok(c) = c else { + return Err(Error::AvmError(uri_error( + activation, + &format!("Error #1052: Invalid URI passed to {func_name} function."), + 1052, + )?)); + }; + + if c != '%' { + output.push_char(c); + continue; + } + + bytes.clear(); + let Some(byte) = handle_percent(&mut chars) else { + return Err(Error::AvmError(uri_error( + activation, + &format!("Error #1052: Invalid URI passed to {func_name} function."), + 1052, + )?)); + }; + bytes.push(byte); + if (byte & 0x80) != 0 { + let n = byte.leading_ones(); + + if n == 1 || n > 4 { + return Err(Error::AvmError(uri_error( + activation, + &format!("Error #1052: Invalid URI passed to {func_name} function."), + 1052, + )?)); + } + + for _ in 1..n { + if chars.next() != Some(Ok('%')) { + return Err(Error::AvmError(uri_error( + activation, + &format!("Error #1052: Invalid URI passed to {func_name} function."), + 1052, + )?)); + }; // consume % + + let Some(byte) = handle_percent(&mut chars) else { + return Err(Error::AvmError(uri_error( + activation, + &format!("Error #1052: Invalid URI passed to {func_name} function."), + 1052, + )?)); + }; + + if (byte & 0xC0) != 0x80 { + return Err(Error::AvmError(uri_error( + activation, + &format!("Error #1052: Invalid URI passed to {func_name} function."), + 1052, + )?)); + } + + bytes.push(byte); + } + } + + let Ok(decoded) = std::str::from_utf8(&bytes) else { + return Err(Error::AvmError(uri_error( + activation, + &format!("Error #1052: Invalid URI passed to {func_name} function."), + 1052, + )?)); + }; + if reserved_set.contains(decoded) { + for byte in &bytes { + write!(output, "%{x:02X}", x = byte).unwrap(); + } + } else { + output.push_utf8(decoded); + } + } + + Ok(AvmString::new(activation.context.gc_context, output).into()) +} diff --git a/tests/tests/swfs/avm2/decode_uri/Test.as b/tests/tests/swfs/avm2/decode_uri/Test.as new file mode 100644 index 000000000..1cce5ae07 --- /dev/null +++ b/tests/tests/swfs/avm2/decode_uri/Test.as @@ -0,0 +1,81 @@ +package { + + public class Test { + } +} + +import flash.utils.getDefinitionByName; + +var fns = ["decodeURI", "decodeURIComponent"]; +for each (var fnName in fns) { + var fn = getDefinitionByName(fnName); + trace("// " + fnName + "()"); + trace(fn()); + trace(""); + + trace("// " + fnName + "(undefined)"); + trace(fn(undefined)); + trace(""); + + trace("// typeof(" + fnName + "(undefined))"); + trace(typeof(fn(undefined))); + trace(""); + + trace("// " + fnName + "(null)"); + trace(fn(null)); + trace(""); + + var input = "test"; + trace("// " + fnName + "(\"" + input + "\")"); + trace(fn(input)); + trace(""); + + var input = "%3A"; + trace("// " + fnName + "(\"" + input + "\")"); + trace(fn(input)); + trace(""); + + var input = "%E0%A4%A"; + trace("// " + fnName + "(\"" + input + "\")"); + try { + trace(fn(input)); + } catch (e) { + trace(e); + } + trace(""); + var input = "%FFabcd"; + trace("// " + fnName + "(\"" + input + "\")"); + try { + trace(fn(input)); + } catch (e) { + trace(e); + } + trace(""); + + var src:String = String.fromCharCode(0xD842, 0xDF9F); + var input = encodeURIComponent(src); + trace("// " + fnName + "(\"" + input + "\")"); + try { + trace(fn(input)); + } catch (e) { + trace(e); + } + trace(""); + + + var input = "\x05"; + trace("// " + fnName + "(\"\\x05\")"); + trace(fn(input)); + trace(""); + + var input = "😭"; + trace("// " + fnName + "(\"" + input + "\")"); + trace(fn(input)); + trace(""); + + var input = "~!%40%23%24%25%5E%26*()_%2B%5B%5D%5C%7B%7D%7C%3B'%2C.%2F%3C%3E%3F"; + trace("// " + fnName + "(\"" + input + "\")"); + trace(fn(input)); + trace(""); + +} diff --git a/tests/tests/swfs/avm2/decode_uri/output.txt b/tests/tests/swfs/avm2/decode_uri/output.txt new file mode 100644 index 000000000..882c728c3 --- /dev/null +++ b/tests/tests/swfs/avm2/decode_uri/output.txt @@ -0,0 +1,72 @@ +// decodeURI() +undefined + +// decodeURI(undefined) +null + +// typeof(decodeURI(undefined)) +string + +// decodeURI(null) +null + +// decodeURI("test") +test + +// decodeURI("%3A") +%3A + +// decodeURI("%E0%A4%A") +URIError: Error #1052: Invalid URI passed to decodeURI function. + +// decodeURI("%FFabcd") +URIError: Error #1052: Invalid URI passed to decodeURI function. + +// decodeURI("%F0%A0%AE%9F") +𠮟 + +// decodeURI("\x05") + + +// decodeURI("😭") +😭 + +// decodeURI("~!%40%23%24%25%5E%26*()_%2B%5B%5D%5C%7B%7D%7C%3B'%2C.%2F%3C%3E%3F") +~!%40%23%24%^%26*()_%2B[]\{}|%3B'%2C.%2F<>%3F + +// decodeURIComponent() +undefined + +// decodeURIComponent(undefined) +null + +// typeof(decodeURIComponent(undefined)) +string + +// decodeURIComponent(null) +null + +// decodeURIComponent("test") +test + +// decodeURIComponent("%3A") +: + +// decodeURIComponent("%E0%A4%A") +URIError: Error #1052: Invalid URI passed to decodeURIComponent function. + +// decodeURIComponent("%FFabcd") +URIError: Error #1052: Invalid URI passed to decodeURIComponent function. + +// decodeURIComponent("%F0%A0%AE%9F") +𠮟 + +// decodeURIComponent("\x05") + + +// decodeURIComponent("😭") +😭 + +// decodeURIComponent("~!%40%23%24%25%5E%26*()_%2B%5B%5D%5C%7B%7D%7C%3B'%2C.%2F%3C%3E%3F") +~!@#$%^&*()_+[]\{}|;',./<>? + diff --git a/tests/tests/swfs/avm2/decode_uri/test.fla b/tests/tests/swfs/avm2/decode_uri/test.fla new file mode 100644 index 000000000..b27a5e649 Binary files /dev/null and b/tests/tests/swfs/avm2/decode_uri/test.fla differ diff --git a/tests/tests/swfs/avm2/decode_uri/test.swf b/tests/tests/swfs/avm2/decode_uri/test.swf new file mode 100644 index 000000000..f6e285607 Binary files /dev/null and b/tests/tests/swfs/avm2/decode_uri/test.swf differ diff --git a/tests/tests/swfs/avm2/decode_uri/test.toml b/tests/tests/swfs/avm2/decode_uri/test.toml new file mode 100644 index 000000000..dbee897f5 --- /dev/null +++ b/tests/tests/swfs/avm2/decode_uri/test.toml @@ -0,0 +1 @@ +num_frames = 1