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
@@ -13,11 +13,11 @@ use std::ops::Range;
13
13
 
14
14
  use rustc_hash::FxHashMap;
15
15
 
16
- use super::reference::{EXPANDED_REFERENCE, 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(Clone, Debug)]
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(Clone, Debug, Default)]
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
- pub(crate) fn between<S: AsRef<[String]>>(session: &[S], reference: &[Vec<String>]) -> Self {
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
- for (position, strings) in session.iter().enumerate() {
156
- let Some(partner) = self.partner(position) else {
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
- for pair in aligned_names(strings.as_ref(), &reference[partner - 1], encoded) {
161
- *votes.entry(pair).or_default() += 1;
162
- *all.entry(pair).or_default() += 1;
163
- }
164
- for (session, reference) in Names::one_to_one(&votes).to_reference {
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
- for (session, reference) in Names::one_to_one(&all).to_reference {
169
- self.global.pair(session, reference);
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, &|text| {
304
- rewrite_text(text, &|name| {
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, &|text| {
312
- rewrite_text(text, &|name| {
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
- fn expand_lists<S: AsRef<[String]>>(probes: &[S]) -> Vec<Vec<String>> {
320
- probes
321
- .iter()
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
- pub(crate) fn rewrite_object(object: &mut JsObject, rename: &impl Fn(&mut String)) {
425
- for (mut key, mut item) in mem::take(object) {
426
- rewrite(&mut item, rename);
427
- rename(&mut key);
428
- object.insert(key, item);
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
- /// Session names also occur inside error messages, markup, property paths and digest inputs.
433
- /// Read replacements from the original string so a replacement is never renamed twice.
434
- fn rewrite_text<'a>(text: &mut String, rename: &impl Fn(&str) -> Option<&'a String>) {
435
- if let Some(name) = rename(text) {
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| name(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| name(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 ASCII letters and digits, and other characters.
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
- fn alignment_name(text: &str) -> bool {
746
- if matches!(
747
- text,
748
- "Array"
749
- | "Boolean"
750
- | "Date"
751
- | "Error"
752
- | "Function"
753
- | "JSON"
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
- pub(crate) fn is_name(text: &str) -> bool {
773
- if hashed_name(text) {
774
- return true;
775
- }
776
- (4..=9).contains(&text.len())
777
- && text.starts_with(|c: char| c.is_ascii_alphabetic())
778
- && text.chars().all(|c| c.is_ascii_alphanumeric())
779
- && text
780
- .chars()
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 expands_only_valid_decoded_name_lists() {
838
- let valid = "%41%61%61%61%31%7c%42%62%62%62%32%7c%43%63%63%63%33";
839
- let features = strings(&format!("{valid} decodeURIComponent {valid} | split"));
840
- let expanded = expand_lists(&[features]);
841
- assert_eq!(&expanded[0][2..], ["Aaaa1", "Bbbb2", "Cccc3"]);
842
- let fonts = vec![
843
- strings("Sample Sans|Sample Serif|Sample Monospace").join(" "),
844
- "|".into(),
845
- "split".into(),
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
- expand_lists(&[fonts])[0],
849
- ["Sample Sans", "Sample Serif", "Sample Monospace"]
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
- for text in ["%41%", "%41%xx", "%FF", "Aaaa1|Bbbb2", "Aaaa1||Cccc3"] {
852
- let features = strings(&format!("{text} decodeURIComponent {text} | split"));
853
- assert_eq!(expand_lists(std::slice::from_ref(&features))[0], features);
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), false),
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
- pub(crate) mod session_names;
24
+ mod stylesheets;
24
25
  pub mod trace;
25
26
  pub mod vocabulary;
26
27
  mod worker_timing;