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
|
@@ -13,11 +13,11 @@ use std::ops::Range;
|
|
|
13
13
|
|
|
14
14
|
use rustc_hash::FxHashMap;
|
|
15
15
|
|
|
16
|
-
use super::reference::
|
|
16
|
+
use super::reference::REFERENCE;
|
|
17
17
|
use crate::js::{JsObject, JsValue};
|
|
18
18
|
|
|
19
19
|
/// Session names and the reference names they stand for.
|
|
20
|
-
#[derive(
|
|
20
|
+
#[derive(Debug)]
|
|
21
21
|
pub struct Vocabulary {
|
|
22
22
|
/// One-to-one name mapping for the whole program.
|
|
23
23
|
global: Names,
|
|
@@ -39,7 +39,7 @@ pub struct Candidate {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/// A one-to-one map between session and reference names.
|
|
42
|
-
#[derive(
|
|
42
|
+
#[derive(Debug, Default)]
|
|
43
43
|
struct Names {
|
|
44
44
|
to_reference: HashMap<String, String>,
|
|
45
45
|
to_session: HashMap<String, String>,
|
|
@@ -76,97 +76,31 @@ impl Names {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
impl Vocabulary {
|
|
79
|
-
pub(crate) fn extend_expanded<S: AsRef<[String]>>(&mut self, session: &[S]) {
|
|
80
|
-
let session = without_common_names(&expand_lists(session));
|
|
81
|
-
let public: HashSet<&str> = REFERENCE
|
|
82
|
-
.probes
|
|
83
|
-
.iter()
|
|
84
|
-
.flatten()
|
|
85
|
-
.chain(EXPANDED_REFERENCE.probes.iter().flatten())
|
|
86
|
-
.map(String::as_str)
|
|
87
|
-
.collect();
|
|
88
|
-
let clean: Vec<Vec<String>> = session
|
|
89
|
-
.iter()
|
|
90
|
-
.map(|strings| {
|
|
91
|
-
strings
|
|
92
|
-
.iter()
|
|
93
|
-
.filter(|text| {
|
|
94
|
-
!is_log_message(text)
|
|
95
|
-
&& !text.contains('\0')
|
|
96
|
-
&& !text.starts_with("ÿÿ")
|
|
97
|
-
&& (alignment_name(text) || public.contains(text.as_str()))
|
|
98
|
-
})
|
|
99
|
-
.cloned()
|
|
100
|
-
.collect()
|
|
101
|
-
})
|
|
102
|
-
.collect();
|
|
103
|
-
self.align(&clean, &EXPANDED_REFERENCE.probes, true);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
79
|
/// Infers name mappings from each probe's collection strings.
|
|
107
80
|
#[must_use]
|
|
108
81
|
pub fn infer<S: AsRef<[String]>>(session: &[S]) -> Self {
|
|
109
82
|
Self::between(session, &REFERENCE.probes)
|
|
110
83
|
}
|
|
111
84
|
|
|
112
|
-
|
|
113
|
-
let encoded_names = session
|
|
114
|
-
.iter()
|
|
115
|
-
.any(|probe| probe.as_ref().iter().any(|name| hashed_name(name)))
|
|
116
|
-
|| reference.iter().flatten().any(|name| hashed_name(name));
|
|
85
|
+
fn between<S: AsRef<[String]>>(session: &[S], reference: &[Vec<String>]) -> Self {
|
|
117
86
|
let (partners, candidates) = pair_probes(session, reference);
|
|
118
|
-
let mut vocabulary = Self {
|
|
119
|
-
global: Names::default(),
|
|
120
|
-
probes: vec![Names::default(); session.len()],
|
|
121
|
-
partners,
|
|
122
|
-
candidates,
|
|
123
|
-
};
|
|
124
|
-
if encoded_names {
|
|
125
|
-
let session = expand_lists(session);
|
|
126
|
-
let reference = expand_lists(reference);
|
|
127
|
-
let (expanded, candidates) = pair_probes(&session, &reference);
|
|
128
|
-
for (index, partner) in expanded.into_iter().enumerate() {
|
|
129
|
-
if vocabulary.partners[index].is_none()
|
|
130
|
-
&& partner.is_some()
|
|
131
|
-
&& !vocabulary.partners.contains(&partner)
|
|
132
|
-
{
|
|
133
|
-
vocabulary.partners[index] = partner;
|
|
134
|
-
vocabulary.candidates[index] = candidates[index];
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
vocabulary.align(
|
|
138
|
-
&without_common_names(&session),
|
|
139
|
-
&without_common_names(&reference),
|
|
140
|
-
true,
|
|
141
|
-
);
|
|
142
|
-
} else {
|
|
143
|
-
vocabulary.align(session, reference, false);
|
|
144
|
-
}
|
|
145
|
-
vocabulary
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
fn align<S: AsRef<[String]>>(
|
|
149
|
-
&mut self,
|
|
150
|
-
session: &[S],
|
|
151
|
-
reference: &[Vec<String>],
|
|
152
|
-
encoded: bool,
|
|
153
|
-
) {
|
|
154
87
|
let mut all = Votes::new();
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
continue;
|
|
158
|
-
};
|
|
88
|
+
let mut probes = Vec::with_capacity(session.len());
|
|
89
|
+
for (index, strings) in session.iter().enumerate() {
|
|
159
90
|
let mut votes = Votes::new();
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
self.probes[position].pair(session, reference);
|
|
91
|
+
if let Some(partner) = partners[index] {
|
|
92
|
+
for pair in aligned_names(strings.as_ref(), &reference[partner - 1]) {
|
|
93
|
+
*votes.entry(pair).or_default() += 1;
|
|
94
|
+
*all.entry(pair).or_default() += 1;
|
|
95
|
+
}
|
|
166
96
|
}
|
|
97
|
+
probes.push(Names::one_to_one(&votes));
|
|
167
98
|
}
|
|
168
|
-
|
|
169
|
-
|
|
99
|
+
Self {
|
|
100
|
+
global: Names::one_to_one(&all),
|
|
101
|
+
probes,
|
|
102
|
+
partners,
|
|
103
|
+
candidates,
|
|
170
104
|
}
|
|
171
105
|
}
|
|
172
106
|
|
|
@@ -187,76 +121,6 @@ impl Vocabulary {
|
|
|
187
121
|
}
|
|
188
122
|
}
|
|
189
123
|
|
|
190
|
-
pub(crate) fn refine_outputs(
|
|
191
|
-
&mut self,
|
|
192
|
-
position: usize,
|
|
193
|
-
features: &[String],
|
|
194
|
-
writes: &[String],
|
|
195
|
-
internal: &[String],
|
|
196
|
-
common: &HashSet<&str>,
|
|
197
|
-
wanted: &[String],
|
|
198
|
-
) {
|
|
199
|
-
let Some(partner) = self.partner(position) else {
|
|
200
|
-
return;
|
|
201
|
-
};
|
|
202
|
-
if !writes.iter().any(|name| hashed_name(name)) {
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
let candidates: Vec<String> = writes
|
|
206
|
-
.iter()
|
|
207
|
-
.filter(|name| {
|
|
208
|
-
let reference = self.to_reference(position, name);
|
|
209
|
-
if !hashed_name(name)
|
|
210
|
-
|| common.contains(name.as_str())
|
|
211
|
-
|| (internal.contains(name) && !wanted.iter().any(|field| field == reference))
|
|
212
|
-
{
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
reference == name.as_str() || wanted.iter().any(|field| field == reference)
|
|
216
|
-
})
|
|
217
|
-
.cloned()
|
|
218
|
-
.collect();
|
|
219
|
-
if candidates.len() == wanted.len()
|
|
220
|
-
&& wanted.iter().zip(&candidates).all(|(name, candidate)| {
|
|
221
|
-
let mapped = self.to_session(position, name);
|
|
222
|
-
mapped == name || mapped == candidate
|
|
223
|
-
})
|
|
224
|
-
{
|
|
225
|
-
self.pin(
|
|
226
|
-
position,
|
|
227
|
-
candidates
|
|
228
|
-
.iter()
|
|
229
|
-
.cloned()
|
|
230
|
-
.zip(wanted.iter().map(String::as_str)),
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
let keep = |text: &String, names: &[String]| !alignment_name(text) || names.contains(text);
|
|
234
|
-
let session: Vec<String> = features
|
|
235
|
-
.iter()
|
|
236
|
-
.filter(|text| keep(text, &candidates))
|
|
237
|
-
.cloned()
|
|
238
|
-
.collect();
|
|
239
|
-
let reference: Vec<String> = REFERENCE.probes[partner - 1]
|
|
240
|
-
.iter()
|
|
241
|
-
.filter(|text| keep(text, wanted))
|
|
242
|
-
.cloned()
|
|
243
|
-
.collect();
|
|
244
|
-
let mut votes = Votes::new();
|
|
245
|
-
for pair in aligned_names(&session, &reference, true) {
|
|
246
|
-
if candidates.iter().any(|name| name == pair.0)
|
|
247
|
-
&& wanted.iter().any(|name| name == pair.1)
|
|
248
|
-
{
|
|
249
|
-
*votes.entry(pair).or_default() += 1;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
for (session, reference) in Names::one_to_one(&votes).to_reference {
|
|
253
|
-
let mapped = self.to_session(position, &reference);
|
|
254
|
-
if mapped == reference || !writes.iter().any(|name| name == mapped) {
|
|
255
|
-
self.pin(position, [(session, reference.as_str())]);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
124
|
/// The number of the reference probe the session probe at `index` was paired with.
|
|
261
125
|
#[must_use]
|
|
262
126
|
pub fn partner(&self, index: usize) -> Option<usize> {
|
|
@@ -300,116 +164,28 @@ impl Vocabulary {
|
|
|
300
164
|
}
|
|
301
165
|
|
|
302
166
|
pub fn rewrite_to_reference(&self, index: usize, object: &mut JsObject) {
|
|
303
|
-
rewrite_object(object, &|
|
|
304
|
-
|
|
305
|
-
self.find(index, |names| &names.to_reference, name)
|
|
306
|
-
});
|
|
167
|
+
rewrite_object(object, &|name| {
|
|
168
|
+
self.find(index, |names| &names.to_reference, name)
|
|
307
169
|
});
|
|
308
170
|
}
|
|
309
171
|
|
|
310
172
|
pub fn rewrite_to_session(&self, index: usize, object: &mut JsObject) {
|
|
311
|
-
rewrite_object(object, &|
|
|
312
|
-
|
|
313
|
-
self.find(index, |names| &names.to_session, name)
|
|
314
|
-
});
|
|
173
|
+
rewrite_object(object, &|name| {
|
|
174
|
+
self.find(index, |names| &names.to_session, name)
|
|
315
175
|
});
|
|
316
176
|
}
|
|
317
177
|
}
|
|
318
178
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
.map(|probe| {
|
|
323
|
-
let decoded: HashMap<&str, String> = probe
|
|
324
|
-
.as_ref()
|
|
325
|
-
.windows(2)
|
|
326
|
-
.filter_map(|pair| {
|
|
327
|
-
let text = if pair[1] == "decodeURIComponent" {
|
|
328
|
-
&pair[0]
|
|
329
|
-
} else if pair[0] == "decodeURIComponent" {
|
|
330
|
-
&pair[1]
|
|
331
|
-
} else {
|
|
332
|
-
return None;
|
|
333
|
-
};
|
|
334
|
-
let bytes = text.as_bytes();
|
|
335
|
-
if bytes.iter().enumerate().any(|(at, byte)| {
|
|
336
|
-
*byte == b'%'
|
|
337
|
-
&& !bytes
|
|
338
|
-
.get(at + 1..at + 3)
|
|
339
|
-
.is_some_and(|hex| hex.iter().all(u8::is_ascii_hexdigit))
|
|
340
|
-
}) {
|
|
341
|
-
return None;
|
|
342
|
-
}
|
|
343
|
-
let decoded = percent_encoding::percent_decode_str(text)
|
|
344
|
-
.decode_utf8()
|
|
345
|
-
.ok()?;
|
|
346
|
-
(decoded != text.as_str()).then(|| (text.as_str(), decoded.into_owned()))
|
|
347
|
-
})
|
|
348
|
-
.collect();
|
|
349
|
-
let prepared: Vec<_> = probe
|
|
350
|
-
.as_ref()
|
|
351
|
-
.iter()
|
|
352
|
-
.map(|text| decoded.get(text.as_str()).unwrap_or(text).clone())
|
|
353
|
-
.collect();
|
|
354
|
-
let mut strings = prepared.as_slice();
|
|
355
|
-
let mut expanded = Vec::new();
|
|
356
|
-
while !strings.is_empty() {
|
|
357
|
-
if let [list, separator, method, ..] = strings
|
|
358
|
-
&& method == "split"
|
|
359
|
-
&& separator.chars().count() == 1
|
|
360
|
-
{
|
|
361
|
-
let parts: Vec<_> = list.split(separator).collect();
|
|
362
|
-
if parts.len() >= 3
|
|
363
|
-
&& parts.iter().all(|part| !part.is_empty())
|
|
364
|
-
&& (list.len() > 32 || parts.iter().all(|part| is_name(part)))
|
|
365
|
-
{
|
|
366
|
-
expanded.extend(parts.into_iter().map(str::to_owned));
|
|
367
|
-
strings = &strings[3..];
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
expanded.push(strings[0].clone());
|
|
372
|
-
strings = &strings[1..];
|
|
373
|
-
}
|
|
374
|
-
expanded
|
|
375
|
-
})
|
|
376
|
-
.collect()
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/// Inline decoders expose wrapper names previously held only in header registers.
|
|
380
|
-
/// Those names belong to the common envelope and cannot align probe-specific fields.
|
|
381
|
-
fn without_common_names<S: AsRef<[String]>>(probes: &[S]) -> Vec<Vec<String>> {
|
|
382
|
-
let mut common: HashSet<&str> = probes
|
|
383
|
-
.first()
|
|
384
|
-
.into_iter()
|
|
385
|
-
.flat_map(|probe| probe.as_ref())
|
|
386
|
-
.map(String::as_str)
|
|
387
|
-
.filter(|text| is_name(text))
|
|
388
|
-
.collect();
|
|
389
|
-
if probes.len() < 3 {
|
|
390
|
-
common.clear();
|
|
391
|
-
}
|
|
392
|
-
for probe in probes.iter().skip(1) {
|
|
393
|
-
common.retain(|name| probe.as_ref().iter().any(|text| text == name));
|
|
394
|
-
}
|
|
395
|
-
probes
|
|
396
|
-
.iter()
|
|
397
|
-
.map(|probe| {
|
|
398
|
-
probe
|
|
399
|
-
.as_ref()
|
|
400
|
-
.iter()
|
|
401
|
-
.filter(|text| !common.contains(text.as_str()))
|
|
402
|
-
.cloned()
|
|
403
|
-
.collect()
|
|
404
|
-
})
|
|
405
|
-
.collect()
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/// Visits string values and object keys, preserving the recorded field order.
|
|
409
|
-
fn rewrite(value: &mut JsValue, rename: &impl Fn(&mut String)) {
|
|
179
|
+
/// Renames strings and object keys using `rename`, including randomized names inside longer
|
|
180
|
+
/// strings, such as a list of names or a style rule's selector.
|
|
181
|
+
fn rewrite<'a>(value: &mut JsValue, rename: &impl Fn(&str) -> Option<&'a String>) {
|
|
410
182
|
match value {
|
|
411
183
|
JsValue::String(text) => {
|
|
412
|
-
rename(text)
|
|
184
|
+
if let Some(name) = rename(text) {
|
|
185
|
+
*text = name.clone();
|
|
186
|
+
} else if let Some(renamed) = rename_embedded(text, rename) {
|
|
187
|
+
*text = renamed;
|
|
188
|
+
}
|
|
413
189
|
}
|
|
414
190
|
JsValue::Array(items) => {
|
|
415
191
|
for item in items {
|
|
@@ -421,50 +197,34 @@ fn rewrite(value: &mut JsValue, rename: &impl Fn(&mut String)) {
|
|
|
421
197
|
}
|
|
422
198
|
}
|
|
423
199
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
200
|
+
/// Renames each randomized name `text` contains, wherever it starts. Names can adjoin other
|
|
201
|
+
/// hex digits, as in `_<name><hash>_<name>`, so matches need no boundary. Returns `None` if
|
|
202
|
+
/// nothing was renamed.
|
|
203
|
+
fn rename_embedded<'a>(text: &str, rename: &impl Fn(&str) -> Option<&'a String>) -> Option<String> {
|
|
204
|
+
const LENGTH: usize = 25;
|
|
205
|
+
let mut renamed = String::with_capacity(text.len());
|
|
206
|
+
let mut copied = 0;
|
|
207
|
+
for (at, _) in text.match_indices('_') {
|
|
208
|
+
let candidate = text.get(at..at + LENGTH).filter(|name| is_hex_name(name));
|
|
209
|
+
if at >= copied
|
|
210
|
+
&& let Some(name) = candidate.and_then(rename)
|
|
211
|
+
{
|
|
212
|
+
renamed.push_str(&text[copied..at]);
|
|
213
|
+
renamed.push_str(name);
|
|
214
|
+
copied = at + LENGTH;
|
|
215
|
+
}
|
|
429
216
|
}
|
|
217
|
+
(copied > 0).then(|| {
|
|
218
|
+
renamed.push_str(&text[copied..]);
|
|
219
|
+
renamed
|
|
220
|
+
})
|
|
430
221
|
}
|
|
431
222
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
*text = name.clone();
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
let mut result = String::with_capacity(text.len());
|
|
440
|
-
let mut at = 0;
|
|
441
|
-
while at < text.len() {
|
|
442
|
-
let rest = &text[at..];
|
|
443
|
-
let found = [25, 9, 8, 7, 6, 5, 4].into_iter().find_map(|length| {
|
|
444
|
-
let name = rest.get(..length)?;
|
|
445
|
-
let identifier = |c: char| c.is_alphanumeric() || matches!(c, '_' | '$');
|
|
446
|
-
let before = !text[..at].chars().next_back().is_some_and(identifier);
|
|
447
|
-
let after = !rest[length..].chars().next().is_some_and(identifier);
|
|
448
|
-
// Fingerprint strings can concatenate a name with a digest or counter. Keep
|
|
449
|
-
// those edge matches; inside a string, require a complete identifier.
|
|
450
|
-
if !hashed_name(name) && at != 0 && at + length != text.len() && !(before && after) {
|
|
451
|
-
return None;
|
|
452
|
-
}
|
|
453
|
-
is_name(name)
|
|
454
|
-
.then(|| rename(name))
|
|
455
|
-
.flatten()
|
|
456
|
-
.map(|replacement| (length, replacement))
|
|
457
|
-
});
|
|
458
|
-
if let Some((length, replacement)) = found {
|
|
459
|
-
result.push_str(replacement);
|
|
460
|
-
at += length;
|
|
461
|
-
} else {
|
|
462
|
-
let character = rest.chars().next().unwrap();
|
|
463
|
-
result.push(character);
|
|
464
|
-
at += character.len_utf8();
|
|
465
|
-
}
|
|
223
|
+
fn rewrite_object<'a>(object: &mut JsObject, rename: &impl Fn(&str) -> Option<&'a String>) {
|
|
224
|
+
for (key, mut item) in mem::take(object) {
|
|
225
|
+
rewrite(&mut item, rename);
|
|
226
|
+
object.insert(rename(&key).cloned().unwrap_or(key), item);
|
|
466
227
|
}
|
|
467
|
-
*text = result;
|
|
468
228
|
}
|
|
469
229
|
|
|
470
230
|
/// Chooses each name's pair with the most votes. Breaks ties by pair ordering so results are
|
|
@@ -577,11 +337,7 @@ fn is_log_message(text: &str) -> bool {
|
|
|
577
337
|
/// Aligns probe string sequences and pairs names between shared anchors. Requires equal counts
|
|
578
338
|
/// on both sides. Also compares differing strings at matching positions to recover embedded
|
|
579
339
|
/// names.
|
|
580
|
-
fn aligned_names<'a>(
|
|
581
|
-
session: &'a [String],
|
|
582
|
-
reference: &'a [String],
|
|
583
|
-
encoded: bool,
|
|
584
|
-
) -> Vec<(&'a str, &'a str)> {
|
|
340
|
+
fn aligned_names<'a>(session: &'a [String], reference: &'a [String]) -> Vec<(&'a str, &'a str)> {
|
|
585
341
|
// Collapse repeated API loads so duplicate reads for one call do not split runs used to
|
|
586
342
|
// align neighboring output names.
|
|
587
343
|
let mut session: Vec<&str> = session.iter().map(String::as_str).collect();
|
|
@@ -597,16 +353,15 @@ fn aligned_names<'a>(
|
|
|
597
353
|
.collect();
|
|
598
354
|
let mut pairs = Vec::new();
|
|
599
355
|
let (mut i, mut j) = (0, 0);
|
|
600
|
-
let name = if encoded { alignment_name } else { is_name };
|
|
601
356
|
for run in matching_runs(&session, &reference) {
|
|
602
357
|
let (mut left_names, left_texts): (Vec<_>, Vec<_>) = session[i..run.session]
|
|
603
358
|
.iter()
|
|
604
359
|
.copied()
|
|
605
|
-
.partition(|text|
|
|
360
|
+
.partition(|text| is_name(text));
|
|
606
361
|
let (mut right_names, right_texts): (Vec<_>, Vec<_>) = reference[j..run.reference]
|
|
607
362
|
.iter()
|
|
608
363
|
.copied()
|
|
609
|
-
.partition(|text|
|
|
364
|
+
.partition(|text| is_name(text));
|
|
610
365
|
// Repeats of a name in a row count once, even across non-name text.
|
|
611
366
|
left_names.dedup();
|
|
612
367
|
right_names.dedup();
|
|
@@ -646,18 +401,20 @@ fn embedded_names<'a>(session: &'a str, reference: &'a str) -> Vec<(&'a str, &'a
|
|
|
646
401
|
names
|
|
647
402
|
}
|
|
648
403
|
|
|
649
|
-
/// Splits a string into alternating runs of
|
|
404
|
+
/// Splits a string into alternating runs of name bytes (see [`is_name_byte`]) and other bytes.
|
|
650
405
|
fn tokens(text: &str) -> Vec<&str> {
|
|
651
406
|
text.as_bytes()
|
|
652
|
-
.chunk_by(|left, right|
|
|
653
|
-
(left.is_ascii_alphanumeric() || *left == b'_')
|
|
654
|
-
== (right.is_ascii_alphanumeric() || *right == b'_')
|
|
655
|
-
})
|
|
407
|
+
.chunk_by(|left, right| is_name_byte(*left) == is_name_byte(*right))
|
|
656
408
|
// Boundaries fall only beside ASCII, never inside a UTF-8 character.
|
|
657
409
|
.map(|part| std::str::from_utf8(part).expect("runs split only beside ASCII bytes"))
|
|
658
410
|
.collect()
|
|
659
411
|
}
|
|
660
412
|
|
|
413
|
+
/// Name characters, including the `_` that starts hex names.
|
|
414
|
+
fn is_name_byte(byte: u8) -> bool {
|
|
415
|
+
byte.is_ascii_alphanumeric() || byte == b'_'
|
|
416
|
+
}
|
|
417
|
+
|
|
661
418
|
/// A run of strings the session and reference sequences share.
|
|
662
419
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
663
420
|
struct Run {
|
|
@@ -741,44 +498,27 @@ fn longest_run(
|
|
|
741
498
|
best
|
|
742
499
|
}
|
|
743
500
|
|
|
744
|
-
/// Whether a string could be one of the program's randomized names
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
|
750
|
-
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
| "Math"
|
|
755
|
-
| "Number"
|
|
756
|
-
| "Object"
|
|
757
|
-
| "Promise"
|
|
758
|
-
| "Reflect"
|
|
759
|
-
| "String"
|
|
760
|
-
| "Symbol"
|
|
761
|
-
) {
|
|
762
|
-
return false;
|
|
763
|
-
}
|
|
764
|
-
is_name(text)
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
pub(crate) fn hashed_name(text: &str) -> bool {
|
|
768
|
-
text.strip_prefix('_')
|
|
769
|
-
.is_some_and(|hex| hex.len() == 24 && hex.bytes().all(|byte| byte.is_ascii_hexdigit()))
|
|
501
|
+
/// Whether a string could be one of the program's randomized names: four to nine letters and
|
|
502
|
+
/// digits such as `XNWt6`, or a hex name (see [`is_hex_name`]).
|
|
503
|
+
fn is_name(text: &str) -> bool {
|
|
504
|
+
is_hex_name(text)
|
|
505
|
+
|| ((4..=9).contains(&text.len())
|
|
506
|
+
&& text.starts_with(|c: char| c.is_ascii_alphabetic())
|
|
507
|
+
&& text.chars().all(|c| c.is_ascii_alphanumeric())
|
|
508
|
+
&& text
|
|
509
|
+
.chars()
|
|
510
|
+
.any(|c| c.is_ascii_uppercase() || c.is_ascii_digit()))
|
|
770
511
|
}
|
|
771
512
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
.any(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
|
|
513
|
+
/// Whether `text` is a randomized name in the form newer builds use: `_` and 24 lowercase hex
|
|
514
|
+
/// digits, such as `_2b78d2cde0fa83bfaec4edae`.
|
|
515
|
+
pub(crate) fn is_hex_name(text: &str) -> bool {
|
|
516
|
+
text.strip_prefix('_').is_some_and(|hex| {
|
|
517
|
+
hex.len() == 24
|
|
518
|
+
&& hex
|
|
519
|
+
.bytes()
|
|
520
|
+
.all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
|
|
521
|
+
})
|
|
782
522
|
}
|
|
783
523
|
|
|
784
524
|
#[cfg(test)]
|
|
@@ -786,72 +526,42 @@ mod tests {
|
|
|
786
526
|
use super::*;
|
|
787
527
|
use crate::js::json;
|
|
788
528
|
|
|
789
|
-
#[test]
|
|
790
|
-
fn rewrites_embedded_names_once_and_preserves_unicode() {
|
|
791
|
-
let names = HashMap::from([
|
|
792
|
-
("_0123456789abcdef01234567".to_owned(), "Abcd1".to_owned()),
|
|
793
|
-
("Abcd1".to_owned(), "Efgh2".to_owned()),
|
|
794
|
-
]);
|
|
795
|
-
let mut text = "é😀<p>_0123456789abcdef01234567</p>o.Abcd1".to_owned();
|
|
796
|
-
rewrite_text(&mut text, &|name| names.get(name));
|
|
797
|
-
assert_eq!(text, "é😀<p>Abcd1</p>o.Efgh2");
|
|
798
|
-
for original in ["abcAbcd1xyz", "prefix_Abcd1_suffix", "$Abcd1$", "πAbcd1中"] {
|
|
799
|
-
let mut opaque = original.to_owned();
|
|
800
|
-
rewrite_text(&mut opaque, &|name| names.get(name));
|
|
801
|
-
assert_eq!(opaque, original);
|
|
802
|
-
}
|
|
803
|
-
let mut fingerprint = "Abcd1611".to_owned();
|
|
804
|
-
rewrite_text(&mut fingerprint, &|name| names.get(name));
|
|
805
|
-
assert_eq!(fingerprint, "Efgh2611");
|
|
806
|
-
}
|
|
807
|
-
|
|
808
529
|
#[test]
|
|
809
530
|
fn token_boundaries_preserve_non_ascii_markup() {
|
|
531
|
+
// `_` belongs to names, as in hex names.
|
|
810
532
|
assert_eq!(tokens("é😀Aa12-中_foo"), ["é😀", "Aa12", "-中", "_foo"]);
|
|
811
533
|
}
|
|
812
534
|
|
|
813
|
-
fn strings(text: &str) -> Vec<String> {
|
|
814
|
-
text.split(' ').map(str::to_owned).collect()
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
#[test]
|
|
818
|
-
fn packed_names_align_in_both_directions_across_identifier_formats() {
|
|
819
|
-
let packed = "%41%61%61%61%31%7c%42%62%62%62%32%7c%43%63%63%63%33";
|
|
820
|
-
let short = [strings(&format!(
|
|
821
|
-
"Intl ListFormat style long type disjunction format {packed} decodeURIComponent {packed} | split | format Result1"
|
|
822
|
-
))];
|
|
823
|
-
let hashed = [strings(
|
|
824
|
-
"Intl ListFormat style long type disjunction format _111111111111111111111111 _222222222222222222222222 _333333333333333333333333 format _444444444444444444444444",
|
|
825
|
-
)];
|
|
826
|
-
for (session, reference, expected) in [
|
|
827
|
-
(&short, &hashed, ("Aaaa1", "_111111111111111111111111")),
|
|
828
|
-
(&hashed, &short, ("_111111111111111111111111", "Aaaa1")),
|
|
829
|
-
] {
|
|
830
|
-
let vocabulary = Vocabulary::between(session, reference);
|
|
831
|
-
assert_eq!(vocabulary.to_reference(0, expected.0), expected.1);
|
|
832
|
-
assert_eq!(vocabulary.to_session(0, expected.1), expected.0);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
535
|
#[test]
|
|
837
|
-
fn
|
|
838
|
-
let
|
|
839
|
-
let
|
|
840
|
-
let
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
536
|
+
fn renames_names_inside_longer_strings() {
|
|
537
|
+
let name = |digit: &str| format!("_{}", digit.repeat(24));
|
|
538
|
+
let (a, b, c, x, y) = (name("a"), name("b"), name("c"), name("1"), name("2"));
|
|
539
|
+
let rename = |name: &str| match name {
|
|
540
|
+
name if name == a => Some(&x),
|
|
541
|
+
name if name == b => Some(&y),
|
|
542
|
+
_ => None,
|
|
543
|
+
};
|
|
544
|
+
let mut value = json!({
|
|
545
|
+
"list": format!("{a}, {b}, or {c}"),
|
|
546
|
+
"rule": format!(".{a} {{ color: red; }}"),
|
|
547
|
+
"joined": format!("{a}c52d060a{b}"),
|
|
548
|
+
"whole": a.clone(),
|
|
549
|
+
});
|
|
550
|
+
rewrite(&mut value, &rename);
|
|
847
551
|
assert_eq!(
|
|
848
|
-
|
|
849
|
-
|
|
552
|
+
value,
|
|
553
|
+
json!({
|
|
554
|
+
"list": format!("{x}, {y}, or {c}"),
|
|
555
|
+
"rule": format!(".{x} {{ color: red; }}"),
|
|
556
|
+
"joined": format!("{x}c52d060a{y}"),
|
|
557
|
+
"whole": x.clone(),
|
|
558
|
+
}),
|
|
559
|
+
"names without a pair stay, and names adjoining hex digits are renamed"
|
|
850
560
|
);
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
fn strings(text: &str) -> Vec<String> {
|
|
564
|
+
text.split(' ').map(str::to_owned).collect()
|
|
855
565
|
}
|
|
856
566
|
|
|
857
567
|
#[test]
|
|
@@ -899,7 +609,7 @@ mod tests {
|
|
|
899
609
|
];
|
|
900
610
|
for (session, reference, expected) in cases {
|
|
901
611
|
assert_eq!(
|
|
902
|
-
aligned_names(&strings(session), &strings(reference)
|
|
612
|
+
aligned_names(&strings(session), &strings(reference)),
|
|
903
613
|
expected
|
|
904
614
|
);
|
|
905
615
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
//! are assembled from recovered [`payload::Layout`] data.
|
|
10
10
|
|
|
11
11
|
pub mod analysis;
|
|
12
|
+
mod audit_globals;
|
|
12
13
|
pub(crate) mod benchmark;
|
|
13
14
|
pub mod call_stack;
|
|
14
15
|
pub(crate) mod challenge_page;
|
|
@@ -20,7 +21,7 @@ pub mod records;
|
|
|
20
21
|
pub(crate) mod reference;
|
|
21
22
|
pub mod registry;
|
|
22
23
|
pub mod replay;
|
|
23
|
-
|
|
24
|
+
mod stylesheets;
|
|
24
25
|
pub mod trace;
|
|
25
26
|
pub mod vocabulary;
|
|
26
27
|
mod worker_timing;
|