flowproof 0.12.2__tar.gz → 0.13.0__tar.gz
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.
- {flowproof-0.12.2 → flowproof-0.13.0}/Cargo.lock +7 -7
- {flowproof-0.12.2 → flowproof-0.13.0}/Cargo.toml +1 -1
- {flowproof-0.12.2 → flowproof-0.13.0}/PKG-INFO +1 -1
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_linux.rs +9 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/sap_com.rs +305 -79
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/web.rs +139 -9
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/author.rs +27 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/lib.rs +2 -1
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/llm.rs +28 -8
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/recorder.rs +127 -8
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/spec.rs +4 -4
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/tests/model_client_http.rs +30 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/src/lib.rs +296 -21
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/sap_e2e.rs +1 -1
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/sap_sim_e2e.rs +34 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/support/sap_simulator.py +76 -30
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/lib.rs +3 -1
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/recording.rs +360 -10
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-python/src/lib.rs +58 -6
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-replay/src/lib.rs +61 -6
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-replay/tests/replay_calc.rs +75 -1
- {flowproof-0.12.2 → flowproof-0.13.0}/flowproof/__init__.py +1 -1
- {flowproof-0.12.2 → flowproof-0.13.0}/flowproof/flow.py +62 -8
- {flowproof-0.12.2 → flowproof-0.13.0}/flowproof/mcp_server.py +29 -6
- {flowproof-0.12.2 → flowproof-0.13.0}/pyproject.toml +1 -1
- {flowproof-0.12.2 → flowproof-0.13.0}/README.md +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/Cargo.toml +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/agent_proxy.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/agent_runner.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/audit.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/capture.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/env_block.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/filters.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/identity.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/logon.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/netevents.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/run.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/spawn.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/wfp.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/fs_observe.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/lib.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/mcp_core.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/mcp_http.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/mcp_stdio.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/vision.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/tests/drag_spike.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/tests/fs_observation_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/tests/windows_containment_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/Cargo.toml +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/agent_steps.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/clarify.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/heal.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/rules.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/Cargo.toml +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/src/agent_flow.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/src/capture.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/src/main.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/agent_flow_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/agent_url_record_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/api_pipeline.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/audit_record_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/calc_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/clock_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/cookie_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/doctor_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/egress_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/examples_resolve.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/falsifiability_arg_matchers.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/falsifiability_audit_since.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/falsifiability_call_order.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/falsifiability_capability_regression.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/falsifiability_control_verdict.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/falsifiability_mcp_kill.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/falsifiability_no_tool_call.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/falsifiability_reply_contains.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/grid_cell_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/iframe_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/llm_author_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/mcp_http_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/mcp_stdio_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/notepad_author_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/notepad_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/page_title_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/pinned_random_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/sap_pipeline.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/scoped_container_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/select_missing_option_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/skip_unless_env.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/suite_env_from.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/suite_flow_isolation.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/suite_missing_trace.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/visibility_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/vision_pipeline.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/web_e2e.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/Cargo.toml +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/app.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/backend.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/gdi.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/mock.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/oob.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/redact.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/visual.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/window.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-python/Cargo.toml +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-replay/Cargo.toml +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-replay/src/report.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-replay/src/runrecord.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/Cargo.toml +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/schema/trace-v1.schema.json +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/captures.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/cassette.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/cassette_diff.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/egress.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/format.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/lib.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/secret.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/secret_scan.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/substitution.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/toolcalls.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/tests/schema_conformance.rs +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/flowproof/cli.py +0 -0
- {flowproof-0.12.2 → flowproof-0.13.0}/flowproof/py.typed +0 -0
|
@@ -760,7 +760,7 @@ dependencies = [
|
|
|
760
760
|
|
|
761
761
|
[[package]]
|
|
762
762
|
name = "flowproof-adapters"
|
|
763
|
-
version = "0.
|
|
763
|
+
version = "0.13.0"
|
|
764
764
|
dependencies = [
|
|
765
765
|
"anyhow",
|
|
766
766
|
"flowproof-driver",
|
|
@@ -779,7 +779,7 @@ dependencies = [
|
|
|
779
779
|
|
|
780
780
|
[[package]]
|
|
781
781
|
name = "flowproof-agent"
|
|
782
|
-
version = "0.
|
|
782
|
+
version = "0.13.0"
|
|
783
783
|
dependencies = [
|
|
784
784
|
"chrono",
|
|
785
785
|
"flowproof-driver",
|
|
@@ -796,7 +796,7 @@ dependencies = [
|
|
|
796
796
|
|
|
797
797
|
[[package]]
|
|
798
798
|
name = "flowproof-cli"
|
|
799
|
-
version = "0.
|
|
799
|
+
version = "0.13.0"
|
|
800
800
|
dependencies = [
|
|
801
801
|
"ab_glyph",
|
|
802
802
|
"chrono",
|
|
@@ -817,7 +817,7 @@ dependencies = [
|
|
|
817
817
|
|
|
818
818
|
[[package]]
|
|
819
819
|
name = "flowproof-driver"
|
|
820
|
-
version = "0.
|
|
820
|
+
version = "0.13.0"
|
|
821
821
|
dependencies = [
|
|
822
822
|
"image",
|
|
823
823
|
"postgres",
|
|
@@ -831,7 +831,7 @@ dependencies = [
|
|
|
831
831
|
|
|
832
832
|
[[package]]
|
|
833
833
|
name = "flowproof-python"
|
|
834
|
-
version = "0.
|
|
834
|
+
version = "0.13.0"
|
|
835
835
|
dependencies = [
|
|
836
836
|
"flowproof-agent",
|
|
837
837
|
"flowproof-cli",
|
|
@@ -843,7 +843,7 @@ dependencies = [
|
|
|
843
843
|
|
|
844
844
|
[[package]]
|
|
845
845
|
name = "flowproof-replay"
|
|
846
|
-
version = "0.
|
|
846
|
+
version = "0.13.0"
|
|
847
847
|
dependencies = [
|
|
848
848
|
"chrono",
|
|
849
849
|
"flowproof-agent",
|
|
@@ -857,7 +857,7 @@ dependencies = [
|
|
|
857
857
|
|
|
858
858
|
[[package]]
|
|
859
859
|
name = "flowproof-trace"
|
|
860
|
-
version = "0.
|
|
860
|
+
version = "0.13.0"
|
|
861
861
|
dependencies = [
|
|
862
862
|
"jsonschema",
|
|
863
863
|
"regex",
|
|
@@ -22,7 +22,7 @@ members = ["crates/flowproof-driver", "crates/flowproof-trace", "crates/flowproo
|
|
|
22
22
|
exclude = ["spike/windows-containment"]
|
|
23
23
|
|
|
24
24
|
[workspace.package]
|
|
25
|
-
version = "0.
|
|
25
|
+
version = "0.13.0"
|
|
26
26
|
edition = "2021"
|
|
27
27
|
license = "Apache-2.0"
|
|
28
28
|
repository = "https://github.com/automators-com/flowproof"
|
|
@@ -1465,6 +1465,12 @@ fn notif_id_valid(fd: RawFd, id: u64) -> bool {
|
|
|
1465
1465
|
}
|
|
1466
1466
|
|
|
1467
1467
|
/// Extract `(msg_name, msg_namelen, msg_controllen)` from a raw msghdr copy.
|
|
1468
|
+
// `msg_controllen` is not the same type on every Linux target: `size_t` under
|
|
1469
|
+
// glibc, `socklen_t` under uclibc/arm and l4re. Clippy only ever sees the
|
|
1470
|
+
// target it is running on, so on x86_64-gnu it calls the cast redundant. It is
|
|
1471
|
+
// load-bearing everywhere else - libc writes the same `as usize` in its own
|
|
1472
|
+
// portable CMSG helpers. Removing it breaks those targets.
|
|
1473
|
+
#[allow(clippy::unnecessary_cast)]
|
|
1468
1474
|
fn msghdr_fields(hdr: &[u8]) -> (u64, u32, usize) {
|
|
1469
1475
|
let msg: libc::msghdr =
|
|
1470
1476
|
unsafe { std::ptr::read_unaligned(hdr.as_ptr() as *const libc::msghdr) };
|
|
@@ -1476,6 +1482,9 @@ fn msghdr_fields(hdr: &[u8]) -> (u64, u32, usize) {
|
|
|
1476
1482
|
}
|
|
1477
1483
|
|
|
1478
1484
|
/// Extract `(msg_iov ptr, msg_iovlen)` from a raw msghdr copy.
|
|
1485
|
+
// Same story as `msghdr_fields`: `msg_iovlen` is `size_t` under glibc and
|
|
1486
|
+
// `c_int` under uclibc/arm, uclibc/mips32, l4re and emscripten.
|
|
1487
|
+
#[allow(clippy::unnecessary_cast)]
|
|
1479
1488
|
fn msghdr_iov(hdr: &[u8]) -> (u64, usize) {
|
|
1480
1489
|
let msg: libc::msghdr =
|
|
1481
1490
|
unsafe { std::ptr::read_unaligned(hdr.as_ptr() as *const libc::msghdr) };
|
|
@@ -30,6 +30,9 @@ pub struct SapElement {
|
|
|
30
30
|
pub text: String,
|
|
31
31
|
/// Human-readable tooltip / quick info.
|
|
32
32
|
pub tooltip: String,
|
|
33
|
+
/// SAP status-bar severity (`S`, `W`, `E`, `A`, or empty). Other
|
|
34
|
+
/// component types normally leave this empty.
|
|
35
|
+
pub message_type: String,
|
|
33
36
|
/// Whether the element accepts input right now.
|
|
34
37
|
pub changeable: bool,
|
|
35
38
|
/// Checkbox / radio state, when the element has one.
|
|
@@ -66,9 +69,9 @@ impl SapElement {
|
|
|
66
69
|
/// Implemented by the COM bridge on Windows and by [`fake::FakeEngine`]
|
|
67
70
|
/// everywhere for tests.
|
|
68
71
|
pub trait SapEngine {
|
|
69
|
-
/// Attach to a
|
|
70
|
-
///
|
|
71
|
-
///
|
|
72
|
+
/// Attach to a logged-in SAP GUI session. `connection` is the SAP Logon
|
|
73
|
+
/// entry to select or open, starting SAP Logon when necessary (empty =
|
|
74
|
+
/// attach-only).
|
|
72
75
|
fn connect(&mut self, connection: &str, timeout: Duration) -> Result<(), DriverError>;
|
|
73
76
|
/// Look an element up by scripting id. `Ok(None)` = not on screen.
|
|
74
77
|
fn find_by_id(&mut self, id: &str) -> Result<Option<SapElement>, DriverError>;
|
|
@@ -161,6 +164,27 @@ impl<E: SapEngine> SapAppDriver<E> {
|
|
|
161
164
|
DriverError::Uia(format!("sap: no element matches selector [{selector}]"))
|
|
162
165
|
})
|
|
163
166
|
}
|
|
167
|
+
|
|
168
|
+
/// SAP GUI scripting calls can return successfully even when the
|
|
169
|
+
/// application rejected the action. The actual failure is reported in
|
|
170
|
+
/// `wnd[0]/sbar`, so promote error/abort messages to driver failures.
|
|
171
|
+
fn reject_status_error(&mut self, action: &str) -> Result<(), DriverError> {
|
|
172
|
+
let Some(status) = self.engine.find_by_id("wnd[0]/sbar")? else {
|
|
173
|
+
return Ok(());
|
|
174
|
+
};
|
|
175
|
+
if matches!(status.message_type.trim(), "E" | "A") {
|
|
176
|
+
let message = status.text.trim();
|
|
177
|
+
return Err(DriverError::Uia(if message.is_empty() {
|
|
178
|
+
format!(
|
|
179
|
+
"sap: {action} was rejected (status {})",
|
|
180
|
+
status.message_type
|
|
181
|
+
)
|
|
182
|
+
} else {
|
|
183
|
+
format!("sap: {action} was rejected: {message}")
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
Ok(())
|
|
187
|
+
}
|
|
164
188
|
}
|
|
165
189
|
|
|
166
190
|
#[cfg(windows)]
|
|
@@ -198,7 +222,8 @@ impl<E: SapEngine> AppDriver for SapAppDriver<E> {
|
|
|
198
222
|
// Labels, list rows, everything else: focus is the closest
|
|
199
223
|
// scripted equivalent of a click.
|
|
200
224
|
_ => self.engine.set_focus(&element.id),
|
|
201
|
-
}
|
|
225
|
+
}?;
|
|
226
|
+
self.reject_status_error(&format!("action on '{}'", element.id))
|
|
202
227
|
}
|
|
203
228
|
|
|
204
229
|
fn read_text(&mut self, selector: &UiaSelector) -> Result<String, DriverError> {
|
|
@@ -233,7 +258,8 @@ impl<E: SapEngine> AppDriver for SapAppDriver<E> {
|
|
|
233
258
|
(supported: Enter, F1–F12, Shift/Ctrl+F1–F12)"
|
|
234
259
|
))
|
|
235
260
|
})?;
|
|
236
|
-
self.engine.send_vkey(vkey)
|
|
261
|
+
self.engine.send_vkey(vkey)?;
|
|
262
|
+
self.reject_status_error(&format!("key '{key}'"))
|
|
237
263
|
}
|
|
238
264
|
|
|
239
265
|
fn surface_text(&mut self) -> Result<String, DriverError> {
|
|
@@ -255,8 +281,22 @@ impl<E: SapEngine> AppDriver for SapAppDriver<E> {
|
|
|
255
281
|
fn navigate(&mut self, path: &str) -> Result<(), DriverError> {
|
|
256
282
|
// `Go to /nVA01` — type the transaction code into the command
|
|
257
283
|
// field and hit Enter, exactly how a user navigates SAP.
|
|
258
|
-
|
|
259
|
-
|
|
284
|
+
let transaction = path.trim();
|
|
285
|
+
if transaction.is_empty() {
|
|
286
|
+
return Err(DriverError::Uia(
|
|
287
|
+
"sap: transaction code must not be empty".into(),
|
|
288
|
+
));
|
|
289
|
+
}
|
|
290
|
+
// A human may simply say `Go to VA01`; `/n` is the deterministic SAP
|
|
291
|
+
// OK-code form for replacing the current transaction.
|
|
292
|
+
let ok_code = if transaction.starts_with('/') {
|
|
293
|
+
transaction.to_string()
|
|
294
|
+
} else {
|
|
295
|
+
format!("/n{transaction}")
|
|
296
|
+
};
|
|
297
|
+
self.engine.set_text(OKCODE_FIELD, &ok_code)?;
|
|
298
|
+
self.engine.send_vkey(0)?;
|
|
299
|
+
self.reject_status_error(&format!("transaction '{ok_code}'"))
|
|
260
300
|
}
|
|
261
301
|
|
|
262
302
|
fn screen_size(&mut self) -> Result<(u32, u32), DriverError> {
|
|
@@ -421,6 +461,8 @@ pub mod fake {
|
|
|
421
461
|
/// → `FindById` / property access), so behavior matches SAP's own tooling.
|
|
422
462
|
#[cfg(windows)]
|
|
423
463
|
pub mod com {
|
|
464
|
+
use std::path::PathBuf;
|
|
465
|
+
use std::process::Command;
|
|
424
466
|
use std::time::{Duration, Instant};
|
|
425
467
|
|
|
426
468
|
use windows::core::{Interface, BSTR, GUID, PCWSTR};
|
|
@@ -583,6 +625,7 @@ pub mod com {
|
|
|
583
625
|
name: element.get_string("Name"),
|
|
584
626
|
text: element.get_string("Text"),
|
|
585
627
|
tooltip: element.get_string("Tooltip"),
|
|
628
|
+
message_type: element.get_string("MessageType"),
|
|
586
629
|
changeable: element.get_bool("Changeable").unwrap_or(false),
|
|
587
630
|
selected: element.get_bool("Selected"),
|
|
588
631
|
rect,
|
|
@@ -605,37 +648,126 @@ pub mod com {
|
|
|
605
648
|
.map(Disp)
|
|
606
649
|
}
|
|
607
650
|
|
|
651
|
+
/// Start SAP Logon when a flow names a connection but no scripting
|
|
652
|
+
/// root is running yet. SAP GUI does not register `SAPGUI` in the ROT
|
|
653
|
+
/// until SAP Logon starts, so OpenConnection cannot be the first call.
|
|
654
|
+
/// `SAP_LOGON_EXE` handles non-default and centrally managed installs;
|
|
655
|
+
/// the two standard install roots and PATH cover normal clients.
|
|
656
|
+
fn start_sap_logon() -> Result<(), DriverError> {
|
|
657
|
+
let mut candidates = Vec::<PathBuf>::new();
|
|
658
|
+
if let Some(explicit) = std::env::var("SAP_LOGON_EXE")
|
|
659
|
+
.ok()
|
|
660
|
+
.map(|value| value.trim().trim_matches('"').to_string())
|
|
661
|
+
.filter(|value| !value.is_empty())
|
|
662
|
+
{
|
|
663
|
+
candidates.push(PathBuf::from(explicit));
|
|
664
|
+
}
|
|
665
|
+
if let Some(home) = std::env::var("SAPGUI_HOME")
|
|
666
|
+
.ok()
|
|
667
|
+
.filter(|value| !value.trim().is_empty())
|
|
668
|
+
{
|
|
669
|
+
candidates.push(PathBuf::from(home).join("saplogon.exe"));
|
|
670
|
+
}
|
|
671
|
+
for root in ["ProgramFiles(x86)", "ProgramFiles", "ProgramW6432"] {
|
|
672
|
+
if let Some(root) = std::env::var(root)
|
|
673
|
+
.ok()
|
|
674
|
+
.filter(|value| !value.trim().is_empty())
|
|
675
|
+
{
|
|
676
|
+
candidates.push(
|
|
677
|
+
PathBuf::from(root)
|
|
678
|
+
.join("SAP")
|
|
679
|
+
.join("FrontEnd")
|
|
680
|
+
.join("SAPgui")
|
|
681
|
+
.join("saplogon.exe"),
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
candidates.push(PathBuf::from("saplogon.exe"));
|
|
686
|
+
|
|
687
|
+
let mut failures = Vec::new();
|
|
688
|
+
for executable in candidates {
|
|
689
|
+
match Command::new(&executable).spawn() {
|
|
690
|
+
Ok(_) => {
|
|
691
|
+
eprintln!("sap-com: started SAP Logon; waiting for scripting");
|
|
692
|
+
return Ok(());
|
|
693
|
+
}
|
|
694
|
+
Err(error) => failures.push(format!("{}: {error}", executable.display())),
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
Err(DriverError::Uia(format!(
|
|
698
|
+
"sap-com: could not start SAP Logon; set SAP_LOGON_EXE to saplogon.exe \
|
|
699
|
+
({})",
|
|
700
|
+
failures.join("; ")
|
|
701
|
+
)))
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/// SAP exposes several names for a connection depending on GUI
|
|
705
|
+
/// version and landscape source. Match the Logon entry description
|
|
706
|
+
/// first, while also accepting its technical name/system id.
|
|
707
|
+
fn connection_matches(candidate: &Disp, requested: &str) -> bool {
|
|
708
|
+
let requested = requested.trim();
|
|
709
|
+
!requested.is_empty()
|
|
710
|
+
&& ["Description", "Name", "SystemName"]
|
|
711
|
+
.iter()
|
|
712
|
+
.map(|property| candidate.get_string(property))
|
|
713
|
+
.any(|value| value.trim().eq_ignore_ascii_case(requested))
|
|
714
|
+
}
|
|
715
|
+
|
|
608
716
|
/// Log in on SAP's own logon screen, using the standard fields every
|
|
609
717
|
/// SAP system ships (client/user/password - the same ones a VBScript
|
|
610
|
-
/// recording of a manual login would drive).
|
|
611
|
-
///
|
|
612
|
-
///
|
|
613
|
-
///
|
|
614
|
-
///
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
let (
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
718
|
+
/// recording of a manual login would drive). Returns `Ok(false)` when
|
|
719
|
+
/// neither credential is configured. A half-configured login and a
|
|
720
|
+
/// non-standard login screen are errors instead of a silent timeout.
|
|
721
|
+
/// Values are read fresh and used only for these property puts - never
|
|
722
|
+
/// logged, traced, or stored by this function.
|
|
723
|
+
fn try_auto_login(session: &Disp) -> Result<bool, DriverError> {
|
|
724
|
+
let user = std::env::var("SAP_USER").ok();
|
|
725
|
+
let password = std::env::var("SAP_PASSWORD").ok();
|
|
726
|
+
let (user, password) = match (user, password) {
|
|
727
|
+
(None, None) => return Ok(false),
|
|
728
|
+
(Some(user), Some(password)) if !user.is_empty() && !password.is_empty() => {
|
|
729
|
+
(user, password)
|
|
730
|
+
}
|
|
731
|
+
_ => {
|
|
732
|
+
return Err(DriverError::Uia(
|
|
733
|
+
"sap-com: auto-login requires both SAP_USER and SAP_PASSWORD".into(),
|
|
734
|
+
))
|
|
735
|
+
}
|
|
621
736
|
};
|
|
622
737
|
eprintln!("sap-com: not logged in - attempting auto-login");
|
|
623
738
|
if let Some(client) = std::env::var("SAP_CLIENT").ok().filter(|c| !c.is_empty()) {
|
|
624
739
|
if let Some(field) = Self::find_in(session, "wnd[0]/usr/txtRSYST-MANDT") {
|
|
625
|
-
|
|
740
|
+
field.put("Text", VARIANT::from(BSTR::from(client.as_str())))?;
|
|
626
741
|
}
|
|
627
742
|
}
|
|
628
|
-
if let Some(
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
let
|
|
743
|
+
if let Some(language) = std::env::var("SAP_LANGUAGE")
|
|
744
|
+
.ok()
|
|
745
|
+
.filter(|value| !value.is_empty())
|
|
746
|
+
{
|
|
747
|
+
if let Some(field) = Self::find_in(session, "wnd[0]/usr/txtRSYST-LANGU") {
|
|
748
|
+
field.put("Text", VARIANT::from(BSTR::from(language.as_str())))?;
|
|
749
|
+
}
|
|
633
750
|
}
|
|
751
|
+
let user_field = Self::find_in(session, "wnd[0]/usr/txtRSYST-BNAME").ok_or_else(|| {
|
|
752
|
+
DriverError::Uia(
|
|
753
|
+
"sap-com: login screen has no standard user field; SSO or a custom login dialog needs an already logged-in session".into(),
|
|
754
|
+
)
|
|
755
|
+
})?;
|
|
756
|
+
user_field.put("Text", VARIANT::from(BSTR::from(user.as_str())))?;
|
|
757
|
+
let password_field = Self::find_in(session, "wnd[0]/usr/pwdRSYST-BCODE")
|
|
758
|
+
.ok_or_else(|| {
|
|
759
|
+
DriverError::Uia(
|
|
760
|
+
"sap-com: login screen has no standard password field; SSO or a custom login dialog needs an already logged-in session".into(),
|
|
761
|
+
)
|
|
762
|
+
})?;
|
|
763
|
+
password_field.put("Text", VARIANT::from(BSTR::from(password.as_str())))?;
|
|
634
764
|
// Submit - Enter on the window, exactly how navigate() confirms
|
|
635
765
|
// any other screen.
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
766
|
+
let window = Self::find_in(session, "wnd[0]").ok_or_else(|| {
|
|
767
|
+
DriverError::Uia("sap-com: login screen has no main window".into())
|
|
768
|
+
})?;
|
|
769
|
+
window.call("SendVKey", vec![VARIANT::from(0i32)])?;
|
|
770
|
+
Ok(true)
|
|
639
771
|
}
|
|
640
772
|
|
|
641
773
|
fn walk_into(element: &Disp, depth: u32, out: &mut Vec<SapElement>) {
|
|
@@ -729,77 +861,142 @@ pub mod com {
|
|
|
729
861
|
// COM is already usable here.
|
|
730
862
|
let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED);
|
|
731
863
|
}
|
|
864
|
+
// Starting SAP Logon, crossing SAProuter, and authenticating is a
|
|
865
|
+
// materially longer bootstrap than attaching to an existing app.
|
|
866
|
+
// Keep attach-only fast, but give named connections a practical
|
|
867
|
+
// default and an explicit override for slow landscapes.
|
|
868
|
+
let timeout = if connection.is_empty() {
|
|
869
|
+
timeout
|
|
870
|
+
} else {
|
|
871
|
+
std::env::var("FLOWPROOF_SAP_CONNECT_TIMEOUT_MS")
|
|
872
|
+
.ok()
|
|
873
|
+
.and_then(|value| value.parse::<u64>().ok())
|
|
874
|
+
.filter(|value| *value > 0)
|
|
875
|
+
.map(Duration::from_millis)
|
|
876
|
+
.unwrap_or_else(|| timeout.max(Duration::from_secs(60)))
|
|
877
|
+
};
|
|
732
878
|
let deadline = Instant::now() + timeout;
|
|
733
|
-
let mut
|
|
734
|
-
let mut
|
|
879
|
+
let mut opened_connection: Option<Disp> = None;
|
|
880
|
+
let mut sap_logon_start_attempted = false;
|
|
881
|
+
let mut login_checked = false;
|
|
882
|
+
let mut auto_login_submitted = false;
|
|
883
|
+
let mut saw_login_screen = false;
|
|
884
|
+
let mut startup_error: Option<DriverError> = None;
|
|
885
|
+
let mut login_error: Option<DriverError> = None;
|
|
886
|
+
let mut last_error: Option<DriverError> = None;
|
|
735
887
|
loop {
|
|
736
|
-
let attempt = (|| -> Result<
|
|
737
|
-
let sapgui =
|
|
888
|
+
let attempt = (|| -> Result<Option<Disp>, DriverError> {
|
|
889
|
+
let sapgui = match attach_to_sapgui() {
|
|
890
|
+
Ok(sapgui) => Disp(sapgui),
|
|
891
|
+
Err(error) => {
|
|
892
|
+
// Attach-only remains attach-only. A named
|
|
893
|
+
// connection, however, contains enough intent to
|
|
894
|
+
// start SAP Logon before opening that connection.
|
|
895
|
+
if !connection.is_empty() && !sap_logon_start_attempted {
|
|
896
|
+
sap_logon_start_attempted = true;
|
|
897
|
+
if let Err(start_error) = Self::start_sap_logon() {
|
|
898
|
+
startup_error = Some(start_error);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return Err(error);
|
|
902
|
+
}
|
|
903
|
+
};
|
|
738
904
|
let engine = sapgui.call_disp("GetScriptingEngine", vec![])?;
|
|
739
905
|
let connections = engine.get_disp("Children")?;
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
906
|
+
|
|
907
|
+
let mut candidates = Vec::new();
|
|
908
|
+
if let Some(opened) = &opened_connection {
|
|
909
|
+
candidates.push(opened.clone());
|
|
910
|
+
} else {
|
|
911
|
+
let count = connections.get_i32("Count").unwrap_or(0);
|
|
912
|
+
for index in 0..count {
|
|
913
|
+
let candidate =
|
|
914
|
+
connections.call_disp("ElementAt", vec![VARIANT::from(index)])?;
|
|
915
|
+
if connection.is_empty()
|
|
916
|
+
|| Self::connection_matches(&candidate, connection)
|
|
917
|
+
{
|
|
918
|
+
candidates.push(candidate);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// SAP may already have an unrelated connection (or a
|
|
923
|
+
// connection object with no session). That must not
|
|
924
|
+
// suppress opening the connection named by this flow.
|
|
925
|
+
if candidates.is_empty() && !connection.is_empty() {
|
|
926
|
+
let opened = engine.call_disp(
|
|
743
927
|
"OpenConnection",
|
|
744
928
|
vec![VARIANT::from(BSTR::from(connection)), VARIANT::from(true)],
|
|
745
929
|
)?;
|
|
930
|
+
opened_connection = Some(opened.clone());
|
|
931
|
+
candidates.push(opened);
|
|
746
932
|
}
|
|
747
|
-
return Ok((None, false)); // keep waiting for a session
|
|
748
|
-
}
|
|
749
|
-
let conn = connections.call_disp("ElementAt", vec![VARIANT::from(0)])?;
|
|
750
|
-
let sessions = conn.get_disp("Children")?;
|
|
751
|
-
if sessions.get_i32("Count").unwrap_or(0) == 0 {
|
|
752
|
-
return Ok((None, false));
|
|
753
933
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
934
|
+
|
|
935
|
+
for candidate in candidates {
|
|
936
|
+
let sessions = candidate.get_disp("Children")?;
|
|
937
|
+
let count = sessions.get_i32("Count").unwrap_or(0);
|
|
938
|
+
for index in 0..count {
|
|
939
|
+
let session =
|
|
940
|
+
sessions.call_disp("ElementAt", vec![VARIANT::from(index)])?;
|
|
941
|
+
// A session object exists as soon as its window
|
|
942
|
+
// opens, including SAP's login screen. Info.User
|
|
943
|
+
// is populated only after authentication.
|
|
944
|
+
let logged_in = session
|
|
945
|
+
.get_disp("Info")
|
|
946
|
+
.map(|info| !info.get_string("User").is_empty())
|
|
947
|
+
.unwrap_or(false);
|
|
948
|
+
if logged_in {
|
|
949
|
+
return Ok(Some(session));
|
|
950
|
+
}
|
|
951
|
+
saw_login_screen = true;
|
|
952
|
+
if !login_checked {
|
|
953
|
+
login_checked = true;
|
|
954
|
+
match Self::try_auto_login(&session) {
|
|
955
|
+
Ok(submitted) => auto_login_submitted = submitted,
|
|
956
|
+
Err(error) => login_error = Some(error),
|
|
957
|
+
}
|
|
958
|
+
}
|
|
776
959
|
}
|
|
777
|
-
return Ok((None, true)); // keep waiting - not logged in yet
|
|
778
960
|
}
|
|
779
|
-
Ok(
|
|
961
|
+
Ok(None)
|
|
780
962
|
})();
|
|
781
963
|
|
|
782
964
|
match attempt {
|
|
783
|
-
Ok(
|
|
965
|
+
Ok(Some(session)) => {
|
|
784
966
|
self.session = Some(session);
|
|
785
967
|
return Ok(());
|
|
786
968
|
}
|
|
787
|
-
Ok(
|
|
788
|
-
|
|
789
|
-
login_attempted = login_attempted || at_login_screen;
|
|
790
|
-
}
|
|
791
|
-
Err(e) if Instant::now() >= deadline => {
|
|
792
|
-
return Err(DriverError::Uia(format!(
|
|
793
|
-
"{e} — start SAP Logon, log in, and enable scripting \
|
|
794
|
-
(sapgui/user_scripting = TRUE)"
|
|
795
|
-
)));
|
|
796
|
-
}
|
|
797
|
-
Err(_) => {}
|
|
969
|
+
Ok(None) => {}
|
|
970
|
+
Err(error) => last_error = Some(error),
|
|
798
971
|
}
|
|
799
972
|
if Instant::now() >= deadline {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
)
|
|
973
|
+
let detail = if let Some(error) = login_error {
|
|
974
|
+
error.to_string()
|
|
975
|
+
} else if let Some(error) = startup_error {
|
|
976
|
+
error.to_string()
|
|
977
|
+
} else if saw_login_screen && !auto_login_submitted {
|
|
978
|
+
"SAP reached its login screen; set both SAP_USER and SAP_PASSWORD \
|
|
979
|
+
(optionally SAP_CLIENT and SAP_LANGUAGE), or log in interactively"
|
|
980
|
+
.to_string()
|
|
981
|
+
} else if saw_login_screen {
|
|
982
|
+
"SAP credentials were submitted but no logged-in session appeared; \
|
|
983
|
+
check the client and credentials, and dismiss any multi-login, SSO, \
|
|
984
|
+
password-change, or license dialog"
|
|
985
|
+
.to_string()
|
|
986
|
+
} else if let Some(error) = last_error {
|
|
987
|
+
error.to_string()
|
|
988
|
+
} else if connection.is_empty() {
|
|
989
|
+
"no logged-in SAP session was found; add `connection:` to open one, \
|
|
990
|
+
or log in before running an attach-only flow"
|
|
991
|
+
.to_string()
|
|
992
|
+
} else {
|
|
993
|
+
format!("connection '{connection}' did not produce an SAP session")
|
|
994
|
+
};
|
|
995
|
+
return Err(DriverError::Uia(format!(
|
|
996
|
+
"sap-com: timed out waiting for a logged-in SAP GUI session: {detail}. \
|
|
997
|
+
Ensure SAP GUI scripting is enabled on the client and server \
|
|
998
|
+
(sapgui/user_scripting = TRUE)"
|
|
999
|
+
)));
|
|
803
1000
|
}
|
|
804
1001
|
std::thread::sleep(Duration::from_millis(500));
|
|
805
1002
|
}
|
|
@@ -985,6 +1182,35 @@ mod tests {
|
|
|
985
1182
|
assert_eq!(d.engine.vkeys, vec![0]);
|
|
986
1183
|
}
|
|
987
1184
|
|
|
1185
|
+
#[test]
|
|
1186
|
+
fn navigate_normalizes_a_bare_transaction_code() {
|
|
1187
|
+
let mut d = driver();
|
|
1188
|
+
d.navigate("VA01").expect("navigates");
|
|
1189
|
+
assert_eq!(
|
|
1190
|
+
d.engine.set_texts,
|
|
1191
|
+
vec![("wnd[0]/tbar[0]/okcd".to_string(), "/nVA01".to_string())]
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
#[test]
|
|
1196
|
+
fn navigate_reports_sap_status_bar_errors() {
|
|
1197
|
+
let mut d = driver();
|
|
1198
|
+
let status = d
|
|
1199
|
+
.engine
|
|
1200
|
+
.elements
|
|
1201
|
+
.iter_mut()
|
|
1202
|
+
.find(|element| element.id == "wnd[0]/sbar")
|
|
1203
|
+
.expect("status bar");
|
|
1204
|
+
status.message_type = "E".into();
|
|
1205
|
+
status.text = "This function is not possible".into();
|
|
1206
|
+
|
|
1207
|
+
let error = d.navigate("/nSESSION_MANAGER").expect_err("SAP rejects it");
|
|
1208
|
+
assert!(error
|
|
1209
|
+
.to_string()
|
|
1210
|
+
.contains("transaction '/nSESSION_MANAGER'"));
|
|
1211
|
+
assert!(error.to_string().contains("This function is not possible"));
|
|
1212
|
+
}
|
|
1213
|
+
|
|
988
1214
|
#[test]
|
|
989
1215
|
fn vkey_mapping_covers_enter_and_function_keys() {
|
|
990
1216
|
assert_eq!(vkey_for("Enter", &[]), Some(0));
|