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
|
@@ -378,14 +378,6 @@
|
|
|
378
378
|
{
|
|
379
379
|
"candidate": "candidate:3747697418 1 udp 2113937151 f9501b39-1d57-4eb6-8029-16167a5a330b.local 51498 typ host generation 0 ufrag mtbG network-cost 999",
|
|
380
380
|
"observed_ms": 1792876992039
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
"candidate": "candidate:1648342598 1 udp 1677729535 192.0.2.1 64597 typ srflx raddr 0.0.0.0 rport 0 generation 0 ufrag mtbG network-cost 999",
|
|
384
|
-
"observed_ms": 1792876992052
|
|
385
|
-
},
|
|
386
|
-
{
|
|
387
|
-
"candidate": "candidate:2007285888 1 udp 1677732095 2001:db8::1 59340 typ srflx raddr :: rport 0 generation 0 ufrag mtbG network-cost 999",
|
|
388
|
-
"observed_ms": 1792876992056
|
|
389
381
|
}
|
|
390
382
|
],
|
|
391
383
|
"data_channel": true,
|
data/lib/cfsolve/version.rb
CHANGED
data/vendor/cfsolve/README.md
CHANGED
|
@@ -21,41 +21,6 @@ set to the address of a running `capture-proxy`, which records the decoded
|
|
|
21
21
|
payloads sent to Cloudflare. Currently only Safari and Chrome(ium) profile
|
|
22
22
|
types are supported.
|
|
23
23
|
|
|
24
|
-
Newer builds also rename values embedded in probe answers, namespace properties,
|
|
25
|
-
and stylesheet selectors on every session. Rebuild the answers with
|
|
26
|
-
`cargo run --release --example build_profile -- capture.json > answers.json`
|
|
27
|
-
to include the source evidence needed to refresh those values. Existing answer
|
|
28
|
-
files remain readable, but do not contain this additional evidence.
|
|
29
|
-
|
|
30
|
-
For builds that vary stylesheet selectors, record the profile browser's CSSOM
|
|
31
|
-
formatting once. In the challenge iframe's execution context, save the JSON
|
|
32
|
-
result of:
|
|
33
|
-
|
|
34
|
-
```javascript
|
|
35
|
-
JSON.stringify(Array.from(document.styleSheets, sheet =>
|
|
36
|
-
Array.from(sheet.cssRules, rule => rule.cssText)))
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Pass that file as the second argument to `build_profile`. The answers then store
|
|
40
|
-
the stylesheet source alongside its browser formatting. At solve time cfsolve
|
|
41
|
-
checks that only randomized names have changed, substitutes the current names,
|
|
42
|
-
and recomputes the stylesheet measurements without a browser. If the stylesheet
|
|
43
|
-
structure changes, capture the new revision; the solver rejects an unmatched
|
|
44
|
-
revision instead of sending an obsolete digest. The `stylesheets` array can hold
|
|
45
|
-
multiple captured revisions for the same browser profile. Keep captures and
|
|
46
|
-
device profiles private; they are not needed in bug reports or pull requests.
|
|
47
|
-
|
|
48
|
-
Replay keeps browser measurements while deriving field names, namespace functions
|
|
49
|
-
and trace frames from the current program. Function declaration order and function
|
|
50
|
-
counts may change between builds. Trace frames come from the current program;
|
|
51
|
-
captured frames are not replayed. Unmapped fields and ambiguous stylesheet
|
|
52
|
-
markers are errors, rather than guessed answers.
|
|
53
|
-
|
|
54
|
-
The internal `Replay::new` constructor now takes the current `Source` and
|
|
55
|
-
`Installed` facts and returns a `Result`. It validates the profile and field
|
|
56
|
-
mapping before use; there is no separate unchecked replay constructor. Stylesheet
|
|
57
|
-
updates use the same replay path, preserving field order and late callback fields.
|
|
58
|
-
|
|
59
24
|
## Building
|
|
60
25
|
|
|
61
26
|
You need the Rust version pinned in `rust-toolchain.toml`, plus CMake, a C/C++
|
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
//! Store a real browser's probe answers for a device profile.
|
|
2
|
-
//! Usage: build_profile <capture.json> [cssom
|
|
2
|
+
//! Usage: build_profile <capture.json> [cssom.json] > answers.json
|
|
3
3
|
//!
|
|
4
4
|
//! The capture is a session's `capture.json` from the capture proxy, or what
|
|
5
5
|
//! `scripts/cdp_capture.mjs` writes: the iframe's HTML, its POSTs with their
|
|
6
6
|
//! responses, and the plaintext payloads. Each probe's record from the second
|
|
7
7
|
//! payload is stored under the reference build's names; see
|
|
8
|
-
//! `cfsolve::stage2::replay`.
|
|
8
|
+
//! `cfsolve::stage2::replay`. An adjacent `iframe.deobfuscated.js` (from
|
|
9
|
+
//! `capture_session`) is used instead of deobfuscating the capture's script.
|
|
9
10
|
|
|
10
|
-
use std::env;
|
|
11
11
|
use std::path::Path;
|
|
12
|
+
use std::{env, fs};
|
|
12
13
|
|
|
13
14
|
use anyhow::{Context, Result};
|
|
14
15
|
use cfsolve::capture::CapturedSession;
|
|
15
|
-
use cfsolve::stage2::replay::
|
|
16
|
+
use cfsolve::stage2::replay::Answers;
|
|
16
17
|
|
|
17
18
|
fn main() -> Result<()> {
|
|
18
|
-
let
|
|
19
|
+
let mut arguments = env::args().skip(1);
|
|
20
|
+
let path = arguments.next().context("supply a capture JSON file")?;
|
|
19
21
|
let capture = CapturedSession::read(Path::new(&path))?;
|
|
20
|
-
let
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
let source = capture.source_beside(Path::new(&path))?;
|
|
23
|
+
let program = capture.program()?;
|
|
24
|
+
let mut answers = Answers::capture(&source, &program, capture.probe_payload()?)?;
|
|
25
|
+
if let Some(path) = arguments.next() {
|
|
26
|
+
answers.measure_stylesheets(
|
|
27
|
+
&source,
|
|
28
|
+
&program,
|
|
29
|
+
capture.iframe_html(),
|
|
30
|
+
serde_json::from_str(&fs::read_to_string(path)?)?,
|
|
31
|
+
)?;
|
|
30
32
|
}
|
|
31
33
|
println!("{}", serde_json::to_string_pretty(&answers)?);
|
|
32
34
|
Ok(())
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//! Write a capture as a session directory, for `stage2_snapshot` and other tools that read the
|
|
2
|
+
//! challenge CLI's output.
|
|
3
|
+
//! Usage: capture_session <capture.json> <output-directory>
|
|
4
|
+
//!
|
|
5
|
+
//! Writes `iframe.js`, `iframe.deobfuscated.js`, and each POST response's program as
|
|
6
|
+
//! `response.decoded.txt`, `second-response.decoded.txt` and `third-response.decoded.txt`.
|
|
7
|
+
|
|
8
|
+
use std::path::Path;
|
|
9
|
+
use std::{env, fs};
|
|
10
|
+
|
|
11
|
+
use anyhow::{Context, Result};
|
|
12
|
+
use base64::Engine;
|
|
13
|
+
use base64::engine::general_purpose::STANDARD;
|
|
14
|
+
use cfsolve::capture::CapturedSession;
|
|
15
|
+
|
|
16
|
+
fn main() -> Result<()> {
|
|
17
|
+
let mut arguments = env::args().skip(1);
|
|
18
|
+
let capture = arguments.next().context("supply a capture.json")?;
|
|
19
|
+
let directory = arguments.next().context("supply an output directory")?;
|
|
20
|
+
let capture = CapturedSession::read(Path::new(&capture))?;
|
|
21
|
+
let directory = Path::new(&directory);
|
|
22
|
+
fs::create_dir_all(directory)?;
|
|
23
|
+
let javascript = capture.javascript()?;
|
|
24
|
+
fs::write(directory.join("iframe.js"), &javascript)?;
|
|
25
|
+
fs::write(
|
|
26
|
+
directory.join("iframe.deobfuscated.js"),
|
|
27
|
+
cfsolve::deobfuscate(javascript)?.text,
|
|
28
|
+
)?;
|
|
29
|
+
let names = ["response", "second-response", "third-response"];
|
|
30
|
+
for (name, program) in names.iter().zip(capture.programs()?) {
|
|
31
|
+
fs::write(
|
|
32
|
+
directory.join(format!("{name}.decoded.txt")),
|
|
33
|
+
STANDARD.encode(program),
|
|
34
|
+
)?;
|
|
35
|
+
}
|
|
36
|
+
Ok(())
|
|
37
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
//! Check stored answers against captured sessions of the current build.
|
|
2
|
+
//! Usage: check_answers <answers.json> <capture.json>...
|
|
3
|
+
//!
|
|
4
|
+
//! Translates each replayed probe's stored record into the session's names, as a solve does,
|
|
5
|
+
//! and compares its fields with the record the browser sent. Prints the fields only one side
|
|
6
|
+
//! has; a field the stored record keeps under a name the session lacks means the vocabulary
|
|
7
|
+
//! could not translate it.
|
|
8
|
+
|
|
9
|
+
use std::collections::BTreeSet;
|
|
10
|
+
use std::path::Path;
|
|
11
|
+
use std::{env, fs};
|
|
12
|
+
|
|
13
|
+
use anyhow::{Context, Result};
|
|
14
|
+
use cfsolve::capture::CapturedSession;
|
|
15
|
+
use cfsolve::js::JsValue;
|
|
16
|
+
use cfsolve::stage2::analysis;
|
|
17
|
+
use cfsolve::stage2::replay::Answers;
|
|
18
|
+
|
|
19
|
+
fn main() -> Result<()> {
|
|
20
|
+
let mut arguments = env::args().skip(1);
|
|
21
|
+
let answers = arguments.next().context("supply answers.json")?;
|
|
22
|
+
let answers: Answers = serde_json::from_str(&fs::read_to_string(&answers)?)?;
|
|
23
|
+
let mut mismatched = 0;
|
|
24
|
+
for path in arguments {
|
|
25
|
+
let capture = CapturedSession::read(Path::new(&path))?;
|
|
26
|
+
let analysis = analysis::analyze(&capture.source()?, &capture.program()?)?;
|
|
27
|
+
let plan = analysis.plan();
|
|
28
|
+
let vocabulary = plan.vocabulary();
|
|
29
|
+
let envelope = plan.record_fields.names();
|
|
30
|
+
let payload = capture.probe_payload()?;
|
|
31
|
+
let (mut checked, mut differing) = (0, 0);
|
|
32
|
+
for probe in plan.probes() {
|
|
33
|
+
let position = probe.position();
|
|
34
|
+
let Some(stored) = vocabulary
|
|
35
|
+
.partner(position)
|
|
36
|
+
.and_then(|partner| answers.probes.get(&partner))
|
|
37
|
+
else {
|
|
38
|
+
continue;
|
|
39
|
+
};
|
|
40
|
+
let mut fields = stored.fields.clone();
|
|
41
|
+
vocabulary.rewrite_to_session(position, &mut fields);
|
|
42
|
+
let ours: BTreeSet<&String> = fields.keys().collect();
|
|
43
|
+
let sent: BTreeSet<&String> = payload
|
|
44
|
+
.get(&probe.index.to_string())
|
|
45
|
+
.and_then(JsValue::as_object)
|
|
46
|
+
.into_iter()
|
|
47
|
+
.flat_map(|record| record.keys())
|
|
48
|
+
.filter(|name| !envelope.contains(&name.as_str()))
|
|
49
|
+
.collect();
|
|
50
|
+
checked += 1;
|
|
51
|
+
if ours != sent {
|
|
52
|
+
differing += 1;
|
|
53
|
+
println!(
|
|
54
|
+
"{path} probe {}: only stored {:?}, only sent {:?}",
|
|
55
|
+
probe.index,
|
|
56
|
+
ours.difference(&sent).collect::<Vec<_>>(),
|
|
57
|
+
sent.difference(&ours).collect::<Vec<_>>(),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
println!("{path}: {checked} replayed records, {differing} differ");
|
|
62
|
+
mismatched += differing;
|
|
63
|
+
}
|
|
64
|
+
anyhow::ensure!(mismatched == 0, "{mismatched} records differ");
|
|
65
|
+
Ok(())
|
|
66
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
//! Compare a solve's payloads with a browser capture of the same build.
|
|
2
|
+
//! Usage: compare_session <session-directory> <capture.json> [--values]
|
|
3
|
+
//!
|
|
4
|
+
//! The session directory is one the challenge CLI writes. Probe records are compared in the
|
|
5
|
+
//! reference build's names, by key and JSON type; the other payload fields, whose names the
|
|
6
|
+
//! vocabulary does not cover, by count and type. Timing values are ignored. `--values` also
|
|
7
|
+
//! prints each probe field whose value differs.
|
|
8
|
+
|
|
9
|
+
use std::collections::BTreeMap;
|
|
10
|
+
use std::path::Path;
|
|
11
|
+
use std::{env, fs};
|
|
12
|
+
|
|
13
|
+
use anyhow::{Context, Result};
|
|
14
|
+
use base64::Engine;
|
|
15
|
+
use base64::engine::general_purpose::STANDARD;
|
|
16
|
+
use cfsolve::capture::CapturedSession;
|
|
17
|
+
use cfsolve::js::{JsObject, JsValue};
|
|
18
|
+
use cfsolve::script::Source;
|
|
19
|
+
use cfsolve::stage2::analysis::{self, Analysis};
|
|
20
|
+
|
|
21
|
+
fn main() -> Result<()> {
|
|
22
|
+
let mut arguments = env::args().skip(1);
|
|
23
|
+
let directory = arguments.next().context("supply a session directory")?;
|
|
24
|
+
let capture_path = arguments.next().context("supply a capture.json")?;
|
|
25
|
+
let directory = Path::new(&directory);
|
|
26
|
+
let read = |name: &str| fs::read_to_string(directory.join(name));
|
|
27
|
+
let read_payload = |name: &str| -> Result<JsObject> { Ok(serde_json::from_str(&read(name)?)?) };
|
|
28
|
+
|
|
29
|
+
let ours = analysis::analyze(
|
|
30
|
+
&Source::try_new(&read("iframe.deobfuscated.js")?)?,
|
|
31
|
+
&STANDARD.decode(read("response.decoded.txt")?.trim())?,
|
|
32
|
+
)?;
|
|
33
|
+
let capture = CapturedSession::read(Path::new(&capture_path))?;
|
|
34
|
+
let theirs = analysis::analyze(&capture.source()?, &capture.program()?)?;
|
|
35
|
+
|
|
36
|
+
let our_second = read_payload("second-payload.json")?;
|
|
37
|
+
let their_second: &JsObject = capture.probe_payload()?;
|
|
38
|
+
let our_values = values(&ours, &our_second);
|
|
39
|
+
let their_values = values(&theirs, their_second);
|
|
40
|
+
if env::args().any(|argument| argument == "--values") {
|
|
41
|
+
for (probe, their_record) in &their_values {
|
|
42
|
+
let Some(our_record) = our_values.get(probe) else {
|
|
43
|
+
continue;
|
|
44
|
+
};
|
|
45
|
+
for (name, their_value) in their_record {
|
|
46
|
+
let our_value = our_record.get(name).map_or("<missing>".to_owned(), short);
|
|
47
|
+
let their_value = short(their_value);
|
|
48
|
+
if our_value != their_value {
|
|
49
|
+
println!("{probe} {name}\n ours {our_value}\n theirs {their_value}");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
let our_records = records(&our_values);
|
|
55
|
+
let their_records = records(&their_values);
|
|
56
|
+
let only = |a: &BTreeMap<String, String>, b: &BTreeMap<String, String>| -> Vec<String> {
|
|
57
|
+
a.iter()
|
|
58
|
+
.filter(|(key, kind)| b.get(*key) != Some(*kind))
|
|
59
|
+
.map(|(key, kind)| format!("{key}:{kind}"))
|
|
60
|
+
.collect()
|
|
61
|
+
};
|
|
62
|
+
for (probe, theirs) in &their_records {
|
|
63
|
+
match our_records.get(probe) {
|
|
64
|
+
None => println!("{probe}: only in the capture"),
|
|
65
|
+
Some(ours) if ours != theirs => {
|
|
66
|
+
println!(
|
|
67
|
+
"{probe}: ours {:?} theirs {:?}",
|
|
68
|
+
only(ours, theirs),
|
|
69
|
+
only(theirs, ours)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
Some(_) => {}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for probe in our_records.keys() {
|
|
76
|
+
if !their_records.contains_key(probe) {
|
|
77
|
+
println!("{probe}: only in ours");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
println!(
|
|
82
|
+
"second payload top level: {}",
|
|
83
|
+
top_level(&our_second, their_second)
|
|
84
|
+
);
|
|
85
|
+
if let (Ok(ours), Some(theirs)) = (read_payload("third-payload.json"), capture.last_payload()) {
|
|
86
|
+
println!("third payload top level: {}", top_level(&ours, theirs));
|
|
87
|
+
if let (Some(ours), Some(theirs)) = (last_record(&ours), last_record(theirs)) {
|
|
88
|
+
println!(
|
|
89
|
+
"checkbox record: ours {} theirs {}",
|
|
90
|
+
shape(ours),
|
|
91
|
+
shape(theirs)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if let (Ok(ours), Ok(theirs)) = (read_payload("payload.json"), capture.first_payload()) {
|
|
96
|
+
println!("first payload top level: {}", top_level(&ours, theirs));
|
|
97
|
+
}
|
|
98
|
+
Ok(())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Each probe's record in reference names, keyed by the reference probe or, for unpaired
|
|
102
|
+
/// probes, the family.
|
|
103
|
+
fn values(analysis: &Analysis, payload: &JsObject) -> BTreeMap<String, JsObject> {
|
|
104
|
+
let plan = analysis.plan();
|
|
105
|
+
let vocabulary = plan.vocabulary();
|
|
106
|
+
let envelope = plan.record_fields.names();
|
|
107
|
+
plan.probes()
|
|
108
|
+
.iter()
|
|
109
|
+
.filter_map(|probe| {
|
|
110
|
+
let mut record = payload.get(&probe.index.to_string())?.as_object()?.clone();
|
|
111
|
+
record.retain(|name, _| !envelope.contains(&name.as_str()));
|
|
112
|
+
vocabulary.rewrite_to_reference(probe.position(), &mut record);
|
|
113
|
+
let key = match (vocabulary.partner(probe.position()), &probe.operation) {
|
|
114
|
+
(Some(reference), _) => format!("reference {reference:02}"),
|
|
115
|
+
(None, Some(operation)) => format!("unpaired {}", operation.kind()),
|
|
116
|
+
(None, None) => "unpaired ?".to_owned(),
|
|
117
|
+
};
|
|
118
|
+
Some((key, record))
|
|
119
|
+
})
|
|
120
|
+
.collect()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Each record from [`values`] with its fields' JSON types.
|
|
124
|
+
fn records(values: &BTreeMap<String, JsObject>) -> BTreeMap<String, BTreeMap<String, String>> {
|
|
125
|
+
values
|
|
126
|
+
.iter()
|
|
127
|
+
.map(|(probe, record)| {
|
|
128
|
+
let types = record
|
|
129
|
+
.iter()
|
|
130
|
+
.map(|(name, value)| (name.clone(), kind(value)))
|
|
131
|
+
.collect();
|
|
132
|
+
(probe.clone(), types)
|
|
133
|
+
})
|
|
134
|
+
.collect()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// The record under the highest numeric key, which in the third payload is the checkbox's.
|
|
138
|
+
fn last_record(payload: &JsObject) -> Option<&JsObject> {
|
|
139
|
+
payload
|
|
140
|
+
.iter()
|
|
141
|
+
.filter_map(|(key, value)| Some((key.parse::<usize>().ok()?, value)))
|
|
142
|
+
.max_by_key(|(index, _)| *index)
|
|
143
|
+
.and_then(|(_, value)| value.as_object())
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn short(value: &JsValue) -> String {
|
|
147
|
+
let text = value.stringify().unwrap_or_else(|| "undefined".into());
|
|
148
|
+
text.chars().take(240).collect()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fn kind(value: &JsValue) -> String {
|
|
152
|
+
match value {
|
|
153
|
+
JsValue::Null => "null".into(),
|
|
154
|
+
JsValue::Bool(_) => "bool".into(),
|
|
155
|
+
JsValue::Number(_) => "number".into(),
|
|
156
|
+
JsValue::String(text) if text.is_empty() => "empty".into(),
|
|
157
|
+
JsValue::String(_) => "string".into(),
|
|
158
|
+
JsValue::Array(items) => format!("array{}", items.len()),
|
|
159
|
+
JsValue::Object(object) => format!("object{}", object.len()),
|
|
160
|
+
JsValue::Undefined => "undefined".into(),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Non-record fields: count and multiset of types on each side.
|
|
165
|
+
fn top_level(ours: &JsObject, theirs: &JsObject) -> String {
|
|
166
|
+
let mut counts: BTreeMap<String, [usize; 2]> = BTreeMap::new();
|
|
167
|
+
for (side, payload) in [ours, theirs].into_iter().enumerate() {
|
|
168
|
+
for (key, value) in payload {
|
|
169
|
+
if key.parse::<usize>().is_err() {
|
|
170
|
+
counts.entry(kind(value)).or_default()[side] += 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
let differing: Vec<String> = counts
|
|
175
|
+
.iter()
|
|
176
|
+
.filter(|(_, [ours, theirs])| ours != theirs)
|
|
177
|
+
.map(|(kind, [ours, theirs])| format!("{kind} {ours}/{theirs}"))
|
|
178
|
+
.collect();
|
|
179
|
+
let total = |side: usize| counts.values().map(|count| count[side]).sum::<usize>();
|
|
180
|
+
format!(
|
|
181
|
+
"ours {} theirs {} fields; differing types (ours/theirs): {differing:?}",
|
|
182
|
+
total(0),
|
|
183
|
+
total(1),
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
fn shape(record: &JsObject) -> String {
|
|
188
|
+
let kinds: Vec<String> = record.values().map(kind).collect();
|
|
189
|
+
format!("{} fields {kinds:?}", record.len())
|
|
190
|
+
}
|
|
@@ -54,7 +54,10 @@ fn main() -> Result<()> {
|
|
|
54
54
|
let frames = fields
|
|
55
55
|
.calls
|
|
56
56
|
.context("the stack probe's calls have an unrecognized shape")
|
|
57
|
-
.and_then(|calls|
|
|
57
|
+
.and_then(|calls| {
|
|
58
|
+
let resumed = call_stack::Resumption::WorkerMessage;
|
|
59
|
+
call_stack::recover_probe(&original, &cases, calls, resumed)
|
|
60
|
+
})
|
|
58
61
|
.map(|geometry| {
|
|
59
62
|
json!({
|
|
60
63
|
"frames": geometry.frames,
|