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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +10 -9
- data/UPSTREAM.md +6 -6
- data/lib/cfsolve/profiles/safari-26-ios/answers.json +2562 -1887
- data/lib/cfsolve/profiles/safari-26-ios/challenge-page-answers.json +1873 -1198
- data/lib/cfsolve/profiles/safari-26-ios.json +0 -8
- data/lib/cfsolve/version.rb +2 -2
- data/vendor/cfsolve/README.md +0 -35
- data/vendor/cfsolve/examples/build_profile.rs +17 -15
- data/vendor/cfsolve/examples/capture_session.rs +37 -0
- data/vendor/cfsolve/examples/check_answers.rs +66 -0
- data/vendor/cfsolve/examples/compare_session.rs +190 -0
- data/vendor/cfsolve/examples/stage2_snapshot.rs +4 -1
- data/vendor/cfsolve/profiles/safari-26-ios/answers.json +2562 -1887
- data/vendor/cfsolve/profiles/safari-26-ios/challenge-page-answers.json +1873 -1198
- data/vendor/cfsolve/profiles/safari-26-ios.json +0 -8
- data/vendor/cfsolve/scripts/capture_cssom.mjs +42 -0
- data/vendor/cfsolve/scripts/serve_cssom.py +111 -0
- data/vendor/cfsolve/src/capture/profile.rs +7 -18
- data/vendor/cfsolve/src/capture/recorded.rs +42 -7
- data/vendor/cfsolve/src/challenge/installed/audit.rs +131 -0
- data/vendor/cfsolve/src/challenge/installed.rs +122 -271
- data/vendor/cfsolve/src/challenge/payload.rs +107 -5
- data/vendor/cfsolve/src/challenge/state.rs +98 -16
- data/vendor/cfsolve/src/deobfuscate/encoded_strings.rs +3 -2
- data/vendor/cfsolve/src/deobfuscate/helper_objects.rs +17 -32
- data/vendor/cfsolve/src/deobfuscate/register_arrays.rs +18 -5
- data/vendor/cfsolve/src/embedding_page/extra_params.rs +0 -14
- data/vendor/cfsolve/src/embedding_page/page.rs +28 -2
- data/vendor/cfsolve/src/http/client.rs +11 -0
- data/vendor/cfsolve/src/js/ast.rs +12 -3
- data/vendor/cfsolve/src/js/objects.rs +9 -2
- data/vendor/cfsolve/src/listing/fold.rs +0 -83
- data/vendor/cfsolve/src/listing/query.rs +25 -244
- data/vendor/cfsolve/src/listing.rs +0 -1
- data/vendor/cfsolve/src/session/initial.rs +14 -5
- data/vendor/cfsolve/src/session/probes.rs +160 -125
- data/vendor/cfsolve/src/session/timing.rs +58 -53
- data/vendor/cfsolve/src/session/widget.rs +5 -6
- data/vendor/cfsolve/src/stage2/analysis/analyze.rs +50 -33
- data/vendor/cfsolve/src/stage2/analysis/discovery.rs +61 -66
- data/vendor/cfsolve/src/stage2/analysis/envelope.rs +31 -21
- data/vendor/cfsolve/src/stage2/analysis/recognize.rs +3 -5
- data/vendor/cfsolve/src/stage2/analysis.rs +0 -1
- data/vendor/cfsolve/src/stage2/audit_globals.rs +310 -0
- data/vendor/cfsolve/src/stage2/benchmark.rs +5 -8
- data/vendor/cfsolve/src/stage2/call_stack.rs +72 -16
- data/vendor/cfsolve/src/stage2/interaction/native.rs +2 -1
- data/vendor/cfsolve/src/stage2/interaction/plan.rs +0 -3
- data/vendor/cfsolve/src/stage2/payload/recover.rs +54 -23
- data/vendor/cfsolve/src/stage2/probes/ice.rs +11 -10
- data/vendor/cfsolve/src/stage2/probes/media.rs +47 -33
- data/vendor/cfsolve/src/stage2/probes/performance.rs +1 -0
- data/vendor/cfsolve/src/stage2/probes/rects.rs +12 -17
- data/vendor/cfsolve/src/stage2/probes/stack.rs +50 -103
- data/vendor/cfsolve/src/stage2/probes/websocket_echo.rs +17 -86
- data/vendor/cfsolve/src/stage2/probes.rs +0 -1
- data/vendor/cfsolve/src/stage2/records.rs +4 -13
- data/vendor/cfsolve/src/stage2/reference.json +1 -1
- data/vendor/cfsolve/src/stage2/reference.rs +98 -100
- data/vendor/cfsolve/src/stage2/registry.rs +41 -34
- data/vendor/cfsolve/src/stage2/replay.rs +182 -254
- data/vendor/cfsolve/src/stage2/stylesheets.rs +176 -0
- data/vendor/cfsolve/src/stage2/trace.rs +18 -0
- data/vendor/cfsolve/src/stage2/vocabulary.rs +111 -401
- data/vendor/cfsolve/src/stage2.rs +2 -1
- data/vendor/cfsolve/src/vm/disassemble.rs +39 -10
- data/vendor/cfsolve/src/vm/interpret/access.rs +5 -21
- data/vendor/cfsolve/src/vm/interpret/builtin.rs +6 -53
- data/vendor/cfsolve/src/vm/interpret/call.rs +86 -9
- data/vendor/cfsolve/src/vm/interpret/compile.rs +626 -0
- data/vendor/cfsolve/src/vm/interpret/copies.rs +232 -120
- data/vendor/cfsolve/src/vm/interpret/evaluate/tests.rs +96 -8
- data/vendor/cfsolve/src/vm/interpret/evaluate.rs +219 -92
- data/vendor/cfsolve/src/vm/interpret/explore.rs +155 -39
- data/vendor/cfsolve/src/vm/interpret/frozen.rs +171 -0
- data/vendor/cfsolve/src/vm/interpret/intrinsic.rs +350 -0
- data/vendor/cfsolve/src/vm/interpret/plan.rs +71 -44
- data/vendor/cfsolve/src/vm/interpret/run.rs +202 -13
- data/vendor/cfsolve/src/vm/interpret.rs +7 -2
- data/vendor/cfsolve/src/vm/machine.rs +6 -2
- data/vendor/cfsolve/src/vm/packed_names.rs +307 -0
- data/vendor/cfsolve/src/vm/session.rs +36 -14
- data/vendor/cfsolve/src/vm/value.rs +13 -0
- data/vendor/cfsolve/src/vm.rs +1 -0
- data/vendor/cfsolve/vendor/btls-sys/src/lib.rs +3 -0
- metadata +13 -6
- data/vendor/cfsolve/src/listing/normalize.rs +0 -264
- data/vendor/cfsolve/src/stage2/analysis/outputs.rs +0 -327
- data/vendor/cfsolve/src/stage2/probes/stylesheets.rs +0 -249
- data/vendor/cfsolve/src/stage2/reference-expanded.json +0 -42
- 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::
|
|
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
|
|
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.
|
|
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
|
-
///
|
|
232
|
-
///
|
|
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
|
|
238
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
) -> (Vec<String>, Vec<String>) {
|
|
247
|
+
) -> (Vec<String>, BTreeSet<String>) {
|
|
249
248
|
let mut strings: BTreeMap<u32, Vec<String>> = BTreeMap::new();
|
|
250
|
-
let mut
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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()
|
|
324
|
-
|
|
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
|
|
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(®ister).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
|
-
.
|
|
70
|
-
.map(|(field, _)| field
|
|
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
|
-
|
|
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(),
|
|
169
|
+
assert_eq!(probes.len(), 39);
|
|
172
170
|
for kind in [
|
|
173
171
|
Kind::Media,
|
|
174
172
|
Kind::PrivateToken,
|