ballistics_engine/bridge/
ffi.rs1use std::ffi::{c_char, CStr, CString};
21
22use super::{bridge_call, BridgeErrorCode, BRIDGE_API_VERSION};
23
24fn respond(json: String) -> *mut c_char {
25 CString::new(json)
29 .unwrap_or_else(|_| {
30 CString::new(format!(
31 r#"{{"ok":false,"api_version":{BRIDGE_API_VERSION},"error":{{"code":"internal_error","message":"response contained an interior NUL"}}}}"#
32 ))
33 .expect("fallback envelope contains no NUL")
34 })
35 .into_raw()
36}
37
38fn error_envelope(code: BridgeErrorCode, message: &str) -> String {
39 let code = serde_json::to_string(&code).unwrap_or_else(|_| "\"internal_error\"".into());
42 format!(
43 r#"{{"ok":false,"api_version":{BRIDGE_API_VERSION},"error":{{"code":{code},"message":"{message}"}}}}"#
44 )
45}
46
47#[no_mangle]
53pub unsafe extern "C" fn ballistics_bridge_call(request_json: *const c_char) -> *mut c_char {
54 if request_json.is_null() {
55 return respond(error_envelope(
56 BridgeErrorCode::InvalidJson,
57 "request pointer is NULL",
58 ));
59 }
60 let bytes = CStr::from_ptr(request_json).to_bytes();
61 call_with_bytes(bytes)
62}
63
64#[no_mangle]
70pub unsafe extern "C" fn ballistics_bridge_call_n(request: *const u8, len: usize) -> *mut c_char {
71 if request.is_null() {
72 return respond(error_envelope(
73 BridgeErrorCode::InvalidJson,
74 "request pointer is NULL",
75 ));
76 }
77 let bytes = std::slice::from_raw_parts(request, len);
78 call_with_bytes(bytes)
79}
80
81fn call_with_bytes(bytes: &[u8]) -> *mut c_char {
82 match std::str::from_utf8(bytes) {
83 Ok(text) => respond(bridge_call(text)),
84 Err(_) => respond(error_envelope(
85 BridgeErrorCode::InvalidJson,
86 "request is not valid UTF-8",
87 )),
88 }
89}
90
91#[no_mangle]
98pub unsafe extern "C" fn ballistics_bridge_free(response: *mut c_char) {
99 if !response.is_null() {
100 drop(CString::from_raw(response));
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use std::ffi::CString;
108
109 fn roundtrip(request: &str) -> serde_json::Value {
110 let c_request = CString::new(request).unwrap();
111 let raw = unsafe { ballistics_bridge_call(c_request.as_ptr()) };
112 assert!(!raw.is_null(), "bridge must never return NULL");
113 let text = unsafe { CStr::from_ptr(raw) }.to_str().unwrap().to_owned();
114 unsafe { ballistics_bridge_free(raw) };
115 serde_json::from_str(&text).expect("bridge output is JSON")
116 }
117
118 #[test]
119 fn c_abi_smoke_meta_version() {
120 let out = roundtrip(r#"{"api_version":1,"command":"meta.version"}"#);
121 assert_eq!(out["ok"], true);
122 assert_eq!(out["result"]["engine_version"], env!("CARGO_PKG_VERSION"));
123 }
124
125 #[test]
126 fn null_request_is_an_envelope() {
127 let raw = unsafe { ballistics_bridge_call(std::ptr::null()) };
128 let text = unsafe { CStr::from_ptr(raw) }.to_str().unwrap().to_owned();
129 unsafe { ballistics_bridge_free(raw) };
130 let out: serde_json::Value = serde_json::from_str(&text).unwrap();
131 assert_eq!(out["ok"], false);
132 assert_eq!(out["error"]["code"], "invalid_json");
133 }
134
135 #[test]
136 fn length_variant_handles_non_terminated_buffers() {
137 let payload = br#"{"api_version":1,"command":"meta.version"}"#;
138 let raw = unsafe { ballistics_bridge_call_n(payload.as_ptr(), payload.len()) };
139 let text = unsafe { CStr::from_ptr(raw) }.to_str().unwrap().to_owned();
140 unsafe { ballistics_bridge_free(raw) };
141 let out: serde_json::Value = serde_json::from_str(&text).unwrap();
142 assert_eq!(out["ok"], true);
143 }
144
145 #[test]
146 fn invalid_utf8_is_an_envelope() {
147 let payload = [0xFFu8, 0xFE, 0x00];
148 let raw = unsafe { ballistics_bridge_call_n(payload.as_ptr(), payload.len()) };
149 let text = unsafe { CStr::from_ptr(raw) }.to_str().unwrap().to_owned();
150 unsafe { ballistics_bridge_free(raw) };
151 let out: serde_json::Value = serde_json::from_str(&text).unwrap();
152 assert_eq!(out["error"]["code"], "invalid_json");
153 }
154
155 #[test]
156 fn free_null_is_a_no_op() {
157 unsafe { ballistics_bridge_free(std::ptr::null_mut()) };
158 }
159
160 fn modified_utf8(text: &str) -> Vec<u8> {
165 let mut out = Vec::new();
166 for unit in text.encode_utf16() {
167 match unit {
168 0 => out.extend_from_slice(&[0xC0, 0x80]),
169 0x0001..=0x007F => out.push(unit as u8),
170 0x0080..=0x07FF => {
171 out.push(0xC0 | (unit >> 6) as u8);
172 out.push(0x80 | (unit & 0x3F) as u8);
173 }
174 _ => {
175 out.push(0xE0 | (unit >> 12) as u8);
176 out.push(0x80 | ((unit >> 6) & 0x3F) as u8);
177 out.push(0x80 | (unit & 0x3F) as u8);
178 }
179 }
180 }
181 out
182 }
183
184 #[test]
193 fn a_request_in_jni_modified_utf8_is_refused() {
194 let request = r#"{"api_version":1,"command":"no.such.command.🎯"}"#;
196 let standard = request.as_bytes();
197 let modified = modified_utf8(request);
198 assert_ne!(
199 modified.as_slice(),
200 standard,
201 "the fixture must actually exercise the divergence"
202 );
203
204 let decoded = |bytes: &[u8]| {
205 let raw = unsafe { ballistics_bridge_call_n(bytes.as_ptr(), bytes.len()) };
206 let text = unsafe { CStr::from_ptr(raw) }.to_str().unwrap().to_owned();
207 unsafe { ballistics_bridge_free(raw) };
208 serde_json::from_str::<serde_json::Value>(&text).unwrap()
209 };
210
211 let out = decoded(standard);
212 assert_eq!(out["error"]["code"], "unknown_command", "{out}");
213
214 let out = decoded(&modified);
215 assert_eq!(out["error"]["code"], "invalid_json", "{out}");
216 assert_eq!(
217 out["error"]["message"], "request is not valid UTF-8",
218 "{out}"
219 );
220 }
221
222 #[test]
226 fn modified_utf8_and_utf8_agree_below_the_supplementary_planes() {
227 for text in ["ascii", "München", "Ω", "中文", "\u{FFFF}"] {
228 assert_eq!(
229 modified_utf8(text),
230 text.as_bytes(),
231 "{text} must encode identically in both"
232 );
233 }
234 for text in ["🎯", "\u{10000}", "\u{10FFFF}"] {
235 assert_ne!(modified_utf8(text), text.as_bytes(), "{text} must diverge");
236 }
237 assert_eq!(modified_utf8("\0"), vec![0xC0, 0x80]);
240 }
241}