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)]
186pub unsafe extern "C" fn agent_doc_apply_patch_with_caret(
187 doc: *const c_char,
188 component_name: *const c_char,
189 content: *const c_char,
190 mode: *const c_char,
191 caret_offset: i32,
192) -> FfiPatchResult {
193 let make_err = |msg: &str| FfiPatchResult {
194 text: ptr::null_mut(),
195 error: CString::new(msg).unwrap_or_default().into_raw(),
196 };
197
198 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
199 Ok(s) => s,
200 Err(e) => return make_err(&format!("invalid doc UTF-8: {e}")),
201 };
202 let name = match unsafe { CStr::from_ptr(component_name) }.to_str() {
203 Ok(s) => s,
204 Err(e) => return make_err(&format!("invalid component name UTF-8: {e}")),
205 };
206 let patch_content = match unsafe { CStr::from_ptr(content) }.to_str() {
207 Ok(s) => s,
208 Err(e) => return make_err(&format!("invalid content UTF-8: {e}")),
209 };
210 let mode_str = match unsafe { CStr::from_ptr(mode) }.to_str() {
211 Ok(s) => s,
212 Err(e) => return make_err(&format!("invalid mode UTF-8: {e}")),
213 };
214
215 if mode_str == "append" && caret_offset >= 0 {
217 let components = match component::parse(doc_str) {
218 Ok(c) => c,
219 Err(e) => return make_err(&format!("{e}")),
220 };
221 if let Some(comp) = components.iter().find(|c| c.name == name) {
222 let result = comp.append_with_caret(
223 doc_str,
224 patch_content,
225 Some(caret_offset as usize),
226 );
227 return FfiPatchResult {
228 text: CString::new(result).unwrap_or_default().into_raw(),
229 error: ptr::null_mut(),
230 };
231 }
232 }
233
234 let patch = template::PatchBlock {
236 name: name.to_string(),
237 content: patch_content.to_string(),
238 };
239 let mut overrides = std::collections::HashMap::new();
240 overrides.insert(name.to_string(), mode_str.to_string());
241 let dummy_path = std::path::Path::new("/dev/null");
242 match template::apply_patches_with_overrides(doc_str, &[patch], "", dummy_path, &overrides) {
243 Ok(result) => FfiPatchResult {
244 text: CString::new(result).unwrap_or_default().into_raw(),
245 error: ptr::null_mut(),
246 },
247 Err(e) => make_err(&format!("{e}")),
248 }
249}
250
251#[unsafe(no_mangle)]
262pub unsafe extern "C" fn agent_doc_crdt_merge(
263 base_state: *const u8,
264 base_state_len: usize,
265 ours: *const c_char,
266 theirs: *const c_char,
267) -> FfiMergeResult {
268 let make_err = |msg: &str| FfiMergeResult {
269 text: ptr::null_mut(),
270 state: ptr::null_mut(),
271 state_len: 0,
272 error: CString::new(msg).unwrap_or_default().into_raw(),
273 };
274
275 let ours_str = match unsafe { CStr::from_ptr(ours) }.to_str() {
276 Ok(s) => s,
277 Err(e) => return make_err(&format!("invalid ours UTF-8: {e}")),
278 };
279 let theirs_str = match unsafe { CStr::from_ptr(theirs) }.to_str() {
280 Ok(s) => s,
281 Err(e) => return make_err(&format!("invalid theirs UTF-8: {e}")),
282 };
283
284 let base = if base_state.is_null() {
285 None
286 } else {
287 Some(unsafe { std::slice::from_raw_parts(base_state, base_state_len) })
288 };
289
290 match crdt::merge(base, ours_str, theirs_str) {
291 Ok(merged_text) => {
292 let doc = crdt::CrdtDoc::from_text(&merged_text);
294 let state_bytes = doc.encode_state();
295 let state_len = state_bytes.len();
296 let state_ptr = {
297 let mut boxed = state_bytes.into_boxed_slice();
298 let ptr = boxed.as_mut_ptr();
299 std::mem::forget(boxed);
300 ptr
301 };
302
303 FfiMergeResult {
304 text: CString::new(merged_text).unwrap_or_default().into_raw(),
305 state: state_ptr,
306 state_len,
307 error: ptr::null_mut(),
308 }
309 }
310 Err(e) => make_err(&format!("{e}")),
311 }
312}
313
314#[unsafe(no_mangle)]
323pub unsafe extern "C" fn agent_doc_merge_frontmatter(
324 doc: *const c_char,
325 yaml_fields: *const c_char,
326) -> FfiPatchResult {
327 let make_err = |msg: &str| FfiPatchResult {
328 text: ptr::null_mut(),
329 error: CString::new(msg).unwrap_or_default().into_raw(),
330 };
331
332 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
333 Ok(s) => s,
334 Err(e) => return make_err(&format!("invalid doc UTF-8: {e}")),
335 };
336 let fields_str = match unsafe { CStr::from_ptr(yaml_fields) }.to_str() {
337 Ok(s) => s,
338 Err(e) => return make_err(&format!("invalid yaml_fields UTF-8: {e}")),
339 };
340
341 match frontmatter::merge_fields(doc_str, fields_str) {
342 Ok(result) => FfiPatchResult {
343 text: CString::new(result).unwrap_or_default().into_raw(),
344 error: ptr::null_mut(),
345 },
346 Err(e) => make_err(&format!("{e}")),
347 }
348}
349
350#[unsafe(no_mangle)]
356pub unsafe extern "C" fn agent_doc_free_string(ptr: *mut c_char) {
357 if !ptr.is_null() {
358 drop(unsafe { CString::from_raw(ptr) });
359 }
360}
361
362#[unsafe(no_mangle)]
368pub unsafe extern "C" fn agent_doc_free_state(ptr: *mut u8, len: usize) {
369 if !ptr.is_null() {
370 drop(unsafe { Vec::from_raw_parts(ptr, len, len) });
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn parse_components_roundtrip() {
380 let doc = "before\n<!-- agent:status -->\nhello\n<!-- /agent:status -->\nafter\n";
381 let c_doc = CString::new(doc).unwrap();
382 let result = unsafe { agent_doc_parse_components(c_doc.as_ptr()) };
383 assert_eq!(result.count, 1);
384 assert!(!result.json.is_null());
385 let json_str = unsafe { CStr::from_ptr(result.json) }.to_str().unwrap();
386 let parsed: Vec<serde_json::Value> = serde_json::from_str(json_str).unwrap();
387 assert_eq!(parsed[0]["name"], "status");
388 assert_eq!(parsed[0]["content"], "hello\n");
389 unsafe { agent_doc_free_string(result.json) };
390 }
391
392 #[test]
393 fn apply_patch_replace() {
394 let doc = "<!-- agent:output -->\nold\n<!-- /agent:output -->\n";
395 let c_doc = CString::new(doc).unwrap();
396 let c_name = CString::new("output").unwrap();
397 let c_content = CString::new("new content\n").unwrap();
398 let c_mode = CString::new("replace").unwrap();
399 let result = unsafe {
400 agent_doc_apply_patch(c_doc.as_ptr(), c_name.as_ptr(), c_content.as_ptr(), c_mode.as_ptr())
401 };
402 assert!(result.error.is_null());
403 assert!(!result.text.is_null());
404 let text = unsafe { CStr::from_ptr(result.text) }.to_str().unwrap();
405 assert!(text.contains("new content"));
406 assert!(!text.contains("old"));
407 unsafe { agent_doc_free_string(result.text) };
408 }
409
410 #[test]
411 fn merge_frontmatter_adds_field() {
412 let doc = "---\nagent_doc_session: abc\n---\nBody\n";
413 let fields = "model: opus";
414 let c_doc = CString::new(doc).unwrap();
415 let c_fields = CString::new(fields).unwrap();
416 let result = unsafe {
417 agent_doc_merge_frontmatter(c_doc.as_ptr(), c_fields.as_ptr())
418 };
419 assert!(result.error.is_null());
420 assert!(!result.text.is_null());
421 let text = unsafe { CStr::from_ptr(result.text) }.to_str().unwrap();
422 assert!(text.contains("model: opus"));
423 assert!(text.contains("agent_doc_session: abc"));
424 assert!(text.contains("Body"));
425 unsafe { agent_doc_free_string(result.text) };
426 }
427
428 #[test]
429 fn crdt_merge_no_base() {
430 let c_ours = CString::new("hello world").unwrap();
431 let c_theirs = CString::new("hello world").unwrap();
432 let result = unsafe {
433 agent_doc_crdt_merge(ptr::null(), 0, c_ours.as_ptr(), c_theirs.as_ptr())
434 };
435 assert!(result.error.is_null());
436 assert!(!result.text.is_null());
437 let text = unsafe { CStr::from_ptr(result.text) }.to_str().unwrap();
438 assert_eq!(text, "hello world");
439 unsafe {
440 agent_doc_free_string(result.text);
441 agent_doc_free_state(result.state, result.state_len);
442 };
443 }
444}