PINK: fix ctypes c_char_p null-byte truncation (INVALID_INTENT_PARSE)
_to_rust_bytes() centralises all Python→Rust JSON serialisation:
- _json_null_clean() strips U+0000 from all string values recursively
- ensure_ascii=True guarantees no 0x00 in output bytes
- All _json() call sites migrated; mode/verbosity now .encode("ascii")
- 9 null-safety unit tests added to TestRustBytesNullSafety
Root cause: ctypes.c_char_p silently truncates at first 0x00 byte,
causing serde_json "premature end of input at column 41" on EXIT intents
with BNB-USDT leverage values. Long-term fix: Rust FFI (ptr, len) pairs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1479,3 +1479,80 @@ class TestNormalizeEngForTui:
|
||||
eng = {"open_positions": 0, "slot": {}}
|
||||
out = self._norm(eng)
|
||||
assert out["open_positions"] == [], "zero open_positions must become empty list"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# _to_rust_bytes / _json_null_clean — null-byte safety
|
||||
# ============================================================
|
||||
|
||||
class TestRustBytesNullSafety:
|
||||
"""_to_rust_bytes must never produce a 0x00 byte in its output.
|
||||
|
||||
Root cause: ctypes.c_char_p treats the first 0x00 as a C null terminator,
|
||||
silently truncating the JSON before Rust's serde_json sees the full payload.
|
||||
Reproduces the INVALID_INTENT_PARSE bug seen during BingX VST smoke test.
|
||||
"""
|
||||
|
||||
def _encode(self, payload):
|
||||
from prod.clean_arch.dita_v2.rust_backend import _to_rust_bytes
|
||||
return _to_rust_bytes(payload)
|
||||
|
||||
def _clean(self, obj):
|
||||
from prod.clean_arch.dita_v2.rust_backend import _json_null_clean
|
||||
return _json_null_clean(obj)
|
||||
|
||||
def test_no_null_bytes_in_normal_exit_intent(self):
|
||||
payload = {
|
||||
"action": "EXIT",
|
||||
"asset": "BNB-USDT",
|
||||
"leverage": 1.3465735902799727,
|
||||
"target_size": 1.76,
|
||||
"reference_price": 66337.09,
|
||||
"limit_price": 0.0,
|
||||
"trade_id": "t1",
|
||||
"metadata": {},
|
||||
}
|
||||
encoded = self._encode(payload)
|
||||
assert b"\x00" not in encoded, "EXIT intent must have no null bytes"
|
||||
|
||||
def test_no_null_bytes_when_string_contains_u0000(self):
|
||||
"""A string value containing \\u0000 must not produce a null byte in output."""
|
||||
payload = {"event_id": "BX\x00data", "price": 100.0}
|
||||
encoded = self._encode(payload)
|
||||
assert b"\x00" not in encoded, "Null char in string must not produce null byte"
|
||||
|
||||
def test_no_null_bytes_in_seen_event_ids(self):
|
||||
"""seen_event_ids list is serialized with all other slot fields."""
|
||||
payload = {"seen_event_ids": ["123", "456\x00789", "999"], "size": 1.76}
|
||||
encoded = self._encode(payload)
|
||||
assert b"\x00" not in encoded, "seen_event_ids with null chars must be clean"
|
||||
|
||||
def test_no_null_bytes_in_nested_metadata(self):
|
||||
payload = {"metadata": {"venue_note": "order\x00ok", "id": 42}, "asset": "ENJ-USDT"}
|
||||
encoded = self._encode(payload)
|
||||
assert b"\x00" not in encoded, "Nested metadata null chars must be sanitized"
|
||||
|
||||
def test_output_is_valid_json(self):
|
||||
import json
|
||||
payload = {"action": "ENTER", "asset": "BNB-USDT", "leverage": 2.7, "seen_event_ids": ["e1"]}
|
||||
encoded = self._encode(payload)
|
||||
parsed = json.loads(encoded)
|
||||
assert parsed["action"] == "ENTER"
|
||||
|
||||
def test_json_null_clean_replaces_null_in_string(self):
|
||||
result = self._clean({"key": "val\x00ue"})
|
||||
assert "\x00" not in result["key"]
|
||||
assert "val" in result["key"]
|
||||
|
||||
def test_json_null_clean_recursion(self):
|
||||
obj = {"nested": {"list": ["a\x00b", 1, {"deep": "x\x00y"}]}}
|
||||
cleaned = self._clean(obj)
|
||||
assert "\x00" not in cleaned["nested"]["list"][0]
|
||||
assert "\x00" not in cleaned["nested"]["list"][2]["deep"]
|
||||
|
||||
def test_normal_ascii_payload_roundtrips_intact(self):
|
||||
import json
|
||||
payload = {"action": "EXIT", "asset": "BTC-USDT", "leverage": 1.5, "size": 0.001}
|
||||
encoded = self._encode(payload)
|
||||
assert json.loads(encoded)["asset"] == "BTC-USDT"
|
||||
assert json.loads(encoded)["leverage"] == 1.5
|
||||
|
||||
Reference in New Issue
Block a user