Skip to main content

tinyxml2_capi/
lib.rs

1//! C FFI compatibility layer for tinyxml2-rs.
2//!
3//! This crate provides `extern "C"` functions that expose the tinyxml2 Rust API
4//! through a C-compatible ABI. It produces both static and shared libraries.
5//!
6//! # Safety
7//!
8//! This crate necessarily uses `unsafe` at the FFI boundary. All public functions
9//! validate inputs and catch panics to prevent undefined behavior across the
10//! FFI boundary.
11//!
12//! # String Lifetimes
13//!
14//! Functions returning `*const c_char` return pointers to strings cached inside
15//! the `TxDocument` or `TxPrinter` wrapper. These pointers remain valid until:
16//! - The wrapper is freed (`tx_document_free` / `tx_printer_free`).
17//! - A mutating operation is called on the wrapper (e.g., parse, insert, delete).
18
19// Line numbers are always small positive u32 values; the cast to c_int (i32)
20// will not wrap in any realistic scenario.
21#![allow(clippy::cast_possible_wrap)]
22#![warn(missing_docs)]
23
24mod types;
25
26pub use types::{TX_NULL_NODE, TxDocument, TxError, TxNodeId, TxNodeType, TxPrinter};
27
28use std::ffi::{CStr, CString, c_char, c_double, c_int};
29use std::ptr;
30
31// ============================================================
32// Internal helpers
33// ============================================================
34
35/// Converts a C string pointer to a Rust `&str`.
36///
37/// Returns `None` if the pointer is null or the string is not valid UTF-8.
38///
39/// # Safety
40///
41/// The caller must ensure that `s` points to a valid, null-terminated C string
42/// (or is null). The returned `&str` borrows the C string's memory.
43unsafe fn cstr_to_str<'a>(s: *const c_char) -> Option<&'a str> {
44    if s.is_null() {
45        return None;
46    }
47    unsafe { CStr::from_ptr(s) }.to_str().ok()
48}
49
50/// Caches a Rust string slice in the document's string cache and returns
51/// a pointer to the cached C string.
52///
53/// Returns null if the string contains interior null bytes.
54fn cache_str(doc: &mut TxDocument, s: &str) -> *const c_char {
55    match CString::new(s) {
56        Ok(cs) => {
57            let ptr = cs.as_ptr();
58            doc.string_cache.push(cs);
59            ptr
60        }
61        Err(_) => ptr::null(),
62    }
63}
64
65/// Wraps a closure in `catch_unwind` with `AssertUnwindSafe`, returning a
66/// default value if a panic occurs.
67macro_rules! ffi_catch {
68    ($default:expr, $body:expr) => {
69        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| $body)) {
70            Ok(val) => val,
71            Err(_) => $default,
72        }
73    };
74}
75
76// ============================================================
77// Document Lifecycle
78// ============================================================
79
80/// Creates a new, empty XML document.
81///
82/// The returned pointer must eventually be freed with [`tx_document_free`].
83///
84/// # Safety
85///
86/// The caller must free the returned pointer with [`tx_document_free`] when
87/// done. Returns null on allocation failure.
88#[unsafe(no_mangle)]
89pub extern "C" fn tx_document_new() -> *mut TxDocument {
90    ffi_catch!(ptr::null_mut(), {
91        Box::into_raw(Box::new(TxDocument::new()))
92    })
93}
94
95/// Frees a document previously created with [`tx_document_new`].
96///
97/// # Safety
98///
99/// `doc` must be a valid pointer returned by [`tx_document_new`], or null.
100/// After this call, the pointer must not be used again.
101#[unsafe(no_mangle)]
102pub unsafe extern "C" fn tx_document_free(doc: *mut TxDocument) {
103    if doc.is_null() {
104        return;
105    }
106    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
107        drop(unsafe { Box::from_raw(doc) });
108    }));
109}
110
111/// Clears the document, removing all nodes and resetting to an empty state.
112///
113/// # Safety
114///
115/// `doc` must be a valid, non-null pointer to a `TxDocument`.
116#[unsafe(no_mangle)]
117pub unsafe extern "C" fn tx_document_clear(doc: *mut TxDocument) {
118    if doc.is_null() {
119        return;
120    }
121    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
122        let doc = unsafe { &mut *doc };
123        doc.invalidate_caches();
124        doc.doc.clear();
125    }));
126}
127
128/// Parses an XML string into the document, replacing any existing content.
129///
130/// # Safety
131///
132/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
133/// - `xml` must be a valid, non-null pointer to a null-terminated UTF-8 C string.
134#[unsafe(no_mangle)]
135pub unsafe extern "C" fn tx_document_parse(doc: *mut TxDocument, xml: *const c_char) -> TxError {
136    if doc.is_null() {
137        return TxError::TxErrorInvalidNodeId;
138    }
139    ffi_catch!(TxError::TxErrorInvalidNodeId, {
140        let doc = unsafe { &mut *doc };
141        let Some(xml_str) = (unsafe { cstr_to_str(xml) }) else {
142            return TxError::TxErrorEmptyDocument;
143        };
144        doc.invalidate_caches();
145        TxError::from_result(&doc.doc.parse_str(xml_str))
146    })
147}
148
149/// Loads and parses an XML file.
150///
151/// # Safety
152///
153/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
154/// - `path` must be a valid, non-null pointer to a null-terminated UTF-8 C string
155///   containing a filesystem path.
156#[unsafe(no_mangle)]
157pub unsafe extern "C" fn tx_document_load_file(
158    doc: *mut TxDocument,
159    path: *const c_char,
160) -> TxError {
161    if doc.is_null() {
162        return TxError::TxErrorInvalidNodeId;
163    }
164    ffi_catch!(TxError::TxErrorInvalidNodeId, {
165        let doc = unsafe { &mut *doc };
166        let Some(path_str) = (unsafe { cstr_to_str(path) }) else {
167            return TxError::TxErrorFileNotFound;
168        };
169        doc.invalidate_caches();
170        TxError::from_result(&doc.doc.load_file_mut(path_str))
171    })
172}
173
174/// Saves the document to a file (pretty-printed).
175///
176/// # Safety
177///
178/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
179/// - `path` must be a valid, non-null pointer to a null-terminated UTF-8 C string
180///   containing a filesystem path.
181#[unsafe(no_mangle)]
182pub unsafe extern "C" fn tx_document_save_file(
183    doc: *const TxDocument,
184    path: *const c_char,
185) -> TxError {
186    if doc.is_null() {
187        return TxError::TxErrorInvalidNodeId;
188    }
189    ffi_catch!(TxError::TxErrorInvalidNodeId, {
190        let doc = unsafe { &*doc };
191        let Some(path_str) = (unsafe { cstr_to_str(path) }) else {
192            return TxError::TxErrorFileNotFound;
193        };
194        TxError::from_result(&doc.doc.save_file(path_str))
195    })
196}
197
198/// Returns the pretty-printed XML string for the document.
199///
200/// The returned pointer is valid until the next mutating operation on the
201/// document or until the document is freed.
202///
203/// # Safety
204///
205/// `doc` must be a valid, non-null pointer to a `TxDocument`.
206#[unsafe(no_mangle)]
207pub unsafe extern "C" fn tx_document_to_string(doc: *mut TxDocument) -> *const c_char {
208    if doc.is_null() {
209        return ptr::null();
210    }
211    ffi_catch!(ptr::null(), {
212        let doc = unsafe { &mut *doc };
213        let s = doc.doc.to_string();
214        match CString::new(s) {
215            Ok(cs) => {
216                let ptr = cs.as_ptr();
217                doc.cached_to_string = Some(cs);
218                ptr
219            }
220            Err(_) => ptr::null(),
221        }
222    })
223}
224
225/// Returns the compact XML string for the document.
226///
227/// The returned pointer is valid until the next mutating operation on the
228/// document or until the document is freed.
229///
230/// # Safety
231///
232/// `doc` must be a valid, non-null pointer to a `TxDocument`.
233#[unsafe(no_mangle)]
234pub unsafe extern "C" fn tx_document_to_string_compact(doc: *mut TxDocument) -> *const c_char {
235    if doc.is_null() {
236        return ptr::null();
237    }
238    ffi_catch!(ptr::null(), {
239        let doc = unsafe { &mut *doc };
240        let s = doc.doc.to_string_compact();
241        match CString::new(s) {
242            Ok(cs) => {
243                let ptr = cs.as_ptr();
244                doc.cached_to_string_compact = Some(cs);
245                ptr
246            }
247            Err(_) => ptr::null(),
248        }
249    })
250}
251
252/// Returns the current error code of the document.
253///
254/// Returns [`TxError::TxSuccess`] if no error has occurred.
255///
256/// # Safety
257///
258/// `doc` must be a valid, non-null pointer to a `TxDocument`.
259#[unsafe(no_mangle)]
260pub unsafe extern "C" fn tx_document_error(doc: *const TxDocument) -> TxError {
261    if doc.is_null() {
262        return TxError::TxErrorInvalidNodeId;
263    }
264    ffi_catch!(TxError::TxErrorInvalidNodeId, {
265        let doc = unsafe { &*doc };
266        match doc.doc.error() {
267            Some(e) => TxError::from_xml_error(&e),
268            None => TxError::TxSuccess,
269        }
270    })
271}
272
273/// Returns the line number of the current error, or 0 if no error.
274///
275/// # Safety
276///
277/// `doc` must be a valid, non-null pointer to a `TxDocument`.
278#[unsafe(no_mangle)]
279pub unsafe extern "C" fn tx_document_error_line(doc: *const TxDocument) -> c_int {
280    if doc.is_null() {
281        return 0;
282    }
283    ffi_catch!(0, {
284        let doc = unsafe { &*doc };
285        doc.doc.error_line().unwrap_or(0) as c_int
286    })
287}
288
289/// Returns the error name string, or null if no error.
290///
291/// The returned pointer is valid until the next mutating operation on the
292/// document or until the document is freed.
293///
294/// # Safety
295///
296/// `doc` must be a valid, non-null pointer to a `TxDocument`.
297#[unsafe(no_mangle)]
298pub unsafe extern "C" fn tx_document_error_name(doc: *mut TxDocument) -> *const c_char {
299    if doc.is_null() {
300        return ptr::null();
301    }
302    ffi_catch!(ptr::null(), {
303        let doc = unsafe { &mut *doc };
304        match doc.doc.error() {
305            Some(e) => {
306                let name = e.name();
307                match CString::new(name) {
308                    Ok(cs) => {
309                        let ptr = cs.as_ptr();
310                        doc.cached_error_name = Some(cs);
311                        ptr
312                    }
313                    Err(_) => ptr::null(),
314                }
315            }
316            None => ptr::null(),
317        }
318    })
319}
320
321// ============================================================
322// Factory Functions
323// ============================================================
324
325/// Creates a new element node with the given tag name.
326///
327/// # Safety
328///
329/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
330/// - `name` must be a valid, non-null pointer to a null-terminated UTF-8 string.
331#[unsafe(no_mangle)]
332pub unsafe extern "C" fn tx_new_element(doc: *mut TxDocument, name: *const c_char) -> TxNodeId {
333    if doc.is_null() {
334        return TX_NULL_NODE;
335    }
336    ffi_catch!(TX_NULL_NODE, {
337        let doc = unsafe { &mut *doc };
338        let Some(name_str) = (unsafe { cstr_to_str(name) }) else {
339            return TX_NULL_NODE;
340        };
341        doc.invalidate_caches();
342        TxNodeId::from_node_id(doc.doc.new_element(name_str))
343    })
344}
345
346/// Creates a new text node with the given content.
347///
348/// # Safety
349///
350/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
351/// - `text` must be a valid, non-null pointer to a null-terminated UTF-8 string.
352#[unsafe(no_mangle)]
353pub unsafe extern "C" fn tx_new_text(doc: *mut TxDocument, text: *const c_char) -> TxNodeId {
354    if doc.is_null() {
355        return TX_NULL_NODE;
356    }
357    ffi_catch!(TX_NULL_NODE, {
358        let doc = unsafe { &mut *doc };
359        let Some(text_str) = (unsafe { cstr_to_str(text) }) else {
360            return TX_NULL_NODE;
361        };
362        doc.invalidate_caches();
363        TxNodeId::from_node_id(doc.doc.new_text(text_str))
364    })
365}
366
367/// Creates a new comment node.
368///
369/// # Safety
370///
371/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
372/// - `text` must be a valid, non-null pointer to a null-terminated UTF-8 string.
373#[unsafe(no_mangle)]
374pub unsafe extern "C" fn tx_new_comment(doc: *mut TxDocument, text: *const c_char) -> TxNodeId {
375    if doc.is_null() {
376        return TX_NULL_NODE;
377    }
378    ffi_catch!(TX_NULL_NODE, {
379        let doc = unsafe { &mut *doc };
380        let Some(text_str) = (unsafe { cstr_to_str(text) }) else {
381            return TX_NULL_NODE;
382        };
383        doc.invalidate_caches();
384        TxNodeId::from_node_id(doc.doc.new_comment(text_str))
385    })
386}
387
388/// Creates a new declaration node.
389///
390/// # Safety
391///
392/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
393/// - `text` must be a valid, non-null pointer to a null-terminated UTF-8 string.
394#[unsafe(no_mangle)]
395pub unsafe extern "C" fn tx_new_declaration(doc: *mut TxDocument, text: *const c_char) -> TxNodeId {
396    if doc.is_null() {
397        return TX_NULL_NODE;
398    }
399    ffi_catch!(TX_NULL_NODE, {
400        let doc = unsafe { &mut *doc };
401        let Some(text_str) = (unsafe { cstr_to_str(text) }) else {
402            return TX_NULL_NODE;
403        };
404        doc.invalidate_caches();
405        TxNodeId::from_node_id(doc.doc.new_declaration(text_str))
406    })
407}
408
409/// Creates a new "unknown" node.
410///
411/// # Safety
412///
413/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
414/// - `text` must be a valid, non-null pointer to a null-terminated UTF-8 string.
415#[unsafe(no_mangle)]
416pub unsafe extern "C" fn tx_new_unknown(doc: *mut TxDocument, text: *const c_char) -> TxNodeId {
417    if doc.is_null() {
418        return TX_NULL_NODE;
419    }
420    ffi_catch!(TX_NULL_NODE, {
421        let doc = unsafe { &mut *doc };
422        let Some(text_str) = (unsafe { cstr_to_str(text) }) else {
423            return TX_NULL_NODE;
424        };
425        doc.invalidate_caches();
426        TxNodeId::from_node_id(doc.doc.new_unknown(text_str))
427    })
428}
429
430// ============================================================
431// DOM Tree Mutations
432// ============================================================
433
434/// Inserts `child` as the last child of `parent`.
435///
436/// # Safety
437///
438/// `doc` must be a valid, non-null pointer to a `TxDocument`.
439#[unsafe(no_mangle)]
440pub unsafe extern "C" fn tx_insert_end_child(
441    doc: *mut TxDocument,
442    parent: TxNodeId,
443    child: TxNodeId,
444) -> TxError {
445    if doc.is_null() {
446        return TxError::TxErrorInvalidNodeId;
447    }
448    ffi_catch!(TxError::TxErrorInvalidNodeId, {
449        let doc = unsafe { &mut *doc };
450        doc.invalidate_caches();
451        let result = doc
452            .doc
453            .insert_end_child(parent.to_node_id(), child.to_node_id());
454        TxError::from_result(&result)
455    })
456}
457
458/// Inserts `child` as the first child of `parent`.
459///
460/// # Safety
461///
462/// `doc` must be a valid, non-null pointer to a `TxDocument`.
463#[unsafe(no_mangle)]
464pub unsafe extern "C" fn tx_insert_first_child(
465    doc: *mut TxDocument,
466    parent: TxNodeId,
467    child: TxNodeId,
468) -> TxError {
469    if doc.is_null() {
470        return TxError::TxErrorInvalidNodeId;
471    }
472    ffi_catch!(TxError::TxErrorInvalidNodeId, {
473        let doc = unsafe { &mut *doc };
474        doc.invalidate_caches();
475        let result = doc
476            .doc
477            .insert_first_child(parent.to_node_id(), child.to_node_id());
478        TxError::from_result(&result)
479    })
480}
481
482/// Inserts `child` as the next sibling after `after`.
483///
484/// # Safety
485///
486/// `doc` must be a valid, non-null pointer to a `TxDocument`.
487#[unsafe(no_mangle)]
488pub unsafe extern "C" fn tx_insert_after_child(
489    doc: *mut TxDocument,
490    after: TxNodeId,
491    child: TxNodeId,
492) -> TxError {
493    if doc.is_null() {
494        return TxError::TxErrorInvalidNodeId;
495    }
496    ffi_catch!(TxError::TxErrorInvalidNodeId, {
497        let doc = unsafe { &mut *doc };
498        doc.invalidate_caches();
499        let result = doc
500            .doc
501            .insert_after_child(after.to_node_id(), child.to_node_id());
502        TxError::from_result(&result)
503    })
504}
505
506/// Deletes `child` from `parent`.
507///
508/// # Safety
509///
510/// `doc` must be a valid, non-null pointer to a `TxDocument`.
511#[unsafe(no_mangle)]
512pub unsafe extern "C" fn tx_delete_child(
513    doc: *mut TxDocument,
514    parent: TxNodeId,
515    child: TxNodeId,
516) -> TxError {
517    if doc.is_null() {
518        return TxError::TxErrorInvalidNodeId;
519    }
520    ffi_catch!(TxError::TxErrorInvalidNodeId, {
521        let doc = unsafe { &mut *doc };
522        doc.invalidate_caches();
523        let result = doc
524            .doc
525            .delete_child(parent.to_node_id(), child.to_node_id());
526        TxError::from_result(&result)
527    })
528}
529
530/// Deletes all children of `parent`.
531///
532/// # Safety
533///
534/// `doc` must be a valid, non-null pointer to a `TxDocument`.
535#[unsafe(no_mangle)]
536pub unsafe extern "C" fn tx_delete_children(doc: *mut TxDocument, parent: TxNodeId) -> TxError {
537    if doc.is_null() {
538        return TxError::TxErrorInvalidNodeId;
539    }
540    ffi_catch!(TxError::TxErrorInvalidNodeId, {
541        let doc = unsafe { &mut *doc };
542        doc.invalidate_caches();
543        TxError::from_result(&doc.doc.delete_children(parent.to_node_id()))
544    })
545}
546
547/// Deletes a node and all its descendants from the document.
548///
549/// # Safety
550///
551/// `doc` must be a valid, non-null pointer to a `TxDocument`.
552#[unsafe(no_mangle)]
553pub unsafe extern "C" fn tx_delete_node(doc: *mut TxDocument, node: TxNodeId) -> TxError {
554    if doc.is_null() {
555        return TxError::TxErrorInvalidNodeId;
556    }
557    ffi_catch!(TxError::TxErrorInvalidNodeId, {
558        let doc = unsafe { &mut *doc };
559        doc.invalidate_caches();
560        TxError::from_result(&doc.doc.delete_node(node.to_node_id()))
561    })
562}
563
564// ============================================================
565// DOM Navigation
566// ============================================================
567
568/// Returns the parent of the given node, or `TX_NULL_NODE` if none.
569///
570/// # Safety
571///
572/// `doc` must be a valid, non-null pointer to a `TxDocument`.
573#[unsafe(no_mangle)]
574pub unsafe extern "C" fn tx_parent(doc: *const TxDocument, node: TxNodeId) -> TxNodeId {
575    if doc.is_null() {
576        return TX_NULL_NODE;
577    }
578    ffi_catch!(TX_NULL_NODE, {
579        let doc = unsafe { &*doc };
580        TxNodeId::from_option(doc.doc.parent(node.to_node_id()))
581    })
582}
583
584/// Returns the first child of the given node, or `TX_NULL_NODE` if none.
585///
586/// # Safety
587///
588/// `doc` must be a valid, non-null pointer to a `TxDocument`.
589#[unsafe(no_mangle)]
590pub unsafe extern "C" fn tx_first_child(doc: *const TxDocument, node: TxNodeId) -> TxNodeId {
591    if doc.is_null() {
592        return TX_NULL_NODE;
593    }
594    ffi_catch!(TX_NULL_NODE, {
595        let doc = unsafe { &*doc };
596        TxNodeId::from_option(doc.doc.first_child(node.to_node_id()))
597    })
598}
599
600/// Returns the last child of the given node, or `TX_NULL_NODE` if none.
601///
602/// # Safety
603///
604/// `doc` must be a valid, non-null pointer to a `TxDocument`.
605#[unsafe(no_mangle)]
606pub unsafe extern "C" fn tx_last_child(doc: *const TxDocument, node: TxNodeId) -> TxNodeId {
607    if doc.is_null() {
608        return TX_NULL_NODE;
609    }
610    ffi_catch!(TX_NULL_NODE, {
611        let doc = unsafe { &*doc };
612        TxNodeId::from_option(doc.doc.last_child(node.to_node_id()))
613    })
614}
615
616/// Returns the previous sibling of the given node, or `TX_NULL_NODE` if none.
617///
618/// # Safety
619///
620/// `doc` must be a valid, non-null pointer to a `TxDocument`.
621#[unsafe(no_mangle)]
622pub unsafe extern "C" fn tx_prev_sibling(doc: *const TxDocument, node: TxNodeId) -> TxNodeId {
623    if doc.is_null() {
624        return TX_NULL_NODE;
625    }
626    ffi_catch!(TX_NULL_NODE, {
627        let doc = unsafe { &*doc };
628        TxNodeId::from_option(doc.doc.prev_sibling(node.to_node_id()))
629    })
630}
631
632/// Returns the next sibling of the given node, or `TX_NULL_NODE` if none.
633///
634/// # Safety
635///
636/// `doc` must be a valid, non-null pointer to a `TxDocument`.
637#[unsafe(no_mangle)]
638pub unsafe extern "C" fn tx_next_sibling(doc: *const TxDocument, node: TxNodeId) -> TxNodeId {
639    if doc.is_null() {
640        return TX_NULL_NODE;
641    }
642    ffi_catch!(TX_NULL_NODE, {
643        let doc = unsafe { &*doc };
644        TxNodeId::from_option(doc.doc.next_sibling(node.to_node_id()))
645    })
646}
647
648/// Returns the first child element, optionally filtered by tag name.
649///
650/// If `name` is null, returns the first child element regardless of its name.
651///
652/// # Safety
653///
654/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
655/// - `name`, if non-null, must be a valid null-terminated UTF-8 string.
656#[unsafe(no_mangle)]
657pub unsafe extern "C" fn tx_first_child_element(
658    doc: *const TxDocument,
659    node: TxNodeId,
660    name: *const c_char,
661) -> TxNodeId {
662    if doc.is_null() {
663        return TX_NULL_NODE;
664    }
665    ffi_catch!(TX_NULL_NODE, {
666        let doc = unsafe { &*doc };
667        let name_opt = unsafe { cstr_to_str(name) };
668        TxNodeId::from_option(doc.doc.first_child_element(node.to_node_id(), name_opt))
669    })
670}
671
672/// Returns the next sibling element, optionally filtered by tag name.
673///
674/// If `name` is null, returns the next sibling element regardless of its name.
675///
676/// # Safety
677///
678/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
679/// - `name`, if non-null, must be a valid null-terminated UTF-8 string.
680#[unsafe(no_mangle)]
681pub unsafe extern "C" fn tx_next_sibling_element(
682    doc: *const TxDocument,
683    node: TxNodeId,
684    name: *const c_char,
685) -> TxNodeId {
686    if doc.is_null() {
687        return TX_NULL_NODE;
688    }
689    ffi_catch!(TX_NULL_NODE, {
690        let doc = unsafe { &*doc };
691        let name_opt = unsafe { cstr_to_str(name) };
692        TxNodeId::from_option(doc.doc.next_sibling_element(node.to_node_id(), name_opt))
693    })
694}
695
696/// Returns the root element of the document, or `TX_NULL_NODE` if the
697/// document is empty.
698///
699/// # Safety
700///
701/// `doc` must be a valid, non-null pointer to a `TxDocument`.
702#[unsafe(no_mangle)]
703pub unsafe extern "C" fn tx_root_element(doc: *const TxDocument) -> TxNodeId {
704    if doc.is_null() {
705        return TX_NULL_NODE;
706    }
707    ffi_catch!(TX_NULL_NODE, {
708        let doc = unsafe { &*doc };
709        TxNodeId::from_option(doc.doc.root_element())
710    })
711}
712
713// ============================================================
714// Element & Attribute Helpers
715// ============================================================
716
717/// Returns the tag name of an element node.
718///
719/// The returned pointer is valid until the next mutating operation on the
720/// document or until the document is freed.
721///
722/// # Safety
723///
724/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
725/// - `element` must identify an element node.
726#[unsafe(no_mangle)]
727pub unsafe extern "C" fn tx_element_name(doc: *mut TxDocument, element: TxNodeId) -> *const c_char {
728    if doc.is_null() {
729        return ptr::null();
730    }
731    ffi_catch!(ptr::null(), {
732        let doc = unsafe { &mut *doc };
733        let val = doc
734            .doc
735            .node_ref(element.to_node_id())
736            .map(|nr| nr.value().to_owned());
737        match val {
738            Some(s) => cache_str(doc, &s),
739            None => ptr::null(),
740        }
741    })
742}
743
744/// Returns the value of the named attribute on an element.
745///
746/// Returns null if the element or attribute does not exist.
747///
748/// # Safety
749///
750/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
751/// - `name` must be a valid, non-null pointer to a null-terminated UTF-8 string.
752#[unsafe(no_mangle)]
753pub unsafe extern "C" fn tx_element_attribute(
754    doc: *mut TxDocument,
755    el: TxNodeId,
756    name: *const c_char,
757) -> *const c_char {
758    if doc.is_null() {
759        return ptr::null();
760    }
761    ffi_catch!(ptr::null(), {
762        let doc = unsafe { &mut *doc };
763        let Some(name_str) = (unsafe { cstr_to_str(name) }) else {
764            return ptr::null();
765        };
766        let val = doc
767            .doc
768            .attribute(el.to_node_id(), name_str)
769            .map(str::to_owned);
770        match val {
771            Some(s) => cache_str(doc, &s),
772            None => ptr::null(),
773        }
774    })
775}
776
777/// Sets an attribute on an element. Creates the attribute if it doesn't exist,
778/// or updates its value if it does.
779///
780/// # Safety
781///
782/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
783/// - `name` and `value` must be valid, non-null pointers to null-terminated
784///   UTF-8 strings.
785#[unsafe(no_mangle)]
786pub unsafe extern "C" fn tx_element_set_attribute(
787    doc: *mut TxDocument,
788    el: TxNodeId,
789    name: *const c_char,
790    value: *const c_char,
791) -> TxError {
792    if doc.is_null() {
793        return TxError::TxErrorInvalidNodeId;
794    }
795    ffi_catch!(TxError::TxErrorInvalidNodeId, {
796        let doc = unsafe { &mut *doc };
797        let Some(name_str) = (unsafe { cstr_to_str(name) }) else {
798            return TxError::TxErrorNoAttribute;
799        };
800        let Some(val_str) = (unsafe { cstr_to_str(value) }) else {
801            return TxError::TxErrorNoAttribute;
802        };
803        doc.invalidate_caches();
804        TxError::from_result(&doc.doc.set_attribute(el.to_node_id(), name_str, val_str))
805    })
806}
807
808/// Deletes an attribute from an element by name.
809///
810/// # Safety
811///
812/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
813/// - `name` must be a valid, non-null pointer to a null-terminated UTF-8 string.
814#[unsafe(no_mangle)]
815pub unsafe extern "C" fn tx_element_delete_attribute(
816    doc: *mut TxDocument,
817    el: TxNodeId,
818    name: *const c_char,
819) -> TxError {
820    if doc.is_null() {
821        return TxError::TxErrorInvalidNodeId;
822    }
823    ffi_catch!(TxError::TxErrorInvalidNodeId, {
824        let doc = unsafe { &mut *doc };
825        let Some(name_str) = (unsafe { cstr_to_str(name) }) else {
826            return TxError::TxErrorNoAttribute;
827        };
828        doc.invalidate_caches();
829        TxError::from_result(&doc.doc.delete_attribute(el.to_node_id(), name_str))
830    })
831}
832
833/// Returns the text content of an element's first child text node.
834///
835/// Returns null if no text child exists.
836///
837/// # Safety
838///
839/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
840/// - `element` must identify an element node.
841#[unsafe(no_mangle)]
842pub unsafe extern "C" fn tx_element_get_text(
843    doc: *mut TxDocument,
844    element: TxNodeId,
845) -> *const c_char {
846    if doc.is_null() {
847        return ptr::null();
848    }
849    ffi_catch!(ptr::null(), {
850        let doc = unsafe { &mut *doc };
851        let val = doc.doc.get_text(element.to_node_id()).map(str::to_owned);
852        match val {
853            Some(s) => cache_str(doc, &s),
854            None => ptr::null(),
855        }
856    })
857}
858
859/// Sets the text content of an element.
860///
861/// If a child text node exists, its content is replaced. Otherwise, a new
862/// text node is created and inserted as the first child.
863///
864/// # Safety
865///
866/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
867/// - `text` must be a valid, non-null pointer to a null-terminated UTF-8 string.
868#[unsafe(no_mangle)]
869pub unsafe extern "C" fn tx_element_set_text(
870    doc: *mut TxDocument,
871    element: TxNodeId,
872    text: *const c_char,
873) -> TxError {
874    if doc.is_null() {
875        return TxError::TxErrorInvalidNodeId;
876    }
877    ffi_catch!(TxError::TxErrorInvalidNodeId, {
878        let doc = unsafe { &mut *doc };
879        let Some(text_str) = (unsafe { cstr_to_str(text) }) else {
880            return TxError::TxErrorNoTextNode;
881        };
882        doc.invalidate_caches();
883        TxError::from_result(&doc.doc.set_text(element.to_node_id(), text_str))
884    })
885}
886
887// ============================================================
888// Typed Attribute Accessors
889// ============================================================
890
891/// Queries an integer attribute value.
892///
893/// On success, writes the value to `*value` and returns `TxSuccess`.
894///
895/// # Safety
896///
897/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
898/// - `name` must be a valid, non-null pointer to a null-terminated UTF-8 string.
899/// - `value` must be a valid, non-null pointer to a `c_int`.
900#[unsafe(no_mangle)]
901pub unsafe extern "C" fn tx_query_int_attribute(
902    doc: *const TxDocument,
903    el: TxNodeId,
904    name: *const c_char,
905    value: *mut c_int,
906) -> TxError {
907    if doc.is_null() || value.is_null() {
908        return TxError::TxErrorInvalidNodeId;
909    }
910    ffi_catch!(TxError::TxErrorInvalidNodeId, {
911        let doc = unsafe { &*doc };
912        let Some(name_str) = (unsafe { cstr_to_str(name) }) else {
913            return TxError::TxErrorNoAttribute;
914        };
915        match doc.doc.query_int_attribute(el.to_node_id(), name_str) {
916            Ok(v) => {
917                unsafe { *value = v as c_int };
918                TxError::TxSuccess
919            }
920            Err(e) => TxError::from_xml_error(&e),
921        }
922    })
923}
924
925/// Queries a boolean attribute value.
926///
927/// On success, writes the value to `*value` and returns `TxSuccess`.
928///
929/// # Safety
930///
931/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
932/// - `name` must be a valid, non-null pointer to a null-terminated UTF-8 string.
933/// - `value` must be a valid, non-null pointer to a `bool`.
934#[unsafe(no_mangle)]
935pub unsafe extern "C" fn tx_query_bool_attribute(
936    doc: *const TxDocument,
937    el: TxNodeId,
938    name: *const c_char,
939    value: *mut bool,
940) -> TxError {
941    if doc.is_null() || value.is_null() {
942        return TxError::TxErrorInvalidNodeId;
943    }
944    ffi_catch!(TxError::TxErrorInvalidNodeId, {
945        let doc = unsafe { &*doc };
946        let Some(name_str) = (unsafe { cstr_to_str(name) }) else {
947            return TxError::TxErrorNoAttribute;
948        };
949        match doc.doc.query_bool_attribute(el.to_node_id(), name_str) {
950            Ok(v) => {
951                unsafe { *value = v };
952                TxError::TxSuccess
953            }
954            Err(e) => TxError::from_xml_error(&e),
955        }
956    })
957}
958
959/// Queries a double (f64) attribute value.
960///
961/// On success, writes the value to `*value` and returns `TxSuccess`.
962///
963/// # Safety
964///
965/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
966/// - `name` must be a valid, non-null pointer to a null-terminated UTF-8 string.
967/// - `value` must be a valid, non-null pointer to a `c_double`.
968#[unsafe(no_mangle)]
969pub unsafe extern "C" fn tx_query_double_attribute(
970    doc: *const TxDocument,
971    el: TxNodeId,
972    name: *const c_char,
973    value: *mut c_double,
974) -> TxError {
975    if doc.is_null() || value.is_null() {
976        return TxError::TxErrorInvalidNodeId;
977    }
978    ffi_catch!(TxError::TxErrorInvalidNodeId, {
979        let doc = unsafe { &*doc };
980        let Some(name_str) = (unsafe { cstr_to_str(name) }) else {
981            return TxError::TxErrorNoAttribute;
982        };
983        match doc.doc.query_double_attribute(el.to_node_id(), name_str) {
984            Ok(v) => {
985                unsafe { *value = v };
986                TxError::TxSuccess
987            }
988            Err(e) => TxError::from_xml_error(&e),
989        }
990    })
991}
992
993/// Returns an integer attribute value, or `default_val` if the attribute
994/// does not exist or cannot be parsed.
995///
996/// # Safety
997///
998/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
999/// - `name` must be a valid, non-null pointer to a null-terminated UTF-8 string.
1000#[unsafe(no_mangle)]
1001pub unsafe extern "C" fn tx_int_attribute(
1002    doc: *const TxDocument,
1003    el: TxNodeId,
1004    name: *const c_char,
1005    default_val: c_int,
1006) -> c_int {
1007    if doc.is_null() {
1008        return default_val;
1009    }
1010    ffi_catch!(default_val, {
1011        let doc = unsafe { &*doc };
1012        let Some(name_str) = (unsafe { cstr_to_str(name) }) else {
1013            return default_val;
1014        };
1015        doc.doc
1016            .int_attribute(el.to_node_id(), name_str, default_val)
1017    })
1018}
1019
1020/// Returns a boolean attribute value, or `default_val` if the attribute
1021/// does not exist or cannot be parsed.
1022///
1023/// # Safety
1024///
1025/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
1026/// - `name` must be a valid, non-null pointer to a null-terminated UTF-8 string.
1027#[unsafe(no_mangle)]
1028pub unsafe extern "C" fn tx_bool_attribute(
1029    doc: *const TxDocument,
1030    el: TxNodeId,
1031    name: *const c_char,
1032    default_val: bool,
1033) -> bool {
1034    if doc.is_null() {
1035        return default_val;
1036    }
1037    ffi_catch!(default_val, {
1038        let doc = unsafe { &*doc };
1039        let Some(name_str) = (unsafe { cstr_to_str(name) }) else {
1040            return default_val;
1041        };
1042        doc.doc
1043            .bool_attribute(el.to_node_id(), name_str, default_val)
1044    })
1045}
1046
1047/// Returns a double (f64) attribute value, or `default_val` if the attribute
1048/// does not exist or cannot be parsed.
1049///
1050/// # Safety
1051///
1052/// - `doc` must be a valid, non-null pointer to a `TxDocument`.
1053/// - `name` must be a valid, non-null pointer to a null-terminated UTF-8 string.
1054#[unsafe(no_mangle)]
1055pub unsafe extern "C" fn tx_double_attribute(
1056    doc: *const TxDocument,
1057    el: TxNodeId,
1058    name: *const c_char,
1059    default_val: c_double,
1060) -> c_double {
1061    if doc.is_null() {
1062        return default_val;
1063    }
1064    ffi_catch!(default_val, {
1065        let doc = unsafe { &*doc };
1066        let Some(name_str) = (unsafe { cstr_to_str(name) }) else {
1067            return default_val;
1068        };
1069        doc.doc
1070            .double_attribute(el.to_node_id(), name_str, default_val)
1071    })
1072}
1073
1074// ============================================================
1075// Printer / Streaming API
1076// ============================================================
1077
1078/// Creates a new XML printer (pretty-print mode).
1079///
1080/// # Safety
1081///
1082/// The returned pointer must eventually be freed with [`tx_printer_free`].
1083#[unsafe(no_mangle)]
1084pub extern "C" fn tx_printer_new() -> *mut TxPrinter {
1085    ffi_catch!(ptr::null_mut(), {
1086        Box::into_raw(Box::new(TxPrinter::new(false)))
1087    })
1088}
1089
1090/// Creates a new XML printer (compact mode, no whitespace).
1091///
1092/// # Safety
1093///
1094/// The returned pointer must eventually be freed with [`tx_printer_free`].
1095#[unsafe(no_mangle)]
1096pub extern "C" fn tx_printer_new_compact() -> *mut TxPrinter {
1097    ffi_catch!(ptr::null_mut(), {
1098        Box::into_raw(Box::new(TxPrinter::new(true)))
1099    })
1100}
1101
1102/// Frees a printer previously created with [`tx_printer_new`] or
1103/// [`tx_printer_new_compact`].
1104///
1105/// # Safety
1106///
1107/// `printer` must be a valid pointer returned by a printer constructor, or null.
1108/// After this call, the pointer must not be used again.
1109#[unsafe(no_mangle)]
1110pub unsafe extern "C" fn tx_printer_free(printer: *mut TxPrinter) {
1111    if printer.is_null() {
1112        return;
1113    }
1114    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1115        drop(unsafe { Box::from_raw(printer) });
1116    }));
1117}
1118
1119/// Opens an element tag in the printer output.
1120///
1121/// # Safety
1122///
1123/// - `printer` must be a valid, non-null pointer to a `TxPrinter`.
1124/// - `name` must be a valid, non-null pointer to a null-terminated UTF-8 string.
1125#[unsafe(no_mangle)]
1126pub unsafe extern "C" fn tx_printer_open_element(printer: *mut TxPrinter, name: *const c_char) {
1127    if printer.is_null() {
1128        return;
1129    }
1130    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1131        let p = unsafe { &mut *printer };
1132        if let Some(name_str) = unsafe { cstr_to_str(name) } {
1133            p.cached_result = None;
1134            p.printer.open_element(name_str);
1135        }
1136    }));
1137}
1138
1139/// Pushes an attribute onto the currently open element.
1140///
1141/// # Safety
1142///
1143/// - `printer` must be a valid, non-null pointer to a `TxPrinter`.
1144/// - `name` and `value` must be valid, non-null pointers to null-terminated
1145///   UTF-8 strings.
1146#[unsafe(no_mangle)]
1147pub unsafe extern "C" fn tx_printer_push_attribute(
1148    printer: *mut TxPrinter,
1149    name: *const c_char,
1150    value: *const c_char,
1151) {
1152    if printer.is_null() {
1153        return;
1154    }
1155    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1156        let p = unsafe { &mut *printer };
1157        if let (Some(n), Some(v)) = (unsafe { cstr_to_str(name) }, unsafe { cstr_to_str(value) }) {
1158            p.cached_result = None;
1159            p.printer.push_attribute(n, v);
1160        }
1161    }));
1162}
1163
1164/// Closes the most recently opened element.
1165///
1166/// # Safety
1167///
1168/// `printer` must be a valid, non-null pointer to a `TxPrinter`.
1169#[unsafe(no_mangle)]
1170pub unsafe extern "C" fn tx_printer_close_element(printer: *mut TxPrinter) {
1171    if printer.is_null() {
1172        return;
1173    }
1174    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1175        let p = unsafe { &mut *printer };
1176        p.cached_result = None;
1177        p.printer.close_element();
1178    }));
1179}
1180
1181/// Pushes text content into the current element.
1182///
1183/// # Safety
1184///
1185/// - `printer` must be a valid, non-null pointer to a `TxPrinter`.
1186/// - `text` must be a valid, non-null pointer to a null-terminated UTF-8 string.
1187#[unsafe(no_mangle)]
1188pub unsafe extern "C" fn tx_printer_push_text(printer: *mut TxPrinter, text: *const c_char) {
1189    if printer.is_null() {
1190        return;
1191    }
1192    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1193        let p = unsafe { &mut *printer };
1194        if let Some(text_str) = unsafe { cstr_to_str(text) } {
1195            p.cached_result = None;
1196            p.printer.push_text(text_str);
1197        }
1198    }));
1199}
1200
1201/// Pushes a comment into the printer output.
1202///
1203/// # Safety
1204///
1205/// - `printer` must be a valid, non-null pointer to a `TxPrinter`.
1206/// - `text` must be a valid, non-null pointer to a null-terminated UTF-8 string.
1207#[unsafe(no_mangle)]
1208pub unsafe extern "C" fn tx_printer_push_comment(printer: *mut TxPrinter, text: *const c_char) {
1209    if printer.is_null() {
1210        return;
1211    }
1212    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1213        let p = unsafe { &mut *printer };
1214        if let Some(text_str) = unsafe { cstr_to_str(text) } {
1215            p.cached_result = None;
1216            p.printer.push_comment(text_str);
1217        }
1218    }));
1219}
1220
1221/// Returns the accumulated printer output as a C string.
1222///
1223/// The returned pointer is valid until the printer is modified or freed.
1224///
1225/// # Safety
1226///
1227/// `printer` must be a valid, non-null pointer to a `TxPrinter`.
1228#[unsafe(no_mangle)]
1229pub unsafe extern "C" fn tx_printer_result(printer: *mut TxPrinter) -> *const c_char {
1230    if printer.is_null() {
1231        return ptr::null();
1232    }
1233    ffi_catch!(ptr::null(), {
1234        let p = unsafe { &mut *printer };
1235        let s = p.printer.result();
1236        match CString::new(s) {
1237            Ok(cs) => {
1238                let ptr = cs.as_ptr();
1239                p.cached_result = Some(cs);
1240                ptr
1241            }
1242            Err(_) => ptr::null(),
1243        }
1244    })
1245}
1246
1247/// Clears the printer output, resetting it to empty.
1248///
1249/// # Safety
1250///
1251/// `printer` must be a valid, non-null pointer to a `TxPrinter`.
1252#[unsafe(no_mangle)]
1253pub unsafe extern "C" fn tx_printer_clear(printer: *mut TxPrinter) {
1254    if printer.is_null() {
1255        return;
1256    }
1257    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1258        let p = unsafe { &mut *printer };
1259        p.cached_result = None;
1260        p.printer.clear();
1261    }));
1262}
1263
1264// ============================================================
1265// Node Type Inspection
1266// ============================================================
1267
1268/// Returns the type of the given node.
1269///
1270/// # Safety
1271///
1272/// `doc` must be a valid, non-null pointer to a `TxDocument`.
1273#[unsafe(no_mangle)]
1274pub unsafe extern "C" fn tx_node_type(doc: *const TxDocument, node: TxNodeId) -> TxNodeType {
1275    if doc.is_null() {
1276        return TxNodeType::TxNodeUnknown;
1277    }
1278    ffi_catch!(TxNodeType::TxNodeUnknown, {
1279        let doc = unsafe { &*doc };
1280        match doc.doc.node_kind(node.to_node_id()) {
1281            Some(kind) => TxNodeType::from_node_kind(kind),
1282            None => TxNodeType::TxNodeUnknown,
1283        }
1284    })
1285}
1286
1287/// Returns `true` if the given node ID is the null sentinel.
1288///
1289/// This function is safe to call without a document pointer.
1290#[unsafe(no_mangle)]
1291pub extern "C" fn tx_node_is_null(node: TxNodeId) -> bool {
1292    node.is_null()
1293}
1294
1295/// Returns the "value" of a node based on its type:
1296///
1297/// - Element → tag name
1298/// - Text → text content
1299/// - Comment → comment text
1300/// - Declaration → declaration name
1301/// - Unknown → raw content
1302/// - Document → empty string
1303///
1304/// # Safety
1305///
1306/// `doc` must be a valid, non-null pointer to a `TxDocument`.
1307#[unsafe(no_mangle)]
1308pub unsafe extern "C" fn tx_node_value(doc: *mut TxDocument, node: TxNodeId) -> *const c_char {
1309    if doc.is_null() {
1310        return ptr::null();
1311    }
1312    ffi_catch!(ptr::null(), {
1313        let doc = unsafe { &mut *doc };
1314        let val = doc
1315            .doc
1316            .node_ref(node.to_node_id())
1317            .map(|nr| nr.value().to_owned());
1318        match val {
1319            Some(s) => cache_str(doc, &s),
1320            None => ptr::null(),
1321        }
1322    })
1323}
1324
1325/// Returns the 1-based source line number where the node was parsed,
1326/// or 0 if the node was not created by parsing.
1327///
1328/// # Safety
1329///
1330/// `doc` must be a valid, non-null pointer to a `TxDocument`.
1331#[unsafe(no_mangle)]
1332pub unsafe extern "C" fn tx_node_line(doc: *const TxDocument, node: TxNodeId) -> c_int {
1333    if doc.is_null() {
1334        return 0;
1335    }
1336    ffi_catch!(0, {
1337        let doc = unsafe { &*doc };
1338        doc.doc.line_num(node.to_node_id()).unwrap_or(0) as c_int
1339    })
1340}