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
@@ -9,14 +9,15 @@ use serde::Serialize;
9
9
  use super::recognize::{ProbeView, Strings};
10
10
  use super::{audit, controller, discovery, envelope};
11
11
  use crate::candidates;
12
- use crate::js::JsValue;
12
+ use crate::js::{JsObject, JsValue};
13
13
  use crate::listing::Index;
14
14
  use crate::script::Source;
15
15
  use crate::stage2::payload::{self, Layout};
16
16
  use crate::stage2::records::{Envelope, Probe, ProbePlan, WriteOrder};
17
- use crate::stage2::registry::{AUDIT_PROBE, Operation};
17
+ use crate::stage2::reference::ISOLATION_FAILURE_FIELDS;
18
+ use crate::stage2::registry::{AUDIT_PROBE, Kind, Operation};
18
19
  use crate::stage2::trace::Trace;
19
- use crate::stage2::vocabulary::Vocabulary;
20
+ use crate::stage2::vocabulary::{self, Vocabulary};
20
21
  use crate::vm::{Failure, Position};
21
22
 
22
23
  /// A recovered probe plan with supporting analysis evidence.
@@ -66,8 +67,8 @@ impl Analysis {
66
67
  function: (evidence.function.pc, evidence.function.key),
67
68
  operation: probe.operation.as_ref(),
68
69
  features: &probe.features,
69
- expanded_features: &probe.expanded_features,
70
70
  writes: &probe.writes,
71
+ replay_literals: &probe.replay_literals,
71
72
  reference: evidence.reference,
72
73
  });
73
74
  Artifact {
@@ -112,8 +113,8 @@ struct ProbeArtifact<'analysis> {
112
113
  function: (u32, u8),
113
114
  operation: Option<&'analysis Operation>,
114
115
  features: &'analysis [String],
115
- expanded_features: &'analysis [String],
116
116
  writes: &'analysis WriteOrder,
117
+ replay_literals: &'analysis JsObject,
117
118
  reference: Option<usize>,
118
119
  }
119
120
 
@@ -122,23 +123,10 @@ pub fn analyze(source: &Source, program: &[u8]) -> Result<Analysis> {
122
123
  // Include `cf-chl-peek` when resolving hashed comparisons. The worker-fetch probe reads
123
124
  // this header but does not store its name as a program string.
124
125
  let machine = source.machine()?;
125
- let mut listing = machine.disassemble_from(program, &[machine.entry()], &["cf-chl-peek"])?;
126
- crate::listing::normalize::constants(&mut listing);
126
+ let listing = machine.disassemble_from(program, &[machine.entry()], &["cf-chl-peek"])?;
127
127
  let index = Index::new(&listing);
128
128
  let (pool_pc, pool) = index.pool()?;
129
129
  let discovery = discovery::discover(&index, pool)?;
130
- let session_features: Vec<&[String]> = discovery
131
- .probes
132
- .iter()
133
- .map(|probe| probe.features.as_slice())
134
- .collect();
135
- let mut vocabulary = Vocabulary::infer(&session_features);
136
- let expanded_features: Vec<&[String]> = discovery
137
- .probes
138
- .iter()
139
- .map(|probe| probe.expanded_features.as_slice())
140
- .collect();
141
- vocabulary.extend_expanded(&expanded_features);
142
130
  let mut wrapper: BTreeSet<String> = discovery
143
131
  .probes
144
132
  .first()
@@ -147,27 +135,29 @@ pub fn analyze(source: &Source, program: &[u8]) -> Result<Analysis> {
147
135
  for probe in &discovery.probes {
148
136
  wrapper.retain(|text| probe.features.contains(text));
149
137
  }
138
+ // Builds with hex names decode program-wide names, such as the record envelope's, in every
139
+ // probe instead of loading them once in the header. Recognizers treat them as header names.
140
+ let hex_names = wrapper.iter().filter(|text| vocabulary::is_hex_name(text));
141
+ let header: BTreeSet<String> = index.header().values().chain(hex_names).cloned().collect();
142
+ let session_features: Vec<&[String]> = discovery
143
+ .probes
144
+ .iter()
145
+ .map(|probe| probe.features.as_slice())
146
+ .collect();
147
+ let mut vocabulary = Vocabulary::infer(&session_features);
150
148
  for (position, raw) in discovery.probes.iter().enumerate() {
151
- if let Some(reference) = vocabulary.partner(position) {
152
- vocabulary.pin(
153
- position,
154
- super::outputs::names(&index, raw.function.pc, reference, &wrapper),
155
- );
156
- }
157
149
  if vocabulary.partner(position) == Some(AUDIT_PROBE)
158
150
  && let Some(names) = audit::names(&index, &raw.writes.fields)
159
151
  {
160
152
  vocabulary.pin(position, names);
161
153
  }
162
154
  }
163
- let mut header: BTreeSet<String> = index.header().values().cloned().collect();
164
- header.extend(envelope::counter(&index, &discovery.creation));
165
155
  let mut probes = Vec::new();
166
156
  let mut evidence = Vec::new();
167
157
  for (position, raw) in discovery.probes.into_iter().enumerate() {
168
158
  let to_reference = |text: &String| vocabulary.to_reference(position, text).to_owned();
169
159
  let features: Vec<String> = raw.features.iter().map(to_reference).collect();
170
- let outputs: BTreeSet<String> = raw.writes.fields.iter().map(to_reference).collect();
160
+ let outputs: BTreeSet<String> = raw.outputs.iter().map(to_reference).collect();
171
161
  let reference = vocabulary.partner(position);
172
162
  // Treat an unrecognized computed probe as a device fact and replay its paired reference
173
163
  // answer.
@@ -184,14 +174,38 @@ pub fn analyze(source: &Source, program: &[u8]) -> Result<Analysis> {
184
174
  strings: Strings::new(&features, &outputs),
185
175
  })
186
176
  .or(reference.map(|reference| Operation::Replay { reference }));
177
+ let mut replay_literals = JsObject::new();
178
+ // The last history result write is its exception branch. The eval callback reports the
179
+ // same failure marker. Both are program literals.
180
+ if reference.and_then(Kind::from_reference) == Some(Kind::Isolation)
181
+ && let Some(marker) = index
182
+ .stores()
183
+ .iter()
184
+ .rev()
185
+ .filter(|store| {
186
+ vocabulary.to_reference(position, &store.key) == ISOLATION_FAILURE_FIELDS[1]
187
+ })
188
+ .find_map(|store| {
189
+ index
190
+ .text(store.pc, &store.value)
191
+ .filter(|text| vocabulary::is_hex_name(text))
192
+ })
193
+ {
194
+ for name in ISOLATION_FAILURE_FIELDS {
195
+ replay_literals.insert(
196
+ vocabulary.to_session(position, name).to_owned(),
197
+ JsValue::String(marker.clone()),
198
+ );
199
+ }
200
+ }
187
201
  probes.push(Probe {
188
202
  index: raw.index,
189
203
  token: raw.token,
190
204
  input: raw.input,
191
205
  operation,
192
206
  features: raw.features,
193
- expanded_features: raw.expanded_features,
194
207
  writes: raw.writes,
208
+ replay_literals,
195
209
  });
196
210
  evidence.push(ProbeEvidence {
197
211
  states: raw.states,
@@ -228,12 +242,15 @@ pub fn analyze(source: &Source, program: &[u8]) -> Result<Analysis> {
228
242
  })
229
243
  }
230
244
 
231
- /// Finds the frame left by the probe immediately before the uniquely paired environment audit.
232
- /// If the audit runs first, retains the iframe's root frames.
245
+ /// The early audit runs in the worker-navigator continuation if that probe has finished.
246
+ /// Synchronous probes between the worker and the audit do not replace that continuation.
247
+ /// Without a preceding worker, the iframe callback remains the controller frame.
233
248
  fn controller_frame(index: &Index<'_>, evidence: &[ProbeEvidence], push: &str) -> Option<String> {
234
249
  let audits =
235
250
  (0..evidence.len()).filter(|&position| evidence[position].reference == Some(AUDIT_PROBE));
236
251
  let position = candidates::exactly_one(audits, "environment audit").ok()?;
237
- let preceding = evidence.get(position.checked_sub(1)?)?;
238
- controller::controller_trace(index, push, preceding.function.pc)
252
+ let worker = evidence[..position].iter().rev().find(|probe| {
253
+ probe.reference.and_then(Kind::from_reference) == Some(Kind::WorkerNavigator)
254
+ })?;
255
+ controller::controller_trace(index, push, worker.function.pc)
239
256
  }
@@ -40,7 +40,8 @@ pub(super) struct RawProbe {
40
40
  pub function: Position,
41
41
  /// Collection-function strings in order, including duplicates.
42
42
  pub features: Vec<String>,
43
- pub expanded_features: Vec<String>,
43
+ /// The names it stores into registers or properties.
44
+ pub outputs: BTreeSet<String>,
44
45
  pub writes: WriteOrder,
45
46
  }
46
47
 
@@ -128,8 +129,7 @@ pub(super) fn discover(index: &Index<'_>, pool: &str) -> Result<ProbeDiscovery>
128
129
  _ => None,
129
130
  })
130
131
  .context("probe has no collection function")?;
131
- let (features, expanded_features) =
132
- collection_strings(index, function.pc, &dispatcher, pool);
132
+ let (features, outputs) = collection_strings(index, function.pc, &dispatcher);
133
133
  traced.push(trace_writes(index, function.pc, &dispatcher));
134
134
  probes.push(RawProbe {
135
135
  index: index_one,
@@ -138,7 +138,7 @@ pub(super) fn discover(index: &Index<'_>, pool: &str) -> Result<ProbeDiscovery>
138
138
  input,
139
139
  function,
140
140
  features,
141
- expanded_features,
141
+ outputs,
142
142
  writes: WriteOrder::default(),
143
143
  });
144
144
  }
@@ -244,23 +244,10 @@ fn collection_strings(
244
244
  index: &Index<'_>,
245
245
  entry: u32,
246
246
  dispatcher: &RangeInclusive<u32>,
247
- pool: &str,
248
- ) -> (Vec<String>, Vec<String>) {
247
+ ) -> (Vec<String>, BTreeSet<String>) {
249
248
  let mut strings: BTreeMap<u32, Vec<String>> = BTreeMap::new();
250
- let mut expanded: BTreeMap<u32, Vec<String>> = BTreeMap::new();
249
+ let mut outputs = BTreeSet::new();
251
250
  let outside = |pc: u32| !dispatcher.contains(&pc);
252
- // Keep the original feature sequence stable as newly recovered exception paths add
253
- // evidence. The expanded sequence includes those paths for cross-build name recovery.
254
- let ordinary = query::walk(
255
- index.listing(),
256
- entry,
257
- outside,
258
- |pc, instruction, pending| {
259
- pending.extend(instruction.exits());
260
- pending.extend(index.closures_in(pc..=pc));
261
- pending.extend(index.hidden_at(pc).iter().map(|hidden| hidden.target.pc));
262
- },
263
- );
264
251
  query::walk(
265
252
  index.listing(),
266
253
  entry,
@@ -269,62 +256,64 @@ fn collection_strings(
269
256
  pending.extend(instruction.exits());
270
257
  for path in &instruction.paths {
271
258
  let mut at_pc = Vec::new();
272
- let mut expanded_at_pc = Vec::new();
273
259
  for effect in &path.effects {
274
- effect.visit_terms(&mut |term| {
275
- if let Some(text) = index
276
- .text(pc, term)
277
- .filter(|text| text.len() <= FEATURE_LENGTH)
278
- {
279
- let decoded = (text.starts_with('\0') || text.starts_with("ÿÿ"))
280
- .then(|| listing::decode_string(&text, pool).ok())
281
- .flatten()
282
- .filter(|decoded| crate::stage2::vocabulary::is_name(decoded));
283
- let text = decoded.unwrap_or(text);
284
- if !expanded_at_pc.contains(&text) {
285
- expanded_at_pc.push(text);
286
- }
287
- }
288
- });
260
+ let output = match effect {
261
+ Effect::Set(_, term) => Some(term),
262
+ Effect::SetProperty { key, .. } => Some(&**key),
263
+ _ => None,
264
+ };
265
+ if let Some(Term::Const(Constant::String(name))) = output {
266
+ outputs.insert(name.clone());
267
+ }
289
268
  effect.visit_terms(&mut |term| match term {
290
269
  Term::Const(Constant::String(text)) if text.len() <= FEATURE_LENGTH => {
291
- let decoded = (text.starts_with('\0') || text.starts_with("ÿÿ"))
292
- .then(|| listing::decode_string(text, pool).ok())
293
- .flatten()
294
- .filter(|decoded| crate::stage2::vocabulary::is_name(decoded));
295
- let text = decoded.as_ref().unwrap_or(text);
296
270
  if !at_pc.contains(text) {
297
271
  at_pc.push(text.clone());
298
272
  }
299
273
  }
300
274
  Term::Closure { pc, .. } => pending.push(*pc),
301
- // A frame pushed for a catch handler: [pc, key, ...].
302
- Term::Array(frame) => {
303
- pending.extend(vm::catch_frame(frame).map(|handler| handler.pc));
304
- }
275
+ // Exception results are recovered separately from collection paths.
305
276
  _ => {}
306
277
  });
307
278
  }
308
- if ordinary.contains(&pc) {
309
- strings.entry(pc).or_default().extend(at_pc);
310
- }
311
- expanded.entry(pc).or_default().extend(expanded_at_pc);
279
+ strings.entry(pc).or_default().extend(at_pc);
312
280
  }
313
281
  for hidden in index.hidden_at(pc) {
314
- if ordinary.contains(&pc) {
315
- strings.entry(pc).or_default().push(hidden.string.clone());
316
- }
317
- expanded.entry(pc).or_default().push(hidden.string.clone());
282
+ strings.entry(pc).or_default().push(hidden.string.clone());
318
283
  pending.push(hidden.target.pc);
319
284
  }
320
285
  },
321
286
  );
322
287
  (
323
- strings.into_values().flatten().collect(),
324
- expanded.into_values().flatten().collect(),
288
+ without_inline_descriptors(strings.into_values().flatten()),
289
+ outputs,
325
290
  )
326
291
  }
327
292
 
293
+ /// Drops string descriptors the program decodes in place, keeping the decoded text that follows
294
+ /// each one (see `vm::packed_names`). Other builds decode descriptors where the listing cannot
295
+ /// follow, so recognizers read those descriptors themselves.
296
+ fn without_inline_descriptors(strings: impl Iterator<Item = String>) -> Vec<String> {
297
+ // An inline descriptor: a two-unit big-endian length, then at least that much text.
298
+ let describes = |descriptor: &str, text: &str| {
299
+ let units: Vec<u16> = descriptor.encode_utf16().collect();
300
+ let [high @ 0..=255, low @ 0..=255, rest @ ..] = units.as_slice() else {
301
+ return false;
302
+ };
303
+ let length = usize::from(*high) * 256 + usize::from(*low);
304
+ rest.get(..length)
305
+ .is_some_and(|described| described.iter().copied().eq(text.encode_utf16()))
306
+ };
307
+ let mut kept: Vec<String> = Vec::new();
308
+ for text in strings {
309
+ if kept.last().is_some_and(|last| describes(last, &text)) {
310
+ kept.pop();
311
+ }
312
+ kept.push(text);
313
+ }
314
+ kept
315
+ }
316
+
328
317
  /// Writes made by a collection function, in execution order.
329
318
  #[derive(Default)]
330
319
  struct Writes {
@@ -369,22 +358,11 @@ impl Writes {
369
358
  .min()
370
359
  else {
371
360
  return WriteOrder {
372
- internal: Vec::new(),
373
361
  fields: self.fields,
374
362
  late: Vec::new(),
375
363
  };
376
364
  };
377
365
  let descriptors: HashSet<u32> = descriptor_pushes.iter().map(|push| push.object).collect();
378
- let internal: Vec<String> = self
379
- .body_stores
380
- .iter()
381
- .filter(|store| {
382
- store
383
- .object
384
- .is_some_and(|object| descriptors.contains(&object))
385
- })
386
- .map(|store| store.key.clone())
387
- .collect();
388
366
  // Objects stored into a descriptor hold its defaults.
389
367
  let defaults: HashSet<u32> = self
390
368
  .body_stores
@@ -409,7 +387,6 @@ impl Writes {
409
387
  .cloned()
410
388
  .collect();
411
389
  WriteOrder {
412
- internal,
413
390
  fields: self.fields,
414
391
  late,
415
392
  }
@@ -540,3 +517,21 @@ fn pushed_object(
540
517
  stores_before,
541
518
  })
542
519
  }
520
+
521
+ #[cfg(test)]
522
+ mod tests {
523
+ use super::without_inline_descriptors;
524
+
525
+ #[test]
526
+ fn drops_only_descriptors_followed_by_their_text() {
527
+ let strings = [
528
+ "\0\u{3}abc\0\0", // decoded in place
529
+ "abc",
530
+ "\0\u{3}xyz\0", // decoded elsewhere
531
+ "toString", // reads as an oversized length
532
+ "String",
533
+ ];
534
+ let kept = without_inline_descriptors(strings.into_iter().map(String::from));
535
+ assert_eq!(kept, ["abc", "\0\u{3}xyz\0", "toString", "String"]);
536
+ }
537
+ }
@@ -3,7 +3,6 @@
3
3
  use std::collections::BTreeSet;
4
4
 
5
5
  use anyhow::{Context, Result};
6
- use swc_core::ecma::ast::BinaryOp;
7
6
 
8
7
  use crate::js::JsValue;
9
8
  use crate::listing::fold::{Frame, Known};
@@ -13,22 +12,6 @@ use crate::stage2::payload::RecordLiteral;
13
12
  use crate::stage2::records::{Completion, Envelope, INITIAL_STATUS, Probe};
14
13
  use crate::vm::{Effect, Term};
15
14
 
16
- pub(super) fn counter(index: &Index<'_>, creation: &BTreeSet<u32>) -> Option<String> {
17
- let fields: BTreeSet<String> = index
18
- .stores()
19
- .iter()
20
- .filter(|store| creation.contains(&store.pc))
21
- .filter(|store| {
22
- matches!(
23
- index.dereference(store.pc, &store.value),
24
- Some((_, Term::Binary(BinaryOp::Add, _, _)))
25
- )
26
- })
27
- .map(|store| store.key.clone())
28
- .collect();
29
- crate::candidates::exactly_one(fields, "record counter").ok()
30
- }
31
-
32
15
  /// Recovers record-envelope fields: creation counter, literal token and input, wrapper status
33
16
  /// and message, and completion timestamps and duration.
34
17
  pub(super) fn recover(
@@ -40,7 +23,28 @@ pub(super) fn recover(
40
23
  literals: &[RecordLiteral],
41
24
  ) -> Result<Envelope> {
42
25
  let listing = index.listing();
43
- let counter = counter(index, creation).context("record counter field not found")?;
26
+ let header = index.header();
27
+ // The counter's key is a header register, or, in programs that unpack names where they
28
+ // use them, a register loaded in the creation state.
29
+ let keys = || {
30
+ let instructions = creation
31
+ .iter()
32
+ .filter_map(|pc| listing.instructions.get(pc));
33
+ instructions.flat_map(|instruction| {
34
+ instruction
35
+ .effects()
36
+ .filter_map(move |effect| match effect {
37
+ Effect::SetProperty { key, .. } if let Term::Reg(register) = **key => {
38
+ Some((instruction.pc, register))
39
+ }
40
+ _ => None,
41
+ })
42
+ })
43
+ };
44
+ let counter = keys()
45
+ .find_map(|(_, register)| header.get(&register).cloned())
46
+ .or_else(|| keys().find_map(|(pc, register)| index.text(pc, &Term::Reg(register))))
47
+ .context("record counter field not found")?;
44
48
  let written_from = |matches: &dyn Fn(&Probe, &JsValue) -> bool| {
45
49
  literals
46
50
  .iter()
@@ -63,11 +67,17 @@ pub(super) fn recover(
63
67
  },
64
68
  );
65
69
  let wrapper = property_values(index, &main);
70
+ // Prefers header names, as for the counter.
66
71
  let wrapper_field = |what: &str, wanted: fn(&Known<Term>) -> bool| {
67
- wrapper
72
+ let mut fields = wrapper
68
73
  .iter()
69
- .find(|(_, value)| wanted(value))
70
- .map(|(field, _)| field.clone())
74
+ .filter(|(_, value)| wanted(value))
75
+ .map(|(field, _)| field);
76
+ fields
77
+ .clone()
78
+ .find(|field| header.values().any(|name| name == *field))
79
+ .or_else(|| fields.next())
80
+ .cloned()
71
81
  .with_context(|| format!("record {what} field not found"))
72
82
  };
73
83
  let status = wrapper_field("status", |value| {
@@ -57,10 +57,8 @@ impl<'probe> Strings<'probe> {
57
57
  /// Decodes the inline descriptor after the first occurrence of `text` that has one.
58
58
  #[must_use]
59
59
  pub(crate) fn decoded_after(&self, text: &str) -> Option<String> {
60
- self.features
61
- .windows(2)
62
- .filter(|pair| pair[0] == text && pair[1].starts_with('\0'))
63
- .find_map(|pair| listing::decode_string(&pair[1], "").ok())
60
+ let pair = self.features.windows(2).find(|pair| pair[0] == text)?;
61
+ listing::decode_string(&pair[1], "").ok()
64
62
  }
65
63
 
66
64
  /// Returns the first inline-decoded string starting with `prefix`.
@@ -168,7 +166,7 @@ mod tests {
168
166
  #[test]
169
167
  fn each_recognizer_recovers_only_its_own_reference_probe() {
170
168
  let probes = &REFERENCE.probes;
171
- assert_eq!(probes.len(), 38);
169
+ assert_eq!(probes.len(), 39);
172
170
  for kind in [
173
171
  Kind::Media,
174
172
  Kind::PrivateToken,
@@ -16,7 +16,6 @@ mod controller;
16
16
  mod discovery;
17
17
  mod envelope;
18
18
  mod outcome;
19
- mod outputs;
20
19
  pub(crate) mod recognize;
21
20
 
22
21
  pub use analyze::{Analysis, Artifact, ProbeEvidence, analyze};