From f49ce49d28dd1d27b1684023d02288d274d0e76b Mon Sep 17 00:00:00 2001 From: Moulins Date: Tue, 28 Sep 2021 17:48:14 +0200 Subject: [PATCH] core: add FromWStr trait and WStr::parse --- core/src/avm1/activation.rs | 9 +- core/src/avm1/globals/movie_clip.rs | 5 +- core/src/avm1/globals/stage.rs | 2 + core/src/avm1/object/stage_object.rs | 14 +- core/src/avm1/value.rs | 48 ++---- core/src/avm2/globals/flash/display/stage.rs | 3 + core/src/display_object/stage.rs | 17 ++ core/src/string.rs | 2 + core/src/string/common.rs | 5 + core/src/string/parse.rs | 172 +++++++++++++++++++ 10 files changed, 229 insertions(+), 48 deletions(-) create mode 100644 core/src/string/parse.rs diff --git a/core/src/avm1/activation.rs b/core/src/avm1/activation.rs index 9943279ae..2904c281a 100644 --- a/core/src/avm1/activation.rs +++ b/core/src/avm1/activation.rs @@ -762,11 +762,11 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { let frame_path = arg.coerce_to_string(self)?; if let Some((clip, frame)) = self.resolve_variable_path(target, frame_path.borrow())? { if let Some(clip) = clip.as_display_object().and_then(|o| o.as_movie_clip()) { - let frame = frame.to_utf8_lossy(); // TODO: avoid this UTF8 conversion. if let Ok(frame) = frame.parse().map(f64_to_wrapping_u32) { // First try to parse as a frame number. call_frame = Some((clip, frame)); - } else if let Some(frame) = clip.frame_label_to_number(&frame) { + // TODO(moulins): remove this UTF8 conversion + } else if let Some(frame) = clip.frame_label_to_number(&frame.to_utf8_lossy()) { // Otherwise, it's a frame label. call_frame = Some((clip, frame.into())); } @@ -1331,9 +1331,10 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { } } return Ok(FrameControl::Continue); - } else if window_target.as_str().starts_with("_level") && window_target.len() > 6 { + } else if window_target.starts_with(WStr::from_units(b"_level")) && window_target.len() > 6 + { // target of `_level#` indicates a `loadMovieNum` call. - match window_target[6..].parse::() { + match window_target.slice(6..).parse::() { Ok(level_id) => { let fetch = self.context.navigator.fetch(&url, RequestOptions::get()); let level = self.resolve_level(level_id); diff --git a/core/src/avm1/globals/movie_clip.rs b/core/src/avm1/globals/movie_clip.rs index 3aa413f6f..6b97e27b6 100644 --- a/core/src/avm1/globals/movie_clip.rs +++ b/core/src/avm1/globals/movie_clip.rs @@ -1007,12 +1007,11 @@ pub fn goto_frame<'gc>( activation.resolve_variable_path(movie_clip.into(), frame_path.borrow())? { if let Some(clip) = clip.as_display_object().and_then(|o| o.as_movie_clip()) { - // TODO(moulins): we need WStr::parse for avoiding allocation here. - let frame = frame.to_string(); if let Ok(frame) = frame.parse().map(f64_to_wrapping_i32) { // First try to parse as a frame number. call_frame = Some((clip, frame)); - } else if let Some(frame) = clip.frame_label_to_number(&frame) { + // TODO(moulins): remove this UTF8 conversion + } else if let Some(frame) = clip.frame_label_to_number(&frame.to_utf8_lossy()) { // Otherwise, it's a frame label. call_frame = Some((clip, frame as i32)); } diff --git a/core/src/avm1/globals/stage.rs b/core/src/avm1/globals/stage.rs index 58cca5bce..ecbc501a2 100644 --- a/core/src/avm1/globals/stage.rs +++ b/core/src/avm1/globals/stage.rs @@ -69,6 +69,7 @@ fn set_align<'gc>( .get(0) .unwrap_or(&Value::Undefined) .coerce_to_string(activation)? + .as_str() .parse() .unwrap_or_default(); activation @@ -107,6 +108,7 @@ fn set_scale_mode<'gc>( .get(0) .unwrap_or(&Value::Undefined) .coerce_to_string(activation)? + .as_str() .parse() .unwrap_or_default(); activation diff --git a/core/src/avm1/object/stage_object.rs b/core/src/avm1/object/stage_object.rs index ab45d08cd..52c663e21 100644 --- a/core/src/avm1/object/stage_object.rs +++ b/core/src/avm1/object/stage_object.rs @@ -8,7 +8,7 @@ use crate::avm1::{Object, ObjectPtr, ScriptObject, TDisplayObject, TObject, Valu use crate::avm_warn; use crate::context::UpdateContext; use crate::display_object::{DisplayObject, EditText, MovieClip, TDisplayObjectContainer}; -use crate::string::{AvmString, BorrowWStr}; +use crate::string::{AvmString, BorrowWStr, WStr}; use crate::types::Percent; use gc_arena::{Collect, GcCell, MutationContext}; use std::fmt; @@ -118,15 +118,15 @@ impl<'gc> StageObject<'gc> { context: &mut UpdateContext<'_, 'gc, '_>, case_sensitive: bool, ) -> Option> { - let name = name.as_str(); - if let Some(slice) = name.get(0..6) { + if let Some(slice) = name.try_slice(0..6) { + let level_prefix = WStr::from_units(b"_level"); let is_level = if case_sensitive { - slice == "_level" + slice == level_prefix } else { - slice.eq_ignore_ascii_case("_level") + slice.eq_ignore_case(level_prefix) }; if is_level { - if let Some(level_id) = name.get(6..).and_then(|v| v.parse::().ok()) { + if let Some(level_id) = name.try_slice(6..).and_then(|v| v.parse::().ok()) { let level = context .stage .child_by_depth(level_id) @@ -918,7 +918,7 @@ fn set_quality<'gc>( _this: DisplayObject<'gc>, val: Value<'gc>, ) -> Result<(), Error<'gc>> { - if let Ok(quality) = val.coerce_to_string(activation)?.parse() { + if let Ok(quality) = val.coerce_to_string(activation)?.as_str().parse() { activation .context .stage diff --git a/core/src/avm1/value.rs b/core/src/avm1/value.rs index e37835446..c0906561a 100644 --- a/core/src/avm1/value.rs +++ b/core/src/avm1/value.rs @@ -6,9 +6,10 @@ use crate::ecma_conversions::{ f64_to_string, f64_to_wrapping_i16, f64_to_wrapping_i32, f64_to_wrapping_u16, f64_to_wrapping_u32, }; -use crate::string::{AvmString, WStr}; +use crate::string::{AvmString, BorrowWStr, Integer, WStr}; use gc_arena::Collect; use std::borrow::Cow; +use std::num::Wrapping; #[derive(Debug, Clone, Copy, Collect)] #[collect(no_drop)] @@ -125,8 +126,7 @@ impl<'gc> Value<'gc> { match self { Value::Bool(true) => 1.0, Value::Number(v) => v, - // TODO: avoid this conversion to UTF8? - Value::String(v) => v.to_utf8_lossy().parse().unwrap_or(0.0), + Value::String(v) => v.parse().unwrap_or(0.0), _ => 0.0, } } @@ -158,36 +158,19 @@ impl<'gc> Value<'gc> { if activation.swf_version() >= 6 { if let Some(v) = v.strip_prefix(WStr::from_units(b"0x")) { - let mut n: u32 = 0; - for c in &v { - n = n.wrapping_shl(4); - n |= match u8::try_from(c) { - Ok(b'0'..=b'9') => c as u32 - b'0' as u32, - Ok(b'A'..=b'F') => c as u32 - b'A' as u32 + 10, - Ok(b'a'..=b'f') => c as u32 - b'a' as u32 + 10, - _ => return f64::NAN, - } - } - return f64::from(n as i32); - } - - if (v.starts_with(b'0') + // Flash allows the '-' sign here. + return match Wrapping::::from_wstr_radix(v, 16) { + Ok(n) => f64::from(n.0 as i32), + Err(_) => f64::NAN, + }; + } else if v.starts_with(b'0') || v.starts_with(WStr::from_units(b"+0")) - || v.starts_with(WStr::from_units(b"-0"))) - && v.slice(1..) - .iter() - .all(|c| c >= b'0'.into() && c <= b'7'.into()) + || v.starts_with(WStr::from_units(b"-0")) { - let trimmed = v.trim_start_matches(&b"+-"[..]); - let mut n: u32 = 0; - for c in &trimmed { - n = n.wrapping_shl(3); - n |= (c - b'0' as u16) as u32; + // Flash allows the '-' sign here. + if let Ok(n) = Wrapping::::from_wstr_radix(v.borrow(), 8) { + return f64::from(n.0); } - if v.starts_with(b'-') { - n = n.wrapping_neg(); - } - return f64::from(n as i32); } } @@ -201,8 +184,6 @@ impl<'gc> Value<'gc> { f64::NAN } else { v.trim_start_matches(&b"\t\n\r "[..]) - // TODO: avoid this conversion to UTF8? - .to_utf8_lossy() .parse() .unwrap_or(f64::NAN) } @@ -450,8 +431,7 @@ impl<'gc> Value<'gc> { if swf_version >= 7 { !v.is_empty() } else { - // TODO: avoid this conversion to UTF8? - let num = v.to_utf8_lossy().parse().unwrap_or(0.0); + let num = v.parse().unwrap_or(0.0); num != 0.0 } } diff --git a/core/src/avm2/globals/flash/display/stage.rs b/core/src/avm2/globals/flash/display/stage.rs index 0ea39be48..03b945e6e 100644 --- a/core/src/avm2/globals/flash/display/stage.rs +++ b/core/src/avm2/globals/flash/display/stage.rs @@ -299,6 +299,7 @@ pub fn set_align<'gc>( .get(0) .unwrap_or(&Value::Undefined) .coerce_to_string(activation)? + .as_str() .parse() .unwrap_or_default(); activation @@ -530,6 +531,7 @@ pub fn set_scale_mode<'gc>( .get(0) .unwrap_or(&Value::Undefined) .coerce_to_string(activation)? + .as_str() .parse() { activation @@ -640,6 +642,7 @@ pub fn set_quality<'gc>( .get(0) .unwrap_or(&Value::Undefined) .coerce_to_string(activation)? + .as_str() .parse() { activation diff --git a/core/src/display_object/stage.rs b/core/src/display_object/stage.rs index c64e1763c..87596afcd 100644 --- a/core/src/display_object/stage.rs +++ b/core/src/display_object/stage.rs @@ -18,6 +18,7 @@ use crate::display_object::{ }; use crate::events::{ClipEvent, ClipEventResult}; use crate::prelude::*; +use crate::string::{FromWStr, WStr}; use crate::vminterface::{AvmType, Instantiator}; use bitflags::bitflags; use gc_arena::{Collect, GcCell, MutationContext}; @@ -828,6 +829,22 @@ impl FromStr for StageDisplayState { } } +impl FromWStr for StageDisplayState { + type Err = ParseEnumError; + + fn from_wstr(s: WStr<'_>) -> Result { + if s.eq_ignore_case(WStr::from_units(b"fullscreen")) { + Ok(StageDisplayState::FullScreen) + } else if s.eq_ignore_case(WStr::from_units(b"fullscreeninteractive")) { + Ok(StageDisplayState::FullScreenInteractive) + } else if s.eq_ignore_case(WStr::from_units(b"normal")) { + Ok(StageDisplayState::Normal) + } else { + Err(ParseEnumError) + } + } +} + bitflags! { /// The alignment of the stage. /// This controls the position of the movie after scaling to fill the viewport. diff --git a/core/src/string.rs b/core/src/string.rs index b215d6a87..d6540031e 100644 --- a/core/src/string.rs +++ b/core/src/string.rs @@ -12,6 +12,7 @@ mod common; mod avm; mod buf; mod ops; +mod parse; mod pattern; mod raw; mod slice; @@ -25,6 +26,7 @@ pub use avm::AvmString; pub use buf::WString; pub use common::{BorrowWStr, BorrowWStrMut, Units}; pub use ops::{Iter, Split, WStrToUtf8}; +pub use parse::{FromWStr, Integer}; pub use pattern::Pattern; pub use slice::{WStr, WStrMut}; diff --git a/core/src/string/common.rs b/core/src/string/common.rs index 261bd127e..5a3f26a92 100644 --- a/core/src/string/common.rs +++ b/core/src/string/common.rs @@ -153,6 +153,11 @@ macro_rules! impl_str_methods { crate::string::ops::str_cmp_ignore_case($deref, other) } + #[inline] + pub fn parse($self: $receiver) -> Result { + T::from_wstr($deref) + } + /// Returns `true` is the string contains only LATIN1 characters. /// /// Note that this doesn't necessarily means that `self.is_wide()` is `false`. diff --git a/core/src/string/parse.rs b/core/src/string/parse.rs new file mode 100644 index 000000000..834801e1e --- /dev/null +++ b/core/src/string/parse.rs @@ -0,0 +1,172 @@ +use std::num::Wrapping; + +use super::WStr; + +/// Analog of [`std::str::FromStr`], but for Ruffle's [`WStr<'_>`]. +pub trait FromWStr: Sized { + type Err; + + fn from_wstr(s: WStr<'_>) -> Result; +} + +/// Error returned by [`Integer::from_str_radix`]. +#[derive(Debug, thiserror::Error)] +#[error("failed to parse integer")] +pub struct ParseIntError(()); + +/// Trait implemented for all integer types that can be parsed from a [`WStr<'_>`]. +pub trait Integer: FromWStr { + fn from_wstr_radix(s: WStr<'_>, radix: u32) -> Result; +} + +impl FromWStr for f64 { + type Err = std::num::ParseFloatError; + + fn from_wstr(s: WStr<'_>) -> Result { + // TODO(moulins): avoid the utf8 conversion when we know the string can't + // possibly represent a floating point number. + s.to_utf8_lossy().parse() + } +} + +mod int_parse { + use super::*; + use std::convert::TryFrom; + + pub trait IntParse: Sized { + const SIGNED: bool; + + fn from_digit(n: u32) -> Self; + fn checked_add(self, n: u32) -> Option; + fn checked_sub(self, n: u32) -> Option; + fn checked_mul(self, n: u32) -> Option; + } + + pub fn from_str_radix(s: WStr<'_>, radix: u32) -> Option { + assert!( + radix >= 2 && radix <= 36, + "from_str_radix: radix must be between 2 and 36, got {}", + radix + ); + + let (is_neg, digits) = match s.try_get(0).map(u8::try_from) { + Some(Ok(b'-')) => (true, s.slice(1..)), + Some(Ok(b'+')) => (false, s.slice(1..)), + Some(_) => (false, s), + None => return None, + }; + + if is_neg && !T::SIGNED { + return None; + } + + digits.iter().try_fold(T::from_digit(0), |num, c| { + let byte = u8::try_from(c).ok()?; + let digit = (byte as char).to_digit(radix)?; + let num = num.checked_mul(radix)?; + if is_neg { + num.checked_sub(digit) + } else { + num.checked_add(digit) + } + }) + } +} + +macro_rules! impl_int_parse { + ($($ty:ty)*) => { $( + impl int_parse::IntParse for $ty { + #[allow(unused_comparisons)] + const SIGNED: bool = <$ty>::MIN < 0; + + #[inline] + fn from_digit(n: u32) -> Self { + n as $ty + } + + #[inline] + fn checked_add(self, n: u32) -> Option { + <$ty>::checked_add(self, n as $ty) + } + + #[inline] + fn checked_sub(self, n: u32) -> Option { + <$ty>::checked_sub(self, n as $ty) + } + + #[inline] + fn checked_mul(self, n: u32) -> Option { + <$ty>::checked_mul(self, n as $ty) + } + } + )* } +} + +impl_int_parse! { u32 i32 usize } + +macro_rules! impl_wrapping_int_parse { + ($($ty:ty)*) => { $( + impl int_parse::IntParse for Wrapping<$ty> { + #[allow(unused_comparisons)] + const SIGNED: bool = <$ty>::MIN < 0; + + #[inline] + fn from_digit(n: u32) -> Self { + Self(n as $ty) + } + + #[inline] + fn checked_add(self, n: u32) -> Option { + Some(self + Self::from_digit(n)) + } + + #[inline] + fn checked_sub(self, n: u32) -> Option { + Some(self - Self::from_digit(n)) + } + + #[inline] + fn checked_mul(self, n: u32) -> Option { + Some(self * Self::from_digit(n)) + } + } + )* } +} + +impl_wrapping_int_parse! { u32 i32 usize } + +macro_rules! impl_from_str_int { + ($($ty:ty)*) => { $( + impl Integer for $ty { + #[inline] + fn from_wstr_radix(s: WStr<'_>, radix: u32) -> Result { + int_parse::from_str_radix(s, radix).ok_or(ParseIntError(())) + } + } + + impl Integer for Wrapping<$ty> { + #[inline] + fn from_wstr_radix(s: WStr<'_>, radix: u32) -> Result { + int_parse::from_str_radix(s, radix).ok_or(ParseIntError(())) + } + } + + impl FromWStr for $ty { + type Err = ParseIntError; + #[inline] + fn from_wstr(s: WStr<'_>) -> Result { + int_parse::from_str_radix(s, 10).ok_or(ParseIntError(())) + } + } + + impl FromWStr for Wrapping<$ty> { + type Err = ParseIntError; + #[inline] + fn from_wstr(s: WStr<'_>) -> Result { + int_parse::from_str_radix(s, 10).ok_or(ParseIntError(())) + } + } + )* } +} + +impl_from_str_int! { u32 i32 usize }