avm1_emitter/
lib.rs

1mod patchable_buf_writer;
2mod primitives;
3
4use crate::patchable_buf_writer::{BufferHole, PatchableBufWriter};
5use crate::primitives::{emit_c_string, emit_le32_f64, emit_le_f32, emit_le_i16, emit_le_i32, emit_le_u16, emit_u8};
6use avm1_types::cfg::CfgLabel;
7use avm1_types::raw;
8use avm1_types::raw::FromCfgActionError;
9use avm1_types::{cfg, ActionHeader, CatchTarget, GetUrl2Method, PushValue};
10use std::collections::HashMap;
11use std::convert::{TryFrom, TryInto};
12use std::io;
13use std::io::Write;
14
15pub fn emit_cfg(value: &cfg::Cfg) -> io::Result<Vec<u8>> {
16  let mut avm1_writer = PatchableBufWriter::new();
17  write_cfg(&mut avm1_writer, value)?;
18  Ok(avm1_writer.complete())
19}
20
21fn write_cfg(writer: &mut PatchableBufWriter, value: &cfg::Cfg) -> io::Result<()> {
22  write_hard_cfg(writer, value, true)
23}
24
25/// Returns `x: i16` such that `source + x == target`, checking for range
26fn offset_delta_i16(source: usize, target: usize) -> Option<i16> {
27  if target >= source {
28    // x = target - source  ∈ [0, 2¹⁵ -1]
29    i16::try_from(target - source).ok()
30  } else {
31    // target < source
32    // x ∈ [1, usize::MAX]
33    let x: usize = source - target;
34    // x = (source - target - 1)  ∈ [0, usize::MAX - 1]
35    let x: usize = x - 1;
36    // x = (source - target - 1)  ∈ [0, 2¹⁵ - 1]
37    // (assuming usize::MAX >= 2¹⁵, true since usize::MAX >= u16::MAX in Rust)
38    let x = i16::try_from(x).ok()?;
39    // x = -(source - target - 1) = target - source + 1  ∈ [-2¹⁵ + 1, 0]
40    // Unchecked wrapping negation is OK because we know the actual range: it will never wrap
41    let x = x.wrapping_neg();
42    // x = target - source  ∈ [-2¹⁵, -1]
43    let x = x - 1;
44    Some(x)
45  }
46}
47
48fn write_hard_cfg(writer: &mut PatchableBufWriter, value: &cfg::Cfg, append_end_action: bool) -> io::Result<()> {
49  let wi: WriteInfo = write_soft_cfg(writer, value, None)?;
50  let end_offset = writer.len();
51  if append_end_action {
52    write_raw_action(writer, &raw::Action::End)?;
53  }
54
55  for (offset, (hole, target_label)) in wi.jumps.into_iter() {
56    let target_offset: usize = match target_label.as_ref() {
57      Some(cfg_label) => wi.blocks.get(cfg_label).cloned().expect("TargetLabelNotFound"),
58      None => end_offset,
59    };
60    let offset = offset + 2; // Size of the offset itself inside `If` and `Jump` actions
61    let delta = offset_delta_i16(offset, target_offset).expect("TargetOffsetOutOfReach");
62    hole.patch(writer, delta);
63  }
64
65  Ok(())
66}
67
68struct WriteInfo {
69  jumps: HashMap<usize, (BufferHole<i16>, Option<cfg::CfgLabel>)>,
70  blocks: HashMap<cfg::CfgLabel, usize>,
71}
72
73impl WriteInfo {
74  pub fn new() -> Self {
75    Self {
76      jumps: HashMap::new(),
77      blocks: HashMap::new(),
78    }
79  }
80
81  pub fn extend(&mut self, wi: Self) {
82    self.jumps.extend(wi.jumps.into_iter());
83    self.blocks.extend(wi.blocks.into_iter());
84  }
85}
86
87fn write_soft_cfg(
88  writer: &mut PatchableBufWriter,
89  value: &cfg::Cfg,
90  fallthrough_next: Option<&cfg::CfgLabel>,
91) -> io::Result<WriteInfo> {
92  let mut res = WriteInfo::new();
93
94  for (i, block) in value.blocks.iter().enumerate() {
95    let cur_next: Option<&cfg::CfgLabel> = match value.blocks.get(i + 1) {
96      Some(x) => Some(&x.label),
97      None => fallthrough_next,
98    };
99    let wi: WriteInfo = write_block(writer, block, cur_next)?;
100    res.extend(wi);
101  }
102
103  Ok(res)
104}
105
106fn write_block(
107  writer: &mut PatchableBufWriter,
108  value: &cfg::CfgBlock,
109  fallthrough_next: Option<&cfg::CfgLabel>,
110) -> io::Result<WriteInfo> {
111  let mut res = WriteInfo::new();
112
113  res.blocks.insert(value.label.clone(), writer.len());
114
115  for action in value.actions.iter().cloned() {
116    match raw::Action::try_from(action) {
117      Ok(raw) => write_raw_action(writer, &raw)?,
118      Err(FromCfgActionError::DefineFunction(action)) => write_define_function(writer, &action)?,
119      Err(FromCfgActionError::DefineFunction2(action)) => write_define_function2(writer, &action)?,
120    }
121  }
122
123  match &value.flow {
124    cfg::CfgFlow::Error(_) => write_error(writer)?,
125    cfg::CfgFlow::If(ref flow) => {
126      let (offset, hole) = write_if(writer)?;
127      res.jumps.insert(offset, (hole, flow.true_target.clone()));
128      if fallthrough_next != flow.false_target.as_ref() {
129        if let Some(false_target) = flow.false_target.as_ref() {
130          let (offset, hole) = write_jump(writer)?;
131          res.jumps.insert(offset, (hole, Some(false_target.clone())));
132        } else {
133          write_raw_action(writer, &raw::Action::End)?;
134        }
135      }
136    }
137    cfg::CfgFlow::Simple(ref flow) => {
138      if fallthrough_next != flow.next.as_ref() {
139        if let Some(next) = flow.next.as_ref() {
140          let (offset, hole) = write_jump(writer)?;
141          res.jumps.insert(offset, (hole, Some(next.clone())));
142        } else {
143          write_raw_action(writer, &raw::Action::End)?;
144        }
145      }
146    }
147    cfg::CfgFlow::Return => write_raw_action(writer, &raw::Action::Return)?,
148    cfg::CfgFlow::Throw => write_raw_action(writer, &raw::Action::Throw)?,
149    cfg::CfgFlow::Try(ref flow) => {
150      write_try(writer, &mut res, flow, fallthrough_next)?;
151    }
152    cfg::CfgFlow::WaitForFrame(ref flow) => {
153      write_raw_action(
154        writer,
155        &raw::Action::WaitForFrame(raw::WaitForFrame {
156          frame: flow.frame,
157          skip: 1,
158        }),
159      )?;
160      {
161        let (offset, hole) = write_jump(writer)?;
162        res.jumps.insert(offset, (hole, flow.ready_target.clone()));
163      }
164      {
165        let (offset, hole) = write_jump(writer)?;
166        res.jumps.insert(offset, (hole, flow.loading_target.clone()));
167      }
168    }
169    cfg::CfgFlow::WaitForFrame2(ref flow) => {
170      write_raw_action(writer, &raw::Action::WaitForFrame2(raw::WaitForFrame2 { skip: 1 }))?;
171      {
172        let (offset, hole) = write_jump(writer)?;
173        res.jumps.insert(offset, (hole, flow.ready_target.clone()));
174      }
175      {
176        let (offset, hole) = write_jump(writer)?;
177        res.jumps.insert(offset, (hole, flow.loading_target.clone()));
178      }
179    }
180    cfg::CfgFlow::With(ref flow) => {
181      write_with(writer, &mut res, flow, fallthrough_next)?;
182    }
183  }
184
185  Ok(res)
186}
187
188pub fn emit_raw_action(value: &raw::Action) -> io::Result<Vec<u8>> {
189  let mut writer = PatchableBufWriter::new();
190  write_raw_action(&mut writer, value)?;
191  Ok(writer.complete())
192}
193
194fn write_raw_action(writer: &mut PatchableBufWriter, value: &raw::Action) -> io::Result<()> {
195  macro_rules! raw {
196    ($c: literal) => {{
197      emit_u8(writer, $c)?;
198      if $c >= 0x80 {
199        emit_le_u16(writer, 0)?;
200      };
201      Ok(())
202    }};
203    ($c: literal, $f: ident, $a: expr) => {{
204      debug_assert!($c >= 0x80);
205      emit_u8(writer, $c)?;
206      let hole = writer.write_hole_le_u16();
207      let body_start = writer.len();
208      $f(writer, $a)?;
209      let body_end = writer.len();
210      let body_len = body_end - body_start;
211      hole.patch(writer, body_len.try_into().unwrap());
212      Ok(())
213    }};
214  }
215
216  use raw::Action::*;
217
218  match value {
219    Add => raw!(0x0a),
220    Add2 => raw!(0x47),
221    And => raw!(0x10),
222    AsciiToChar => raw!(0x33),
223    BitAnd => raw!(0x60),
224    BitLShift => raw!(0x63),
225    BitOr => raw!(0x61),
226    BitRShift => raw!(0x64),
227    BitURShift => raw!(0x65),
228    BitXor => raw!(0x62),
229    Call => raw!(0x9e),
230    CallFunction => raw!(0x3d),
231    CallMethod => raw!(0x52),
232    CastOp => raw!(0x2b),
233    CharToAscii => raw!(0x32),
234    CloneSprite => raw!(0x24),
235    ConstantPool(ref a) => raw!(0x88, write_raw_constant_pool, a),
236    Decrement => raw!(0x51),
237    DefineFunction(ref a) => raw!(0x9b, write_raw_define_function, a),
238    DefineFunction2(ref a) => raw!(0x8e, write_raw_define_function2, a),
239    DefineLocal => raw!(0x3c),
240    DefineLocal2 => raw!(0x41),
241    Delete => raw!(0x3a),
242    Delete2 => raw!(0x3b),
243    Divide => raw!(0x0d),
244    End => raw!(0x00),
245    EndDrag => raw!(0x28),
246    Enumerate => raw!(0x46),
247    Enumerate2 => raw!(0x55),
248    Equals => raw!(0x0e),
249    Equals2 => raw!(0x49),
250    Error(_) => todo!(),
251    Extends => raw!(0x69),
252    FsCommand2 => raw!(0x2d),
253    GetMember => raw!(0x4e),
254    GetProperty => raw!(0x22),
255    GetTime => raw!(0x34),
256    GetUrl(ref a) => raw!(0x83, write_raw_get_url, a),
257    GetUrl2(ref a) => raw!(0x9a, write_raw_get_url2, a),
258    GetVariable => raw!(0x1c),
259    GotoFrame(ref a) => raw!(0x81, write_raw_goto_frame, a),
260    GotoFrame2(ref a) => raw!(0x9f, write_raw_goto_frame2, a),
261    GotoLabel(ref a) => raw!(0x8c, write_raw_goto_label, a),
262    Greater => raw!(0x67),
263    If(ref a) => raw!(0x9d, write_raw_if, a),
264    ImplementsOp => raw!(0x2c),
265    Increment => raw!(0x50),
266    InitArray => raw!(0x42),
267    InitObject => raw!(0x43),
268    InstanceOf => raw!(0x54),
269    Jump(ref a) => raw!(0x99, write_raw_jump, a),
270    Less => raw!(0x0f),
271    Less2 => raw!(0x48),
272    MbAsciiToChar => raw!(0x37),
273    MbCharToAscii => raw!(0x36),
274    MbStringExtract => raw!(0x35),
275    MbStringLength => raw!(0x31),
276    Modulo => raw!(0x3f),
277    Multiply => raw!(0x0c),
278    NewMethod => raw!(0x53),
279    NewObject => raw!(0x40),
280    NextFrame => raw!(0x04),
281    Not => raw!(0x12),
282    Or => raw!(0x11),
283    Play => raw!(0x06),
284    Pop => raw!(0x17),
285    PrevFrame => raw!(0x05),
286    Push(ref a) => raw!(0x96, write_raw_push, a),
287    PushDuplicate => raw!(0x4c),
288    RandomNumber => raw!(0x30),
289    Raw(ref a) => {
290      emit_u8(writer, a.code)?;
291      if a.code < 0x80 {
292        assert!(a.data.is_empty());
293      } else {
294        let body_len = u16::try_from(a.data.len()).unwrap();
295        emit_le_u16(writer, body_len)?;
296        writer.write_all(&a.data)?;
297      }
298      Ok(())
299    }
300    Return => raw!(0x3e),
301    RemoveSprite => raw!(0x25),
302    SetMember => raw!(0x4f),
303    SetProperty => raw!(0x23),
304    SetTarget(ref a) => raw!(0x8b, write_raw_set_target, a),
305    SetTarget2 => raw!(0x20),
306    SetVariable => raw!(0x1d),
307    StackSwap => raw!(0x4d),
308    StartDrag => raw!(0x27),
309    Stop => raw!(0x07),
310    StopSounds => raw!(0x09),
311    StoreRegister(ref a) => raw!(0x87, write_raw_store_register, a),
312    StrictEquals => raw!(0x66),
313    StrictMode(ref a) => raw!(0x89, write_raw_strict_mode, a),
314    StringAdd => raw!(0x21),
315    StringEquals => raw!(0x13),
316    StringExtract => raw!(0x15),
317    StringGreater => raw!(0x68),
318    StringLength => raw!(0x14),
319    StringLess => raw!(0x29),
320    Subtract => raw!(0x0b),
321    TargetPath => raw!(0x45),
322    Throw => raw!(0x2a),
323    ToInteger => raw!(0x18),
324    ToNumber => raw!(0x4a),
325    ToString => raw!(0x4b),
326    ToggleQuality => raw!(0x08),
327    Trace => raw!(0x26),
328    Try(ref a) => raw!(0x8f, write_raw_try, a),
329    TypeOf => raw!(0x44),
330    WaitForFrame(ref a) => raw!(0x8a, write_raw_wait_for_frame, a),
331    WaitForFrame2(ref a) => raw!(0x8d, write_raw_wait_for_frame2, a),
332    With(ref a) => raw!(0x94, write_raw_with, a),
333  }
334}
335
336fn write_raw_constant_pool<W: io::Write>(writer: &mut W, value: &raw::ConstantPool) -> io::Result<()> {
337  emit_le_u16(writer, value.pool.len().try_into().unwrap())?;
338  for constant in value.pool.iter() {
339    emit_c_string(writer, constant)?;
340  }
341  Ok(())
342}
343
344fn write_raw_define_function<W: io::Write>(writer: &mut W, value: &raw::DefineFunction) -> io::Result<()> {
345  emit_c_string(writer, &value.name)?;
346  emit_le_u16(writer, value.parameters.len().try_into().unwrap())?;
347  for parameter in value.parameters.iter() {
348    emit_c_string(writer, parameter)?;
349  }
350  emit_le_u16(writer, value.body_size)
351}
352
353fn write_raw_define_function2<W: io::Write>(writer: &mut W, value: &raw::DefineFunction2) -> io::Result<()> {
354  emit_c_string(writer, &value.name)?;
355  emit_le_u16(writer, value.parameters.len().try_into().unwrap())?;
356  emit_u8(writer, value.register_count)?;
357
358  let flags: u16 = value.flags.bits();
359  emit_le_u16(writer, flags)?;
360
361  for parameter in value.parameters.iter() {
362    emit_u8(writer, parameter.register)?;
363    emit_c_string(writer, &parameter.name)?;
364  }
365  emit_le_u16(writer, value.body_size)
366}
367
368fn write_raw_get_url<W: io::Write>(writer: &mut W, value: &raw::GetUrl) -> io::Result<()> {
369  emit_c_string(writer, &value.url)?;
370  emit_c_string(writer, &value.target)
371}
372
373fn write_raw_get_url2<W: io::Write>(writer: &mut W, value: &raw::GetUrl2) -> io::Result<()> {
374  let method_code: u8 = match value.method {
375    GetUrl2Method::None => 0,
376    GetUrl2Method::Get => 1,
377    GetUrl2Method::Post => 2,
378  };
379  #[allow(clippy::identity_op)]
380  let flags: u8 = 0
381    | (if value.load_variables { 1 << 0 } else { 0 })
382    | (if value.load_target { 1 << 1 } else { 0 })
383    // Skip bits [2, 5]
384    | (method_code << 6);
385  emit_u8(writer, flags)
386}
387
388fn write_raw_goto_frame<W: io::Write>(writer: &mut W, value: &raw::GotoFrame) -> io::Result<()> {
389  emit_le_u16(writer, value.frame)
390}
391
392fn write_raw_goto_frame2<W: io::Write>(writer: &mut W, value: &raw::GotoFrame2) -> io::Result<()> {
393  let has_scene_bias = value.scene_bias != 0;
394  #[allow(clippy::identity_op)]
395  let flags: u8 = 0
396    // TODO: Find a better way than this comment to prevent rustfmt from changing the layout of this assignment
397    | (if value.play { 1 << 0 } else { 0 })
398    | (if has_scene_bias { 1 << 1 } else { 0 });
399  // Skip bits [2, 7]
400  emit_u8(writer, flags)?;
401  if has_scene_bias {
402    emit_le_u16(writer, value.scene_bias)?;
403  }
404  Ok(())
405}
406
407fn write_raw_goto_label<W: io::Write>(writer: &mut W, value: &raw::GoToLabel) -> io::Result<()> {
408  emit_c_string(writer, &value.label)
409}
410
411fn write_raw_if<W: io::Write>(writer: &mut W, value: &raw::If) -> io::Result<()> {
412  emit_le_i16(writer, value.offset)
413}
414
415fn write_raw_jump<W: io::Write>(writer: &mut W, value: &raw::Jump) -> io::Result<()> {
416  emit_le_i16(writer, value.offset)
417}
418
419fn write_raw_push<W: io::Write>(writer: &mut W, value: &raw::Push) -> io::Result<()> {
420  for pushed in value.values.iter() {
421    match pushed {
422      PushValue::Boolean(v) => {
423        emit_u8(writer, 5)?;
424        emit_u8(writer, if *v { 1 } else { 0 })?;
425      }
426      PushValue::Constant(v) => match u8::try_from(*v) {
427        Ok(v) => {
428          emit_u8(writer, 8)?;
429          emit_u8(writer, v)?;
430        }
431        Err(_) => {
432          emit_u8(writer, 9)?;
433          emit_le_u16(writer, *v)?;
434        }
435      },
436      PushValue::String(v) => {
437        emit_u8(writer, 0)?;
438        emit_c_string(writer, v)?;
439      }
440      PushValue::Sint32(v) => {
441        emit_u8(writer, 7)?;
442        emit_le_i32(writer, *v)?;
443      }
444      PushValue::Float32(v) => {
445        emit_u8(writer, 1)?;
446        emit_le_f32(writer, *v)?;
447      }
448      PushValue::Float64(v) => {
449        emit_u8(writer, 6)?;
450        emit_le32_f64(writer, *v)?;
451      }
452      PushValue::Null => {
453        emit_u8(writer, 2)?;
454      }
455      PushValue::Register(v) => {
456        emit_u8(writer, 4)?;
457        emit_u8(writer, *v)?;
458      }
459      PushValue::Undefined => {
460        emit_u8(writer, 3)?;
461      }
462    };
463  }
464  Ok(())
465}
466
467fn write_raw_set_target<W: io::Write>(writer: &mut W, value: &raw::SetTarget) -> io::Result<()> {
468  emit_c_string(writer, &value.target_name)
469}
470
471fn write_raw_store_register<W: io::Write>(writer: &mut W, value: &raw::StoreRegister) -> io::Result<()> {
472  emit_u8(writer, value.register)
473}
474
475fn write_raw_strict_mode<W: io::Write>(writer: &mut W, value: &raw::StrictMode) -> io::Result<()> {
476  emit_u8(writer, if value.is_strict { 1 } else { 0 })
477}
478
479fn write_raw_try<W: io::Write>(writer: &mut W, value: &raw::Try) -> io::Result<()> {
480  let catch_in_register: bool = value
481    .catch
482    .as_ref()
483    .map(|c| matches!(c.target, CatchTarget::Register(_)))
484    .unwrap_or_default();
485  #[allow(clippy::identity_op)]
486  let flags: u8 = 0
487    | (if value.catch.is_some() { 1 << 0 } else { 0 })
488    | (if value.finally.is_some() { 1 << 1 } else { 0 })
489    | (if catch_in_register { 1 << 2 } else { 0 });
490  // Skip bits [3, 7]
491  emit_u8(writer, flags)?;
492
493  emit_le_u16(writer, value.r#try)?;
494  emit_le_u16(writer, value.catch.as_ref().map(|c| c.size).unwrap_or(0))?;
495  emit_le_u16(writer, value.finally.unwrap_or(0))?;
496
497  if let Some(catch_target) = value.catch.as_ref().map(|c| &c.target) {
498    match catch_target {
499      CatchTarget::Register(ct) => emit_u8(writer, *ct)?,
500      CatchTarget::Variable(ct) => emit_c_string(writer, ct)?,
501    };
502  }
503  Ok(())
504}
505
506fn write_raw_wait_for_frame<W: io::Write>(writer: &mut W, value: &raw::WaitForFrame) -> io::Result<()> {
507  emit_le_u16(writer, value.frame)?;
508  emit_u8(writer, value.skip)
509}
510
511fn write_raw_wait_for_frame2<W: io::Write>(writer: &mut W, value: &raw::WaitForFrame2) -> io::Result<()> {
512  emit_u8(writer, value.skip)
513}
514
515fn write_raw_with<W: io::Write>(writer: &mut W, value: &raw::With) -> io::Result<()> {
516  emit_le_u16(writer, value.size)
517}
518
519fn write_action_header<W: io::Write>(writer: &mut W, value: ActionHeader) -> io::Result<()> {
520  emit_u8(writer, value.code)?;
521  if value.length > 0 {
522    emit_le_u16(writer, value.length)?
523  }
524  Ok(())
525}
526
527fn write_define_function(writer: &mut PatchableBufWriter, value: &cfg::DefineFunction) -> io::Result<()> {
528  let mut body: PatchableBufWriter = PatchableBufWriter::new();
529  write_hard_cfg(&mut body, &value.body, false)?;
530  write_raw_action(
531    writer,
532    &raw::Action::DefineFunction(Box::new(raw::DefineFunction {
533      name: value.name.clone(),
534      parameters: value.parameters.clone(),
535      body_size: body.len().try_into().unwrap(),
536    })),
537  )?;
538  writer.write_all(&body.complete())
539}
540
541fn write_define_function2(writer: &mut PatchableBufWriter, value: &cfg::DefineFunction2) -> io::Result<()> {
542  let mut body: PatchableBufWriter = PatchableBufWriter::new();
543  write_hard_cfg(&mut body, &value.body, false)?;
544  write_raw_action(
545    writer,
546    &raw::Action::DefineFunction2(Box::new(raw::DefineFunction2 {
547      name: value.name.clone(),
548      register_count: value.register_count,
549      flags: value.flags,
550      parameters: value.parameters.clone(),
551      body_size: body.len().try_into().unwrap(),
552    })),
553  )?;
554  writer.write_all(&body.complete())
555}
556
557fn write_error<W: io::Write>(writer: &mut W) -> io::Result<()> {
558  emit_u8(writer, 0x96)?; // push
559  emit_le_u16(writer, 0x0001)?; // data length
560  emit_u8(writer, 0xff) // invalid push value type
561}
562
563fn write_if(writer: &mut PatchableBufWriter) -> io::Result<(usize, BufferHole<i16>)> {
564  write_action_header(writer, ActionHeader { code: 0x9d, length: 2 })?;
565  let pos = writer.len();
566  let hole = writer.write_hole_le_i16();
567  Ok((pos, hole))
568}
569
570fn write_jump(writer: &mut PatchableBufWriter) -> io::Result<(usize, BufferHole<i16>)> {
571  write_action_header(writer, ActionHeader { code: 0x99, length: 2 })?;
572  let pos = writer.len();
573  let hole = writer.write_hole_le_i16();
574  Ok((pos, hole))
575}
576
577fn write_try(
578  writer: &mut PatchableBufWriter,
579  wi: &mut WriteInfo,
580  flow: &cfg::Try,
581  fallthrough_next: Option<&CfgLabel>,
582) -> io::Result<()> {
583  emit_u8(writer, 0x8f)?;
584  let action_size_hole = writer.write_hole_le_u16();
585  let action_start = writer.len();
586  let catch_in_register: bool = flow
587    .catch
588    .as_ref()
589    .map(|c| matches!(c.target, CatchTarget::Register(_)))
590    .unwrap_or_default();
591  #[allow(clippy::identity_op)]
592  let flags: u8 = 0
593    | (if flow.catch.is_some() { 1 << 0 } else { 0 })
594    | (if flow.finally.is_some() { 1 << 1 } else { 0 })
595    | (if catch_in_register { 1 << 2 } else { 0 });
596  // Skip bits [3, 7]
597  emit_u8(writer, flags)?;
598
599  let try_size_hole = writer.write_hole_le_u16();
600  let catch_size_hole = writer.write_hole_le_u16();
601  let finally_size_hole = writer.write_hole_le_u16();
602
603  if let Some(catch_target) = flow.catch.as_ref().map(|c| &c.target) {
604    match catch_target {
605      CatchTarget::Register(ct) => emit_u8(writer, *ct)?,
606      CatchTarget::Variable(ct) => emit_c_string(writer, ct)?,
607    };
608  } else {
609    emit_u8(writer, 0)?;
610  }
611  let action_end = writer.len();
612  let action_size = u16::try_from(action_end - action_start).unwrap();
613  action_size_hole.patch(writer, action_size);
614
615  let finally_next = fallthrough_next;
616  let catch_next = flow.finally.as_ref().map(|x| &x.blocks.first().label).or(finally_next);
617  let try_next = flow
618    .catch
619    .as_ref()
620    .map(|x| &x.body.blocks.first().label)
621    .or(finally_next);
622
623  let try_wi = write_soft_cfg(writer, &flow.r#try, try_next)?;
624  wi.extend(try_wi);
625  let try_end = writer.len();
626  let try_size = u16::try_from(try_end - action_end).unwrap();
627  try_size_hole.patch(writer, try_size);
628
629  if let Some(catch) = flow.catch.as_ref() {
630    let catch_wi = write_soft_cfg(writer, &catch.body, catch_next)?;
631    wi.extend(catch_wi);
632  }
633  let catch_end = writer.len();
634  let catch_size = u16::try_from(catch_end - try_end).unwrap();
635  catch_size_hole.patch(writer, catch_size);
636
637  if let Some(finally) = flow.finally.as_ref() {
638    let finally_wi = write_soft_cfg(writer, finally, finally_next)?;
639    wi.extend(finally_wi);
640  }
641  let finally_end = writer.len();
642  let finally_size = u16::try_from(finally_end - catch_end).unwrap();
643  finally_size_hole.patch(writer, finally_size);
644
645  Ok(())
646}
647
648fn write_with(
649  writer: &mut PatchableBufWriter,
650  wi: &mut WriteInfo,
651  flow: &cfg::With,
652  fallthrough_next: Option<&CfgLabel>,
653) -> io::Result<()> {
654  write_action_header(writer, ActionHeader { code: 0x94, length: 2 })?;
655  let with_size_hole = writer.write_hole_le_u16();
656  let body_start = writer.len();
657  let with_wi = write_soft_cfg(writer, &flow.body, fallthrough_next)?;
658  let body_end = writer.len();
659  with_size_hole.patch(writer, u16::try_from(body_end - body_start).unwrap());
660  wi.extend(with_wi);
661  Ok(())
662}
663
664#[cfg(test)]
665mod tests {
666  use super::*;
667  use ::test_generator::test_resources;
668  use avm1_parser::parse_cfg;
669  use avm1_types::cfg::{Cfg, CfgFlow};
670  use std::io::Write;
671  use std::path::Path;
672
673  #[test_resources("../tests/avm1/[!.]*/*/")]
674  fn test_emit_cfg(path: &str) {
675    use serde::Serialize;
676
677    let path: &Path = Path::new(path);
678    let name_parts: Vec<&str> = path
679      .components()
680      .rev()
681      .take(2)
682      .collect::<Vec<_>>()
683      .iter()
684      .rev()
685      .map(|c| c.as_os_str().to_str().unwrap())
686      .collect();
687
688    match name_parts.join("/").as_str() {
689      "avm1-bytes/misaligned-jump" => return,
690      "samples/delta-of-dir" => return,
691      "samples/parse-data-string" => return,
692      "try/try-empty-catch-overlong-finally-err" => return,
693      "try/try-nested-return" => return,
694      "wait-for-frame/homestuck-beta2" => return,
695      "wait-for-frame/ready-increments" => return,
696      "wait-for-frame/ready-jump-increments" => return,
697      "wait-for-frame/wff2-ready-increments" => return,
698      _ => {}
699    }
700
701    let cfg_path = path.join("cfg.json");
702    let cfg_bytes: Vec<u8> = ::std::fs::read(cfg_path).expect("Failed to read input CFG");
703
704    let cfg: Cfg = ::serde_json_v8::from_slice(&cfg_bytes).expect("Failed to parse input CFG");
705
706    let actual_avm1 = emit_cfg(&cfg).expect("Failed to convert CFG to AVM1");
707
708    let actual_avm1_path = path.join("local-main.rs.avm1");
709    ::std::fs::write(actual_avm1_path, &actual_avm1).expect("Failed to write actual AVM1");
710
711    let actual_cfg = parse_cfg(&actual_avm1);
712    let actual_cfg_path = path.join("local-cfg.rs.json");
713    let actual_cfg_file = ::std::fs::File::create(actual_cfg_path).expect("Failed to create actual CFG file");
714    let actual_cfg_writer = ::std::io::BufWriter::new(actual_cfg_file);
715
716    let mut ser = serde_json_v8::Serializer::pretty(actual_cfg_writer);
717    actual_cfg.serialize(&mut ser).expect("Failed to write actual CFG");
718    ser.into_inner().write_all(b"\n").unwrap();
719
720    assert!(
721      hard_cfg_equivalent(&actual_cfg, &cfg),
722      "round-tripped CFG must be equivalent"
723    )
724  }
725
726  /// Perform a DFS on both control flow graphs at the same time, check if both
727  /// traversal go through exactly the same actions.
728  fn hard_cfg_equivalent(left: &Cfg, right: &Cfg) -> bool {
729    let left_labels = get_hard_cfg_labels(left);
730    let right_labels = get_hard_cfg_labels(right);
731    let left_id: HashMap<&CfgLabel, usize> = left_labels.into_iter().enumerate().map(|(i, l)| (l, i)).collect();
732    let right_id: HashMap<&CfgLabel, usize> = right_labels.into_iter().enumerate().map(|(i, l)| (l, i)).collect();
733
734    let label_eq = |left: Option<&CfgLabel>, right: Option<&CfgLabel>| -> bool {
735      match (left, right) {
736        (Some(l), Some(r)) => left_id.get(l) == right_id.get(r),
737        (l, r) => l == r,
738      }
739    };
740
741    soft_cfg_equivalent(left, right, &label_eq)
742  }
743
744  fn soft_cfg_equivalent(
745    left: &Cfg,
746    right: &Cfg,
747    label_eq: &impl Fn(Option<&CfgLabel>, Option<&CfgLabel>) -> bool,
748  ) -> bool {
749    let left_blocks = &left.blocks;
750    let right_blocks = &right.blocks;
751    if left_blocks.len() != right_blocks.len() {
752      return false;
753    }
754    for (left_block, right_block) in left_blocks.iter().zip(right_blocks.iter()) {
755      if left_block.actions.len() != right_block.actions.len() {
756        return false;
757      }
758      if !label_eq(Some(&left_block.label), Some(&right_block.label)) {
759        return false;
760      }
761      for (left_action, right_action) in left_block.actions.iter().zip(right_block.actions.iter()) {
762        if !action_equivalent(left_action, right_action) {
763          return false;
764        }
765      }
766      let flow_eq = match (&left_block.flow, &right_block.flow) {
767        (CfgFlow::If(l), CfgFlow::If(r)) => {
768          label_eq(l.true_target.as_ref(), r.true_target.as_ref())
769            && label_eq(l.false_target.as_ref(), r.false_target.as_ref())
770        }
771        (CfgFlow::Simple(l), CfgFlow::Simple(r)) => label_eq(l.next.as_ref(), r.next.as_ref()),
772        (CfgFlow::Try(l), CfgFlow::Try(r)) => try_equivalent(l, r, label_eq),
773        (CfgFlow::WaitForFrame(l), CfgFlow::WaitForFrame(r)) => {
774          l.frame == r.frame
775            && label_eq(l.ready_target.as_ref(), r.ready_target.as_ref())
776            && label_eq(l.loading_target.as_ref(), r.loading_target.as_ref())
777        }
778        (CfgFlow::WaitForFrame2(l), CfgFlow::WaitForFrame2(r)) => {
779          label_eq(l.ready_target.as_ref(), r.ready_target.as_ref())
780            && label_eq(l.loading_target.as_ref(), r.loading_target.as_ref())
781        }
782        (CfgFlow::With(l), CfgFlow::With(r)) => soft_cfg_equivalent(&l.body, &r.body, label_eq),
783        (l, r) => l == r,
784      };
785      if !flow_eq {
786        return false;
787      }
788    }
789
790    true
791  }
792
793  fn action_equivalent(left: &cfg::Action, right: &cfg::Action) -> bool {
794    match (left, right) {
795      (cfg::Action::DefineFunction(l), cfg::Action::DefineFunction(r)) => {
796        l.name == r.name && l.parameters == r.parameters && hard_cfg_equivalent(&l.body, &r.body)
797      }
798      (cfg::Action::DefineFunction2(l), cfg::Action::DefineFunction2(r)) => {
799        l.name == r.name
800          && l.register_count == r.register_count
801          && l.flags == r.flags
802          && l.parameters == r.parameters
803          && hard_cfg_equivalent(&l.body, &r.body)
804      }
805      (l, r) => l == r,
806    }
807  }
808
809  fn try_equivalent(
810    left: &cfg::Try,
811    right: &cfg::Try,
812    label_eq: &impl Fn(Option<&CfgLabel>, Option<&CfgLabel>) -> bool,
813  ) -> bool {
814    if !soft_cfg_equivalent(&left.r#try, &right.r#try, label_eq) {
815      return false;
816    }
817    let catch_eq = match (left.catch.as_ref(), right.catch.as_ref()) {
818      (Some(l), Some(r)) => l.target == r.target && soft_cfg_equivalent(&l.body, &r.body, label_eq),
819      (l, r) => l == r,
820    };
821    if !catch_eq {
822      return false;
823    }
824    match (left.finally.as_ref(), right.finally.as_ref()) {
825      (Some(l), Some(r)) => soft_cfg_equivalent(l, r, label_eq),
826      (l, r) => l == r,
827    }
828  }
829
830  fn get_hard_cfg_labels<'a>(hard_cfg: &'a Cfg) -> Vec<&'a CfgLabel> {
831    let mut result: Vec<&'a CfgLabel> = Vec::new();
832
833    fn visit<'a>(cfg: &'a Cfg, result: &mut Vec<&'a CfgLabel>) {
834      for block in cfg.blocks.iter() {
835        result.push(&block.label);
836        match &block.flow {
837          CfgFlow::Try(ref flow) => {
838            visit(&flow.r#try, result);
839            if let Some(catch) = &flow.catch {
840              visit(&catch.body, result);
841            }
842            if let Some(finally) = &flow.finally {
843              visit(finally, result);
844            }
845          }
846          CfgFlow::With(ref flow) => {
847            visit(&flow.body, result);
848          }
849          _ => {}
850        }
851      }
852    }
853
854    visit(hard_cfg, &mut result);
855
856    result
857  }
858}