cfsolve 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +10 -9
  4. data/UPSTREAM.md +6 -6
  5. data/lib/cfsolve/profiles/safari-26-ios/answers.json +2562 -1887
  6. data/lib/cfsolve/profiles/safari-26-ios/challenge-page-answers.json +1873 -1198
  7. data/lib/cfsolve/profiles/safari-26-ios.json +0 -8
  8. data/lib/cfsolve/version.rb +2 -2
  9. data/vendor/cfsolve/README.md +0 -35
  10. data/vendor/cfsolve/examples/build_profile.rs +17 -15
  11. data/vendor/cfsolve/examples/capture_session.rs +37 -0
  12. data/vendor/cfsolve/examples/check_answers.rs +66 -0
  13. data/vendor/cfsolve/examples/compare_session.rs +190 -0
  14. data/vendor/cfsolve/examples/stage2_snapshot.rs +4 -1
  15. data/vendor/cfsolve/profiles/safari-26-ios/answers.json +2562 -1887
  16. data/vendor/cfsolve/profiles/safari-26-ios/challenge-page-answers.json +1873 -1198
  17. data/vendor/cfsolve/profiles/safari-26-ios.json +0 -8
  18. data/vendor/cfsolve/scripts/capture_cssom.mjs +42 -0
  19. data/vendor/cfsolve/scripts/serve_cssom.py +111 -0
  20. data/vendor/cfsolve/src/capture/profile.rs +7 -18
  21. data/vendor/cfsolve/src/capture/recorded.rs +42 -7
  22. data/vendor/cfsolve/src/challenge/installed/audit.rs +131 -0
  23. data/vendor/cfsolve/src/challenge/installed.rs +122 -271
  24. data/vendor/cfsolve/src/challenge/payload.rs +107 -5
  25. data/vendor/cfsolve/src/challenge/state.rs +98 -16
  26. data/vendor/cfsolve/src/deobfuscate/encoded_strings.rs +3 -2
  27. data/vendor/cfsolve/src/deobfuscate/helper_objects.rs +17 -32
  28. data/vendor/cfsolve/src/deobfuscate/register_arrays.rs +18 -5
  29. data/vendor/cfsolve/src/embedding_page/extra_params.rs +0 -14
  30. data/vendor/cfsolve/src/embedding_page/page.rs +28 -2
  31. data/vendor/cfsolve/src/http/client.rs +11 -0
  32. data/vendor/cfsolve/src/js/ast.rs +12 -3
  33. data/vendor/cfsolve/src/js/objects.rs +9 -2
  34. data/vendor/cfsolve/src/listing/fold.rs +0 -83
  35. data/vendor/cfsolve/src/listing/query.rs +25 -244
  36. data/vendor/cfsolve/src/listing.rs +0 -1
  37. data/vendor/cfsolve/src/session/initial.rs +14 -5
  38. data/vendor/cfsolve/src/session/probes.rs +160 -125
  39. data/vendor/cfsolve/src/session/timing.rs +58 -53
  40. data/vendor/cfsolve/src/session/widget.rs +5 -6
  41. data/vendor/cfsolve/src/stage2/analysis/analyze.rs +50 -33
  42. data/vendor/cfsolve/src/stage2/analysis/discovery.rs +61 -66
  43. data/vendor/cfsolve/src/stage2/analysis/envelope.rs +31 -21
  44. data/vendor/cfsolve/src/stage2/analysis/recognize.rs +3 -5
  45. data/vendor/cfsolve/src/stage2/analysis.rs +0 -1
  46. data/vendor/cfsolve/src/stage2/audit_globals.rs +310 -0
  47. data/vendor/cfsolve/src/stage2/benchmark.rs +5 -8
  48. data/vendor/cfsolve/src/stage2/call_stack.rs +72 -16
  49. data/vendor/cfsolve/src/stage2/interaction/native.rs +2 -1
  50. data/vendor/cfsolve/src/stage2/interaction/plan.rs +0 -3
  51. data/vendor/cfsolve/src/stage2/payload/recover.rs +54 -23
  52. data/vendor/cfsolve/src/stage2/probes/ice.rs +11 -10
  53. data/vendor/cfsolve/src/stage2/probes/media.rs +47 -33
  54. data/vendor/cfsolve/src/stage2/probes/performance.rs +1 -0
  55. data/vendor/cfsolve/src/stage2/probes/rects.rs +12 -17
  56. data/vendor/cfsolve/src/stage2/probes/stack.rs +50 -103
  57. data/vendor/cfsolve/src/stage2/probes/websocket_echo.rs +17 -86
  58. data/vendor/cfsolve/src/stage2/probes.rs +0 -1
  59. data/vendor/cfsolve/src/stage2/records.rs +4 -13
  60. data/vendor/cfsolve/src/stage2/reference.json +1 -1
  61. data/vendor/cfsolve/src/stage2/reference.rs +98 -100
  62. data/vendor/cfsolve/src/stage2/registry.rs +41 -34
  63. data/vendor/cfsolve/src/stage2/replay.rs +182 -254
  64. data/vendor/cfsolve/src/stage2/stylesheets.rs +176 -0
  65. data/vendor/cfsolve/src/stage2/trace.rs +18 -0
  66. data/vendor/cfsolve/src/stage2/vocabulary.rs +111 -401
  67. data/vendor/cfsolve/src/stage2.rs +2 -1
  68. data/vendor/cfsolve/src/vm/disassemble.rs +39 -10
  69. data/vendor/cfsolve/src/vm/interpret/access.rs +5 -21
  70. data/vendor/cfsolve/src/vm/interpret/builtin.rs +6 -53
  71. data/vendor/cfsolve/src/vm/interpret/call.rs +86 -9
  72. data/vendor/cfsolve/src/vm/interpret/compile.rs +626 -0
  73. data/vendor/cfsolve/src/vm/interpret/copies.rs +232 -120
  74. data/vendor/cfsolve/src/vm/interpret/evaluate/tests.rs +96 -8
  75. data/vendor/cfsolve/src/vm/interpret/evaluate.rs +219 -92
  76. data/vendor/cfsolve/src/vm/interpret/explore.rs +155 -39
  77. data/vendor/cfsolve/src/vm/interpret/frozen.rs +171 -0
  78. data/vendor/cfsolve/src/vm/interpret/intrinsic.rs +350 -0
  79. data/vendor/cfsolve/src/vm/interpret/plan.rs +71 -44
  80. data/vendor/cfsolve/src/vm/interpret/run.rs +202 -13
  81. data/vendor/cfsolve/src/vm/interpret.rs +7 -2
  82. data/vendor/cfsolve/src/vm/machine.rs +6 -2
  83. data/vendor/cfsolve/src/vm/packed_names.rs +307 -0
  84. data/vendor/cfsolve/src/vm/session.rs +36 -14
  85. data/vendor/cfsolve/src/vm/value.rs +13 -0
  86. data/vendor/cfsolve/src/vm.rs +1 -0
  87. data/vendor/cfsolve/vendor/btls-sys/src/lib.rs +3 -0
  88. metadata +13 -6
  89. data/vendor/cfsolve/src/listing/normalize.rs +0 -264
  90. data/vendor/cfsolve/src/stage2/analysis/outputs.rs +0 -327
  91. data/vendor/cfsolve/src/stage2/probes/stylesheets.rs +0 -249
  92. data/vendor/cfsolve/src/stage2/reference-expanded.json +0 -42
  93. data/vendor/cfsolve/src/stage2/session_names.rs +0 -470
@@ -2,6 +2,7 @@
2
2
  //! journal that undoes speculative attempts.
3
3
 
4
4
  use std::borrow::Cow;
5
+ use std::cell::RefCell;
5
6
  use std::hash::Hash;
6
7
  use std::mem;
7
8
  use std::rc::Rc;
@@ -10,12 +11,13 @@ use swc_core::atoms::Atom;
10
11
  use swc_core::ecma::ast::{BinaryOp, UnaryOp};
11
12
 
12
13
  use super::builtin;
13
- use super::copies::{self, Copies};
14
+ use super::copies::{self, Copies, Fork};
14
15
  use super::explore::Invocation;
16
+ use super::frozen::{self, Frozen};
15
17
  use crate::vm::output::{Constant, Effect, Exit, Path, Position, Term};
16
18
  use crate::vm::session::Session;
17
19
  use crate::vm::value::{Callable, Env, Scope, Value};
18
- use crate::vm::{HashMap, convert};
20
+ use crate::vm::{HashMap, convert, disassemble};
19
21
 
20
22
  /// Why evaluation stopped before completing normally.
21
23
  pub(crate) enum Stop<'ast> {
@@ -51,6 +53,8 @@ pub(crate) struct Run<'ast, 'session> {
51
53
  pub(super) program: &'session [u8],
52
54
  /// Instance fields derived from program bytes by the constructor, once known.
53
55
  constructed: Option<&'session HashMap<String, Value<'ast>>>,
56
+ /// Strings decrypted from the program's records, once a program is loaded.
57
+ pub(super) decrypted: Option<&'session Decrypted<'ast>>,
54
58
  pub(super) registers: HashMap<u8, Value<'ast>>,
55
59
  /// Tags written before their register values; see [`crate::vm::registers`].
56
60
  pub(super) tags: HashMap<u8, Value<'ast>>,
@@ -76,6 +80,23 @@ pub(crate) struct Run<'ast, 'session> {
76
80
  facts: Vec<(Term, Value<'ast>)>,
77
81
  journal: Journal<'ast>,
78
82
  pub(super) spare: Spare<'ast>,
83
+ /// Whether the run shares [`Frozen`] session objects instead of copying them.
84
+ pub(super) sharing: bool,
85
+ /// Whether the run wrote to a shared object, thawing it. Its results are discarded.
86
+ pub(super) thawed: bool,
87
+ }
88
+
89
+ /// Strings decrypted from a program's string records, by record contents (see
90
+ /// [`RecordKey`]).
91
+ pub(crate) type Decrypted<'ast> = RefCell<HashMap<RecordKey, Value<'ast>>>;
92
+
93
+ /// The contents of a string record after the program: numbers and arrays of numbers, as bits.
94
+ pub(crate) type RecordKey = Vec<RecordPart>;
95
+
96
+ #[derive(Clone, PartialEq, Eq, Hash)]
97
+ pub(crate) enum RecordPart {
98
+ Number(u64),
99
+ Numbers(Vec<u64>),
79
100
  }
80
101
 
81
102
  /// Cleared buffers from completed calls. Handler runs take them from the session and
@@ -222,6 +243,7 @@ impl<'ast, 'session> Run<'ast, 'session> {
222
243
  session,
223
244
  program,
224
245
  constructed: None,
246
+ decrypted: None,
225
247
  registers: HashMap::default(),
226
248
  tags: HashMap::default(),
227
249
  fields: HashMap::default(),
@@ -240,21 +262,15 @@ impl<'ast, 'session> Run<'ast, 'session> {
240
262
  facts: Vec::new(),
241
263
  journal: Journal::default(),
242
264
  spare: Spare::default(),
265
+ sharing: false,
266
+ thawed: false,
243
267
  }
244
268
  }
245
269
 
246
270
  /// A handler run that follows `decisions`, then takes any further symbolic branch as true.
247
271
  pub(super) fn handler(invocation: &Invocation<'session, 'ast>, decisions: Vec<bool>) -> Self {
248
- let session = invocation.session;
249
- let mut run = Self::new(
250
- session,
251
- invocation.program,
252
- decisions,
253
- Some(Copies::default()),
254
- );
255
- run.constructed = Some(invocation.fields);
256
- run.spare = session.spare.take();
257
- for (register, value) in &session.initial {
272
+ let mut run = Self::invoked(invocation, decisions, Some(Copies::default()));
273
+ for (register, value) in &invocation.session.initial {
258
274
  let value = run.isolate(value);
259
275
  run.registers.insert(*register, value);
260
276
  }
@@ -263,6 +279,20 @@ impl<'ast, 'session> Run<'ast, 'session> {
263
279
  run
264
280
  }
265
281
 
282
+ /// A run of `invocation`'s handler with no registers set, sharing frozen session objects.
283
+ fn invoked(
284
+ invocation: &Invocation<'session, 'ast>,
285
+ decisions: Vec<bool>,
286
+ copies: Option<Copies<'ast>>,
287
+ ) -> Self {
288
+ let mut run = Self::new(invocation.session, invocation.program, decisions, copies);
289
+ run.constructed = Some(invocation.fields);
290
+ run.decrypted = Some(invocation.decrypted);
291
+ run.spare = invocation.session.spare.take();
292
+ run.sharing = true;
293
+ run
294
+ }
295
+
266
296
  /// Sets the program counter to `pc` and the dispatch key to `key`.
267
297
  pub(super) fn start_at(&mut self, pc: u32, key: u8) {
268
298
  let layout = self.session.layout;
@@ -311,6 +341,19 @@ impl<'ast, 'session> Run<'ast, 'session> {
311
341
  },
312
342
  },
313
343
  };
344
+ // A catch handler can be pushed onto a constructor-created register stack.
345
+ // Native array operations mutate that stack without producing an effect; retain
346
+ // its frame so disassembly can also visit the exception continuation.
347
+ for (register, _) in &self.session.initial {
348
+ if let Some(value @ Value::Array(elements)) = self.registers.get(register)
349
+ && elements.borrow().iter().any(|element| {
350
+ matches!(element.expr(), Term::Array(ref frame)
351
+ if disassemble::catch_frame(frame).is_some())
352
+ })
353
+ {
354
+ self.effects.push(Effect::Set(*register, value.expr()));
355
+ }
356
+ }
314
357
  // The listing retains every path, so release unused vector capacity.
315
358
  let mut conditions = self.branches.conditions;
316
359
  conditions.shrink_to_fit();
@@ -578,10 +621,156 @@ impl<'ast, 'session> Run<'ast, 'session> {
578
621
 
579
622
  /// Copies a session value for use by this run, preserving run-local sharing.
580
623
  pub(super) fn isolate(&mut self, value: &Value<'ast>) -> Value<'ast> {
624
+ let frozen = self.frozen();
581
625
  let Some(copies) = &mut self.copies else {
582
626
  return value.clone();
583
627
  };
584
- copies::copy(value, copies)
628
+ copies::copy(value, copies, frozen)
629
+ }
630
+
631
+ /// The session objects this run shares, if it shares them.
632
+ fn frozen(&self) -> Option<&'session Frozen> {
633
+ self.sharing.then_some(&self.session.frozen)
634
+ }
635
+
636
+ /// Checks that the run may write to `object`. A write to a shared session object thaws its
637
+ /// group and stops the run, whose caller reruns the instruction with the group copied.
638
+ pub(super) fn writable(&mut self, object: &Value<'ast>) -> Evaluation<'ast, ()> {
639
+ if let Some(frozen) = self.frozen()
640
+ && let Some(address) = frozen::address(object)
641
+ && frozen.contains(address)
642
+ {
643
+ frozen.thaw(address);
644
+ self.thawed = true;
645
+ return Err(Stop::unsupported("a write to a shared session object"));
646
+ }
647
+ Ok(())
648
+ }
649
+
650
+ /// Takes a snapshot of a handler run before top-level statement `statement`, with the
651
+ /// handler's `locals`. Returns `None` if the run is inside a speculative attempt or setup
652
+ /// changed which names are unavailable, which a snapshot does not restore.
653
+ pub(super) fn snapshot(
654
+ &self,
655
+ statement: usize,
656
+ locals: &[Option<Value<'ast>>],
657
+ ) -> Option<Snapshot<'ast>> {
658
+ if self.journal.recording()
659
+ || matches!(self.unavailable, Cow::Owned(_))
660
+ || self.frames.len() != 1
661
+ || self.setup.is_some()
662
+ || self.constructed.is_none()
663
+ {
664
+ return None;
665
+ }
666
+ let mut fork = Fork::new(self.frozen());
667
+ let frame = self.frame();
668
+ Some(Snapshot {
669
+ statement,
670
+ decided: self.branches.decisions[..self.branches.decided].to_vec(),
671
+ registers: fork.map(&self.registers),
672
+ tags: fork.map(&self.tags),
673
+ fields: fork.map(&self.fields),
674
+ globals: fork.map(&self.globals),
675
+ copies: self.copies.as_ref().map(|copies| fork.copies(copies)),
676
+ closed: self.closed.iter().map(|scope| fork.scope(scope)).collect(),
677
+ effects: self.effects.clone(),
678
+ temps: self.temps,
679
+ conditions: self.branches.conditions.clone(),
680
+ facts: self
681
+ .facts
682
+ .iter()
683
+ .map(|(term, value)| (term.clone(), fork.value(value)))
684
+ .collect(),
685
+ this: fork.value(&frame.this),
686
+ scope: fork.scope(&frame.scope),
687
+ locals: locals
688
+ .iter()
689
+ .map(|local| local.as_ref().map(|value| fork.value(value)))
690
+ .collect(),
691
+ copied: fork.copied,
692
+ })
693
+ }
694
+
695
+ /// A handler run resumed from `snapshot`, following `decisions`, whose first decisions are
696
+ /// the snapshot's. Returns the run, the handler's locals, and the scopes copied for it,
697
+ /// which the caller clears when the run ends.
698
+ pub(super) fn resume(
699
+ invocation: &Invocation<'session, 'ast>,
700
+ snapshot: &Snapshot<'ast>,
701
+ decisions: Vec<bool>,
702
+ ) -> (Self, Vec<Option<Value<'ast>>>, Vec<Scope<'ast>>) {
703
+ let mut fork = Fork::new(Some(&invocation.session.frozen));
704
+ let copies = snapshot.copies.as_ref().map(|copies| fork.copies(copies));
705
+ let mut run = Self::invoked(invocation, decisions, copies);
706
+ run.registers = fork.map(&snapshot.registers);
707
+ run.tags = fork.map(&snapshot.tags);
708
+ run.fields = fork.map(&snapshot.fields);
709
+ run.globals = fork.map(&snapshot.globals);
710
+ run.closed = snapshot
711
+ .closed
712
+ .iter()
713
+ .map(|scope| fork.scope(scope))
714
+ .collect();
715
+ run.effects = snapshot.effects.clone();
716
+ run.temps = snapshot.temps;
717
+ run.branches.decided = snapshot.decided.len();
718
+ run.branches.conditions = snapshot.conditions.clone();
719
+ run.facts = snapshot
720
+ .facts
721
+ .iter()
722
+ .map(|(term, value)| (term.clone(), fork.value(value)))
723
+ .collect();
724
+ run.frames.push(Frame {
725
+ this: fork.value(&snapshot.this),
726
+ scope: fork.scope(&snapshot.scope),
727
+ });
728
+ let locals = snapshot
729
+ .locals
730
+ .iter()
731
+ .map(|local| local.as_ref().map(|value| fork.value(value)))
732
+ .collect();
733
+ (run, locals, fork.copied)
734
+ }
735
+ }
736
+
737
+ /// A handler run's state before one of the top-level statements of its body. Paths that share
738
+ /// the decisions made before that statement resume here instead of rerunning the statements
739
+ /// before it.
740
+ pub(super) struct Snapshot<'ast> {
741
+ /// The top-level statement to resume at.
742
+ pub(super) statement: usize,
743
+ /// The decisions consumed before the statement.
744
+ decided: Vec<bool>,
745
+ registers: HashMap<u8, Value<'ast>>,
746
+ tags: HashMap<u8, Value<'ast>>,
747
+ fields: HashMap<String, Value<'ast>>,
748
+ globals: HashMap<Atom, Value<'ast>>,
749
+ copies: Option<Copies<'ast>>,
750
+ closed: Vec<Scope<'ast>>,
751
+ effects: Vec<Effect>,
752
+ temps: usize,
753
+ conditions: Vec<(Term, bool)>,
754
+ facts: Vec<(Term, Value<'ast>)>,
755
+ this: Value<'ast>,
756
+ scope: Scope<'ast>,
757
+ locals: Vec<Option<Value<'ast>>>,
758
+ /// Every scope the snapshot holds, cleared when it is dropped to break cycles.
759
+ copied: Vec<Scope<'ast>>,
760
+ }
761
+
762
+ impl Drop for Snapshot<'_> {
763
+ fn drop(&mut self) {
764
+ for scope in &self.copied {
765
+ scope.borrow_mut().variables.clear();
766
+ }
767
+ }
768
+ }
769
+
770
+ impl<'ast> Snapshot<'ast> {
771
+ /// Whether a run following `decisions` makes the same decisions before the statement.
772
+ pub(super) fn resumes(&self, decisions: &[bool]) -> bool {
773
+ decisions.starts_with(&self.decided)
585
774
  }
586
775
  }
587
776
 
@@ -11,16 +11,21 @@
11
11
  mod access;
12
12
  mod builtin;
13
13
  mod call;
14
+ mod compile;
14
15
  mod copies;
15
16
  mod evaluate;
16
17
  mod explore;
18
+ mod frozen;
19
+ mod intrinsic;
17
20
  mod plan;
18
21
  mod run;
19
22
  mod semantics;
20
23
  mod setup;
21
24
 
22
25
  pub(crate) use builtin::Builtin;
23
- pub(crate) use explore::{Invocation, MAXIMUM_PATHS, Way, decode, explore};
26
+ pub(crate) use explore::{Branching, Invocation, MAXIMUM_PATHS, Way, decode, explore};
27
+ pub(crate) use frozen::Frozen;
28
+ pub(crate) use intrinsic::{Intrinsics, intrinsics};
24
29
  pub(crate) use plan::Plans;
25
- pub(crate) use run::{Run, Spare};
30
+ pub(crate) use run::{Decrypted, Run, Spare};
26
31
  pub(crate) use setup::setup;
@@ -6,7 +6,7 @@ use swc_core::ecma::ast::{BlockStmt, Class, Script, Stmt};
6
6
  use super::HashMap;
7
7
  use super::dispatch::{Decoded, Dispatch};
8
8
  use super::extract::{self, Finder};
9
- use super::interpret::{self, Invocation, MAXIMUM_PATHS, Way};
9
+ use super::interpret::{self, Decrypted, Invocation, MAXIMUM_PATHS, Way};
10
10
  use super::output::{Cause, Failure, Instruction, Position};
11
11
  use super::session::Session;
12
12
  use super::value::Value;
@@ -48,11 +48,13 @@ pub struct Machine<'definition> {
48
48
  pub(super) session: Session<'definition>,
49
49
  }
50
50
 
51
- /// A program, with the instance fields the constructor derives from it.
51
+ /// A program, with the instance fields the constructor derives from it and the strings
52
+ /// decrypted from it so far.
52
53
  #[derive(Clone)]
53
54
  pub(super) struct Loaded<'program, 'definition> {
54
55
  program: &'program [u8],
55
56
  pub(super) fields: HashMap<String, Value<'definition>>,
57
+ decrypted: Decrypted<'definition>,
56
58
  }
57
59
 
58
60
  impl<'definition> Machine<'definition> {
@@ -69,6 +71,7 @@ impl<'definition> Machine<'definition> {
69
71
  Loaded {
70
72
  program,
71
73
  fields: self.session.constructed(program),
74
+ decrypted: Decrypted::default(),
72
75
  }
73
76
  }
74
77
 
@@ -144,6 +147,7 @@ impl<'definition> Machine<'definition> {
144
147
  session: &self.session,
145
148
  program: loaded.program,
146
149
  fields: &loaded.fields,
150
+ decrypted: &loaded.decrypted,
147
151
  pc: pc + 1,
148
152
  key: next,
149
153
  handler,
@@ -0,0 +1,307 @@
1
+ //! Folds names the program unpacks at run time. Some programs store a name as
2
+ //! `"\0\x19" + name + NUL padding` and unpack it with
3
+ //! `text.slice(2, 256 * text.charCodeAt(0) + text.charCodeAt(1) + 2)`. Folding `charCodeAt`,
4
+ //! `slice`, `+` and `*` on constant registers rewrites each unpacking call to the resulting
5
+ //! string, so readers of the listing see an ordinary string load.
6
+
7
+ use std::collections::{BTreeMap, HashMap, HashSet};
8
+ use std::convert::Infallible;
9
+ use std::ops::ControlFlow;
10
+ use std::rc::Rc;
11
+
12
+ use swc_core::ecma::ast::BinaryOp;
13
+
14
+ use super::output::{Constant, Effect, Listing, Path, Term};
15
+ use super::{dataflow, disassemble};
16
+
17
+ /// Constant registers on entry to an instruction, shared as the dataflow copies them per path.
18
+ type Registers = BTreeMap<u8, Rc<Constant>>;
19
+
20
+ /// Constants held by temporaries.
21
+ type Temporaries = HashMap<usize, Rc<Constant>>;
22
+
23
+ pub(super) fn fold(listing: &mut Listing) {
24
+ // Unrecorded entries, such as functions and hidden targets, start with nothing known.
25
+ let functions = listing.functions.iter().map(|function| function.pc);
26
+ let hidden = listing.hidden.iter().map(|hidden| hidden.target.pc);
27
+ let incoming = forward(listing, functions.chain(hidden));
28
+ // A catch path must not erase facts already recovered on the normal path.
29
+ // Fold its exclusive instructions separately with unknown entry registers.
30
+ let exceptional = forward(listing, disassemble::exception_entries(listing));
31
+ for (pc, instruction) in &mut listing.instructions {
32
+ let Some(entry) = incoming.get(pc).or_else(|| exceptional.get(pc)) else {
33
+ continue;
34
+ };
35
+ for path in &mut instruction.paths {
36
+ rewrite(path, entry.clone());
37
+ }
38
+ }
39
+ }
40
+
41
+ /// The constant registers on entry to each instruction reachable from `entries`.
42
+ fn forward(listing: &Listing, entries: impl IntoIterator<Item = u32>) -> BTreeMap<u32, Registers> {
43
+ let entries = entries.into_iter().map(|pc| (pc, Registers::new()));
44
+ let ControlFlow::Continue(incoming) = dataflow::forward(listing, entries, |_, path, known| {
45
+ let mut temporaries = Temporaries::new();
46
+ for effect in &path.effects {
47
+ step(effect, known, &mut temporaries);
48
+ }
49
+ ControlFlow::<Infallible, bool>::Continue(true)
50
+ });
51
+ incoming
52
+ }
53
+
54
+ /// Rewrites folded string-method results in `path` to constants.
55
+ fn rewrite(path: &mut Path, mut known: Registers) {
56
+ let mut temporaries = Temporaries::new();
57
+ // Temporaries holding a folded call.
58
+ let mut folded_temporaries = HashSet::new();
59
+ for effect in &mut path.effects {
60
+ let folded = match &*effect {
61
+ // Only `charCodeAt` and `slice` calls fold.
62
+ Effect::Let(_, term @ Term::Call { .. }) | Effect::Set(_, term @ Term::Call { .. }) => {
63
+ value(term, &known, &temporaries)
64
+ }
65
+ Effect::Set(_, Term::Temp(temporary)) if folded_temporaries.contains(temporary) => {
66
+ temporaries.get(temporary).cloned()
67
+ }
68
+ _ => None,
69
+ };
70
+ step(effect, &mut known, &mut temporaries);
71
+ if let Effect::Let(temporary, _) = effect {
72
+ if folded.is_some() {
73
+ folded_temporaries.insert(*temporary);
74
+ } else {
75
+ folded_temporaries.remove(temporary);
76
+ }
77
+ }
78
+ if let (Some(constant), Effect::Let(_, term) | Effect::Set(_, term)) = (folded, effect) {
79
+ *term = Term::Const(Rc::unwrap_or_clone(constant));
80
+ }
81
+ }
82
+ }
83
+
84
+ /// Records the constant an effect writes, if any. As in the other listing folds, calls leave the
85
+ /// caller's registers alone.
86
+ fn step(effect: &Effect, known: &mut Registers, temporaries: &mut Temporaries) {
87
+ match effect {
88
+ Effect::Let(temporary, term) => match value(term, known, temporaries) {
89
+ Some(constant) => temporaries.insert(*temporary, constant),
90
+ None => temporaries.remove(temporary),
91
+ },
92
+ Effect::Set(register, term) => match value(term, known, temporaries) {
93
+ Some(constant) => known.insert(*register, constant),
94
+ None => known.remove(register),
95
+ },
96
+ _ => None,
97
+ };
98
+ }
99
+
100
+ /// The string or number `term` evaluates to, if constant.
101
+ fn value(term: &Term, known: &Registers, temporaries: &Temporaries) -> Option<Rc<Constant>> {
102
+ let value = |term| value(term, known, temporaries);
103
+ let number = |term| match *value(term)? {
104
+ Constant::Number(number) => Some(number),
105
+ _ => None,
106
+ };
107
+ let number_constant = |number| Some(Rc::new(Constant::Number(number)));
108
+ match term {
109
+ Term::Const(constant @ (Constant::String(_) | Constant::Number(_))) => {
110
+ Some(Rc::new(constant.clone()))
111
+ }
112
+ Term::Reg(register) => known.get(register).cloned(),
113
+ Term::Temp(temporary) => temporaries.get(temporary).cloned(),
114
+ Term::Binary(BinaryOp::Add, left, right) => number_constant(number(left)? + number(right)?),
115
+ Term::Binary(BinaryOp::Mul, left, right) => number_constant(number(left)? * number(right)?),
116
+ Term::Call {
117
+ callee,
118
+ this: None,
119
+ arguments,
120
+ } => {
121
+ let Term::Get(receiver, method) = &**callee else {
122
+ return None;
123
+ };
124
+ let receiver = value(receiver)?;
125
+ let (Constant::String(text), Term::Const(Constant::String(method))) =
126
+ (&*receiver, &**method)
127
+ else {
128
+ return None;
129
+ };
130
+ let text: Vec<u16> = text.encode_utf16().collect();
131
+ // Negative and fractional indexes never occur in the unpacking.
132
+ let index = |term| {
133
+ let index = number(term)?;
134
+ (index >= 0.0 && index.fract() == 0.0).then_some(index as usize)
135
+ };
136
+ match (method.as_str(), arguments.as_slice()) {
137
+ ("charCodeAt", [position]) => {
138
+ number_constant(f64::from(*text.get(index(position)?)?))
139
+ }
140
+ ("slice", [start, end]) => {
141
+ let end = index(end)?.min(text.len());
142
+ let start = index(start)?.min(end);
143
+ Some(Rc::new(Constant::String(
144
+ String::from_utf16(&text[start..end]).ok()?,
145
+ )))
146
+ }
147
+ _ => None,
148
+ }
149
+ }
150
+ _ => None,
151
+ }
152
+ }
153
+
154
+ #[cfg(test)]
155
+ mod tests {
156
+ use super::*;
157
+ use crate::vm::output::{Exit, Instruction, Position};
158
+
159
+ fn string(text: &str) -> Term {
160
+ Term::Const(Constant::String(text.into()))
161
+ }
162
+
163
+ fn number(value: f64) -> Term {
164
+ Term::Const(Constant::Number(value))
165
+ }
166
+
167
+ fn method(receiver: u8, name: &str, arguments: Vec<Term>) -> Term {
168
+ Term::Call {
169
+ callee: Box::new(Term::Get(
170
+ Box::new(Term::Reg(receiver)),
171
+ Box::new(string(name)),
172
+ )),
173
+ this: None,
174
+ arguments,
175
+ }
176
+ }
177
+
178
+ fn binary(op: BinaryOp, left: u8, right: u8) -> Term {
179
+ Term::Binary(op, Box::new(Term::Reg(left)), Box::new(Term::Reg(right)))
180
+ }
181
+
182
+ fn insert(listing: &mut Listing, pc: u32, effects: Vec<Effect>, exit: Exit) {
183
+ let path = Path {
184
+ conditions: Vec::new(),
185
+ effects,
186
+ exit,
187
+ };
188
+ let instruction = Instruction {
189
+ pc,
190
+ key: 0,
191
+ opcode: 0,
192
+ paths: vec![path],
193
+ };
194
+ listing.instructions.insert(pc, instruction);
195
+ }
196
+
197
+ #[test]
198
+ fn folds_unpacked_names_to_string_loads() {
199
+ // r1 = packed; r2 = 0; r3 = 1; r4 = 2; r5 = 256;
200
+ // r6 = r1.charCodeAt(r2); r2 = r1.charCodeAt(r3); r6 = r6 * r5 + r2 + r4;
201
+ // r1 = r1.slice(r4, r6); payload[r1] = 0
202
+ let effects = vec![
203
+ vec![Effect::Set(1, string("\0\u{3}abc\0\0\0\0"))],
204
+ vec![
205
+ Effect::Set(2, number(0.0)),
206
+ Effect::Set(3, number(1.0)),
207
+ Effect::Set(4, number(2.0)),
208
+ Effect::Set(5, number(256.0)),
209
+ ],
210
+ vec![
211
+ Effect::Let(0, method(1, "charCodeAt", vec![Term::Reg(2)])),
212
+ Effect::Set(6, Term::Temp(0)),
213
+ ],
214
+ vec![
215
+ Effect::Let(0, method(1, "charCodeAt", vec![Term::Reg(3)])),
216
+ Effect::Set(2, Term::Temp(0)),
217
+ ],
218
+ vec![Effect::Set(6, binary(BinaryOp::Mul, 6, 5))],
219
+ vec![Effect::Set(6, binary(BinaryOp::Add, 6, 2))],
220
+ vec![Effect::Set(6, binary(BinaryOp::Add, 6, 4))],
221
+ vec![
222
+ Effect::Let(0, method(1, "slice", vec![Term::Reg(4), Term::Reg(6)])),
223
+ Effect::Set(1, Term::Temp(0)),
224
+ ],
225
+ ];
226
+ let count = u32::try_from(effects.len()).unwrap();
227
+ let mut listing = Listing {
228
+ functions: vec![Position::new(0, 0)],
229
+ ..Listing::default()
230
+ };
231
+ for (pc, effects) in (0..).zip(effects) {
232
+ let exit = if pc + 1 == count {
233
+ Exit::Return
234
+ } else {
235
+ Exit::Next { pc: pc + 1, key: 0 }
236
+ };
237
+ insert(&mut listing, pc, effects, exit);
238
+ }
239
+ fold(&mut listing);
240
+ let last = &listing.instructions[&(count - 1)].paths[0].effects;
241
+ assert_eq!(
242
+ last,
243
+ &[Effect::Let(0, string("abc")), Effect::Set(1, string("abc"))]
244
+ );
245
+ let first_code = &listing.instructions[&2].paths[0].effects;
246
+ assert_eq!(
247
+ first_code,
248
+ &[Effect::Let(0, number(0.0)), Effect::Set(6, number(0.0))]
249
+ );
250
+ }
251
+
252
+ #[test]
253
+ fn catch_names_fold_without_erasing_normal_path_names() {
254
+ let mut listing = Listing {
255
+ functions: vec![Position::new(0, 0)],
256
+ ..Listing::default()
257
+ };
258
+ insert(
259
+ &mut listing,
260
+ 0,
261
+ vec![
262
+ Effect::Set(1, string("\0\u{3}abc\0")),
263
+ Effect::Set(2, number(2.0)),
264
+ Effect::Set(3, number(5.0)),
265
+ Effect::Set(
266
+ 9,
267
+ Term::Array(vec![Term::Array(vec![
268
+ number(100.0),
269
+ number(0.0),
270
+ Term::Cells,
271
+ number(0.0),
272
+ ])]),
273
+ ),
274
+ ],
275
+ Exit::Next { pc: 1, key: 0 },
276
+ );
277
+ insert(
278
+ &mut listing,
279
+ 1,
280
+ vec![Effect::Set(
281
+ 1,
282
+ method(1, "slice", vec![Term::Reg(2), Term::Reg(3)]),
283
+ )],
284
+ Exit::Return,
285
+ );
286
+ insert(
287
+ &mut listing,
288
+ 100,
289
+ vec![
290
+ Effect::Set(1, string("\0\u{3}xyz\0")),
291
+ Effect::Set(2, number(2.0)),
292
+ Effect::Set(3, number(5.0)),
293
+ Effect::Set(4, method(1, "slice", vec![Term::Reg(2), Term::Reg(3)])),
294
+ ],
295
+ Exit::Next { pc: 1, key: 0 },
296
+ );
297
+ fold(&mut listing);
298
+ assert_eq!(
299
+ listing.instructions[&1].paths[0].effects[0],
300
+ Effect::Set(1, string("abc"))
301
+ );
302
+ assert_eq!(
303
+ listing.instructions[&100].paths[0].effects[3],
304
+ Effect::Set(4, string("xyz"))
305
+ );
306
+ }
307
+ }