diff --git a/Cargo.lock b/Cargo.lock index 4b225782a..50029024a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3439,6 +3439,7 @@ version = "0.1.2" dependencies = [ "approx", "byteorder", + "encoding_rs", "enumset", "flate2", "libflate 1.0.3", diff --git a/core/src/avm1/activation.rs b/core/src/avm1/activation.rs index dc5b899d3..cd63de7d3 100644 --- a/core/src/avm1/activation.rs +++ b/core/src/avm1/activation.rs @@ -25,6 +25,7 @@ use std::convert::TryFrom; use std::fmt; use swf::avm1::read::Reader; use swf::avm1::types::{Action, CatchVar, Function, TryBlock}; +use swf::SwfStr; use url::form_urlencoded; macro_rules! avm_debug { @@ -432,8 +433,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { } pub fn run_actions(&mut self, code: SwfSlice) -> Result, Error<'gc>> { - let mut read = Reader::new(&code.movie.data()[..], self.swf_version()); - read.seek(code.start as isize); + let mut read = Reader::new(&code.movie.data()[code.start..], self.swf_version()); loop { let result = self.do_action(&code, &mut read); @@ -446,10 +446,10 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { } /// Run a single action from a given action reader. - fn do_action( + fn do_action<'b>( &mut self, - data: &SwfSlice, - reader: &mut Reader<'_>, + data: &'b SwfSlice, + reader: &mut Reader<'b>, ) -> Result, Error<'gc>> { self.actions_since_timeout_check += 1; if self.actions_since_timeout_check >= 200 { @@ -459,7 +459,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { } } - if reader.pos() >= data.end { + if reader.get_ref().as_ptr() as usize >= data.as_ref().as_ptr_range().end as usize { //Executing beyond the end of a function constitutes an implicit return. Ok(FrameControl::Return(ReturnType::Implicit)) } else if let Some(action) = reader.read_action()? { @@ -496,7 +496,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { params, actions, } => self.action_define_function( - &name, + name, ¶ms[..], data.to_unbounded_subslice(actions).unwrap(), ), @@ -516,7 +516,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { Action::GetProperty => self.action_get_property(), Action::GetTime => self.action_get_time(), Action::GetVariable => self.action_get_variable(), - Action::GetUrl { url, target } => self.action_get_url(&url, &target), + Action::GetUrl { url, target } => self.action_get_url(url, target), Action::GetUrl2 { send_vars_method, is_target_sprite, @@ -528,14 +528,14 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { scene_offset, } => self.action_goto_frame_2(set_playing, scene_offset), Action::Greater => self.action_greater(), - Action::GotoLabel(label) => self.action_goto_label(&label), - Action::If { offset } => self.action_if(offset, reader), + Action::GotoLabel(label) => self.action_goto_label(label), + Action::If { offset } => self.action_if(offset, reader, data), Action::Increment => self.action_increment(), Action::InitArray => self.action_init_array(), Action::InitObject => self.action_init_object(), Action::ImplementsOp => self.action_implements_op(), Action::InstanceOf => self.action_instance_of(), - Action::Jump { offset } => self.action_jump(offset, reader), + Action::Jump { offset } => self.action_jump(offset, reader, data), Action::Less => self.action_less(), Action::Less2 => self.action_less_2(), Action::MBAsciiToChar => self.action_mb_ascii_to_char(), @@ -559,7 +559,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { Action::Return => self.action_return(), Action::SetMember => self.action_set_member(), Action::SetProperty => self.action_set_property(), - Action::SetTarget(target) => self.action_set_target(&target), + Action::SetTarget(target) => self.action_set_target(&target.to_str_lossy()), Action::SetTarget2 => self.action_set_target2(), Action::SetVariable => self.action_set_variable(), Action::StackSwap => self.action_stack_swap(), @@ -889,11 +889,14 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { fn action_constant_pool( &mut self, - constant_pool: &[&str], + constant_pool: &[SwfStr<'_>], ) -> Result, Error<'gc>> { self.context.avm1.constant_pool = GcCell::allocate( self.context.gc_context, - constant_pool.iter().map(|s| (*s).to_string()).collect(), + constant_pool + .iter() + .map(|s| (*s).to_string_lossy()) + .collect(), ); self.set_constant_pool(self.context.avm1.constant_pool); @@ -908,10 +911,12 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { fn action_define_function( &mut self, - name: &str, - params: &[&str], + name: SwfStr<'_>, + params: &[SwfStr<'_>], actions: SwfSlice, ) -> Result, Error<'gc>> { + let name = name.to_str_lossy(); + let name = name.as_ref(); let swf_version = self.swf_version(); let scope = Scope::new_closure_scope(self.scope_cell(), self.context.gc_context); let constant_pool = self.constant_pool(); @@ -977,7 +982,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { if action_func.name.is_empty() { self.context.avm1.push(func_obj); } else { - self.define(action_func.name, func_obj); + self.define(&action_func.name.to_str_lossy(), func_obj); } Ok(FrameControl::Continue) @@ -1213,9 +1218,15 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { Ok(FrameControl::Continue) } - fn action_get_url(&mut self, url: &str, target: &str) -> Result, Error<'gc>> { + fn action_get_url( + &mut self, + url: SwfStr<'_>, + target: SwfStr<'_>, + ) -> Result, Error<'gc>> { + let target = target.to_str_lossy(); + let target = target.as_ref(); + let url = url.to_string_lossy(); if target.starts_with("_level") && target.len() > 6 { - let url = url.to_string(); match target[6..].parse::() { Ok(level_id) => { let fetch = self.context.navigator.fetch(&url, RequestOptions::get()); @@ -1247,7 +1258,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { return Ok(FrameControl::Continue); } - if let Some(fscommand) = fscommand::parse(url) { + if let Some(fscommand) = fscommand::parse(&url) { fscommand::handle(fscommand, self)?; } else { self.context @@ -1414,10 +1425,10 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { Ok(FrameControl::Continue) } - fn action_goto_label(&mut self, label: &str) -> Result, Error<'gc>> { + fn action_goto_label(&mut self, label: SwfStr<'_>) -> Result, Error<'gc>> { if let Some(clip) = self.target_clip() { if let Some(clip) = clip.as_movie_clip() { - if let Some(frame) = clip.frame_label_to_number(label) { + if let Some(frame) = clip.frame_label_to_number(&label.to_str_lossy()) { clip.goto_frame(&mut self.context, frame, true); } else { avm_warn!(self, "GoToLabel: Frame label '{}' not found", label); @@ -1431,14 +1442,15 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { Ok(FrameControl::Continue) } - fn action_if( + fn action_if<'b>( &mut self, jump_offset: i16, - reader: &mut Reader<'_>, + reader: &mut Reader<'b>, + data: &'b SwfSlice, ) -> Result, Error<'gc>> { let val = self.context.avm1.pop(); if val.as_bool(self.current_swf_version()) { - reader.seek(jump_offset.into()); + self.seek(jump_offset, reader, data)?; } Ok(FrameControl::Continue) } @@ -1515,13 +1527,13 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { Ok(FrameControl::Continue) } - fn action_jump( + fn action_jump<'b>( &mut self, jump_offset: i16, - reader: &mut Reader<'_>, + reader: &mut Reader<'b>, + data: &'b SwfSlice, ) -> Result, Error<'gc>> { - // TODO(Herschel): Handle out-of-bounds. - reader.seek(jump_offset.into()); + self.seek(jump_offset, reader, data)?; Ok(FrameControl::Continue) } @@ -1764,12 +1776,12 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { SwfValue::Float(v) => f64::from(*v).into(), SwfValue::Double(v) => (*v).into(), SwfValue::Str(v) => { - AvmString::new(self.context.gc_context, (*v).to_string()).into() + AvmString::new(self.context.gc_context, v.to_string_lossy()).into() } SwfValue::Register(v) => self.current_register(*v), SwfValue::ConstantPool(i) => { if let Some(value) = self.constant_pool().read().get(*i as usize) { - AvmString::new(self.context.gc_context, value.to_string()).into() + AvmString::new(self.context.gc_context, value).into() } else { avm_warn!( self, @@ -2267,7 +2279,9 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { ); match catch_vars { - CatchVar::Var(name) => activation.set_variable(name, value.to_owned())?, + CatchVar::Var(name) => { + activation.set_variable(&name.to_str_lossy(), value.to_owned())? + } CatchVar::Register(id) => { activation.set_current_register(*id, value.to_owned()) } @@ -2968,4 +2982,18 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { Ok(FrameControl::Continue) } } + + fn seek<'b>( + &self, + jump_offset: i16, + reader: &mut Reader<'b>, + data: &'b SwfSlice, + ) -> Result<(), Error<'gc>> { + let slice = data.movie.data(); + let mut pos = reader.get_ref().as_ptr() as usize - slice.as_ptr() as usize; + pos = (pos as isize + isize::from(jump_offset)) as usize; + pos = pos.min(slice.len()); + *reader.get_mut() = &slice[pos as usize..]; + Ok(()) + } } diff --git a/core/src/avm1/function.rs b/core/src/avm1/function.rs index 379fcd1a0..0a60ff0c5 100644 --- a/core/src/avm1/function.rs +++ b/core/src/avm1/function.rs @@ -13,7 +13,7 @@ use enumset::EnumSet; use gc_arena::{Collect, CollectionContext, Gc, GcCell, MutationContext}; use std::borrow::Cow; use std::fmt; -use swf::avm1::types::FunctionParam; +use swf::{avm1::types::FunctionParam, SwfStr}; /// Represents a function defined in Ruffle's code. /// @@ -94,14 +94,15 @@ impl<'gc> Avm1Function<'gc> { swf_version: u8, actions: SwfSlice, name: &str, - params: &[&str], + params: &[SwfStr<'_>], scope: GcCell<'gc, Scope<'gc>>, constant_pool: GcCell<'gc, Vec>, base_clip: DisplayObject<'gc>, ) -> Self { - let name = match name { - "" => None, - name => Some(name.to_string()), + let name = if name.is_empty() { + None + } else { + Some(name.to_string()) }; Avm1Function { @@ -118,7 +119,10 @@ impl<'gc> Avm1Function<'gc> { suppress_this: false, preload_this: false, preload_global: false, - params: params.iter().map(|&s| (None, s.to_string())).collect(), + params: params + .iter() + .map(|&s| (None, s.to_string_lossy())) + .collect(), scope, constant_pool, base_clip, @@ -134,9 +138,10 @@ impl<'gc> Avm1Function<'gc> { constant_pool: GcCell<'gc, Vec>, base_clip: DisplayObject<'gc>, ) -> Self { - let name = match swf_function.name { - "" => None, - name => Some(name.to_string()), + let name = if swf_function.name.is_empty() { + None + } else { + Some(swf_function.name.to_string_lossy()) }; let mut owned_params = Vec::new(); @@ -145,7 +150,7 @@ impl<'gc> Avm1Function<'gc> { register_index: r, } in &swf_function.params { - owned_params.push((*r, (*s).to_string())) + owned_params.push((*r, (*s).to_string_lossy())) } Avm1Function { diff --git a/core/src/display_object.rs b/core/src/display_object.rs index c8ce86c95..0e8c77e29 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -841,7 +841,7 @@ pub trait TDisplayObject<'gc>: self.set_color_transform(gc_context, &color_transform.clone().into()); } if let Some(name) = &place_object.name { - self.set_name(gc_context, name); + self.set_name(gc_context, &name.to_str_lossy()); } if let Some(clip_depth) = place_object.clip_depth { self.set_clip_depth(gc_context, clip_depth.into()); diff --git a/core/src/display_object/edit_text.rs b/core/src/display_object/edit_text.rs index eb9d5d2c8..0766050fd 100644 --- a/core/src/display_object/edit_text.rs +++ b/core/src/display_object/edit_text.rs @@ -156,6 +156,7 @@ impl<'gc> EditText<'gc> { let mut text_spans = FormatSpans::new(); text_spans.set_default_format(default_format.clone()); + let text = text.to_str_lossy(); if is_html { let _ = document .as_node() @@ -211,13 +212,13 @@ impl<'gc> EditText<'gc> { id: swf_tag.id, bounds: swf_tag.bounds, font_id: swf_tag.font_id, - font_class_name: swf_tag.font_class_name.map(str::to_string), + font_class_name: swf_tag.font_class_name.map(|s| s.to_string_lossy()), height: swf_tag.height, color: swf_tag.color.clone(), max_length: swf_tag.max_length, layout: swf_tag.layout.clone(), - variable_name: swf_tag.variable_name.to_string(), - initial_text: swf_tag.initial_text.map(str::to_string), + variable_name: swf_tag.variable_name.to_string_lossy(), + initial_text: swf_tag.initial_text.map(|s| s.to_string_lossy()), is_word_wrap: swf_tag.is_word_wrap, is_multiline: swf_tag.is_multiline, is_password: swf_tag.is_password, @@ -246,7 +247,7 @@ impl<'gc> EditText<'gc> { intrinsic_bounds, bounds, autosize: AutoSizeMode::None, - variable: variable.map(str::to_string), + variable: variable.map(|s| s.to_string_lossy()), bound_stage_object: None, firing_variable_binding: false, selection: None, @@ -293,7 +294,7 @@ impl<'gc> EditText<'gc> { indent: Twips::from_pixels(0.0), leading: Twips::from_pixels(0.0), }), - variable_name: "", //TODO: should be null + variable_name: "".into(), //TODO: should be null initial_text: None, is_word_wrap: false, is_multiline: false, diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index a0543c03e..bafc54af7 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -459,7 +459,7 @@ impl<'gc> MovieClip<'gc> { // giving us a `SwfSlice` for later parsing, so we have to replcate the // *entire* parsing code here. This sucks. let flags = reader.read_u32()?; - let name = reader.read_c_string()?; + let name = reader.read_string()?.to_string_lossy(); let is_lazy_initialize = flags & 1 != 0; let domain = library.avm2_domain(); @@ -499,7 +499,7 @@ impl<'gc> MovieClip<'gc> { for _ in 0..num_symbols { let id = reader.read_u16()?; - let class_name = reader.read_c_string()?; + let class_name = reader.read_string()?.to_string_lossy(); if let Some(name) = Avm2QName::from_symbol_class(&class_name, activation.context.gc_context) @@ -2458,7 +2458,7 @@ impl<'gc, 'a> MovieClipData<'gc> { let font = swf::Font { id: font.id, version: 0, - name: "", + name: "".into(), glyphs, language: swf::Language::Unknown, layout: None, @@ -2609,7 +2609,7 @@ impl<'gc, 'a> MovieClipData<'gc> { let character = context .library .library_for_movie_mut(self.movie()) - .register_export(export.id, &export.name); + .register_export(export.id, &export.name.to_str_lossy()); // TODO: do other types of Character need to know their exported name? if let Some(Character::MovieClip(movie_clip)) = character { @@ -2631,7 +2631,7 @@ impl<'gc, 'a> MovieClipData<'gc> { ) -> DecodeResult { let frame_label = reader.read_frame_label(tag_len)?; // Frame labels are case insensitive (ASCII). - let label = frame_label.label.to_ascii_lowercase(); + let label = frame_label.label.to_str_lossy().to_ascii_lowercase(); if let std::collections::hash_map::Entry::Vacant(v) = static_data.frame_labels.entry(label) { v.insert(cur_frame); diff --git a/core/src/html/text_format.rs b/core/src/html/text_format.rs index a8551b8b2..0ee0dc8c6 100644 --- a/core/src/html/text_format.rs +++ b/core/src/html/text_format.rs @@ -198,7 +198,7 @@ impl TextFormat { let font = et.font_id.and_then(|fid| movie_library.get_font(fid)); let font_class = et .font_class_name - .map(str::to_string) + .map(|s| s.to_string_lossy()) .or_else(|| font.map(|font| font.descriptor().class().to_string())) .unwrap_or_else(|| "Times New Roman".to_string()); let align = et.layout.clone().map(|l| l.align); diff --git a/swf/Cargo.toml b/swf/Cargo.toml index 0967b3084..c59b34b74 100644 --- a/swf/Cargo.toml +++ b/swf/Cargo.toml @@ -11,6 +11,7 @@ description = "Read and write the Adobe Flash SWF file format." [dependencies] byteorder = "1.4" +encoding_rs = "0.8.26" enumset = "1.0.1" num-derive = "0.3" num-traits = "0.2" diff --git a/swf/src/avm1/read.rs b/swf/src/avm1/read.rs index cd8aee744..cf7883bba 100644 --- a/swf/src/avm1/read.rs +++ b/swf/src/avm1/read.rs @@ -1,89 +1,97 @@ #![allow(clippy::unreadable_literal)] -use crate::avm1::opcode::OpCode; -use crate::avm1::types::*; +use crate::avm1::{opcode::OpCode, types::*}; use crate::error::{Error, Result}; use crate::read::SwfRead; -use std::io::Cursor; +use crate::string::SwfStr; +use std::io; #[allow(dead_code)] pub struct Reader<'a> { - inner: Cursor<&'a [u8]>, + input: &'a [u8], version: u8, + encoding: &'static encoding_rs::Encoding, } -impl<'a> SwfRead> for Reader<'a> { - fn get_inner(&mut self) -> &mut Cursor<&'a [u8]> { - &mut self.inner +impl<'a> SwfRead<&'a [u8]> for Reader<'a> { + fn get_inner(&mut self) -> &mut &'a [u8] { + &mut self.input } } impl<'a> Reader<'a> { pub fn new(input: &'a [u8], version: u8) -> Self { Self { - inner: Cursor::new(input), + input, version, + encoding: if version > 5 { + encoding_rs::UTF_8 + } else { + // TODO: Allow configurable encoding + encoding_rs::WINDOWS_1252 + }, } } - #[inline] - pub fn pos(&self) -> usize { - self.inner.position() as usize + pub fn get_ref(&self) -> &'a [u8] { + self.input } - #[inline] - pub fn seek(&mut self, relative_offset: isize) { - let new_pos = self.inner.position() as i64 + relative_offset as i64; - self.inner.set_position(new_pos as u64); + pub fn get_mut(&mut self) -> &mut &'a [u8] { + &mut self.input } - #[inline] - fn read_slice(&mut self, len: usize) -> Result<&'a [u8]> { - let pos = self.pos(); - self.inner.set_position(pos as u64 + len as u64); - let slice = self.inner.get_ref().get(pos..pos + len).ok_or_else(|| { - std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "Buffer underrun") - })?; - Ok(slice) + fn read_slice(&mut self, len: usize) -> io::Result<&'a [u8]> { + if self.input.len() >= len { + let slice = &self.input[..len]; + self.input = &self.input[len..]; + Ok(slice) + } else { + Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "Not enough data for slice", + )) + } } - #[inline] - fn read_c_string(&mut self) -> Result<&'a str> { - // Find zero terminator. - let str_slice = { - let start_pos = self.pos(); - loop { - let byte = self.read_u8()?; - if byte == 0 { - break; - } + pub fn read_string(&mut self) -> io::Result> { + let mut pos = 0; + loop { + let byte = *self.input.get(pos).ok_or(io::Error::new( + io::ErrorKind::UnexpectedEof, + "Not enough data for slice", + ))?; + if byte == 0 { + break; } - &self.inner.get_ref()[start_pos..self.pos() - 1] + pos += 1; + } + + let s = unsafe { + let slice = self.input.get_unchecked(..pos); + SwfStr::from_bytes_unchecked(slice, self.encoding) }; - // TODO: What does Flash do on invalid UTF8? - // Do we silently let it pass? - // TODO: Verify ANSI for SWF 5 and earlier. - std::str::from_utf8(str_slice).map_err(|_| Error::invalid_data("Invalid string data")) + self.input = &self.input[pos + 1..]; + Ok(s) } #[inline] pub fn read_action(&mut self) -> Result>> { let (opcode, mut length) = self.read_opcode_and_length()?; + let start = self.input.as_ref(); - let start_pos = self.pos(); let action = self.read_op(opcode, &mut length); + + let end_pos = (start.as_ptr() as usize + length) as *const u8; if let Err(e) = action { return Err(Error::avm1_parse_error_with_source(opcode, e)); } // Verify that we parsed the correct amount of data. - let end_pos = start_pos + length; - let pos = self.pos(); - if pos != end_pos { + if self.input.as_ptr() != end_pos { + self.input = &start[length.min(start.len())..]; // We incorrectly parsed this action. // Re-sync to the expected end of the action and throw an error. - use std::convert::TryInto; - self.inner.set_position(end_pos.try_into().unwrap()); return Err(Error::avm1_parse_error(opcode)); } action @@ -131,7 +139,7 @@ impl<'a> Reader<'a> { OpCode::ConstantPool => { let mut constants = vec![]; for _ in 0..self.read_u16()? { - constants.push(self.read_c_string()?); + constants.push(self.read_string()?); } Action::ConstantPool(constants) } @@ -153,8 +161,8 @@ impl<'a> Reader<'a> { OpCode::GetProperty => Action::GetProperty, OpCode::GetTime => Action::GetTime, OpCode::GetUrl => Action::GetUrl { - url: self.read_c_string()?, - target: self.read_c_string()?, + url: self.read_string()?, + target: self.read_string()?, }, OpCode::GetUrl2 => { let flags = self.read_u8()?; @@ -189,7 +197,7 @@ impl<'a> Reader<'a> { }, } } - OpCode::GotoLabel => Action::GotoLabel(self.read_c_string()?), + OpCode::GotoLabel => Action::GotoLabel(self.read_string()?), OpCode::Greater => Action::Greater, OpCode::If => Action::If { offset: self.read_i16()?, @@ -226,7 +234,7 @@ impl<'a> Reader<'a> { OpCode::Return => Action::Return, OpCode::SetMember => Action::SetMember, OpCode::SetProperty => Action::SetProperty, - OpCode::SetTarget => Action::SetTarget(self.read_c_string()?), + OpCode::SetTarget => Action::SetTarget(self.read_string()?), OpCode::SetTarget2 => Action::SetTarget2, OpCode::SetVariable => Action::SetVariable, OpCode::StackSwap => Action::StackSwap, @@ -281,9 +289,9 @@ impl<'a> Reader<'a> { } fn read_push(&mut self, length: usize) -> Result> { - let end_pos = self.pos() + length; + let end_pos = (self.input.as_ptr() as usize + length) as *const u8; let mut values = Vec::with_capacity(4); - while self.pos() < end_pos { + while self.input.as_ptr() < end_pos { values.push(self.read_push_value()?); } Ok(Action::Push(values)) @@ -291,7 +299,7 @@ impl<'a> Reader<'a> { fn read_push_value(&mut self) -> Result> { let value = match self.read_u8()? { - 0 => Value::Str(self.read_c_string()?), + 0 => Value::Str(self.read_string()?), 1 => Value::Float(self.read_f32()?), 2 => Value::Null, 3 => Value::Undefined, @@ -307,11 +315,11 @@ impl<'a> Reader<'a> { } fn read_define_function(&mut self, action_length: &mut usize) -> Result> { - let name = self.read_c_string()?; + let name = self.read_string()?; let num_params = self.read_u16()?; let mut params = Vec::with_capacity(num_params as usize); for _ in 0..num_params { - params.push(self.read_c_string()?); + params.push(self.read_string()?); } // code_length isn't included in the DefineFunction's action length. let code_length = usize::from(self.read_u16()?); @@ -324,7 +332,7 @@ impl<'a> Reader<'a> { } fn read_define_function_2(&mut self, action_length: &mut usize) -> Result> { - let name = self.read_c_string()?; + let name = self.read_string()?; let num_params = self.read_u16()?; let register_count = self.read_u8()?; // Number of registers let flags = self.read_u16()?; @@ -332,7 +340,7 @@ impl<'a> Reader<'a> { for _ in 0..num_params { let register = self.read_u8()?; params.push(FunctionParam { - name: self.read_c_string()?, + name: self.read_string()?, register_index: if register == 0 { None } else { Some(register) }, }); } @@ -363,7 +371,7 @@ impl<'a> Reader<'a> { let finally_length = usize::from(self.read_u16()?); *length += try_length + catch_length + finally_length; let catch_var = if flags & 0b100 == 0 { - CatchVar::Var(self.read_c_string()?) + CatchVar::Var(self.read_string()?) } else { CatchVar::Register(self.read_u8()?) }; @@ -421,6 +429,7 @@ pub mod tests { #[test] fn read_define_function() { + use encoding_rs::WINDOWS_1252; // Ensure we read a function properly along with the function data. let action_bytes = vec![ 0x9b, 0x08, 0x00, 0x66, 0x6f, 0x6f, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x96, 0x06, 0x00, @@ -431,7 +440,7 @@ pub mod tests { assert_eq!( action, Action::DefineFunction { - name: "foo", + name: SwfStr::from_str_with_encoding("foo", WINDOWS_1252).unwrap(), params: vec![], actions: &[0x96, 0x06, 0x00, 0x00, 0x74, 0x65, 0x73, 0x74, 0x00, 0x26], } @@ -440,7 +449,12 @@ pub mod tests { if let Action::DefineFunction { actions, .. } = action { let mut reader = Reader::new(actions, 5); let action = reader.read_action().unwrap().unwrap(); - assert_eq!(action, Action::Push(vec![Value::Str("test")])); + assert_eq!( + action, + Action::Push(vec![Value::Str( + SwfStr::from_str_with_encoding("test", WINDOWS_1252).unwrap() + )]) + ); } } diff --git a/swf/src/avm1/types.rs b/swf/src/avm1/types.rs index 3d93c09ea..1e41c8c53 100644 --- a/swf/src/avm1/types.rs +++ b/swf/src/avm1/types.rs @@ -1,3 +1,5 @@ +use crate::string::SwfStr; + #[derive(Clone, Debug, PartialEq)] pub enum Action<'a> { Add, @@ -16,11 +18,11 @@ pub enum Action<'a> { CastOp, CharToAscii, CloneSprite, - ConstantPool(Vec<&'a str>), + ConstantPool(Vec>), Decrement, DefineFunction { - name: &'a str, - params: Vec<&'a str>, + name: SwfStr<'a>, + params: Vec>, actions: &'a [u8], }, DefineFunction2(Function<'a>), @@ -39,8 +41,8 @@ pub enum Action<'a> { GetProperty, GetTime, GetUrl { - url: &'a str, - target: &'a str, + url: SwfStr<'a>, + target: SwfStr<'a>, }, GetUrl2 { send_vars_method: SendVarsMethod, @@ -53,7 +55,7 @@ pub enum Action<'a> { set_playing: bool, scene_offset: u16, }, - GotoLabel(&'a str), + GotoLabel(SwfStr<'a>), Greater, If { offset: i16, @@ -89,7 +91,7 @@ pub enum Action<'a> { Return, SetMember, SetProperty, - SetTarget(&'a str), + SetTarget(SwfStr<'a>), SetTarget2, SetVariable, StackSwap, @@ -138,7 +140,7 @@ pub enum Value<'a> { Int(i32), Float(f32), Double(f64), - Str(&'a str), + Str(SwfStr<'a>), Register(u8), ConstantPool(u16), } @@ -152,7 +154,7 @@ pub enum SendVarsMethod { #[derive(Clone, Debug, PartialEq)] pub struct Function<'a> { - pub name: &'a str, + pub name: SwfStr<'a>, pub register_count: u8, pub params: Vec>, pub preload_parent: bool, @@ -169,7 +171,7 @@ pub struct Function<'a> { #[derive(Clone, Debug, PartialEq, Eq)] pub struct FunctionParam<'a> { - pub name: &'a str, + pub name: SwfStr<'a>, pub register_index: Option, } @@ -182,6 +184,6 @@ pub struct TryBlock<'a> { #[derive(Clone, Debug, PartialEq, Eq)] pub enum CatchVar<'a> { - Var(&'a str), + Var(SwfStr<'a>), Register(u8), } diff --git a/swf/src/avm1/write.rs b/swf/src/avm1/write.rs index 4287c5457..410e40ee2 100644 --- a/swf/src/avm1/write.rs +++ b/swf/src/avm1/write.rs @@ -46,7 +46,7 @@ impl Writer { self.write_action_header(OpCode::ConstantPool, len)?; self.write_u16(constants.len() as u16)?; for constant in constants { - self.write_c_string(constant)?; + self.write_string(*constant)?; } } Action::Decrement => self.write_action_header(OpCode::Decrement, 0)?, @@ -60,10 +60,10 @@ impl Writer { let len = name.len() + 1 + 2 + params.iter().map(|p| p.len() + 1).sum::() + 2; self.write_action_header(OpCode::DefineFunction, len)?; - self.write_c_string(name)?; + self.write_string(*name)?; self.write_u16(params.len() as u16)?; for param in params { - self.write_c_string(param)?; + self.write_string(*param)?; } self.write_u16(actions.len() as u16)?; self.inner.write_all(actions)?; @@ -79,7 +79,7 @@ impl Writer { .sum::() + 4; self.write_action_header(OpCode::DefineFunction2, len)?; - self.write_c_string(&function.name)?; + self.write_string(function.name)?; self.write_u16(function.params.len() as u16)?; self.write_u8(function.register_count)?; let flags = if function.preload_global { @@ -108,7 +108,7 @@ impl Writer { } else { 0 })?; - self.write_c_string(¶m.name)?; + self.write_string(param.name)?; } self.write_u16(function.actions.len() as u16)?; self.inner.write_all(&function.actions)?; @@ -132,8 +132,8 @@ impl Writer { ref target, } => { self.write_action_header(OpCode::GetUrl, url.len() + target.len() + 2)?; - self.write_c_string(url)?; - self.write_c_string(target)?; + self.write_string(*url)?; + self.write_string(*target)?; } Action::GetUrl2 { send_vars_method, @@ -169,7 +169,7 @@ impl Writer { } Action::GotoLabel(ref label) => { self.write_action_header(OpCode::GotoLabel, label.len() + 1)?; - self.write_c_string(label)?; + self.write_string(*label)?; } Action::Greater => self.write_action_header(OpCode::Greater, 0)?, Action::If { offset } => { @@ -232,7 +232,7 @@ impl Writer { Action::SetProperty => self.write_action_header(OpCode::SetProperty, 0)?, Action::SetTarget(ref target) => { self.write_action_header(OpCode::SetTarget, target.len() + 1)?; - self.write_c_string(target)?; + self.write_string(*target)?; } Action::SetTarget2 => self.write_action_header(OpCode::SetTarget2, 0)?, Action::SetVariable => self.write_action_header(OpCode::SetVariable, 0)?, @@ -300,7 +300,7 @@ impl Writer { self.write_u16(catch_length as u16)?; self.write_u16(finally_length as u16)?; match try_block.catch { - Some((CatchVar::Var(ref name), _)) => self.write_c_string(name)?, + Some((CatchVar::Var(name), _)) => self.write_string(name)?, Some((CatchVar::Register(i), _)) => self.write_u8(i)?, _ => (), } @@ -352,9 +352,9 @@ impl Writer { fn write_push_value(&mut self, value: &Value) -> Result<()> { match *value { - Value::Str(ref string) => { + Value::Str(string) => { self.write_u8(0)?; - self.write_c_string(string)?; + self.write_string(string)?; } Value::Float(v) => { self.write_u8(1)?; diff --git a/swf/src/lib.rs b/swf/src/lib.rs index 57df39efb..de5b538d2 100644 --- a/swf/src/lib.rs +++ b/swf/src/lib.rs @@ -21,6 +21,7 @@ pub mod avm1; pub mod avm2; pub mod error; pub mod read; +mod string; mod tag_code; mod types; pub mod write; @@ -30,6 +31,7 @@ mod test_data; /// Reexports pub use read::{read_swf, read_swf_header}; +pub use string::*; pub use tag_code::TagCode; pub use types::*; pub use write::write_swf; diff --git a/swf/src/read.rs b/swf/src/read.rs index 951073c7e..da6194c6b 100644 --- a/swf/src/read.rs +++ b/swf/src/read.rs @@ -6,8 +6,11 @@ clippy::unreadable_literal )] -use crate::error::{Error, Result}; -use crate::types::*; +use crate::{ + error::{Error, Result}, + string::SwfStr, + types::*, +}; use byteorder::{LittleEndian, ReadBytesExt}; use enumset::EnumSet; use std::collections::HashSet; @@ -246,20 +249,6 @@ pub trait SwfRead { num.swap(3, 7); (&num[..]).read_f64::() } - - fn read_c_string(&mut self) -> Result { - let mut bytes = Vec::new(); - loop { - let byte = self.read_u8()?; - if byte == 0 { - break; - } - bytes.push(byte) - } - // TODO: There is probably a better way to do this. - // TODO: Verify ANSI for SWF 5 and earlier. - String::from_utf8(bytes).map_err(|_| Error::invalid_data("Invalid string data")) - } } pub struct BitReader<'a, 'b> { @@ -324,6 +313,7 @@ impl<'a, 'b> BitReader<'a, 'b> { pub struct Reader<'a> { input: &'a [u8], version: u8, + encoding: &'static encoding_rs::Encoding, } impl<'a> SwfRead<&'a [u8]> for Reader<'a> { @@ -366,7 +356,16 @@ impl<'a> SwfRead<&'a [u8]> for Reader<'a> { impl<'a> Reader<'a> { pub fn new(input: &'a [u8], version: u8) -> Reader<'a> { - Reader { input, version } + Reader { + input, + version, + encoding: if version > 5 { + encoding_rs::UTF_8 + } else { + // TODO: Allow configurable encoding + encoding_rs::WINDOWS_1252 + }, + } } pub fn version(&self) -> u8 { @@ -378,6 +377,13 @@ impl<'a> Reader<'a> { self.input } + /// Returns a mutable reference to the underlying `Reader`. + /// + /// Reading from this reference is not recommended. + pub fn get_mut(&mut self) -> &mut &'a [u8] { + &mut self.input + } + fn bits<'b>(&'b mut self) -> BitReader<'a, 'b> { BitReader { input: self.get_inner(), @@ -406,7 +412,7 @@ impl<'a> Reader<'a> { slice } - fn read_string(&mut self) -> io::Result> { + pub fn read_string(&mut self) -> io::Result> { let mut pos = 0; loop { let byte = *self.input.get(pos).ok_or(io::Error::new( @@ -418,26 +424,17 @@ impl<'a> Reader<'a> { } pos += 1; } - // TODO: There is probably a better way to do this. - // TODO: Verify ANSI for SWF 5 and earlier. - let s = unsafe { std::str::from_utf8_unchecked(&self.input.get_unchecked(..pos)) }; + + let s = unsafe { + let slice = self.input.get_unchecked(..pos); + SwfStr::from_bytes_unchecked(slice, self.encoding) + }; self.input = &self.input[pos + 1..]; Ok(s) } fn read_string_with_len(&mut self, len: usize) -> io::Result> { - // TODO: There is probably a better way to do this. - // TODO: Verify ANSI for SWF 5 and earlier. - let mut s = unsafe { std::str::from_utf8_unchecked(&self.read_slice(len)?) }; - s = s.trim_end_matches('\0'); - Ok(s) - } - - /// Returns a mutable reference to the underlying `Reader`. - /// - /// Reading from this reference is not recommended. - pub fn get_mut(&mut self) -> &mut &'a [u8] { - &mut self.input + Ok(SwfStr::from_bytes(&self.read_slice(len)?, self.encoding)) } /// Reads the next SWF tag from the stream. diff --git a/swf/src/string.rs b/swf/src/string.rs new file mode 100644 index 000000000..820514fb3 --- /dev/null +++ b/swf/src/string.rs @@ -0,0 +1,141 @@ +//! String typed used by SWF files. +//! +//! Allows for locale-dependent encoding for SWF version <6. + +use encoding_rs::{Encoding, UTF_8}; +use std::{borrow::Cow, fmt}; + +/// `SwfStr` is returned by SWF and AVM1 parsing functions. +/// `SwfStr` is analogous to `&str`, with some additional allowances: +/// * An encoding is specified along with the string data. +/// * The string contains no null bytes. +/// * Invalid data for any particular encoding is allowed; +/// any conversions to std::String will be lossy for invalid data. +/// This handles the locale dependent encoding of early SWF files and +/// mimics C-style null-terminated string behavior. +/// To convert this to a standard Rust string, use `SwfStr::to_str_lossy`. +/// `SwfStr`s are equal if both their encoding and data matches. +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct SwfStr<'a> { + /// The string bytes. + string: &'a [u8], + + /// The encoding of the string data. + encoding: &'static Encoding, +} + +impl<'a> SwfStr<'a> { + /// Create a new `SwfStr` from a byte slice with a given encoding. + /// The string will be truncated if a null byte is encountered. + /// The data is not required to be valid for the given encoding. + #[inline] + pub fn from_bytes(string: &'a [u8], encoding: &'static Encoding) -> Self { + let i = string + .into_iter() + .position(|&c| c == 0) + .unwrap_or(string.len()); + Self { + string: &string[..i], + encoding, + } + } + + /// Create a new `SwfStr` from a byte slice with a given encoding. + /// The string should contain no null bytes, but ths is not checked. + /// The data is not required to be valid for the given encoding. + #[inline] + pub unsafe fn from_bytes_unchecked(string: &'a [u8], encoding: &'static Encoding) -> Self { + Self { string, encoding } + } + + /// Create a new UTF-8 `SwfStr` from a Rust `str`. + /// The string will be truncated if a null byte is encountered. + #[inline] + pub fn from_str(string: &'a str) -> Self { + Self::from_bytes(string.as_bytes(), UTF_8) + } + + /// Create a new `SwfStr` with the given encoding from a Rust `str`. + /// The string will be re-encoded with the given encoding. + /// The string will be truncated if a null byte is encountered. + /// `None` is returned if the encoding is not lossless. + /// Intended for tests. + pub fn from_str_with_encoding(string: &'a str, encoding: &'static Encoding) -> Option { + if let (Cow::Borrowed(s), _, false) = encoding.encode(&string) { + Some(Self::from_bytes(s, encoding)) + } else { + None + } + } + + /// Returns the byte slice of this string. + #[inline] + pub fn as_bytes(&self) -> &'a [u8] { + self.string + } + + /// Returns the encoding used by this string. + #[inline] + pub fn encoding(&self) -> &'static Encoding { + self.encoding + } + + /// Returns `true` if the string has a length of zero, and `false` otherwise. + #[inline] + pub fn is_empty(&self) -> bool { + self.string.is_empty() + } + + /// Returns the `len` of the string in bytes. + #[inline] + pub fn len(&self) -> usize { + self.string.len() + } + + /// Decodes the string into a Rust UTF-8 `str`. + /// The UTF-8 replacement character will be uses for any invalid data. + #[inline] + pub fn to_str_lossy(&self) -> Cow<'a, str> { + self.encoding.decode_without_bom_handling(self.string).0 + } + + /// Decodes the string into a Rust UTF-8 `String`. + /// The UTF-8 replacement character will be uses for any invalid data. + #[inline] + pub fn to_string_lossy(&self) -> String { + self.to_str_lossy().into_owned() + } +} + +impl<'a> Default for SwfStr<'a> { + fn default() -> Self { + Self { + string: &[], + encoding: UTF_8, + } + } +} + +impl<'a> From<&'a str> for SwfStr<'a> { + fn from(s: &'a str) -> Self { + SwfStr::from_str(s) + } +} + +impl<'a, T: AsRef> PartialEq for SwfStr<'a> { + fn eq(&self, other: &T) -> bool { + self.string == other.as_ref().as_bytes() + } +} + +impl<'a> fmt::Display for SwfStr<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_str_lossy()) + } +} + +impl<'a> fmt::Debug for SwfStr<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_str_lossy()) + } +} diff --git a/swf/src/test_data.rs b/swf/src/test_data.rs index 8012aeea9..69f6aba7f 100644 --- a/swf/src/test_data.rs +++ b/swf/src/test_data.rs @@ -3,11 +3,13 @@ use crate::avm1::types::*; use crate::avm2::read::tests::read_abc_from_file; use crate::avm2::types::*; -use crate::read::{read_swf, read_swf_header}; use crate::read::tests::{read_tag_bytes_from_file, read_tag_bytes_from_file_with_index}; +use crate::read::{read_swf, read_swf_header}; +use crate::string::SwfStr; use crate::tag_code::TagCode; use crate::types::*; use crate::write::write_swf; +use encoding_rs::WINDOWS_1252; use std::fs::File; use std::vec::Vec; @@ -292,7 +294,7 @@ pub fn tag_tests() -> Vec { }, ButtonAction { conditions: vec![ButtonActionCondition::KeyPress].into_iter().collect(), - key_code: Some(3), // Home + key_code: Some(3), // Home action_data: &[150, 3, 0, 0, 66, 0, 38, 0], // trace("B"); }, ], @@ -400,8 +402,8 @@ pub fn tag_tests() -> Vec { indent: Twips::from_pixels(1.0), leading: Twips::from_pixels(2.0), }), - variable_name: "foo", - initial_text: Some("-_-"), + variable_name: SwfStr::from_str_with_encoding("foo", WINDOWS_1252).unwrap(), + initial_text: Some(SwfStr::from_str_with_encoding("-_-", WINDOWS_1252).unwrap()), is_word_wrap: false, is_multiline: true, is_password: false, @@ -670,7 +672,7 @@ pub fn tag_tests() -> Vec { 10, Tag::DefineFont4(Font4 { id: 1, - name: "Dummy", + name: "Dummy".into(), is_italic: false, is_bold: false, data: None, @@ -682,7 +684,7 @@ pub fn tag_tests() -> Vec { Tag::DefineFontInfo(Box::new(FontInfo { id: 1, version: 1, - name: "Verdana", + name: SwfStr::from_str_with_encoding("Verdana", WINDOWS_1252).unwrap(), is_small_text: false, is_ansi: true, is_shift_jis: false, @@ -698,7 +700,7 @@ pub fn tag_tests() -> Vec { Tag::DefineFontInfo(Box::new(FontInfo { id: 1, version: 2, - name: "Verdana", + name: "Verdana".into(), is_small_text: false, is_ansi: true, is_shift_jis: false, @@ -713,8 +715,8 @@ pub fn tag_tests() -> Vec { 9, Tag::DefineFontName { id: 2, - name: "Dummy", - copyright_info: "Dummy font for swf-rs tests", + name: "Dummy".into(), + copyright_info: "Dummy font for swf-rs tests".into(), }, read_tag_bytes_from_file("tests/swfs/DefineFont4.swf", TagCode::DefineFontName), ), @@ -1397,38 +1399,38 @@ pub fn tag_tests() -> Vec { ), ), ( - 1, // Minimum version not listed in SWF19. + 9, // Minimum version not listed in SWF19. Tag::DefineSceneAndFrameLabelData(DefineSceneAndFrameLabelData { scenes: vec![ FrameLabelData { frame_num: 0, - label: "Scene 1", + label: "Scene 1".into(), }, FrameLabelData { frame_num: 25, - label: "Scene2Scene2Scene2Scene2Scene2", + label: "Scene2Scene2Scene2Scene2Scene2".into(), }, FrameLabelData { frame_num: 26, - label: "test日本語test", + label: "test日本語test".into(), }, ], frame_labels: vec![ FrameLabelData { frame_num: 0, - label: "a", + label: "a".into(), }, FrameLabelData { frame_num: 9, - label: "b", + label: "b".into(), }, FrameLabelData { frame_num: 17, - label: "❤😁aaa", + label: "❤😁aaa".into(), }, FrameLabelData { frame_num: 25, - label: "frameInScene2", + label: "frameInScene2".into(), }, ], }), @@ -1954,7 +1956,7 @@ pub fn tag_tests() -> Vec { ), ( 6, - Tag::EnableDebugger("$1$ve$EG3LE6bumvJ2pR8F5qXny/"), + Tag::EnableDebugger("$1$ve$EG3LE6bumvJ2pR8F5qXny/".into()), read_tag_bytes_from_file( "tests/swfs/EnableDebugger2-CS6.swf", TagCode::EnableDebugger2, @@ -1962,9 +1964,7 @@ pub fn tag_tests() -> Vec { ), ( 10, - Tag::EnableTelemetry { - password_hash: &[], - }, + Tag::EnableTelemetry { password_hash: &[] }, read_tag_bytes_from_file("tests/swfs/EnableTelemetry.swf", TagCode::EnableTelemetry), ), ( @@ -1984,7 +1984,7 @@ pub fn tag_tests() -> Vec { 6, Tag::ExportAssets(vec![ExportedAsset { id: 2, - name: "Test💯", + name: "Test💯".into(), }]), read_tag_bytes_from_file("tests/swfs/ExportAssets-CS6.swf", TagCode::ExportAssets), ), @@ -2002,7 +2002,7 @@ pub fn tag_tests() -> Vec { ( 3, Tag::FrameLabel(FrameLabel { - label: "test", + label: SwfStr::from_str_with_encoding("test", WINDOWS_1252).unwrap(), is_anchor: false, }), read_tag_bytes_from_file_with_index( @@ -2014,7 +2014,7 @@ pub fn tag_tests() -> Vec { ( 6, // Anchor tags supported in SWF version 6 and later. Tag::FrameLabel(FrameLabel { - label: "anchor_tag", + label: "anchor_tag".into(), is_anchor: true, }), read_tag_bytes_from_file_with_index( @@ -2026,10 +2026,10 @@ pub fn tag_tests() -> Vec { ( 7, Tag::ImportAssets { - url: "ExportAssets-CS6.swf", + url: "ExportAssets-CS6.swf".into(), imports: vec![ExportedAsset { id: 1, - name: "Test💯", + name: "Test💯".into(), }], }, read_tag_bytes_from_file("tests/swfs/ImportAssets-CS6.swf", TagCode::ImportAssets), @@ -2037,10 +2037,10 @@ pub fn tag_tests() -> Vec { ( 8, Tag::ImportAssets { - url: "ExportAssets-CS6.swf", + url: "ExportAssets-CS6.swf".into(), imports: vec![ExportedAsset { id: 1, - name: "Test💯", + name: "Test💯".into(), }], }, read_tag_bytes_from_file("tests/swfs/ImportAssets2-CS6.swf", TagCode::ImportAssets2), @@ -2084,7 +2084,7 @@ pub fn tag_tests() -> Vec { ), ( 1, - Tag::Metadata("aa!"), + Tag::Metadata(SwfStr::from_str_with_encoding("aa!", WINDOWS_1252).unwrap()), vec![0b01_000100, 0b000_10011, b'a', b'a', b'!', 0], ), ( @@ -2237,7 +2237,7 @@ pub fn tag_tests() -> Vec { b_add: 20, }), ratio: None, - name: Some("test"), + name: Some("test".into()), clip_depth: None, class_name: None, filters: Some(vec![ @@ -2388,7 +2388,10 @@ pub fn tag_tests() -> Vec { ), ( 5, // Password supported in SWF version 5 or later. - Tag::Protect(Some("$1$d/$yMscKH17OJ0paJT.e67iz0")), + Tag::Protect(Some( + SwfStr::from_str_with_encoding("$1$d/$yMscKH17OJ0paJT.e67iz0", WINDOWS_1252) + .unwrap(), + )), read_tag_bytes_from_file("tests/swfs/Protect.swf", TagCode::Protect), ), ( @@ -2443,11 +2446,11 @@ pub fn tag_tests() -> Vec { Tag::SymbolClass(vec![ SymbolClassLink { id: 2, - class_name: "foo.Test", + class_name: "foo.Test".into(), }, SymbolClassLink { id: 0, - class_name: "DocumentTest", + class_name: "DocumentTest".into(), }, ]), read_tag_bytes_from_file("tests/swfs/SymbolClass.swf", TagCode::SymbolClass), @@ -2469,7 +2472,7 @@ pub fn tag_tests() -> Vec { ( 9, Tag::StartSound2 { - class_name: "TestSound", + class_name: "TestSound".into(), sound_info: Box::new(SoundInfo { event: SoundEvent::Event, in_sample: None, @@ -2670,8 +2673,8 @@ pub fn avm1_tests() -> Vec { ( 3, Action::GetUrl { - url: "a", - target: "b", + url: SwfStr::from_str_with_encoding("a", WINDOWS_1252).unwrap(), + target: SwfStr::from_str_with_encoding("b", WINDOWS_1252).unwrap(), }, vec![0x83, 4, 0, 97, 0, 98, 0], ), @@ -2713,7 +2716,7 @@ pub fn avm1_tests() -> Vec { ), ( 3, - Action::GotoLabel("testb"), + Action::GotoLabel(SwfStr::from_str_with_encoding("testb", WINDOWS_1252).unwrap()), vec![0x8C, 6, 0, 116, 101, 115, 116, 98, 0], ), (4, Action::If { offset: 1 }, vec![0x9D, 2, 0, 1, 0]), @@ -2733,7 +2736,9 @@ pub fn avm1_tests() -> Vec { (3, Action::PreviousFrame, vec![0x05]), ( 4, - Action::Push(vec![Value::Str("test")]), + Action::Push(vec![Value::Str( + SwfStr::from_str_with_encoding("test", WINDOWS_1252).unwrap(), + )]), vec![0x96, 6, 0, 0, 116, 101, 115, 116, 0], ), ( @@ -2796,7 +2801,7 @@ pub fn avm1_tests() -> Vec { (4, Action::RandomNumber, vec![0x30]), ( 3, - Action::SetTarget("test"), + Action::SetTarget(SwfStr::from_str_with_encoding("test", WINDOWS_1252).unwrap()), vec![0x8B, 5, 0, 116, 101, 115, 116, 0], ), (4, Action::SetVariable, vec![0x1D]), @@ -2845,8 +2850,11 @@ pub fn avm1_tests() -> Vec { ( 5, Action::DefineFunction { - name: "cliche", - params: vec!["greeting", "name"], + name: SwfStr::from_str_with_encoding("cliche", WINDOWS_1252).unwrap(), + params: vec![ + SwfStr::from_str_with_encoding("greeting", WINDOWS_1252).unwrap(), + SwfStr::from_str_with_encoding("name", WINDOWS_1252).unwrap(), + ], actions: &[ 0x96, 0x0a, 0x00, 0x00, 0x67, 0x72, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x00, 0x1c, 0x96, 0x03, 0x00, 0x00, 0x20, 0x00, 0x47, 0x96, 0x06, 0x00, 0x00, 0x6e, diff --git a/swf/src/types.rs b/swf/src/types.rs index 6138c820f..8ae79ce74 100644 --- a/swf/src/types.rs +++ b/swf/src/types.rs @@ -3,6 +3,7 @@ //! These structures are documented in the Adobe SWF File Format Specification //! version 19 (henceforth SWF19): //! https://www.adobe.com/content/dam/acom/en/devnet/pdf/swf-file-format-spec.pdf +use crate::string::SwfStr; use enumset::{EnumSet, EnumSetType}; use std::collections::HashSet; @@ -10,8 +11,6 @@ mod matrix; pub use matrix::Matrix; -pub type SwfStr<'a> = &'a str; - /// A complete header and tags in the SWF file. /// This is returned by the `swf::read_swf` convenience method. #[derive(Debug, PartialEq)] diff --git a/swf/src/write.rs b/swf/src/write.rs index edec1ea13..9a71ed1c8 100644 --- a/swf/src/write.rs +++ b/swf/src/write.rs @@ -5,9 +5,12 @@ clippy::unreadable_literal )] -use crate::error::{Error, Result}; -use crate::tag_code::TagCode; -use crate::types::*; +use crate::{ + error::{Error, Result}, + string::SwfStr, + tag_code::TagCode, + types::*, +}; use byteorder::{LittleEndian, WriteBytesExt}; use enumset::EnumSet; use std::cmp::max; @@ -185,7 +188,7 @@ pub trait SwfWrite { self.get_inner().write_all(&num) } - fn write_c_string(&mut self, s: &str) -> io::Result<()> { + fn write_string(&mut self, s: SwfStr<'_>) -> io::Result<()> { self.get_inner().write_all(s.as_bytes())?; self.write_u8(0) } @@ -306,8 +309,8 @@ impl SwfWrite for Writer { self.output.write_f64::(n) } - fn write_c_string(&mut self, s: &str) -> io::Result<()> { - self.get_inner().write_all(s.as_bytes())?; + fn write_string(&mut self, s: SwfStr<'_>) -> io::Result<()> { + self.output.write_all(s.as_bytes())?; self.write_u8(0) } } @@ -534,11 +537,11 @@ impl Writer { Tag::ExportAssets(ref exports) => self.write_export_assets(&exports[..])?, - Tag::Protect(ref password) => { - if let Some(ref password_md5) = *password { + Tag::Protect(password) => { + if let Some(password_md5) = password { self.write_tag_header(TagCode::Protect, password_md5.len() as u32 + 3)?; self.write_u16(0)?; // Two null bytes? Not specified in SWF19. - self.write_c_string(password_md5)?; + self.write_string(password_md5)?; } else { self.write_tag_header(TagCode::Protect, 0)?; } @@ -782,14 +785,14 @@ impl Writer { Tag::DefineFontName { id, - ref name, - ref copyright_info, + name, + copyright_info, } => { let len = name.len() + copyright_info.len() + 4; self.write_tag_header(TagCode::DefineFontName, len as u32)?; self.write_character_id(id)?; - self.write_c_string(name)?; - self.write_c_string(copyright_info)?; + self.write_string(name)?; + self.write_string(copyright_info)?; } Tag::DefineMorphShape(ref define_morph_shape) => { @@ -819,7 +822,7 @@ impl Writer { let len = do_abc.data.len() + do_abc.name.len() + 5; self.write_tag_header(TagCode::DoAbc, len as u32)?; self.write_u32(if do_abc.is_lazy_initialize { 1 } else { 0 })?; - self.write_c_string(&do_abc.name)?; + self.write_string(do_abc.name)?; self.output.write_all(&do_abc.data)?; } Tag::DoAction(ref action_data) => { @@ -835,7 +838,7 @@ impl Writer { self.output.write_all(action_data)?; } - Tag::EnableDebugger(ref password_md5) => { + Tag::EnableDebugger(password_md5) => { let len = password_md5.len() as u32 + 1; if self.version >= 6 { // SWF v6+ uses EnableDebugger2 tag. @@ -845,7 +848,7 @@ impl Writer { self.write_tag_header(TagCode::EnableDebugger, len)?; } - self.write_c_string(password_md5)?; + self.write_string(password_md5)?; } Tag::EnableTelemetry { ref password_hash } => { @@ -861,10 +864,7 @@ impl Writer { Tag::End => self.write_tag_header(TagCode::End, 0)?, - Tag::ImportAssets { - ref url, - ref imports, - } => { + Tag::ImportAssets { url, ref imports } => { let len = imports.iter().map(|e| e.name.len() as u32 + 3).sum::() + url.len() as u32 + 1 @@ -872,17 +872,17 @@ impl Writer { // SWF v8 and later use ImportAssets2 tag. if self.version >= 8 { self.write_tag_header(TagCode::ImportAssets2, len + 2)?; - self.write_c_string(url)?; + self.write_string(url)?; self.write_u8(1)?; self.write_u8(0)?; } else { self.write_tag_header(TagCode::ImportAssets, len)?; - self.write_c_string(url)?; + self.write_string(url)?; } self.write_u16(imports.len() as u16)?; - for &ExportedAsset { id, ref name } in imports { + for &ExportedAsset { id, name } in imports { self.write_u16(id)?; - self.write_c_string(name)?; + self.write_string(name)?; } } @@ -891,9 +891,9 @@ impl Writer { self.output.write_all(data)?; } - Tag::Metadata(ref metadata) => { + Tag::Metadata(metadata) => { self.write_tag_header(TagCode::Metadata, metadata.len() as u32 + 1)?; - self.write_c_string(metadata)?; + self.write_string(metadata)?; } // TODO: Allow clone of color. @@ -969,7 +969,7 @@ impl Writer { } Tag::StartSound2 { - ref class_name, + class_name, ref sound_info, } => { let length = class_name.len() as u32 @@ -987,7 +987,7 @@ impl Writer { 0 }; self.write_tag_header(TagCode::StartSound2, length)?; - self.write_c_string(class_name)?; + self.write_string(class_name)?; self.write_sound_info(sound_info)?; } @@ -999,9 +999,9 @@ impl Writer { + 2; self.write_tag_header(TagCode::SymbolClass, len)?; self.write_u16(symbols.len() as u16)?; - for &SymbolClassLink { id, ref class_name } in symbols { + for &SymbolClassLink { id, class_name } in symbols { self.write_u16(id)?; - self.write_c_string(class_name)?; + self.write_string(class_name)?; } } @@ -1033,15 +1033,12 @@ impl Writer { self.write_u32(flags)?; } - Tag::FrameLabel(FrameLabel { - ref label, - is_anchor, - }) => { + Tag::FrameLabel(FrameLabel { label, is_anchor }) => { // TODO: Assert proper version let is_anchor = is_anchor && self.version >= 6; let length = label.len() as u32 + if is_anchor { 2 } else { 1 }; self.write_tag_header(TagCode::FrameLabel, length)?; - self.write_c_string(label)?; + self.write_string(label)?; if is_anchor { self.write_u8(1)?; } @@ -1506,12 +1503,12 @@ impl Writer { writer.write_encoded_u32(data.scenes.len() as u32)?; for scene in &data.scenes { writer.write_encoded_u32(scene.frame_num)?; - writer.write_c_string(&scene.label)?; + writer.write_string(scene.label)?; } writer.write_encoded_u32(data.frame_labels.len() as u32)?; for frame_label in &data.frame_labels { writer.write_encoded_u32(frame_label.frame_num)?; - writer.write_c_string(&frame_label.label)?; + writer.write_string(frame_label.label)?; } } self.write_tag_header(TagCode::DefineSceneAndFrameLabelData, buf.len() as u32)?; @@ -1596,9 +1593,9 @@ impl Writer { + 2; self.write_tag_header(TagCode::ExportAssets, len)?; self.write_u16(exports.len() as u16)?; - for &ExportedAsset { id, ref name } in exports { + for &ExportedAsset { id, name } in exports { self.write_u16(id)?; - self.write_c_string(name)?; + self.write_string(name)?; } Ok(()) } @@ -2035,8 +2032,8 @@ impl Writer { writer.write_u16(place_object.depth)?; if place_object_version >= 3 { - if let Some(ref class_name) = place_object.class_name { - writer.write_c_string(class_name)?; + if let Some(class_name) = place_object.class_name { + writer.write_string(class_name)?; } } @@ -2054,8 +2051,8 @@ impl Writer { if let Some(ratio) = place_object.ratio { writer.write_u16(ratio)?; } - if let Some(ref name) = place_object.name { - writer.write_c_string(name)?; + if let Some(name) = place_object.name { + writer.write_string(name)?; }; if let Some(clip_depth) = place_object.clip_depth { writer.write_u16(clip_depth)?; @@ -2521,7 +2518,7 @@ impl Writer { | if font.is_italic { 0b10 } else { 0 } | if font.is_bold { 0b1 } else { 0 }, )?; - self.write_c_string(&font.name)?; + self.write_string(font.name)?; if let Some(ref data) = font.data { self.output.write_all(data)?; } @@ -2647,8 +2644,8 @@ impl Writer { } // TODO(Herschel): Check SWF version. - if let Some(ref class) = edit_text.font_class_name { - writer.write_c_string(class)?; + if let Some(class) = edit_text.font_class_name { + writer.write_string(class)?; } // TODO(Herschel): Height only exists iff HasFontId, maybe for HasFontClass too? @@ -2677,9 +2674,9 @@ impl Writer { writer.write_i16(layout.leading.get() as i16)?; } - writer.write_c_string(&edit_text.variable_name)?; - if let Some(ref text) = edit_text.initial_text { - writer.write_c_string(text)?; + writer.write_string(edit_text.variable_name)?; + if let Some(text) = edit_text.initial_text { + writer.write_string(text)?; } } @@ -2993,7 +2990,7 @@ mod tests { { // TODO: What if I use a cursor instead of buf ? let mut writer = Writer::new(&mut buf, 1); - writer.write_c_string("Hello!").unwrap(); + writer.write_string("Hello!".into()).unwrap(); } assert_eq!(buf, "Hello!\0".bytes().collect::>()); } @@ -3003,7 +3000,7 @@ mod tests { { // TODO: What if I use a cursor instead of buf ? let mut writer = Writer::new(&mut buf, 1); - writer.write_c_string("😀😂!🐼").unwrap(); + writer.write_string("😀😂!🐼".into()).unwrap(); } assert_eq!(buf, "😀😂!🐼\0".bytes().collect::>()); }