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
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
//! Recovers result fields whose reference names are held in header registers rather than
|
|
2
|
-
//! appearing among a probe's collection strings.
|
|
3
|
-
|
|
4
|
-
use std::collections::{BTreeMap, BTreeSet};
|
|
5
|
-
|
|
6
|
-
use indexmap::IndexMap;
|
|
7
|
-
use swc_core::ecma::ast::BinaryOp;
|
|
8
|
-
|
|
9
|
-
use crate::listing::Index;
|
|
10
|
-
use crate::stage2::reference::{
|
|
11
|
-
COMPUTED_STYLE_MAP, FONT_CANVAS_OUTPUTS, FONT_DOM_OUTPUTS, FONT_FACE_OUTPUTS,
|
|
12
|
-
NATIVE_INSPECTION_OUTPUTS,
|
|
13
|
-
};
|
|
14
|
-
use crate::stage2::{registry::Kind, vocabulary::hashed_name};
|
|
15
|
-
use crate::vm::{Constant, Term};
|
|
16
|
-
|
|
17
|
-
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
18
|
-
enum Object {
|
|
19
|
-
Created(u32),
|
|
20
|
-
Property(u32),
|
|
21
|
-
Cell(u32),
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
#[derive(Default)]
|
|
25
|
-
struct Fields(IndexMap<String, Vec<Term>>);
|
|
26
|
-
|
|
27
|
-
impl Fields {
|
|
28
|
-
fn push(&mut self, name: &str, value: Option<Term>) {
|
|
29
|
-
self.0.entry(name.to_owned()).or_default().extend(value);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
fn has(&self, position: usize, predicate: impl Fn(&Term) -> bool) -> bool {
|
|
33
|
-
self.0
|
|
34
|
-
.get_index(position)
|
|
35
|
-
.is_some_and(|(_, values)| values.iter().any(predicate))
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
pub(super) fn names(
|
|
40
|
-
index: &Index<'_>,
|
|
41
|
-
entry: u32,
|
|
42
|
-
reference: usize,
|
|
43
|
-
common: &BTreeSet<String>,
|
|
44
|
-
) -> Vec<(String, &'static str)> {
|
|
45
|
-
let Some(
|
|
46
|
-
kind @ (Kind::NativeInspection
|
|
47
|
-
| Kind::FontEnumeration
|
|
48
|
-
| Kind::ComputedStyleHash
|
|
49
|
-
| Kind::Rects),
|
|
50
|
-
) = Kind::from_reference(reference)
|
|
51
|
-
else {
|
|
52
|
-
return Vec::new();
|
|
53
|
-
};
|
|
54
|
-
let reached = index.reachable(entry);
|
|
55
|
-
if kind == Kind::Rects {
|
|
56
|
-
let roles = [
|
|
57
|
-
("bottom", "Cmnj0"),
|
|
58
|
-
("height", "VwYDD8"),
|
|
59
|
-
("left", "knUS7"),
|
|
60
|
-
("right", "cyNU4"),
|
|
61
|
-
("top", "nCLYc7"),
|
|
62
|
-
("width", "UCluM4"),
|
|
63
|
-
("x", "YptYf3"),
|
|
64
|
-
("y", "wAVRH3"),
|
|
65
|
-
];
|
|
66
|
-
return roles
|
|
67
|
-
.into_iter()
|
|
68
|
-
.filter_map(|(property, reference)| {
|
|
69
|
-
let names: BTreeSet<_> = index
|
|
70
|
-
.stores()
|
|
71
|
-
.iter()
|
|
72
|
-
.filter(|store| {
|
|
73
|
-
reached.contains(&store.pc)
|
|
74
|
-
&& hashed_name(&store.key)
|
|
75
|
-
&& index.method(store.pc, &store.value).as_deref() == Some(property)
|
|
76
|
-
})
|
|
77
|
-
.map(|store| store.key.clone())
|
|
78
|
-
.collect();
|
|
79
|
-
(names.len() == 1).then(|| (names.into_iter().next().unwrap(), reference))
|
|
80
|
-
})
|
|
81
|
-
.collect();
|
|
82
|
-
}
|
|
83
|
-
let mut groups = BTreeMap::<Object, Fields>::new();
|
|
84
|
-
for store in index
|
|
85
|
-
.stores()
|
|
86
|
-
.iter()
|
|
87
|
-
.filter(|store| reached.contains(&store.pc))
|
|
88
|
-
{
|
|
89
|
-
if !hashed_name(&store.key) || common.contains(&store.key) {
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
let Some((origin, object)) = index.dereference(store.pc, &Term::Reg(store.object)) else {
|
|
93
|
-
continue;
|
|
94
|
-
};
|
|
95
|
-
let object = match object {
|
|
96
|
-
Term::Object(_) => Object::Created(origin),
|
|
97
|
-
Term::Get(_, _) => Object::Property(origin),
|
|
98
|
-
Term::Cell(cell) => Object::Cell(cell),
|
|
99
|
-
_ => continue,
|
|
100
|
-
};
|
|
101
|
-
let value = index
|
|
102
|
-
.dereference(store.pc, &store.value)
|
|
103
|
-
.map(|(_, value)| value);
|
|
104
|
-
groups.entry(object).or_default().push(&store.key, value);
|
|
105
|
-
}
|
|
106
|
-
let mut pairs = Vec::new();
|
|
107
|
-
let mut pin = |candidates: Vec<&Fields>, names: &[&'static str]| {
|
|
108
|
-
if let [fields] = candidates.as_slice() {
|
|
109
|
-
pairs.extend(
|
|
110
|
-
fields
|
|
111
|
-
.0
|
|
112
|
-
.keys()
|
|
113
|
-
.take(names.len())
|
|
114
|
-
.cloned()
|
|
115
|
-
.zip(names.iter().copied()),
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
match kind {
|
|
120
|
-
Kind::NativeInspection => pin(
|
|
121
|
-
groups
|
|
122
|
-
.iter()
|
|
123
|
-
.filter_map(|(object, fields)| {
|
|
124
|
-
(matches!(object, Object::Property(_)) && fields.0.len() == 5).then_some(fields)
|
|
125
|
-
})
|
|
126
|
-
.collect(),
|
|
127
|
-
&NATIVE_INSPECTION_OUTPUTS,
|
|
128
|
-
),
|
|
129
|
-
Kind::ComputedStyleHash => pin(
|
|
130
|
-
groups
|
|
131
|
-
.iter()
|
|
132
|
-
.filter_map(|(object, fields)| {
|
|
133
|
-
(matches!(object, Object::Property(_)) && fields.0.len() == 1).then_some(fields)
|
|
134
|
-
})
|
|
135
|
-
.collect(),
|
|
136
|
-
&[COMPUTED_STYLE_MAP],
|
|
137
|
-
),
|
|
138
|
-
Kind::FontEnumeration => {
|
|
139
|
-
// FontFace's fallback is a list, a count, a status, a hash and an error.
|
|
140
|
-
pin(
|
|
141
|
-
groups
|
|
142
|
-
.iter()
|
|
143
|
-
.filter_map(|(object, fields)| {
|
|
144
|
-
(matches!(object, Object::Created(_))
|
|
145
|
-
&& fields.0.len() == 5
|
|
146
|
-
&& fields.has(
|
|
147
|
-
0,
|
|
148
|
-
|value| matches!(value, Term::Array(values) if values.is_empty()),
|
|
149
|
-
)
|
|
150
|
-
&& fields.has(1, |value| {
|
|
151
|
-
matches!(value, Term::Const(Constant::Number(0.0)))
|
|
152
|
-
}))
|
|
153
|
-
.then_some(fields)
|
|
154
|
-
})
|
|
155
|
-
.collect(),
|
|
156
|
-
&FONT_FACE_OUTPUTS,
|
|
157
|
-
);
|
|
158
|
-
// Canvas measurements have three array defaults.
|
|
159
|
-
pin(
|
|
160
|
-
groups
|
|
161
|
-
.iter()
|
|
162
|
-
.filter_map(|(object, fields)| {
|
|
163
|
-
let arrays = (0..3).all(|position| {
|
|
164
|
-
fields.has(
|
|
165
|
-
position,
|
|
166
|
-
|value| matches!(value, Term::Array(values) if values.is_empty()),
|
|
167
|
-
)
|
|
168
|
-
});
|
|
169
|
-
(matches!(object, Object::Created(_)) && fields.0.len() == 3 && arrays)
|
|
170
|
-
.then_some(fields)
|
|
171
|
-
})
|
|
172
|
-
.collect(),
|
|
173
|
-
&FONT_CANVAS_OUTPUTS,
|
|
174
|
-
);
|
|
175
|
-
// The DOM measurement callback writes an array and a sum into its shared result.
|
|
176
|
-
pin(
|
|
177
|
-
groups
|
|
178
|
-
.iter()
|
|
179
|
-
.filter_map(|(object, fields)| {
|
|
180
|
-
(matches!(object, Object::Cell(_))
|
|
181
|
-
&& fields.0.len() == 2
|
|
182
|
-
&& fields.has(
|
|
183
|
-
0,
|
|
184
|
-
|value| matches!(value, Term::Array(values) if values.is_empty()),
|
|
185
|
-
)
|
|
186
|
-
&& fields.has(1, |value| {
|
|
187
|
-
matches!(value, Term::Binary(BinaryOp::Add, _, _))
|
|
188
|
-
}))
|
|
189
|
-
.then_some(fields)
|
|
190
|
-
})
|
|
191
|
-
.collect(),
|
|
192
|
-
&FONT_DOM_OUTPUTS,
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
_ => {}
|
|
196
|
-
}
|
|
197
|
-
pairs
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
#[cfg(test)]
|
|
201
|
-
mod tests {
|
|
202
|
-
use super::*;
|
|
203
|
-
use crate::vm::{Effect, Exit, Instruction, Listing, Path};
|
|
204
|
-
|
|
205
|
-
fn listing(groups: &[(Term, Vec<Term>)]) -> Listing {
|
|
206
|
-
let mut effects = Vec::new();
|
|
207
|
-
for (group, (object, values)) in groups.iter().enumerate() {
|
|
208
|
-
effects.push(Effect::Set(1, object.clone()));
|
|
209
|
-
for (position, value) in values.iter().enumerate() {
|
|
210
|
-
effects.push(Effect::SetProperty {
|
|
211
|
-
object: Box::new(Term::Reg(1)),
|
|
212
|
-
key: Box::new(Term::Const(Constant::String(format!(
|
|
213
|
-
"_{:024x}",
|
|
214
|
-
group * 10 + position
|
|
215
|
-
)))),
|
|
216
|
-
value: Box::new(value.clone()),
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
let count = effects.len();
|
|
221
|
-
let instructions = effects
|
|
222
|
-
.into_iter()
|
|
223
|
-
.enumerate()
|
|
224
|
-
.map(|(pc, effect)| {
|
|
225
|
-
(
|
|
226
|
-
pc as u32,
|
|
227
|
-
Instruction {
|
|
228
|
-
pc: pc as u32,
|
|
229
|
-
key: 0,
|
|
230
|
-
opcode: 0,
|
|
231
|
-
paths: vec![Path {
|
|
232
|
-
conditions: vec![],
|
|
233
|
-
effects: vec![effect],
|
|
234
|
-
exit: if pc + 1 == count {
|
|
235
|
-
Exit::Return
|
|
236
|
-
} else {
|
|
237
|
-
Exit::Next {
|
|
238
|
-
pc: pc as u32 + 1,
|
|
239
|
-
key: 0,
|
|
240
|
-
}
|
|
241
|
-
},
|
|
242
|
-
}],
|
|
243
|
-
},
|
|
244
|
-
)
|
|
245
|
-
})
|
|
246
|
-
.collect();
|
|
247
|
-
Listing {
|
|
248
|
-
instructions,
|
|
249
|
-
..Listing::default()
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
#[test]
|
|
254
|
-
fn recovers_font_roles_from_distinct_object_shapes_and_rejects_ambiguity() {
|
|
255
|
-
let empty = Term::Array(vec![]);
|
|
256
|
-
let zero = Term::Const(Constant::Number(0.0));
|
|
257
|
-
let face = (
|
|
258
|
-
Term::Object(vec![]),
|
|
259
|
-
vec![
|
|
260
|
-
empty.clone(),
|
|
261
|
-
zero.clone(),
|
|
262
|
-
zero.clone(),
|
|
263
|
-
zero.clone(),
|
|
264
|
-
zero.clone(),
|
|
265
|
-
],
|
|
266
|
-
);
|
|
267
|
-
let mut groups = vec![
|
|
268
|
-
face.clone(),
|
|
269
|
-
(Term::Object(vec![]), vec![empty.clone(); 3]),
|
|
270
|
-
(
|
|
271
|
-
Term::Cell(8),
|
|
272
|
-
vec![
|
|
273
|
-
empty,
|
|
274
|
-
Term::Binary(BinaryOp::Add, Box::new(Term::Reg(9)), Box::new(zero)),
|
|
275
|
-
],
|
|
276
|
-
),
|
|
277
|
-
];
|
|
278
|
-
let reference = Kind::FontEnumeration.references()[0];
|
|
279
|
-
let program = listing(&groups);
|
|
280
|
-
let pairs = names(&Index::new(&program), 0, reference, &BTreeSet::new());
|
|
281
|
-
let expected: Vec<_> = FONT_FACE_OUTPUTS
|
|
282
|
-
.into_iter()
|
|
283
|
-
.chain(FONT_CANVAS_OUTPUTS)
|
|
284
|
-
.chain(FONT_DOM_OUTPUTS)
|
|
285
|
-
.collect();
|
|
286
|
-
assert_eq!(
|
|
287
|
-
pairs
|
|
288
|
-
.iter()
|
|
289
|
-
.map(|(_, reference)| *reference)
|
|
290
|
-
.collect::<Vec<_>>(),
|
|
291
|
-
expected
|
|
292
|
-
);
|
|
293
|
-
groups.push(face);
|
|
294
|
-
let program = listing(&groups);
|
|
295
|
-
let pairs = names(&Index::new(&program), 0, reference, &BTreeSet::new());
|
|
296
|
-
assert!(
|
|
297
|
-
pairs
|
|
298
|
-
.iter()
|
|
299
|
-
.all(|(_, field)| !FONT_FACE_OUTPUTS.contains(field))
|
|
300
|
-
);
|
|
301
|
-
assert_eq!(
|
|
302
|
-
pairs.len(),
|
|
303
|
-
FONT_CANVAS_OUTPUTS.len() + FONT_DOM_OUTPUTS.len()
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
#[test]
|
|
308
|
-
fn native_inspection_requires_a_unique_result_object() {
|
|
309
|
-
let object = Term::Get(
|
|
310
|
-
Box::new(Term::Cell(8)),
|
|
311
|
-
Box::new(Term::Const(Constant::String("result".into()))),
|
|
312
|
-
);
|
|
313
|
-
let group = (object, vec![Term::Const(Constant::Null); 5]);
|
|
314
|
-
let program = listing(std::slice::from_ref(&group));
|
|
315
|
-
let reference = Kind::NativeInspection.references()[0];
|
|
316
|
-
let pairs = names(&Index::new(&program), 0, reference, &BTreeSet::new());
|
|
317
|
-
assert_eq!(
|
|
318
|
-
pairs
|
|
319
|
-
.iter()
|
|
320
|
-
.map(|(_, reference)| *reference)
|
|
321
|
-
.collect::<Vec<_>>(),
|
|
322
|
-
NATIVE_INSPECTION_OUTPUTS
|
|
323
|
-
);
|
|
324
|
-
let program = listing(&[group.clone(), group]);
|
|
325
|
-
assert!(names(&Index::new(&program), 0, reference, &BTreeSet::new()).is_empty());
|
|
326
|
-
}
|
|
327
|
-
}
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
//! Replays captured CSSOM formatting after validating a stylesheet's structure.
|
|
2
|
-
|
|
3
|
-
use anyhow::{Context, Result, bail, ensure};
|
|
4
|
-
use regex::Regex;
|
|
5
|
-
use serde::{Deserialize, Serialize};
|
|
6
|
-
use sha2::{Digest, Sha256};
|
|
7
|
-
use std::collections::BTreeMap;
|
|
8
|
-
use std::sync::LazyLock;
|
|
9
|
-
|
|
10
|
-
use crate::stage2::{
|
|
11
|
-
records::Probe,
|
|
12
|
-
reference::{STYLESHEET_DIGEST, STYLESHEET_MARKER},
|
|
13
|
-
vocabulary,
|
|
14
|
-
};
|
|
15
|
-
use crate::{
|
|
16
|
-
candidates,
|
|
17
|
-
js::{JsObject, json},
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
static SESSION_NAME: LazyLock<Regex> =
|
|
21
|
-
LazyLock::new(|| Regex::new(r"_[0-9a-fA-F]{24}|[A-Za-z]{3,5}[0-9]").unwrap());
|
|
22
|
-
|
|
23
|
-
/// Source text paired with the rules the profile's browser exposed through CSSOM.
|
|
24
|
-
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
25
|
-
pub struct Sheet {
|
|
26
|
-
pub source: String,
|
|
27
|
-
pub rules: Vec<String>,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/// One captured stylesheet revision. Multiple revisions can share a device profile.
|
|
31
|
-
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
32
|
-
pub struct Snapshot {
|
|
33
|
-
pub sheets: Vec<Sheet>,
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
impl Snapshot {
|
|
37
|
-
pub fn capture(html: &str, rules: Vec<Vec<String>>) -> Result<Self> {
|
|
38
|
-
let sources = sources(html);
|
|
39
|
-
ensure!(
|
|
40
|
-
sources.len() == rules.len(),
|
|
41
|
-
"CSSOM rule lists do not match the iframe's stylesheets"
|
|
42
|
-
);
|
|
43
|
-
Ok(Self {
|
|
44
|
-
sheets: sources
|
|
45
|
-
.into_iter()
|
|
46
|
-
.zip(rules)
|
|
47
|
-
.map(|(source, rules)| Sheet { source, rules })
|
|
48
|
-
.collect(),
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/// Requires identical text outside randomized names and a consistent one-to-one name map.
|
|
53
|
-
pub fn replay(&self, sources: &[String]) -> Result<Vec<Vec<String>>> {
|
|
54
|
-
ensure!(
|
|
55
|
-
sources.len() == self.sheets.len(),
|
|
56
|
-
"stylesheet count changed"
|
|
57
|
-
);
|
|
58
|
-
let mut names = BTreeMap::new();
|
|
59
|
-
let mut reverse = BTreeMap::new();
|
|
60
|
-
for (sheet, current) in self.sheets.iter().zip(sources) {
|
|
61
|
-
ensure!(
|
|
62
|
-
SESSION_NAME
|
|
63
|
-
.split(&sheet.source)
|
|
64
|
-
.eq(SESSION_NAME.split(current)),
|
|
65
|
-
"stylesheet structure changed"
|
|
66
|
-
);
|
|
67
|
-
for (old, new) in SESSION_NAME
|
|
68
|
-
.find_iter(&sheet.source)
|
|
69
|
-
.zip(SESSION_NAME.find_iter(current))
|
|
70
|
-
{
|
|
71
|
-
let (old, new) = (old.as_str(), new.as_str());
|
|
72
|
-
ensure!(
|
|
73
|
-
names
|
|
74
|
-
.insert(old, new)
|
|
75
|
-
.is_none_or(|previous| previous == new),
|
|
76
|
-
"inconsistent stylesheet name"
|
|
77
|
-
);
|
|
78
|
-
ensure!(
|
|
79
|
-
reverse
|
|
80
|
-
.insert(new, old)
|
|
81
|
-
.is_none_or(|previous| previous == old),
|
|
82
|
-
"ambiguous stylesheet name"
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
Ok(self
|
|
87
|
-
.sheets
|
|
88
|
-
.iter()
|
|
89
|
-
.map(|sheet| {
|
|
90
|
-
sheet
|
|
91
|
-
.rules
|
|
92
|
-
.iter()
|
|
93
|
-
.map(|rule| {
|
|
94
|
-
SESSION_NAME
|
|
95
|
-
.replace_all(rule, |captures: ®ex::Captures<'_>| {
|
|
96
|
-
names
|
|
97
|
-
.get(&captures[0])
|
|
98
|
-
.copied()
|
|
99
|
-
.unwrap_or(&captures[0])
|
|
100
|
-
.to_owned()
|
|
101
|
-
})
|
|
102
|
-
.into_owned()
|
|
103
|
-
})
|
|
104
|
-
.collect()
|
|
105
|
-
})
|
|
106
|
-
.collect())
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
#[must_use]
|
|
111
|
-
pub(crate) fn sources(html: &str) -> Vec<String> {
|
|
112
|
-
let document = scraper::Html::parse_document(html);
|
|
113
|
-
let selector = scraper::Selector::parse("style").unwrap();
|
|
114
|
-
document
|
|
115
|
-
.select(&selector)
|
|
116
|
-
.map(|style| style.inner_html())
|
|
117
|
-
.collect()
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/// Updates the stored fields before replay translates reference names and arranges the record.
|
|
121
|
-
pub(crate) fn refresh(
|
|
122
|
-
snapshots: &[Snapshot],
|
|
123
|
-
probe: &Probe,
|
|
124
|
-
html: &str,
|
|
125
|
-
fields: &mut JsObject,
|
|
126
|
-
) -> Result<()> {
|
|
127
|
-
let markers = probe
|
|
128
|
-
.features
|
|
129
|
-
.iter()
|
|
130
|
-
.filter(|text| text.strip_prefix('.').is_some_and(vocabulary::is_name));
|
|
131
|
-
let marker = candidates::at_most_one_distinct(markers, "stylesheet marker")?
|
|
132
|
-
.context("no stylesheet marker")?;
|
|
133
|
-
let (digest, marker_css) = measurement(snapshots, html, marker)?;
|
|
134
|
-
for (field, value) in [(STYLESHEET_DIGEST, digest), (STYLESHEET_MARKER, marker_css)] {
|
|
135
|
-
*fields
|
|
136
|
-
.get_mut(field)
|
|
137
|
-
.with_context(|| format!("missing stylesheet field {field}"))? = json!(value);
|
|
138
|
-
}
|
|
139
|
-
Ok(())
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
fn measurement(snapshots: &[Snapshot], html: &str, marker: &str) -> Result<(String, String)> {
|
|
143
|
-
let sources = sources(html);
|
|
144
|
-
for snapshot in snapshots {
|
|
145
|
-
let Ok(sheets) = snapshot.replay(&sources) else {
|
|
146
|
-
continue;
|
|
147
|
-
};
|
|
148
|
-
let mut marker_text = None;
|
|
149
|
-
let mut measured = Vec::new();
|
|
150
|
-
for (index, rules) in sheets.iter().enumerate() {
|
|
151
|
-
let text = rules.join("|");
|
|
152
|
-
let indexed = format!("{text}{index}");
|
|
153
|
-
if text.contains(marker) {
|
|
154
|
-
ensure!(
|
|
155
|
-
marker_text.replace(indexed).is_none(),
|
|
156
|
-
"multiple marker stylesheets"
|
|
157
|
-
);
|
|
158
|
-
} else {
|
|
159
|
-
measured.push(indexed);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
let marker_text = marker_text.context("the current marker stylesheet is absent")?;
|
|
163
|
-
let digest = format!("{:x}", Sha256::digest(measured.join("|").as_bytes()));
|
|
164
|
-
return Ok((digest[..32].to_owned(), marker_text));
|
|
165
|
-
}
|
|
166
|
-
bail!("no captured stylesheet revision matches the current iframe")
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
#[cfg(test)]
|
|
170
|
-
mod tests {
|
|
171
|
-
use super::*;
|
|
172
|
-
|
|
173
|
-
#[test]
|
|
174
|
-
fn refreshes_names_in_browser_serialized_rules_without_running_a_browser() {
|
|
175
|
-
let snapshot = Snapshot {
|
|
176
|
-
sheets: vec![Sheet {
|
|
177
|
-
source: ".Abcd1{margin:0}.Abcd1 .Efgh2{color:red}".into(),
|
|
178
|
-
rules: vec![
|
|
179
|
-
".Abcd1 { margin: 0px; }".into(),
|
|
180
|
-
".Abcd1 .Efgh2 { color: red; }".into(),
|
|
181
|
-
],
|
|
182
|
-
}],
|
|
183
|
-
};
|
|
184
|
-
assert_eq!(
|
|
185
|
-
snapshot
|
|
186
|
-
.replay(&[".Ijkl3{margin:0}.Ijkl3 .Mnop4{color:red}".into()])
|
|
187
|
-
.unwrap(),
|
|
188
|
-
[vec![
|
|
189
|
-
".Ijkl3 { margin: 0px; }",
|
|
190
|
-
".Ijkl3 .Mnop4 { color: red; }"
|
|
191
|
-
]]
|
|
192
|
-
);
|
|
193
|
-
assert!(
|
|
194
|
-
snapshot
|
|
195
|
-
.replay(&[".Ijkl3{margin:1}.Ijkl3 .Mnop4{color:red}".into()])
|
|
196
|
-
.is_err()
|
|
197
|
-
);
|
|
198
|
-
assert!(
|
|
199
|
-
snapshot
|
|
200
|
-
.replay(&[".Ijkl3{margin:0}.Mnop4 .Qrst5{color:red}".into()])
|
|
201
|
-
.is_err()
|
|
202
|
-
);
|
|
203
|
-
assert!(
|
|
204
|
-
snapshot
|
|
205
|
-
.replay(&[".Ijkl3{margin:0}.Ijkl3 .Ijkl3{color:red}".into()])
|
|
206
|
-
.is_err()
|
|
207
|
-
);
|
|
208
|
-
assert!(snapshot.replay(&[]).is_err());
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
#[test]
|
|
212
|
-
fn measures_current_rules_and_excludes_the_marker_sheet() {
|
|
213
|
-
let html = "<style>.Abcd1{color:red}</style><style>.Efgh2{margin:0}</style>";
|
|
214
|
-
let snapshot = Snapshot::capture(
|
|
215
|
-
html,
|
|
216
|
-
vec![
|
|
217
|
-
vec![".Abcd1 { color: red; }".into()],
|
|
218
|
-
vec![".Efgh2 { margin: 0px; }".into()],
|
|
219
|
-
],
|
|
220
|
-
)
|
|
221
|
-
.unwrap();
|
|
222
|
-
let fresh = "<style>.Ijkl3{color:red}</style><style>.Mnop4{margin:0}</style>";
|
|
223
|
-
let (hash, marker) = measurement(std::slice::from_ref(&snapshot), fresh, ".Mnop4").unwrap();
|
|
224
|
-
assert_eq!(
|
|
225
|
-
hash,
|
|
226
|
-
format!("{:x}", Sha256::digest(b".Ijkl3 { color: red; }0"))[..32]
|
|
227
|
-
);
|
|
228
|
-
assert_eq!(marker, ".Mnop4 { margin: 0px; }1");
|
|
229
|
-
assert!(measurement(&[snapshot], fresh, ".Missing5").is_err());
|
|
230
|
-
assert!(measurement(&[], fresh, ".Mnop4").is_err());
|
|
231
|
-
assert!(Snapshot::capture(html, vec![]).is_err());
|
|
232
|
-
let duplicate = Snapshot::capture(
|
|
233
|
-
"<style>.Abcd1{color:red}</style><style>.Abcd1{margin:0}</style>",
|
|
234
|
-
vec![
|
|
235
|
-
vec![".Abcd1 { color: red; }".into()],
|
|
236
|
-
vec![".Abcd1 { margin: 0px; }".into()],
|
|
237
|
-
],
|
|
238
|
-
)
|
|
239
|
-
.unwrap();
|
|
240
|
-
assert!(
|
|
241
|
-
measurement(
|
|
242
|
-
&[duplicate],
|
|
243
|
-
"<style>.Abcd1{color:red}</style><style>.Abcd1{margin:0}</style>",
|
|
244
|
-
".Abcd1"
|
|
245
|
-
)
|
|
246
|
-
.is_err()
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
}
|