1use std::ffi::{CStr, CString, c_char};
14use std::ptr;
15
16use crate::component;
17use crate::crdt;
18use crate::template;
19
20#[repr(C)]
22pub struct FfiComponentList {
23 pub json: *mut c_char,
25 pub count: usize,
27}
28
29#[repr(C)]
31pub struct FfiPatchResult {
32 pub text: *mut c_char,
34 pub error: *mut c_char,
36}
37
38#[repr(C)]
40pub struct FfiMergeResult {
41 pub text: *mut c_char,
43 pub state: *mut u8,
45 pub state_len: usize,
47 pub error: *mut c_char,
49}
50
51#[unsafe(no_mangle)]
61pub unsafe extern "C" fn agent_doc_parse_components(doc: *const c_char) -> FfiComponentList {
62 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
63 Ok(s) => s,
64 Err(_) => {
65 return FfiComponentList {
66 json: ptr::null_mut(),
67 count: 0,
68 };
69 }
70 };
71
72 let components = match component::parse(doc_str) {
73 Ok(c) => c,
74 Err(_) => {
75 return FfiComponentList {
76 json: ptr::null_mut(),
77 count: 0,
78 };
79 }
80 };
81
82 let count = components.len();
83
84 let json_items: Vec<serde_json::Value> = components
86 .iter()
87 .map(|c| {
88 serde_json::json!({
89 "name": c.name,
90 "attrs": c.attrs,
91 "open_start": c.open_start,
92 "open_end": c.open_end,
93 "close_start": c.close_start,
94 "close_end": c.close_end,
95 "content": c.content(doc_str),
96 })
97 })
98 .collect();
99
100 let json_str = serde_json::to_string(&json_items).unwrap_or_default();
101 let c_json = CString::new(json_str).unwrap_or_default();
102
103 FfiComponentList {
104 json: c_json.into_raw(),
105 count,
106 }
107}
108
109#[unsafe(no_mangle)]
117pub unsafe extern "C" fn agent_doc_apply_patch(
118 doc: *const c_char,
119 component_name: *const c_char,
120 content: *const c_char,
121 mode: *const c_char,
122) -> FfiPatchResult {
123 let make_err = |msg: &str| FfiPatchResult {
124 text: ptr::null_mut(),
125 error: CString::new(msg).unwrap_or_default().into_raw(),
126 };
127
128 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
129 Ok(s) => s,
130 Err(e) => return make_err(&format!("invalid doc UTF-8: {e}")),
131 };
132 let name = match unsafe { CStr::from_ptr(component_name) }.to_str() {
133 Ok(s) => s,
134 Err(e) => return make_err(&format!("invalid component name UTF-8: {e}")),
135 };
136 let patch_content = match unsafe { CStr::from_ptr(content) }.to_str() {
137 Ok(s) => s,
138 Err(e) => return make_err(&format!("invalid content UTF-8: {e}")),
139 };
140 let mode_str = match unsafe { CStr::from_ptr(mode) }.to_str() {
141 Ok(s) => s,
142 Err(e) => return make_err(&format!("invalid mode UTF-8: {e}")),
143 };
144
145 let patch = template::PatchBlock {
147 name: name.to_string(),
148 content: patch_content.to_string(),
149 };
150
151 let mut overrides = std::collections::HashMap::new();
153 overrides.insert(name.to_string(), mode_str.to_string());
154
155 let dummy_path = std::path::Path::new("/dev/null");
158 match template::apply_patches_with_overrides(
159 doc_str,
160 &[patch],
161 "",
162 dummy_path,
163 &overrides,
164 ) {
165 Ok(result) => FfiPatchResult {
166 text: CString::new(result).unwrap_or_default().into_raw(),
167 error: ptr::null_mut(),
168 },
169 Err(e) => make_err(&format!("{e}")),
170 }
171}
172
173#[unsafe(no_mangle)]
184pub unsafe extern "C" fn agent_doc_crdt_merge(
185 base_state: *const u8,
186 base_state_len: usize,
187 ours: *const c_char,
188 theirs: *const c_char,
189) -> FfiMergeResult {
190 let make_err = |msg: &str| FfiMergeResult {
191 text: ptr::null_mut(),
192 state: ptr::null_mut(),
193 state_len: 0,
194 error: CString::new(msg).unwrap_or_default().into_raw(),
195 };
196
197 let ours_str = match unsafe { CStr::from_ptr(ours) }.to_str() {
198 Ok(s) => s,
199 Err(e) => return make_err(&format!("invalid ours UTF-8: {e}")),
200 };
201 let theirs_str = match unsafe { CStr::from_ptr(theirs) }.to_str() {
202 Ok(s) => s,
203 Err(e) => return make_err(&format!("invalid theirs UTF-8: {e}")),
204 };
205
206 let base = if base_state.is_null() {
207 None
208 } else {
209 Some(unsafe { std::slice::from_raw_parts(base_state, base_state_len) })
210 };
211
212 match crdt::merge(base, ours_str, theirs_str) {
213 Ok(merged_text) => {
214 let doc = crdt::CrdtDoc::from_text(&merged_text);
216 let state_bytes = doc.encode_state();
217 let state_len = state_bytes.len();
218 let state_ptr = {
219 let mut boxed = state_bytes.into_boxed_slice();
220 let ptr = boxed.as_mut_ptr();
221 std::mem::forget(boxed);
222 ptr
223 };
224
225 FfiMergeResult {
226 text: CString::new(merged_text).unwrap_or_default().into_raw(),
227 state: state_ptr,
228 state_len,
229 error: ptr::null_mut(),
230 }
231 }
232 Err(e) => make_err(&format!("{e}")),
233 }
234}
235
236#[unsafe(no_mangle)]
242pub unsafe extern "C" fn agent_doc_free_string(ptr: *mut c_char) {
243 if !ptr.is_null() {
244 drop(unsafe { CString::from_raw(ptr) });
245 }
246}
247
248#[unsafe(no_mangle)]
254pub unsafe extern "C" fn agent_doc_free_state(ptr: *mut u8, len: usize) {
255 if !ptr.is_null() {
256 drop(unsafe { Vec::from_raw_parts(ptr, len, len) });
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn parse_components_roundtrip() {
266 let doc = "before\n<!-- agent:status -->\nhello\n<!-- /agent:status -->\nafter\n";
267 let c_doc = CString::new(doc).unwrap();
268 let result = unsafe { agent_doc_parse_components(c_doc.as_ptr()) };
269 assert_eq!(result.count, 1);
270 assert!(!result.json.is_null());
271 let json_str = unsafe { CStr::from_ptr(result.json) }.to_str().unwrap();
272 let parsed: Vec<serde_json::Value> = serde_json::from_str(json_str).unwrap();
273 assert_eq!(parsed[0]["name"], "status");
274 assert_eq!(parsed[0]["content"], "hello\n");
275 unsafe { agent_doc_free_string(result.json) };
276 }
277
278 #[test]
279 fn apply_patch_replace() {
280 let doc = "<!-- agent:output -->\nold\n<!-- /agent:output -->\n";
281 let c_doc = CString::new(doc).unwrap();
282 let c_name = CString::new("output").unwrap();
283 let c_content = CString::new("new content\n").unwrap();
284 let c_mode = CString::new("replace").unwrap();
285 let result = unsafe {
286 agent_doc_apply_patch(c_doc.as_ptr(), c_name.as_ptr(), c_content.as_ptr(), c_mode.as_ptr())
287 };
288 assert!(result.error.is_null());
289 assert!(!result.text.is_null());
290 let text = unsafe { CStr::from_ptr(result.text) }.to_str().unwrap();
291 assert!(text.contains("new content"));
292 assert!(!text.contains("old"));
293 unsafe { agent_doc_free_string(result.text) };
294 }
295
296 #[test]
297 fn crdt_merge_no_base() {
298 let c_ours = CString::new("hello world").unwrap();
299 let c_theirs = CString::new("hello world").unwrap();
300 let result = unsafe {
301 agent_doc_crdt_merge(ptr::null(), 0, c_ours.as_ptr(), c_theirs.as_ptr())
302 };
303 assert!(result.error.is_null());
304 assert!(!result.text.is_null());
305 let text = unsafe { CStr::from_ptr(result.text) }.to_str().unwrap();
306 assert_eq!(text, "hello world");
307 unsafe {
308 agent_doc_free_string(result.text);
309 agent_doc_free_state(result.state, result.state_len);
310 };
311 }
312}