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.
Files changed (125) hide show
  1. {flowproof-0.12.2 → flowproof-0.13.0}/Cargo.lock +7 -7
  2. {flowproof-0.12.2 → flowproof-0.13.0}/Cargo.toml +1 -1
  3. {flowproof-0.12.2 → flowproof-0.13.0}/PKG-INFO +1 -1
  4. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_linux.rs +9 -0
  5. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/sap_com.rs +305 -79
  6. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/web.rs +139 -9
  7. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/author.rs +27 -0
  8. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/lib.rs +2 -1
  9. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/llm.rs +28 -8
  10. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/recorder.rs +127 -8
  11. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/spec.rs +4 -4
  12. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/tests/model_client_http.rs +30 -0
  13. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/src/lib.rs +296 -21
  14. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/sap_e2e.rs +1 -1
  15. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/sap_sim_e2e.rs +34 -0
  16. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/support/sap_simulator.py +76 -30
  17. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/lib.rs +3 -1
  18. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/recording.rs +360 -10
  19. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-python/src/lib.rs +58 -6
  20. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-replay/src/lib.rs +61 -6
  21. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-replay/tests/replay_calc.rs +75 -1
  22. {flowproof-0.12.2 → flowproof-0.13.0}/flowproof/__init__.py +1 -1
  23. {flowproof-0.12.2 → flowproof-0.13.0}/flowproof/flow.py +62 -8
  24. {flowproof-0.12.2 → flowproof-0.13.0}/flowproof/mcp_server.py +29 -6
  25. {flowproof-0.12.2 → flowproof-0.13.0}/pyproject.toml +1 -1
  26. {flowproof-0.12.2 → flowproof-0.13.0}/README.md +0 -0
  27. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/Cargo.toml +0 -0
  28. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/agent_proxy.rs +0 -0
  29. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/agent_runner.rs +0 -0
  30. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress.rs +0 -0
  31. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/audit.rs +0 -0
  32. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/capture.rs +0 -0
  33. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/env_block.rs +0 -0
  34. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/filters.rs +0 -0
  35. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/identity.rs +0 -0
  36. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/logon.rs +0 -0
  37. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/netevents.rs +0 -0
  38. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/run.rs +0 -0
  39. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/spawn.rs +0 -0
  40. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows/wfp.rs +0 -0
  41. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/egress_windows.rs +0 -0
  42. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/fs_observe.rs +0 -0
  43. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/lib.rs +0 -0
  44. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/mcp_core.rs +0 -0
  45. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/mcp_http.rs +0 -0
  46. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/mcp_stdio.rs +0 -0
  47. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/src/vision.rs +0 -0
  48. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/tests/drag_spike.rs +0 -0
  49. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/tests/fs_observation_e2e.rs +0 -0
  50. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-adapters/tests/windows_containment_e2e.rs +0 -0
  51. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/Cargo.toml +0 -0
  52. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/agent_steps.rs +0 -0
  53. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/clarify.rs +0 -0
  54. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/heal.rs +0 -0
  55. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-agent/src/rules.rs +0 -0
  56. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/Cargo.toml +0 -0
  57. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/src/agent_flow.rs +0 -0
  58. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/src/capture.rs +0 -0
  59. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/src/main.rs +0 -0
  60. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/agent_flow_e2e.rs +0 -0
  61. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/agent_url_record_e2e.rs +0 -0
  62. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/api_pipeline.rs +0 -0
  63. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/audit_record_e2e.rs +0 -0
  64. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/calc_e2e.rs +0 -0
  65. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/clock_e2e.rs +0 -0
  66. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/cookie_e2e.rs +0 -0
  67. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/doctor_e2e.rs +0 -0
  68. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/egress_e2e.rs +0 -0
  69. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/examples_resolve.rs +0 -0
  70. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/falsifiability_arg_matchers.rs +0 -0
  71. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/falsifiability_audit_since.rs +0 -0
  72. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/falsifiability_call_order.rs +0 -0
  73. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/falsifiability_capability_regression.rs +0 -0
  74. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/falsifiability_control_verdict.rs +0 -0
  75. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/falsifiability_mcp_kill.rs +0 -0
  76. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/falsifiability_no_tool_call.rs +0 -0
  77. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/falsifiability_reply_contains.rs +0 -0
  78. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/grid_cell_e2e.rs +0 -0
  79. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/iframe_e2e.rs +0 -0
  80. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/llm_author_e2e.rs +0 -0
  81. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/mcp_http_e2e.rs +0 -0
  82. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/mcp_stdio_e2e.rs +0 -0
  83. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/notepad_author_e2e.rs +0 -0
  84. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/notepad_e2e.rs +0 -0
  85. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/page_title_e2e.rs +0 -0
  86. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/pinned_random_e2e.rs +0 -0
  87. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/sap_pipeline.rs +0 -0
  88. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/scoped_container_e2e.rs +0 -0
  89. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/select_missing_option_e2e.rs +0 -0
  90. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/skip_unless_env.rs +0 -0
  91. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/suite_env_from.rs +0 -0
  92. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/suite_flow_isolation.rs +0 -0
  93. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/suite_missing_trace.rs +0 -0
  94. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/visibility_e2e.rs +0 -0
  95. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/vision_pipeline.rs +0 -0
  96. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-cli/tests/web_e2e.rs +0 -0
  97. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/Cargo.toml +0 -0
  98. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/app.rs +0 -0
  99. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/backend.rs +0 -0
  100. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/gdi.rs +0 -0
  101. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/mock.rs +0 -0
  102. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/oob.rs +0 -0
  103. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/redact.rs +0 -0
  104. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/visual.rs +0 -0
  105. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-driver/src/window.rs +0 -0
  106. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-python/Cargo.toml +0 -0
  107. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-replay/Cargo.toml +0 -0
  108. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-replay/src/report.rs +0 -0
  109. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-replay/src/runrecord.rs +0 -0
  110. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/Cargo.toml +0 -0
  111. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/schema/trace-v1.schema.json +0 -0
  112. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/captures.rs +0 -0
  113. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/cassette.rs +0 -0
  114. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/cassette_diff.rs +0 -0
  115. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/egress.rs +0 -0
  116. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/format.rs +0 -0
  117. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/lib.rs +0 -0
  118. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/secret.rs +0 -0
  119. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/secret_scan.rs +0 -0
  120. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/substitution.rs +0 -0
  121. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/src/toolcalls.rs +0 -0
  122. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl +0 -0
  123. {flowproof-0.12.2 → flowproof-0.13.0}/crates/flowproof-trace/tests/schema_conformance.rs +0 -0
  124. {flowproof-0.12.2 → flowproof-0.13.0}/flowproof/cli.py +0 -0
  125. {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.12.2"
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.12.2"
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.12.2"
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.12.2"
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.12.2"
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.12.2"
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.12.2"
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.12.2"
25
+ version = "0.13.0"
26
26
  edition = "2021"
27
27
  license = "Apache-2.0"
28
28
  repository = "https://github.com/automators-com/flowproof"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowproof
3
- Version: 0.12.2
3
+ Version: 0.13.0
4
4
  Classifier: Development Status :: 2 - Pre-Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Programming Language :: Python :: 3
@@ -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 running, logged-in SAP GUI session. `connection` is the
70
- /// SAP Logon connection description to open if no session exists yet
71
- /// (empty = attach-only).
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
- self.engine.set_text(OKCODE_FIELD, path)?;
259
- self.engine.send_vkey(0)
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). A no-op unless both
611
- /// SAP_USER and SAP_PASSWORD are set: attach-only and an explicit
612
- /// OpenConnection-without-login stay exactly as they were before this
613
- /// existed. Values are read fresh from the environment and used only
614
- /// as this one property-put each - never logged, never traced, never
615
- /// stored anywhere by this function.
616
- fn try_auto_login(session: &Disp) {
617
- let (Ok(user), Ok(password)) =
618
- (std::env::var("SAP_USER"), std::env::var("SAP_PASSWORD"))
619
- else {
620
- return;
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
- let _ = field.put("Text", VARIANT::from(BSTR::from(client.as_str())));
740
+ field.put("Text", VARIANT::from(BSTR::from(client.as_str())))?;
626
741
  }
627
742
  }
628
- if let Some(field) = Self::find_in(session, "wnd[0]/usr/txtRSYST-BNAME") {
629
- let _ = field.put("Text", VARIANT::from(BSTR::from(user.as_str())));
630
- }
631
- if let Some(field) = Self::find_in(session, "wnd[0]/usr/pwdRSYST-BCODE") {
632
- let _ = field.put("Text", VARIANT::from(BSTR::from(password.as_str())));
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
- if let Some(window) = Self::find_in(session, "wnd[0]") {
637
- let _ = window.call("SendVKey", vec![VARIANT::from(0i32)]);
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 opened = false;
734
- let mut login_attempted = false;
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<(Option<Disp>, bool), DriverError> {
737
- let sapgui = Disp(attach_to_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
- if connections.get_i32("Count").unwrap_or(0) == 0 {
741
- if !connection.is_empty() && !opened {
742
- engine.call(
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
- let session = sessions.call_disp("ElementAt", vec![VARIANT::from(0)])?;
755
- // A session object exists as soon as its window opens -
756
- // including the SAP login screen itself, and logging off
757
- // leaves that same window (and session object) open too.
758
- // Existence alone can't tell "logged in" from "sitting at
759
- // the login screen" - the login screen has the standard
760
- // toolbar and command field just like any other screen.
761
- // `Info.User` is empty until a user actually authenticates
762
- // (the login screen itself reports transaction `S000`,
763
- // SAP's own code for "not logged on yet").
764
- let logged_in = session
765
- .get_disp("Info")
766
- .map(|info| !info.get_string("User").is_empty())
767
- .unwrap_or(false);
768
- if !logged_in {
769
- // #226 follow-up: an unattended nightly run has
770
- // nobody to log back in when a session expires.
771
- // SAP_USER/SAP_PASSWORD let us do that ourselves,
772
- // once per attach - a no-op when they're unset, so
773
- // attach-only and interactive use are unchanged.
774
- if !login_attempted {
775
- Self::try_auto_login(&session);
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((Some(session), false))
961
+ Ok(None)
780
962
  })();
781
963
 
782
964
  match attempt {
783
- Ok((Some(session), _)) => {
965
+ Ok(Some(session)) => {
784
966
  self.session = Some(session);
785
967
  return Ok(());
786
968
  }
787
- Ok((None, at_login_screen)) => {
788
- opened = !connection.is_empty();
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
- return Err(DriverError::Uia(
801
- "sap-com: timed out waiting for a logged-in SAP GUI session".into(),
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));