1use std::ffi::{CStr, CString, c_char};
14use std::ptr;
15
16use crate::component;
17use crate::crdt;
18use crate::frontmatter;
19use crate::template;
20
21#[repr(C)]
23pub struct FfiComponentList {
24 pub json: *mut c_char,
26 pub count: usize,
28}
29
30#[repr(C)]
32pub struct FfiPatchResult {
33 pub text: *mut c_char,
35 pub error: *mut c_char,
37}
38
39#[repr(C)]
41pub struct FfiMergeResult {
42 pub text: *mut c_char,
44 pub state: *mut u8,
46 pub state_len: usize,
48 pub error: *mut c_char,
50}
51
52#[unsafe(no_mangle)]
62pub unsafe extern "C" fn agent_doc_parse_components(doc: *const c_char) -> FfiComponentList {
63 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
64 Ok(s) => s,
65 Err(_) => {
66 return FfiComponentList {
67 json: ptr::null_mut(),
68 count: 0,
69 };
70 }
71 };
72
73 let components = match component::parse(doc_str) {
74 Ok(c) => c,
75 Err(_) => {
76 return FfiComponentList {
77 json: ptr::null_mut(),
78 count: 0,
79 };
80 }
81 };
82
83 let count = components.len();
84
85 let json_items: Vec<serde_json::Value> = components
87 .iter()
88 .map(|c| {
89 serde_json::json!({
90 "name": c.name,
91 "attrs": c.attrs,
92 "open_start": c.open_start,
93 "open_end": c.open_end,
94 "close_start": c.close_start,
95 "close_end": c.close_end,
96 "content": c.content(doc_str),
97 })
98 })
99 .collect();
100
101 let json_str = serde_json::to_string(&json_items).unwrap_or_default();
102 let c_json = CString::new(json_str).unwrap_or_default();
103
104 FfiComponentList {
105 json: c_json.into_raw(),
106 count,
107 }
108}
109
110#[unsafe(no_mangle)]
118pub unsafe extern "C" fn agent_doc_apply_patch(
119 doc: *const c_char,
120 component_name: *const c_char,
121 content: *const c_char,
122 mode: *const c_char,
123) -> FfiPatchResult {
124 let make_err = |msg: &str| FfiPatchResult {
125 text: ptr::null_mut(),
126 error: CString::new(msg).unwrap_or_default().into_raw(),
127 };
128
129 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
130 Ok(s) => s,
131 Err(e) => return make_err(&format!("invalid doc UTF-8: {e}")),
132 };
133 let name = match unsafe { CStr::from_ptr(component_name) }.to_str() {
134 Ok(s) => s,
135 Err(e) => return make_err(&format!("invalid component name UTF-8: {e}")),
136 };
137 let patch_content = match unsafe { CStr::from_ptr(content) }.to_str() {
138 Ok(s) => s,
139 Err(e) => return make_err(&format!("invalid content UTF-8: {e}")),
140 };
141 let mode_str = match unsafe { CStr::from_ptr(mode) }.to_str() {
142 Ok(s) => s,
143 Err(e) => return make_err(&format!("invalid mode UTF-8: {e}")),
144 };
145
146 let patch = template::PatchBlock {
148 name: name.to_string(),
149 content: patch_content.to_string(),
150 };
151
152 let mut overrides = std::collections::HashMap::new();
154 overrides.insert(name.to_string(), mode_str.to_string());
155
156 let dummy_path = std::path::Path::new("/dev/null");
159 match template::apply_patches_with_overrides(
160 doc_str,
161 &[patch],
162 "",
163 dummy_path,
164 &overrides,
165 ) {
166 Ok(result) => FfiPatchResult {
167 text: CString::new(result).unwrap_or_default().into_raw(),
168 error: ptr::null_mut(),
169 },
170 Err(e) => make_err(&format!("{e}")),
171 }
172}
173
174#[unsafe(no_mangle)]
185pub unsafe extern "C" fn agent_doc_crdt_merge(
186 base_state: *const u8,
187 base_state_len: usize,
188 ours: *const c_char,
189 theirs: *const c_char,
190) -> FfiMergeResult {
191 let make_err = |msg: &str| FfiMergeResult {
192 text: ptr::null_mut(),
193 state: ptr::null_mut(),
194 state_len: 0,
195 error: CString::new(msg).unwrap_or_default().into_raw(),
196 };
197
198 let ours_str = match unsafe { CStr::from_ptr(ours) }.to_str() {
199 Ok(s) => s,
200 Err(e) => return make_err(&format!("invalid ours UTF-8: {e}")),
201 };
202 let theirs_str = match unsafe { CStr::from_ptr(theirs) }.to_str() {
203 Ok(s) => s,
204 Err(e) => return make_err(&format!("invalid theirs UTF-8: {e}")),
205 };
206
207 let base = if base_state.is_null() {
208 None
209 } else {
210 Some(unsafe { std::slice::from_raw_parts(base_state, base_state_len) })
211 };
212
213 match crdt::merge(base, ours_str, theirs_str) {
214 Ok(merged_text) => {
215 let doc = crdt::CrdtDoc::from_text(&merged_text);
217 let state_bytes = doc.encode_state();
218 let state_len = state_bytes.len();
219 let state_ptr = {
220 let mut boxed = state_bytes.into_boxed_slice();
221 let ptr = boxed.as_mut_ptr();
222 std::mem::forget(boxed);
223 ptr
224 };
225
226 FfiMergeResult {
227 text: CString::new(merged_text).unwrap_or_default().into_raw(),
228 state: state_ptr,
229 state_len,
230 error: ptr::null_mut(),
231 }
232 }
233 Err(e) => make_err(&format!("{e}")),
234 }
235}
236
237#[unsafe(no_mangle)]
246pub unsafe extern "C" fn agent_doc_merge_frontmatter(
247 doc: *const c_char,
248 yaml_fields: *const c_char,
249) -> FfiPatchResult {
250 let make_err = |msg: &str| FfiPatchResult {
251 text: ptr::null_mut(),
252 error: CString::new(msg).unwrap_or_default().into_raw(),
253 };
254
255 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
256 Ok(s) => s,
257 Err(e) => return make_err(&format!("invalid doc UTF-8: {e}")),
258 };
259 let fields_str = match unsafe { CStr::from_ptr(yaml_fields) }.to_str() {
260 Ok(s) => s,
261 Err(e) => return make_err(&format!("invalid yaml_fields UTF-8: {e}")),
262 };
263
264 match frontmatter::merge_fields(doc_str, fields_str) {
265 Ok(result) => FfiPatchResult {
266 text: CString::new(result).unwrap_or_default().into_raw(),
267 error: ptr::null_mut(),
268 },
269 Err(e) => make_err(&format!("{e}")),
270 }
271}
272
273#[unsafe(no_mangle)]
279pub unsafe extern "C" fn agent_doc_free_string(ptr: *mut c_char) {
280 if !ptr.is_null() {
281 drop(unsafe { CString::from_raw(ptr) });
282 }
283}
284
285#[unsafe(no_mangle)]
291pub unsafe extern "C" fn agent_doc_free_state(ptr: *mut u8, len: usize) {
292 if !ptr.is_null() {
293 drop(unsafe { Vec::from_raw_parts(ptr, len, len) });
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn parse_components_roundtrip() {
303 let doc = "before\n<!-- agent:status -->\nhello\n<!-- /agent:status -->\nafter\n";
304 let c_doc = CString::new(doc).unwrap();
305 let result = unsafe { agent_doc_parse_components(c_doc.as_ptr()) };
306 assert_eq!(result.count, 1);
307 assert!(!result.json.is_null());
308 let json_str = unsafe { CStr::from_ptr(result.json) }.to_str().unwrap();
309 let parsed: Vec<serde_json::Value> = serde_json::from_str(json_str).unwrap();
310 assert_eq!(parsed[0]["name"], "status");
311 assert_eq!(parsed[0]["content"], "hello\n");
312 unsafe { agent_doc_free_string(result.json) };
313 }
314
315 #[test]
316 fn apply_patch_replace() {
317 let doc = "<!-- agent:output -->\nold\n<!-- /agent:output -->\n";
318 let c_doc = CString::new(doc).unwrap();
319 let c_name = CString::new("output").unwrap();
320 let c_content = CString::new("new content\n").unwrap();
321 let c_mode = CString::new("replace").unwrap();
322 let result = unsafe {
323 agent_doc_apply_patch(c_doc.as_ptr(), c_name.as_ptr(), c_content.as_ptr(), c_mode.as_ptr())
324 };
325 assert!(result.error.is_null());
326 assert!(!result.text.is_null());
327 let text = unsafe { CStr::from_ptr(result.text) }.to_str().unwrap();
328 assert!(text.contains("new content"));
329 assert!(!text.contains("old"));
330 unsafe { agent_doc_free_string(result.text) };
331 }
332
333 #[test]
334 fn merge_frontmatter_adds_field() {
335 let doc = "---\nagent_doc_session: abc\n---\nBody\n";
336 let fields = "model: opus";
337 let c_doc = CString::new(doc).unwrap();
338 let c_fields = CString::new(fields).unwrap();
339 let result = unsafe {
340 agent_doc_merge_frontmatter(c_doc.as_ptr(), c_fields.as_ptr())
341 };
342 assert!(result.error.is_null());
343 assert!(!result.text.is_null());
344 let text = unsafe { CStr::from_ptr(result.text) }.to_str().unwrap();
345 assert!(text.contains("model: opus"));
346 assert!(text.contains("agent_doc_session: abc"));
347 assert!(text.contains("Body"));
348 unsafe { agent_doc_free_string(result.text) };
349 }
350
351 #[test]
352 fn crdt_merge_no_base() {
353 let c_ours = CString::new("hello world").unwrap();
354 let c_theirs = CString::new("hello world").unwrap();
355 let result = unsafe {
356 agent_doc_crdt_merge(ptr::null(), 0, c_ours.as_ptr(), c_theirs.as_ptr())
357 };
358 assert!(result.error.is_null());
359 assert!(!result.text.is_null());
360 let text = unsafe { CStr::from_ptr(result.text) }.to_str().unwrap();
361 assert_eq!(text, "hello world");
362 unsafe {
363 agent_doc_free_string(result.text);
364 agent_doc_free_state(result.state, result.state_len);
365 };
366 }
367}