Skip to content

Commit 934cb05

Browse files
authored
avm2: implement string.replace(...) with fn, for now regex only. (ruffle-rs#7429)
* avm2: implement string.replace(...) with fn, for now regex only. * string - added path for replacing regex with fn (replacing string with fn is still unimplemented) * regex - factored out common replace logic for when replacement is a string and when it is a function * added tests * Addressed review comments * removed tinkering cruft; formatting * addressed review comments
1 parent 2ffded7 commit 934cb05

File tree

5 files changed

+104
-14
lines changed

5 files changed

+104
-14
lines changed

core/src/avm2/globals/string.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -291,22 +291,25 @@ fn replace<'gc>(
291291
let pattern = args.get(0).unwrap_or(&Value::Undefined);
292292
let replacement = args.get(1).unwrap_or(&Value::Undefined);
293293

294-
if replacement
295-
.as_object()
296-
.and_then(|o| o.as_function_object())
297-
.is_some()
298-
{
299-
log::warn!("string.replace(_, function) - not implemented");
300-
return Err("NotImplemented".into());
294+
if let Some(f) = replacement.as_object().and_then(|o| o.as_function_object()) {
295+
if let Some(mut regexp) = pattern
296+
.as_object()
297+
.as_ref()
298+
.and_then(|o| o.as_regexp_mut(activation.context.gc_context))
299+
{
300+
return Ok(regexp.replace_fn(activation, this, &f)?.into());
301+
} else {
302+
log::warn!("string.replace(string, function) - not implemented");
303+
return Err("NotImplemented".into());
304+
}
301305
}
302306
let replacement = replacement.coerce_to_string(activation)?;
303-
304307
if let Some(mut regexp) = pattern
305308
.as_object()
306309
.as_ref()
307310
.and_then(|o| o.as_regexp_mut(activation.context.gc_context))
308311
{
309-
return Ok(regexp.replace(activation, this, replacement).into());
312+
return Ok(regexp.replace_string(activation, this, replacement)?.into());
310313
} else {
311314
let pattern = pattern.coerce_to_string(activation)?;
312315
if let Some(position) = this.find(&pattern) {

core/src/avm2/regexp.rs

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
use std::borrow::Cow;
44

55
use crate::avm2::activation::Activation;
6+
use crate::avm2::object::FunctionObject;
7+
use crate::avm2::object::TObject;
68
use crate::avm2::Error;
79
use crate::avm2::{ArrayObject, ArrayStorage, Object};
810
use crate::string::WString;
@@ -148,6 +150,8 @@ impl<'gc> RegExp<'gc> {
148150
}
149151
}
150152

153+
/// Helper for replace_string. Evaluates the special $-sequences
154+
/// in `replacement`.
151155
fn effective_replacement(
152156
replacement: &AvmString<'gc>,
153157
text: &AvmString<'gc>,
@@ -200,17 +204,65 @@ impl<'gc> RegExp<'gc> {
200204
ret
201205
}
202206

203-
pub fn replace(
207+
/// Implements string.replace(regex, replacement) where the replacement is
208+
/// a function.
209+
pub fn replace_fn(
210+
&mut self,
211+
activation: &mut Activation<'_, 'gc, '_>,
212+
text: AvmString<'gc>,
213+
f: &FunctionObject<'gc>,
214+
) -> Result<AvmString<'gc>, Error> {
215+
self.replace_with_fn(activation, &text, |activation, txt, m| {
216+
let args = std::iter::once(Some(&m.range))
217+
.chain((m.captures.iter()).map(|x| x.as_ref()))
218+
.map(|o| match o {
219+
Some(r) => {
220+
AvmString::new(activation.context.gc_context, &txt[r.start..r.end]).into()
221+
}
222+
None => "".into(),
223+
})
224+
.chain(std::iter::once(m.range.start.into()))
225+
.chain(std::iter::once((*txt).into()))
226+
.collect::<Vec<_>>();
227+
let r = f.call(activation.global_scope(), &args, activation)?;
228+
return Ok(WString::from(r.coerce_to_string(activation)?.as_wstr()));
229+
})
230+
}
231+
232+
/// Implements string.replace(regex, replacement) where the replacement is
233+
/// a string with $-sequences.
234+
pub fn replace_string(
204235
&mut self,
205236
activation: &mut Activation<'_, 'gc, '_>,
206237
text: AvmString<'gc>,
207238
replacement: AvmString<'gc>,
208-
) -> AvmString<'gc> {
239+
) -> Result<AvmString<'gc>, Error> {
240+
self.replace_with_fn(activation, &text, |_activation, txt, m| {
241+
Ok(Self::effective_replacement(&replacement, txt, m))
242+
})
243+
}
244+
245+
// Helper for replace_string and replace_function.
246+
//
247+
// Replaces occurrences of regex with results of f(activation, &text, &match)
248+
fn replace_with_fn<F>(
249+
&mut self,
250+
activation: &mut Activation<'_, 'gc, '_>,
251+
text: &AvmString<'gc>,
252+
mut f: F,
253+
) -> Result<AvmString<'gc>, Error>
254+
where
255+
F: FnMut(
256+
&mut Activation<'_, 'gc, '_>,
257+
&AvmString<'gc>,
258+
&regress::Match,
259+
) -> Result<WString, Error>,
260+
{
209261
let mut ret = WString::new();
210262
let mut start = 0;
211-
while let Some(m) = self.find_utf16_match(text, start) {
263+
while let Some(m) = self.find_utf16_match(*text, start) {
212264
ret.push_str(&text[start..m.range.start]);
213-
ret.push_str(&Self::effective_replacement(&replacement, &text, &m));
265+
ret.push_str(&f(activation, &text, &m)?);
214266

215267
start = m.range.end;
216268

@@ -228,7 +280,7 @@ impl<'gc> RegExp<'gc> {
228280
}
229281

230282
ret.push_str(&text[start..]);
231-
AvmString::new(activation.context.gc_context, ret)
283+
Ok(AvmString::new(activation.context.gc_context, ret))
232284
}
233285

234286
pub fn split(

tests/tests/swfs/avm2/string_replace/Test.as

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,33 @@ trace("// Two-digit capture group number")
6060
var r=RegExp("(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)")
6161
trace("abbbbbbbbb#bbc".replace(r, "<$10>"))
6262

63+
trace("// replace function")
6364

65+
function replFN():String {
66+
return "foo";
67+
}
68+
69+
70+
trace("abbbb".replace(/a/,replFN))
71+
72+
trace("// replace with functions returning non-string values")
73+
74+
function replFn2() {
75+
return 2;
76+
}
77+
78+
function replFn3() {
79+
}
80+
81+
trace("abbbb".replace(/a/,replFn2))
82+
trace("abbbb".replace(/a/,replFn3))
83+
84+
trace("// replace a regex with function, check arguments")
85+
86+
// relies on implicit coercion to string
87+
function rFN() {
88+
return arguments;
89+
}
6490

91+
// The (b) and (c) groups have no matches.
92+
trace("<<a>>".replace(/(a)(b)?|(c)/, rFN))

tests/tests/swfs/avm2/string_replace/output.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,10 @@ a<$2>c
3131
a<b0>c
3232
// Two-digit capture group number
3333
<b>#bbc
34+
// replace function
35+
foobbbb
36+
// replace with functions returning non-string values
37+
2bbbb
38+
undefinedbbbb
39+
// replace a regex with function, check arguments
40+
<<a,a,,,2,<<a>>>>
230 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)