Skip to main content

libxml_rs/xml/writer/
mod.rs

1//! XML Writer API (§30, §85 Phase 7).
2//!
3//! Streaming XML writer with indentation, encoding, escaping, document lifecycle.
4//!
5//! Provides the `xmlTextWriter*` family of functions that allow constructing
6//! XML documents in a streaming fashion — start/end element, write attributes,
7//! text content, CDATA, comments, processing instructions, and DTD declarations.
8//!
9//! # UPSTREAM-PARITY
10//!
11//! This module mirrors libxml2's `xmlTextWriter` API defined in `xmlwriter.h`.
12//! The writer maintains a state machine that tracks whether we are inside an
13//! element start tag (attributes can be written), inside an attribute value,
14//! inside a CDATA section, etc.
15
16#![allow(
17    missing_docs,
18    non_snake_case,
19    non_camel_case_types,
20    non_upper_case_globals
21)]
22
23use core::ffi::c_void;
24use core::ptr;
25use std::os::raw::{c_char, c_int, c_uint};
26
27use crate::abi::allocator;
28
29use crate::abi::structs::*;
30use crate::abi::types::*;
31use crate::xml::io;
32use crate::xml::tree;
33
34// ═══════════════════════════════════════════════════════════════════════════════
35// Constants
36// ═══════════════════════════════════════════════════════════════════════════════
37
38// ═══════════════════════════════════════════════════════════════════════════════
39// Writer state enumeration
40// ═══════════════════════════════════════════════════════════════════════════════
41
42/// Writer state — tracks what kind of content we are currently inside.
43#[derive(Clone, Copy, PartialEq, Eq, Debug)]
44enum WriterState {
45    /// Initial / idle state — no element open.
46    None,
47    /// Inside an element start tag (attributes may be written).
48    Element,
49    /// Inside an attribute value.
50    Attribute,
51    /// Inside a CDATA section.
52    CData,
53    /// Inside a comment.
54    Comment,
55    /// Inside a processing instruction.
56    PI,
57    /// Inside a DTD declaration (bracket not yet written).
58    DTD,
59    /// Inside a DTD declaration after the internal subset bracket.
60    DTDText,
61    /// Inside a DTD element declaration.
62    DTDElem,
63    /// Inside a DTD element declaration after content.
64    DTDElemText,
65    /// Inside a DTD attribute declaration.
66    DTDAttr,
67    /// Inside a DTD attribute declaration after content.
68    DTDAttrText,
69    /// Inside a DTD entity declaration (no content yet).
70    DTDEntity,
71    /// Inside a DTD entity declaration after content.
72    DTDEntityText,
73    /// Inside a DTD notation declaration.
74    DTDNotation,
75    /// Writing XML declaration.
76    XMLDecl,
77}
78
79// ═══════════════════════════════════════════════════════════════════════════════
80// XmlTextWriter struct
81// ═══════════════════════════════════════════════════════════════════════════════
82
83/// A streaming XML writer.
84///
85/// Corresponds to `xmlTextWriterPtr` in libxml2.
86///
87/// The writer accumulates output into an internal buffer and flushes to the
88/// underlying output buffer on demand. It maintains a stack of element names
89/// for proper nesting, a state machine for content-type tracking, and optional
90/// indentation.
91pub struct XmlTextWriter {
92    /// The output buffer where serialized XML is written.
93    output: *mut _xmlOutputBuffer,
94    /// Whether indentation is enabled (non-zero = enabled).
95    indent: c_int,
96    /// The string used for one level of indentation.
97    indent_string: Vec<u8>,
98    /// Quote character for attribute/entity values (upstream `qchar`).
99    qchar: u8,
100    /// Indent the next closing tag (upstream `doindent`).
101    doindent: bool,
102    /// Current nesting depth.
103    depth: c_int,
104    /// Stack of element local names (for end-element matching).
105    stack: Vec<Vec<u8>>,
106    /// Output encoding name (e.g. "UTF-8").
107    encoding: Vec<u8>,
108    /// Collected error messages.
109    errors: Vec<String>,
110    /// Current writer state.
111    state: WriterState,
112    /// Optional document reference (used when writing to a document tree).
113    doc: *mut _xmlDoc,
114    /// Whether we are in the "start tag" portion of an element (attributes can be written).
115    in_start_tag: bool,
116    /// The element name stack with full qualified names for proper end-element matching.
117    /// Stores (prefix, localname) pairs.
118    elem_stack: Vec<(Vec<u8>, Vec<u8>)>,
119    /// Whether the current DTD entity declaration is a parameter entity
120    /// (upstream XML_TEXTWRITER_DTD_PENT).
121    entity_pe: bool,
122    /// Whether an output encoder has been installed (xmlTextWriterStartDocument
123    /// with a non-NULL encoding). Once set it persists for the writer's life
124    /// and makes byte-writes report 0 bytes (upstream encoder path).
125    encoder_active: bool,
126    /// Pending namespace declarations for the current start tag (upstream
127    /// xmlTextWriterOutputNSDecl defers them until the tag closes).
128    pending_ns: Vec<(Vec<u8>, Vec<u8>)>,
129    /// Open DTD child declarations (upstream stack entries) contributing to
130    /// the indentation depth.
131    dtd_depth: c_int,
132}
133
134impl XmlTextWriter {
135    /// Create a new XML text writer.
136    ///
137    /// # SAFETY
138    ///
139    /// - `output` must be a valid pointer to a mutable `_xmlOutputBuffer` or NULL.
140    unsafe fn new(output: *mut _xmlOutputBuffer) -> *mut Self {
141        let writer = allocator::xmlMallocZero(size_of::<XmlTextWriter>() as usize) as *mut Self;
142        if writer.is_null() {
143            return ptr::null_mut();
144        }
145        unsafe {
146            (*writer).output = output;
147            (*writer).indent = 0;
148            (*writer).indent_string = b" \0".to_vec();
149            (*writer).qchar = b'"';
150            (*writer).doindent = true;
151            (*writer).depth = 0;
152            (*writer).stack = Vec::new();
153            (*writer).encoding = b"UTF-8\0".to_vec();
154            (*writer).errors = Vec::new();
155            (*writer).state = WriterState::None;
156            (*writer).doc = ptr::null_mut();
157            (*writer).in_start_tag = false;
158            (*writer).elem_stack = Vec::new();
159            (*writer).entity_pe = false;
160            (*writer).encoder_active = false;
161            (*writer).pending_ns = Vec::new();
162            (*writer).dtd_depth = 0;
163        }
164        writer
165    }
166
167    /// Write raw bytes to the output buffer.
168    ///
169    /// # SAFETY
170    ///
171    /// - `data` must point to `len` valid bytes.
172    unsafe fn write_raw(&mut self, data: *const u8, len: c_int) -> c_int {
173        if self.output.is_null() || data.is_null() || len <= 0 {
174            return -1;
175        }
176        let rc = io::output_buffer_write(self.output, len, data as *const c_char);
177        // UPSTREAM-PARITY: with an output encoder installed, xmlOutputBufferWrite
178        // reports 0 bytes for writes below the 256-byte conversion threshold.
179        if self.encoder_active {
180            0
181        } else {
182            rc
183        }
184    }
185
186    /// Write a null-terminated string to the output buffer.
187    unsafe fn write_str(&mut self, s: *const u8) -> c_int {
188        if self.output.is_null() || s.is_null() {
189            return -1;
190        }
191        let rc = io::output_buffer_write_string(self.output, s as *const c_char);
192        if self.encoder_active {
193            0
194        } else {
195            rc
196        }
197    }
198
199    /// Write a byte slice to the output buffer.
200    ///
201    /// NOTE: The slice must NOT borrow from `self` to avoid borrow checker conflicts.
202    unsafe fn write_slice(&mut self, slice: &[u8]) -> c_int {
203        if self.output.is_null() || slice.is_empty() {
204            return -1;
205        }
206        let rc = io::output_buffer_write(
207            self.output,
208            slice.len() as c_int,
209            slice.as_ptr() as *const c_char,
210        );
211        if self.encoder_active {
212            0
213        } else {
214            rc
215        }
216    }
217
218    /// Write a single byte to the output buffer.
219    unsafe fn write_byte(&mut self, b: u8) -> c_int {
220        if self.output.is_null() {
221            return -1;
222        }
223        let rc = io::output_buffer_write_char(self.output, b as c_char);
224        if self.encoder_active {
225            0
226        } else {
227            rc
228        }
229    }
230
231    /// Write indentation (if enabled).
232    ///
233    /// Uses a clone of the indent string to avoid borrow checker conflicts.
234    unsafe fn write_indent(&mut self) -> c_int {
235        if self.indent == 0 {
236            return 0;
237        }
238        // UPSTREAM-PARITY (xmlTextWriterWriteIndent): returns the number of
239        // indent strings written, not the byte count. The stored indent
240        // string is NUL-terminated; the NUL must not reach the output.
241        let indent_str = self.indent_string.clone();
242        let body = if indent_str.last() == Some(&0) {
243            &indent_str[..indent_str.len() - 1]
244        } else {
245            &indent_str[..]
246        };
247        let count = self.depth + self.dtd_depth;
248        for _ in 0..count {
249            self.write_slice(body);
250        }
251        count
252    }
253
254    /// Close any open start tag (writing `>` to transition from attribute-writing
255    /// mode to content-writing mode). Returns `(closed, bytes)` — whether a tag
256    /// was actually closed, and the byte count contributed (encoder-muted).
257    /// No newline: the NAME->TEXT transition only emits `>` (the newline after
258    /// the first child comes from the child-start paths, matching
259    /// xmlTextWriterHandleStateDependencies). Pending namespace declarations
260    /// are flushed first (upstream xmlTextWriterOutputNSDecl).
261    unsafe fn close_start_tag(&mut self) -> (bool, c_int) {
262        if self.in_start_tag {
263            self.in_start_tag = false;
264            let mut sum: c_int = self.flush_pending_ns();
265            sum += self.write_byte(b'>');
266            (true, sum)
267        } else {
268            (false, 0)
269        }
270    }
271
272    /// Write the pending namespace declarations (upstream
273    /// xmlTextWriterOutputNSDecl): ` xmlns:prefix="uri"` / ` xmlns="uri"`.
274    unsafe fn flush_pending_ns(&mut self) -> c_int {
275        let mut sum: c_int = 0;
276        let pending = core::mem::take(&mut self.pending_ns);
277        for (prefix, uri) in pending {
278            sum += self.write_byte(b' ');
279            if prefix.is_empty() {
280                sum += self.write_slice(b"xmlns=\"");
281            } else {
282                sum += self.write_slice(b"xmlns:");
283                sum += self.write_slice(&prefix);
284                sum += self.write_slice(b"=\"");
285            }
286            sum += self.write_slice(&uri);
287            sum += self.write_byte(b'"');
288        }
289        sum
290    }
291
292    /// Check if the writer is in a state where element/attribute content can be written.
293    fn can_write_content(&self) -> bool {
294        matches!(
295            self.state,
296            WriterState::None
297                | WriterState::Element
298                | WriterState::Attribute
299                | WriterState::CData
300                | WriterState::Comment
301                | WriterState::PI
302                | WriterState::DTD
303                | WriterState::DTDElem
304                | WriterState::DTDAttr
305                | WriterState::DTDEntity
306                | WriterState::DTDNotation
307                | WriterState::XMLDecl
308        )
309    }
310}
311
312// ═══════════════════════════════════════════════════════════════════════════════
313// Free / destructor
314// ═══════════════════════════════════════════════════════════════════════════════
315
316/// Free an XML text writer.
317///
318/// # UPSTREAM-PARITY
319///
320/// ```c
321/// void xmlFreeTextWriter(xmlTextWriterPtr writer);
322/// ```
323///
324/// # SAFETY
325///
326/// - `writer` must be a valid pointer returned by `xmlNewTextWriter*` or NULL.
327#[no_mangle]
328pub unsafe extern "C" fn xmlFreeTextWriter(writer: *mut XmlTextWriter) {
329    if writer.is_null() {
330        return;
331    }
332    // SAFETY: writer is a valid XmlTextWriter allocated by us.
333    // Flush any pending data
334    if !(*writer).output.is_null() {
335        io::output_buffer_flush((*writer).output);
336    }
337    // Drop Rust-side allocations
338    unsafe {
339        ptr::drop_in_place(&mut (*writer).indent_string);
340        ptr::drop_in_place(&mut (*writer).stack);
341        ptr::drop_in_place(&mut (*writer).encoding);
342        ptr::drop_in_place(&mut (*writer).errors);
343        ptr::drop_in_place(&mut (*writer).elem_stack);
344    }
345    // Free the struct itself
346    unsafe { allocator::xmlFreeImpl(writer as *mut c_void) };
347}
348
349// ═══════════════════════════════════════════════════════════════════════════════
350// Writer creation
351// ═══════════════════════════════════════════════════════════════════════════════
352
353/// Create a new XML text writer from an output buffer.
354///
355/// # UPSTREAM-PARITY
356///
357/// ```c
358/// xmlTextWriterPtr xmlNewTextWriter(xmlOutputBufferPtr out);
359/// ```
360///
361/// # SAFETY
362///
363/// - `out` must be a valid pointer to an `_xmlOutputBuffer` or NULL.
364#[no_mangle]
365pub unsafe extern "C" fn xmlNewTextWriter(out: *mut _xmlOutputBuffer) -> *mut XmlTextWriter {
366    if out.is_null() {
367        return ptr::null_mut();
368    }
369    // SAFETY: out is a valid output buffer.
370    XmlTextWriter::new(out)
371}
372
373/// Create a new XML text writer for a file.
374///
375/// # UPSTREAM-PARITY
376///
377/// ```c
378/// xmlTextWriterPtr xmlNewTextWriterFilename(const char *uri, int compression);
379/// ```
380///
381/// # SAFETY
382///
383/// - `uri` must be a valid null-terminated string or NULL.
384#[no_mangle]
385pub unsafe extern "C" fn xmlNewTextWriterFilename(
386    uri: *const c_char,
387    compression: c_int,
388) -> *mut XmlTextWriter {
389    if uri.is_null() {
390        return ptr::null_mut();
391    }
392    // SAFETY: uri is a valid C string.
393    let out = io::output_buffer_create_filename(uri, ptr::null_mut(), compression);
394    if out.is_null() {
395        return ptr::null_mut();
396    }
397    XmlTextWriter::new(out)
398}
399
400/// Create a new XML text writer for a memory buffer.
401///
402/// # UPSTREAM-PARITY
403///
404/// ```c
405/// xmlTextWriterPtr xmlNewTextWriterMemory(xmlBufferPtr buf, int compression);
406/// ```
407///
408/// # SAFETY
409///
410/// - `buf` must be a valid pointer to an `_xmlBuffer` or NULL.
411#[no_mangle]
412pub unsafe extern "C" fn xmlNewTextWriterMemory(
413    buf: *mut _xmlBuffer,
414    compression: c_int,
415) -> *mut XmlTextWriter {
416    let _ = compression;
417    if buf.is_null() {
418        return ptr::null_mut();
419    }
420    // SAFETY: buf is a valid xmlBuffer.
421    let out = io::output_buffer_create_buffer(buf, ptr::null_mut());
422    if out.is_null() {
423        return ptr::null_mut();
424    }
425    XmlTextWriter::new(out)
426}
427
428/// Create a new XML text writer for a document (tree mode).
429///
430/// # UPSTREAM-PARITY
431///
432/// ```c
433/// xmlTextWriterPtr xmlNewTextWriterDoc(xmlDocPtr *doc, int compression);
434/// ```
435///
436/// # SAFETY
437///
438/// - `doc` must be a valid pointer to a (possibly NULL) xmlDocPtr.
439#[no_mangle]
440pub unsafe extern "C" fn xmlNewTextWriterDoc(
441    doc: *mut *mut _xmlDoc,
442    compression: c_int,
443) -> *mut XmlTextWriter {
444    let _ = compression;
445    if doc.is_null() {
446        return ptr::null_mut();
447    }
448    // Create a new document
449    // SAFETY: doc is a valid pointer to an xmlDocPtr.
450    let new_doc = tree::new_doc(b"1.0\0" as *const u8);
451    if new_doc.is_null() {
452        return ptr::null_mut();
453    }
454    unsafe { *doc = new_doc };
455
456    // Create a memory buffer writer
457    let buf = io::buf_create(io::DEFAULT_BUFFER_SIZE as c_int);
458    if buf.is_null() {
459        tree::free_doc(new_doc);
460        return ptr::null_mut();
461    }
462
463    let out = io::output_buffer_create_buffer(buf, ptr::null_mut());
464    if out.is_null() {
465        io::buf_free(buf);
466        tree::free_doc(new_doc);
467        return ptr::null_mut();
468    }
469
470    let writer = XmlTextWriter::new(out);
471    if !writer.is_null() {
472        unsafe { (*writer).doc = new_doc };
473    }
474    writer
475}
476
477/// Create a new XML text writer for a subtree.
478///
479/// # UPSTREAM-PARITY
480///
481/// ```c
482/// xmlTextWriterPtr xmlNewTextWriterTree(xmlDocPtr doc, xmlNodePtr node, int compression);
483/// ```
484///
485/// # SAFETY
486///
487/// - `doc` must be a valid pointer to an `_xmlDoc` or NULL.
488/// - `node` must be a valid pointer to an `_xmlNode` or NULL.
489#[no_mangle]
490pub unsafe extern "C" fn xmlNewTextWriterTree(
491    doc: *mut _xmlDoc,
492    node: *mut _xmlNode,
493    compression: c_int,
494) -> *mut XmlTextWriter {
495    let _ = compression;
496    let _ = node; // node is kept for future use when we write tree content directly
497    if doc.is_null() {
498        return ptr::null_mut();
499    }
500
501    let buf = io::buf_create(io::DEFAULT_BUFFER_SIZE as c_int);
502    if buf.is_null() {
503        return ptr::null_mut();
504    }
505
506    let out = io::output_buffer_create_buffer(buf, ptr::null_mut());
507    if out.is_null() {
508        io::buf_free(buf);
509        return ptr::null_mut();
510    }
511
512    let writer = XmlTextWriter::new(out);
513    if !writer.is_null() {
514        unsafe { (*writer).doc = doc };
515    }
516    writer
517}
518
519// ═══════════════════════════════════════════════════════════════════════════════
520// Document lifecycle
521// ═══════════════════════════════════════════════════════════════════════════════
522
523/// Start an XML document.
524///
525/// Writes the XML declaration `<?xml version="..." encoding="..." standalone="..."?>`.
526///
527/// # UPSTREAM-PARITY
528///
529/// ```c
530/// int xmlTextWriterStartDocument(xmlTextWriterPtr writer,
531///                                 const char *version,
532///                                 const char *encoding,
533///                                 const char *standalone);
534/// ```
535///
536/// # SAFETY
537///
538/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
539/// - `version`, `encoding`, `standalone` must be valid null-terminated strings or NULL.
540#[no_mangle]
541pub unsafe extern "C" fn xmlTextWriterStartDocument(
542    writer: *mut XmlTextWriter,
543    version: *const c_char,
544    encoding: *const c_char,
545    standalone: *const c_char,
546) -> c_int {
547    if writer.is_null() {
548        return -1;
549    }
550    // SAFETY: writer is a valid XmlTextWriter.
551    let w = unsafe { &mut *writer };
552
553    // UPSTREAM-PARITY (xmlTextWriterStartDocument): the declaration uses the
554    // writer's quote char and always ends with a newline (indent-independent).
555    let mut sum: c_int = 0;
556    sum += w.write_raw(b"<?xml version=" as *const u8, 14);
557    sum += w.write_byte(w.qchar);
558
559    let ver = if version.is_null() {
560        b"1.0\0" as *const u8
561    } else {
562        version as *const u8
563    };
564    sum += w.write_str(ver);
565    sum += w.write_byte(w.qchar);
566
567    if !encoding.is_null() {
568        sum += w.write_raw(b" encoding=" as *const u8, 10);
569        sum += w.write_byte(w.qchar);
570        sum += w.write_str(encoding as *const u8);
571        sum += w.write_byte(w.qchar);
572        // UPSTREAM-PARITY: the output encoder, once installed, persists for
573        // the writer's lifetime (a later StartDocument with encoding=NULL does
574        // NOT clear it — xmlTextWriterStartDocument only resets conv).
575        w.encoder_active = true;
576    }
577
578    if !standalone.is_null() {
579        sum += w.write_raw(b" standalone=" as *const u8, 12);
580        sum += w.write_byte(w.qchar);
581        sum += w.write_str(standalone as *const u8);
582        sum += w.write_byte(w.qchar);
583    }
584
585    sum += w.write_raw(b"?>\n" as *const u8, 3);
586
587    w.state = WriterState::XMLDecl;
588    sum
589}
590
591/// End an XML document.
592///
593/// Flushes any pending output and writes a final newline if indentation is enabled.
594///
595/// # UPSTREAM-PARITY
596///
597/// ```c
598/// int xmlTextWriterEndDocument(xmlTextWriterPtr writer);
599/// ```
600///
601/// # SAFETY
602///
603/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
604#[no_mangle]
605pub unsafe extern "C" fn xmlTextWriterEndDocument(writer: *mut XmlTextWriter) -> c_int {
606    if writer.is_null() {
607        return -1;
608    }
609    // SAFETY: writer is a valid XmlTextWriter.
610    let w = unsafe { &mut *writer };
611
612    // Close any open elements
613    let mut sum: c_int = 0;
614    while w.depth > 0 {
615        sum += xmlTextWriterEndElement(writer);
616    }
617
618    // UPSTREAM-PARITY: the final newline is written when indentation is OFF
619    // (each indented EndElement already wrote its own newline).
620    if w.indent == 0 {
621        sum += w.write_byte(b'\n');
622    }
623
624    // Flush output
625    if !w.output.is_null() {
626        sum += io::output_buffer_flush(w.output);
627    }
628
629    w.state = WriterState::None;
630    sum
631}
632
633// ═══════════════════════════════════════════════════════════════════════════════
634// Element writing
635// ═══════════════════════════════════════════════════════════════════════════════
636
637/// Start an XML element.
638///
639/// # UPSTREAM-PARITY
640///
641/// ```c
642/// int xmlTextWriterStartElement(xmlTextWriterPtr writer, const xmlChar *name);
643/// ```
644///
645/// # SAFETY
646///
647/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
648/// - `name` must be a valid null-terminated xmlChar string or NULL.
649#[no_mangle]
650pub unsafe extern "C" fn xmlTextWriterStartElement(
651    writer: *mut XmlTextWriter,
652    name: *const xmlChar,
653) -> c_int {
654    if writer.is_null() || name.is_null() {
655        return -1;
656    }
657    // SAFETY: writer is a valid XmlTextWriter.
658    let w = unsafe { &mut *writer };
659
660    // Close any open start tag from a previous element.
661    // UPSTREAM-PARITY: closing the parent's start tag emits `>` and, when
662    // indented, a newline (xmlTextWriterStartElement NAME case).
663    let (closed, cnt) = w.close_start_tag();
664    let mut sum: c_int = cnt;
665    if closed && w.indent != 0 {
666        sum += w.write_byte(b'\n');
667    }
668
669    // Write indentation
670    sum += w.write_indent();
671
672    // Write `<name`
673    sum += w.write_byte(b'<');
674    sum += w.write_str(name);
675
676    // Push onto stack (without null terminator)
677    let name_bytes = unsafe { c_str_to_vec(name) };
678    w.elem_stack.push((b"".to_vec(), name_bytes.clone()));
679    // Strip trailing null for stack storage
680    let stack_name = if name_bytes.last() == Some(&0) {
681        name_bytes[..name_bytes.len() - 1].to_vec()
682    } else {
683        name_bytes.clone()
684    };
685    w.stack.push(stack_name);
686    w.depth += 1;
687    w.in_start_tag = true;
688    w.state = WriterState::Element;
689
690    sum
691}
692
693/// End an XML element.
694///
695/// # UPSTREAM-PARITY
696///
697/// ```c
698/// int xmlTextWriterEndElement(xmlTextWriterPtr writer);
699/// ```
700///
701/// # SAFETY
702///
703/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
704#[no_mangle]
705pub unsafe extern "C" fn xmlTextWriterEndElement(writer: *mut XmlTextWriter) -> c_int {
706    if writer.is_null() {
707        return -1;
708    }
709    // SAFETY: writer is a valid XmlTextWriter.
710    let w = unsafe { &mut *writer };
711
712    if w.depth <= 0 {
713        return -1;
714    }
715
716    // UPSTREAM-PARITY (xmlTextWriterEndElement):
717    //   NAME state (start tag still open) -> "/>", doindent=1
718    //   otherwise (content written)       -> indent if doindent, "</name>"
719    //   then, when indented, a trailing newline.
720    let mut sum: c_int = 0;
721    if w.in_start_tag {
722        sum += w.flush_pending_ns();
723        sum += w.write_raw(b"/>" as *const u8, 2);
724        w.in_start_tag = false;
725        w.doindent = true;
726        w.stack.pop();
727    } else {
728        if w.indent != 0 && w.doindent {
729            sum += w.write_indent();
730            w.doindent = true;
731        } else {
732            w.doindent = true;
733        }
734        let name = w.stack.pop().unwrap_or_default();
735        sum += w.write_raw(b"</" as *const u8, 2);
736        sum += w.write_slice(&name);
737        sum += w.write_byte(b'>');
738    }
739
740    if w.indent != 0 {
741        sum += w.write_byte(b'\n');
742    }
743
744    w.depth -= 1;
745    w.elem_stack.pop();
746    w.state = WriterState::None;
747
748    sum
749}
750
751/// Start a namespaced XML element.
752///
753/// # UPSTREAM-PARITY
754///
755/// ```c
756/// int xmlTextWriterStartElementNS(xmlTextWriterPtr writer,
757///                                  const xmlChar *prefix,
758///                                  const xmlChar *name,
759///                                  const xmlChar *namespaceURI);
760/// ```
761///
762/// # SAFETY
763///
764/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
765/// - `prefix`, `name`, `namespaceURI` must be valid null-terminated strings or NULL.
766#[no_mangle]
767pub unsafe extern "C" fn xmlTextWriterStartElementNS(
768    writer: *mut XmlTextWriter,
769    prefix: *const xmlChar,
770    name: *const xmlChar,
771    namespaceURI: *const xmlChar,
772) -> c_int {
773    if writer.is_null() || name.is_null() {
774        return -1;
775    }
776    // SAFETY: writer is a valid XmlTextWriter.
777    let w = unsafe { &mut *writer };
778
779    // UPSTREAM-PARITY: closing the parent's start tag emits `>` and, when
780    // indented, a newline.
781    let (closed, cnt) = w.close_start_tag();
782    let mut sum: c_int = cnt;
783    if closed && w.indent != 0 {
784        sum += w.write_byte(b'\n');
785    }
786    sum += w.write_indent();
787
788    sum += w.write_byte(b'<');
789
790    let prefix_bytes = if prefix.is_null() {
791        Vec::new()
792    } else {
793        unsafe { c_str_to_vec(prefix) }
794    };
795
796    let name_bytes = unsafe { c_str_to_vec(name) };
797
798    if !prefix_bytes.is_empty() {
799        // Strip trailing null before writing
800        let p = if prefix_bytes.last() == Some(&0) {
801            &prefix_bytes[..prefix_bytes.len() - 1]
802        } else {
803            &prefix_bytes
804        };
805        sum += w.write_slice(p);
806        sum += w.write_byte(b':');
807    }
808    // Strip trailing null before writing
809    let n = if name_bytes.last() == Some(&0) {
810        &name_bytes[..name_bytes.len() - 1]
811    } else {
812        &name_bytes
813    };
814    sum += w.write_slice(n);
815
816    // Defer the namespace declaration until the tag closes (upstream
817    // xmlTextWriterOutputNSDecl writes it after the attributes).
818    if !namespaceURI.is_null() {
819        let ns_uri_bytes = unsafe { c_str_to_vec(namespaceURI) };
820        let uri_body = if ns_uri_bytes.last() == Some(&0) {
821            ns_uri_bytes[..ns_uri_bytes.len() - 1].to_vec()
822        } else {
823            ns_uri_bytes
824        };
825        let prefix_body = if prefix_bytes.last() == Some(&0) {
826            prefix_bytes[..prefix_bytes.len() - 1].to_vec()
827        } else {
828            prefix_bytes.clone()
829        };
830        w.pending_ns.push((prefix_body, uri_body));
831    }
832
833    w.elem_stack.push((prefix_bytes, name_bytes.clone()));
834    // Strip trailing null for stack storage
835    let stack_name = if name_bytes.last() == Some(&0) {
836        name_bytes[..name_bytes.len() - 1].to_vec()
837    } else {
838        name_bytes
839    };
840    w.stack.push(stack_name);
841    w.depth += 1;
842    w.in_start_tag = true;
843    w.state = WriterState::Element;
844
845    sum
846}
847
848/// Write an element with inline content.
849///
850/// # UPSTREAM-PARITY
851///
852/// ```c
853/// int xmlTextWriterWriteElement(xmlTextWriterPtr writer,
854///                                const xmlChar *name,
855///                                const xmlChar *content);
856/// ```
857///
858/// # SAFETY
859///
860/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
861/// - `name`, `content` must be valid null-terminated strings or NULL.
862#[no_mangle]
863pub unsafe extern "C" fn xmlTextWriterWriteElement(
864    writer: *mut XmlTextWriter,
865    name: *const xmlChar,
866    content: *const xmlChar,
867) -> c_int {
868    if writer.is_null() || name.is_null() {
869        return -1;
870    }
871    let ret = xmlTextWriterStartElement(writer, name);
872    if ret == -1 {
873        return ret;
874    }
875    if !content.is_null() {
876        let ret2 = xmlTextWriterWriteString(writer, content);
877        if ret2 == -1 {
878            return ret2;
879        }
880    }
881    xmlTextWriterEndElement(writer)
882}
883
884/// Write a namespaced element with inline content.
885///
886/// # UPSTREAM-PARITY
887///
888/// ```c
889/// int xmlTextWriterWriteElementNS(xmlTextWriterPtr writer,
890///                                  const xmlChar *prefix,
891///                                  const xmlChar *name,
892///                                  const xmlChar *nsURI,
893///                                  const xmlChar *content);
894/// ```
895///
896/// # SAFETY
897///
898/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
899/// - `prefix`, `name`, `nsURI`, `content` must be valid null-terminated strings or NULL.
900#[no_mangle]
901pub unsafe extern "C" fn xmlTextWriterWriteElementNS(
902    writer: *mut XmlTextWriter,
903    prefix: *const xmlChar,
904    name: *const xmlChar,
905    nsURI: *const xmlChar,
906    content: *const xmlChar,
907) -> c_int {
908    if writer.is_null() || name.is_null() {
909        return -1;
910    }
911    let ret = xmlTextWriterStartElementNS(writer, prefix, name, nsURI);
912    if ret == -1 {
913        return ret;
914    }
915    if !content.is_null() {
916        let ret2 = xmlTextWriterWriteString(writer, content);
917        if ret2 == -1 {
918            return ret2;
919        }
920    }
921    xmlTextWriterEndElement(writer)
922}
923
924/// Write a full end element (always writes `</name>`, never self-closing).
925///
926/// # UPSTREAM-PARITY
927///
928/// ```c
929/// int xmlTextWriterFullEndElement(xmlTextWriterPtr writer);
930/// ```
931///
932/// # SAFETY
933///
934/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
935#[no_mangle]
936pub unsafe extern "C" fn xmlTextWriterFullEndElement(writer: *mut XmlTextWriter) -> c_int {
937    if writer.is_null() {
938        return -1;
939    }
940    // SAFETY: writer is a valid XmlTextWriter.
941    let w = unsafe { &mut *writer };
942
943    if w.depth <= 0 {
944        return -1;
945    }
946
947    // UPSTREAM-PARITY (xmlTextWriterFullEndElement): always writes `</name>`,
948    // closing the start tag with `>` first if needed.
949    let mut sum: c_int = 0;
950    if w.in_start_tag {
951        sum += w.write_byte(b'>');
952        w.in_start_tag = false;
953    }
954
955    if w.indent != 0 && w.doindent {
956        sum += w.write_indent();
957        w.doindent = true;
958    } else {
959        w.doindent = true;
960    }
961
962    // Write `</name>`
963    let name = w.stack.pop().unwrap_or_default();
964    sum += w.write_raw(b"</" as *const u8, 2);
965    sum += w.write_slice(&name);
966    sum += w.write_byte(b'>');
967
968    if w.indent != 0 {
969        sum += w.write_byte(b'\n');
970    }
971
972    w.depth -= 1;
973    w.elem_stack.pop();
974    w.state = WriterState::None;
975
976    sum
977}
978
979// ═══════════════════════════════════════════════════════════════════════════════
980// Attribute writing
981// ═══════════════════════════════════════════════════════════════════════════════
982
983/// Write an attribute.
984///
985/// # UPSTREAM-PARITY
986///
987/// ```c
988/// int xmlTextWriterWriteAttribute(xmlTextWriterPtr writer,
989///                                  const xmlChar *name,
990///                                  const xmlChar *content);
991/// ```
992///
993/// # SAFETY
994///
995/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
996/// - `name`, `content` must be valid null-terminated strings or NULL.
997#[no_mangle]
998pub unsafe extern "C" fn xmlTextWriterWriteAttribute(
999    writer: *mut XmlTextWriter,
1000    name: *const xmlChar,
1001    content: *const xmlChar,
1002) -> c_int {
1003    if writer.is_null() || name.is_null() || content.is_null() {
1004        return -1;
1005    }
1006    // SAFETY: writer is a valid XmlTextWriter.
1007    let w = unsafe { &mut *writer };
1008
1009    if !w.in_start_tag {
1010        return -1;
1011    }
1012
1013    // Write ` name=` and the quote char.
1014    let mut sum: c_int = 0;
1015    sum += w.write_byte(b' ');
1016    sum += w.write_str(name);
1017    sum += w.write_raw(b"=" as *const u8, 1);
1018    sum += w.write_byte(w.qchar);
1019
1020    // Write escaped content (qchar-aware).
1021    sum += unsafe { write_attr_escaped(w, content) };
1022
1023    sum += w.write_byte(w.qchar);
1024    // UPSTREAM-PARITY: a completed attribute returns the writer to the
1025    // element start-tag state (xmlTextWriterEndAttribute -> NAME).
1026    w.state = WriterState::Element;
1027
1028    sum
1029}
1030
1031/// Write a namespaced attribute.
1032///
1033/// # UPSTREAM-PARITY
1034///
1035/// ```c
1036/// int xmlTextWriterWriteAttributeNS(xmlTextWriterPtr writer,
1037///                                    const xmlChar *prefix,
1038///                                    const xmlChar *name,
1039///                                    const xmlChar *nsURI,
1040///                                    const xmlChar *content);
1041/// ```
1042///
1043/// # SAFETY
1044///
1045/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1046/// - `prefix`, `name`, `nsURI`, `content` must be valid null-terminated strings or NULL.
1047#[no_mangle]
1048pub unsafe extern "C" fn xmlTextWriterWriteAttributeNS(
1049    writer: *mut XmlTextWriter,
1050    prefix: *const xmlChar,
1051    name: *const xmlChar,
1052    nsURI: *const xmlChar,
1053    content: *const xmlChar,
1054) -> c_int {
1055    let _ = nsURI;
1056    if writer.is_null() || name.is_null() || content.is_null() {
1057        return -1;
1058    }
1059    // SAFETY: writer is a valid XmlTextWriter.
1060    let w = unsafe { &mut *writer };
1061
1062    if !w.in_start_tag {
1063        return -1;
1064    }
1065
1066    let mut sum: c_int = 0;
1067    sum += w.write_byte(b' ');
1068
1069    if !prefix.is_null() {
1070        sum += w.write_str(prefix);
1071        sum += w.write_byte(b':');
1072    }
1073    sum += w.write_str(name);
1074
1075    sum += w.write_raw(b"=" as *const u8, 1);
1076    sum += w.write_byte(w.qchar);
1077
1078    // Write escaped content (qchar-aware).
1079    sum += unsafe { write_attr_escaped(w, content) };
1080
1081    sum += w.write_byte(w.qchar);
1082    // UPSTREAM-PARITY: a completed attribute returns the writer to the
1083    // element start-tag state.
1084    w.state = WriterState::Element;
1085
1086    sum
1087}
1088
1089/// Write a formatted attribute.
1090///
1091/// # UPSTREAM-PARITY
1092///
1093/// ```c
1094/// int xmlTextWriterWriteFormatAttribute(xmlTextWriterPtr writer,
1095///                                        const xmlChar *name,
1096///                                        ...);
1097/// ```
1098///
1099/// # SAFETY
1100///
1101/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1102/// - `name` must be a valid null-terminated string.
1103#[no_mangle]
1104
1105/// Start an attribute (to be written incrementally).
1106///
1107/// # UPSTREAM-PARITY
1108///
1109/// ```c
1110/// int xmlTextWriterStartAttribute(xmlTextWriterPtr writer, const xmlChar *name);
1111/// ```
1112///
1113/// # SAFETY
1114///
1115/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1116/// - `name` must be a valid null-terminated string or NULL.
1117#[no_mangle]
1118pub unsafe extern "C" fn xmlTextWriterStartAttribute(
1119    writer: *mut XmlTextWriter,
1120    name: *const xmlChar,
1121) -> c_int {
1122    if writer.is_null() || name.is_null() {
1123        return -1;
1124    }
1125    // SAFETY: writer is a valid XmlTextWriter.
1126    let w = unsafe { &mut *writer };
1127
1128    if !w.in_start_tag {
1129        return -1;
1130    }
1131
1132    let mut sum: c_int = 0;
1133    sum += w.write_byte(b' ');
1134    sum += w.write_str(name);
1135    sum += w.write_raw(b"=" as *const u8, 1);
1136    sum += w.write_byte(w.qchar);
1137    w.state = WriterState::Attribute;
1138
1139    sum
1140}
1141
1142/// Start a namespaced attribute (to be written incrementally).
1143///
1144/// # UPSTREAM-PARITY
1145///
1146/// ```c
1147/// int xmlTextWriterStartAttributeNS(xmlTextWriterPtr writer,
1148///                                    const xmlChar *prefix,
1149///                                    const xmlChar *name,
1150///                                    const xmlChar *nsURI);
1151/// ```
1152///
1153/// # SAFETY
1154///
1155/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1156/// - `prefix`, `name`, `nsURI` must be valid null-terminated strings or NULL.
1157#[no_mangle]
1158pub unsafe extern "C" fn xmlTextWriterStartAttributeNS(
1159    writer: *mut XmlTextWriter,
1160    prefix: *const xmlChar,
1161    name: *const xmlChar,
1162    nsURI: *const xmlChar,
1163) -> c_int {
1164    let _ = nsURI;
1165    if writer.is_null() || name.is_null() {
1166        return -1;
1167    }
1168    // SAFETY: writer is a valid XmlTextWriter.
1169    let w = unsafe { &mut *writer };
1170
1171    if !w.in_start_tag {
1172        return -1;
1173    }
1174
1175    let mut sum: c_int = 0;
1176    sum += w.write_byte(b' ');
1177    if !prefix.is_null() {
1178        sum += w.write_str(prefix);
1179        sum += w.write_byte(b':');
1180    }
1181    sum += w.write_str(name);
1182    sum += w.write_raw(b"=" as *const u8, 1);
1183    sum += w.write_byte(w.qchar);
1184    w.state = WriterState::Attribute;
1185
1186    sum
1187}
1188
1189/// End an attribute (closes the attribute value quote).
1190///
1191/// # UPSTREAM-PARITY
1192///
1193/// ```c
1194/// int xmlTextWriterEndAttribute(xmlTextWriterPtr writer);
1195/// ```
1196///
1197/// # SAFETY
1198///
1199/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1200#[no_mangle]
1201pub unsafe extern "C" fn xmlTextWriterEndAttribute(writer: *mut XmlTextWriter) -> c_int {
1202    if writer.is_null() {
1203        return -1;
1204    }
1205    // SAFETY: writer is a valid XmlTextWriter.
1206    let w = unsafe { &mut *writer };
1207
1208    if w.state != WriterState::Attribute {
1209        return -1;
1210    }
1211
1212    w.write_byte(w.qchar);
1213    w.state = WriterState::Element;
1214
1215    1
1216}
1217
1218// ═══════════════════════════════════════════════════════════════════════════════
1219// Content writing
1220// ═══════════════════════════════════════════════════════════════════════════════
1221
1222/// Escape text like upstream `xmlEncodeSpecialChars(NULL, content)`:
1223/// `&` `<` `>` `"` `'` are all escaped. Returns a NUL-terminated vector.
1224///
1225/// # SAFETY
1226///
1227/// - `content` must be a valid NUL-terminated string.
1228unsafe fn encode_special_chars(content: *const xmlChar) -> Vec<u8> {
1229    let mut out = Vec::new();
1230    let mut p = content;
1231    unsafe {
1232        while !p.is_null() && *p != 0 {
1233            // UPSTREAM-PARITY (xmlEncodeSpecialChars / xmlEscapeText with
1234            // XML_ESCAPE_QUOT): `&<>"` are escaped; the apostrophe is NOT.
1235            match *p {
1236                b'&' => out.extend_from_slice(b"&amp;"),
1237                b'<' => out.extend_from_slice(b"&lt;"),
1238                b'>' => out.extend_from_slice(b"&gt;"),
1239                b'"' => out.extend_from_slice(b"&quot;"),
1240                c => out.push(c),
1241            }
1242            p = p.add(1);
1243        }
1244    }
1245    out.push(0);
1246    out
1247}
1248
1249/// Serialize attribute content with the writer's quote char, mirroring
1250/// `xmlBufAttrSerializeTxtContent` (xmlsave.c): `\n`/`\r`/`\t` become
1251/// character references, `&<>` always escape, and the quote char is escaped.
1252/// Returns the bytes written.
1253///
1254/// # SAFETY
1255///
1256/// - `content` must be a valid NUL-terminated string.
1257unsafe fn write_attr_escaped(w: &mut XmlTextWriter, content: *const xmlChar) -> c_int {
1258    let mut sum: c_int = 0;
1259    let mut p = content;
1260    unsafe {
1261        while !p.is_null() && *p != 0 {
1262            let c = *p;
1263            // UPSTREAM-PARITY (xmlBufAttrSerializeTxtContent -> xmlSerializeText
1264            // with XML_ESCAPE_ATTR): `\n`/`\r`/`\t` become character
1265            // references, `&<>"` escape; the apostrophe is NEVER escaped
1266            // (the qchar only selects the outer quotes).
1267            sum += match c {
1268                b'\n' => w.write_slice(b"&#10;"),
1269                b'\r' => w.write_slice(b"&#13;"),
1270                b'\t' => w.write_slice(b"&#9;"),
1271                b'&' => w.write_slice(b"&amp;"),
1272                b'<' => w.write_slice(b"&lt;"),
1273                b'>' => w.write_slice(b"&gt;"),
1274                b'"' => w.write_slice(b"&quot;"),
1275                c => w.write_byte(c),
1276            };
1277            p = p.add(1);
1278        }
1279    }
1280    sum
1281}
1282
1283/// Write text content.
1284///
1285/// # UPSTREAM-PARITY
1286///
1287/// ```c
1288/// int xmlTextWriterWriteString(xmlTextWriterPtr writer, const xmlChar *content);
1289/// ```
1290///
1291/// NAME/TEXT states escape via xmlEncodeSpecialChars (quotes included);
1292/// ATTRIBUTE escapes via xmlBufAttrSerializeTxtContent (qchar-aware); all
1293/// other states (CDATA/comment/PI/DTD*) write raw through WriteRaw, which
1294/// performs the DTD state transitions.
1295///
1296/// # SAFETY
1297///
1298/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1299/// - `content` must be a valid null-terminated xmlChar string or NULL.
1300#[no_mangle]
1301pub unsafe extern "C" fn xmlTextWriterWriteString(
1302    writer: *mut XmlTextWriter,
1303    content: *const xmlChar,
1304) -> c_int {
1305    if writer.is_null() || content.is_null() {
1306        return -1;
1307    }
1308    // SAFETY: writer is a valid XmlTextWriter.
1309    let w = unsafe { &mut *writer };
1310
1311    match w.state {
1312        WriterState::Attribute => unsafe { write_attr_escaped(w, content) },
1313        WriterState::Element => {
1314            let esc = unsafe { encode_special_chars(content) };
1315            let (_, cnt) = w.close_start_tag();
1316            let mut sum: c_int = cnt;
1317            if !esc.is_empty() {
1318                sum += w.write_slice(&esc[..esc.len() - 1]);
1319            }
1320            w.doindent = false;
1321            sum
1322        }
1323        WriterState::None if w.depth > 0 => {
1324            // Inside an element after content: upstream TEXT state escapes.
1325            let esc = unsafe { encode_special_chars(content) };
1326            let mut sum: c_int = 0;
1327            if !esc.is_empty() {
1328                sum += w.write_slice(&esc[..esc.len() - 1]);
1329            }
1330            w.doindent = false;
1331            sum
1332        }
1333        _ => {
1334            // Raw path (CDATA/comment/PI/DTD*, and top-level with no stack
1335            // entry — upstream writes raw when no element is open): WriteRaw
1336            // performs the state transitions (DTD bracket, entity quote,
1337            // element/attr separators).
1338            let rc = unsafe { xmlTextWriterWriteRaw(writer, content) };
1339            w.doindent = false;
1340            rc
1341        }
1342    }
1343}
1344
1345/// Write raw content (no XML escaping).
1346///
1347/// # UPSTREAM-PARITY
1348///
1349/// ```c
1350/// int xmlTextWriterWriteRaw(xmlTextWriterPtr writer, const xmlChar *content);
1351/// ```
1352///
1353/// Performs the upstream state-dependent transitions before the content:
1354/// DTD -> " [" (+newline when indented), DTD_ELEM/DTD_ATTL -> " ",
1355/// DTD_ENTY/PENT -> " " + quote char, PI -> " ".
1356///
1357/// # SAFETY
1358///
1359/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1360/// - `content` must be a valid null-terminated xmlChar string or NULL.
1361#[no_mangle]
1362pub unsafe extern "C" fn xmlTextWriterWriteRaw(
1363    writer: *mut XmlTextWriter,
1364    content: *const xmlChar,
1365) -> c_int {
1366    if writer.is_null() || content.is_null() {
1367        return -1;
1368    }
1369    // SAFETY: writer is a valid XmlTextWriter.
1370    let w = unsafe { &mut *writer };
1371
1372    // UPSTREAM-PARITY (xmlTextWriterHandleStateDependencies).
1373    let mut sum: c_int = 0;
1374    match w.state {
1375        WriterState::Element => {
1376            let (_, cnt) = w.close_start_tag();
1377            sum += cnt;
1378        }
1379        WriterState::PI => {
1380            sum += w.write_byte(b' ');
1381        }
1382        WriterState::DTD => {
1383            w.state = WriterState::DTDText;
1384            if w.indent != 0 {
1385                sum += w.write_slice(b" [\n");
1386            } else {
1387                sum += w.write_slice(b" [");
1388            }
1389        }
1390        WriterState::DTDElem => {
1391            sum += w.write_byte(b' ');
1392            w.state = WriterState::DTDElemText;
1393        }
1394        WriterState::DTDAttr => {
1395            sum += w.write_byte(b' ');
1396            w.state = WriterState::DTDAttrText;
1397        }
1398        WriterState::DTDEntity => {
1399            sum += w.write_byte(b' ');
1400            sum += w.write_byte(w.qchar);
1401            w.state = WriterState::DTDEntityText;
1402        }
1403        _ => {}
1404    }
1405
1406    if w.indent != 0 {
1407        w.doindent = false;
1408    }
1409
1410    sum += w.write_str(content);
1411    sum
1412}
1413
1414/// Write raw content with explicit length (no XML escaping).
1415///
1416/// # UPSTREAM-PARITY
1417///
1418/// ```c
1419/// int xmlTextWriterWriteRawLen(xmlTextWriterPtr writer,
1420///                               const xmlChar *content,
1421///                               int len);
1422/// ```
1423///
1424/// # SAFETY
1425///
1426/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1427/// - `content` must point to `len` valid bytes or NULL.
1428#[no_mangle]
1429pub unsafe extern "C" fn xmlTextWriterWriteRawLen(
1430    writer: *mut XmlTextWriter,
1431    content: *const xmlChar,
1432    len: c_int,
1433) -> c_int {
1434    if writer.is_null() || content.is_null() || len < 0 {
1435        return -1;
1436    }
1437    // SAFETY: writer is a valid XmlTextWriter.
1438    let w = unsafe { &mut *writer };
1439
1440    // Same state transitions as WriteRaw, then the len-bounded write.
1441    let mut sum: c_int = 0;
1442    match w.state {
1443        WriterState::Element => {
1444            let (_, cnt) = w.close_start_tag();
1445            sum += cnt;
1446        }
1447        WriterState::PI => {
1448            sum += w.write_byte(b' ');
1449        }
1450        WriterState::DTD => {
1451            w.state = WriterState::DTDText;
1452            if w.indent != 0 {
1453                sum += w.write_slice(b" [\n");
1454            } else {
1455                sum += w.write_slice(b" [");
1456            }
1457        }
1458        WriterState::DTDElem => {
1459            sum += w.write_byte(b' ');
1460            w.state = WriterState::DTDElemText;
1461        }
1462        WriterState::DTDAttr => {
1463            sum += w.write_byte(b' ');
1464            w.state = WriterState::DTDAttrText;
1465        }
1466        WriterState::DTDEntity => {
1467            sum += w.write_byte(b' ');
1468            sum += w.write_byte(w.qchar);
1469            w.state = WriterState::DTDEntityText;
1470        }
1471        _ => {}
1472    }
1473
1474    if w.indent != 0 {
1475        w.doindent = false;
1476    }
1477
1478    if len > 0 {
1479        sum += w.write_raw(content, len);
1480    }
1481    sum
1482}
1483
1484/// Write a formatted string.
1485///
1486/// # UPSTREAM-PARITY
1487///
1488/// ```c
1489/// int xmlTextWriterWriteFormatString(xmlTextWriterPtr writer, const char *fmt, ...);
1490/// ```
1491///
1492/// # SAFETY
1493///
1494/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1495#[no_mangle]
1496
1497/// Write Base64-encoded data.
1498///
1499/// # UPSTREAM-PARITY
1500///
1501/// ```c
1502/// int xmlTextWriterWriteBase64(xmlTextWriterPtr writer,
1503///                               const char *data,
1504///                               int start,
1505///                               int len);
1506/// ```
1507///
1508/// # SAFETY
1509///
1510/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1511/// - `data` must be a valid pointer to `start + len` bytes or NULL.
1512#[no_mangle]
1513pub unsafe extern "C" fn xmlTextWriterWriteBase64(
1514    writer: *mut XmlTextWriter,
1515    data: *const c_char,
1516    start: c_int,
1517    len: c_int,
1518) -> c_int {
1519    if writer.is_null() || data.is_null() || len <= 0 || start < 0 {
1520        return -1;
1521    }
1522    // SAFETY: writer is a valid XmlTextWriter.
1523    let w = unsafe { &mut *writer };
1524
1525    w.close_start_tag();
1526
1527    // Base64 encode the data
1528    let data_slice =
1529        unsafe { core::slice::from_raw_parts(data.add(start as usize) as *const u8, len as usize) };
1530    let encoded = base64_encode(data_slice);
1531    w.write_slice(&encoded);
1532
1533    0
1534}
1535
1536/// Write BinHex-encoded data.
1537///
1538/// # UPSTREAM-PARITY
1539///
1540/// ```c
1541/// int xmlTextWriterWriteBinHex(xmlTextWriterPtr writer,
1542///                               const char *data,
1543///                               int start,
1544///                               int len);
1545/// ```
1546///
1547/// # SAFETY
1548///
1549/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1550/// - `data` must be a valid pointer to `start + len` bytes or NULL.
1551#[no_mangle]
1552pub unsafe extern "C" fn xmlTextWriterWriteBinHex(
1553    writer: *mut XmlTextWriter,
1554    data: *const c_char,
1555    start: c_int,
1556    len: c_int,
1557) -> c_int {
1558    if writer.is_null() || data.is_null() || len <= 0 || start < 0 {
1559        return -1;
1560    }
1561    // SAFETY: writer is a valid XmlTextWriter.
1562    let w = unsafe { &mut *writer };
1563
1564    w.close_start_tag();
1565
1566    // Hex encode the data
1567    let data_slice =
1568        unsafe { core::slice::from_raw_parts(data.add(start as usize) as *const u8, len as usize) };
1569    let encoded = hex_encode(data_slice);
1570    w.write_slice(&encoded);
1571
1572    0
1573}
1574
1575/// Write a CDATA section.
1576///
1577/// # UPSTREAM-PARITY
1578///
1579/// ```c
1580/// int xmlTextWriterWriteCDATA(xmlTextWriterPtr writer, const xmlChar *content);
1581/// ```
1582///
1583/// # SAFETY
1584///
1585/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1586/// - `content` must be a valid null-terminated xmlChar string or NULL.
1587#[no_mangle]
1588pub unsafe extern "C" fn xmlTextWriterWriteCDATA(
1589    writer: *mut XmlTextWriter,
1590    content: *const xmlChar,
1591) -> c_int {
1592    let mut sum: c_int = 0;
1593    let ret = unsafe { xmlTextWriterStartCDATA(writer) };
1594    if ret == -1 {
1595        return -1;
1596    }
1597    sum += ret;
1598    if !content.is_null() {
1599        let ret2 = unsafe { xmlTextWriterWriteString(writer, content) };
1600        if ret2 == -1 {
1601            return -1;
1602        }
1603        sum += ret2;
1604    }
1605    let ret3 = unsafe { xmlTextWriterEndCDATA(writer) };
1606    if ret3 == -1 {
1607        return -1;
1608    }
1609    sum + ret3
1610}
1611
1612/// Start a CDATA section.
1613///
1614/// # UPSTREAM-PARITY
1615///
1616/// ```c
1617/// int xmlTextWriterStartCDATA(xmlTextWriterPtr writer);
1618/// ```
1619///
1620/// # SAFETY
1621///
1622/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1623#[no_mangle]
1624pub unsafe extern "C" fn xmlTextWriterStartCDATA(writer: *mut XmlTextWriter) -> c_int {
1625    if writer.is_null() {
1626        return -1;
1627    }
1628    // SAFETY: writer is a valid XmlTextWriter.
1629    let w = unsafe { &mut *writer };
1630
1631    // UPSTREAM-PARITY: closing the parent's start tag emits `>` and, when
1632    // indented, a newline; no indentation precedes `<![CDATA[`.
1633    let (closed, cnt) = w.close_start_tag();
1634    let mut sum: c_int = cnt;
1635    if closed && w.indent != 0 {
1636        sum += w.write_byte(b'\n');
1637    }
1638    sum += w.write_slice(b"<![CDATA[");
1639    w.state = WriterState::CData;
1640    sum
1641}
1642
1643/// End a CDATA section.
1644///
1645/// # UPSTREAM-PARITY
1646///
1647/// ```c
1648/// int xmlTextWriterEndCDATA(xmlTextWriterPtr writer);
1649/// ```
1650///
1651/// # SAFETY
1652///
1653/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1654#[no_mangle]
1655pub unsafe extern "C" fn xmlTextWriterEndCDATA(writer: *mut XmlTextWriter) -> c_int {
1656    if writer.is_null() {
1657        return -1;
1658    }
1659    // SAFETY: writer is a valid XmlTextWriter.
1660    let w = unsafe { &mut *writer };
1661    if w.state != WriterState::CData {
1662        return -1;
1663    }
1664    let sum: c_int = w.write_slice(b"]]>");
1665    w.state = WriterState::None;
1666    sum
1667}
1668
1669/// Write a comment.
1670///
1671/// # UPSTREAM-PARITY
1672///
1673/// ```c
1674/// int xmlTextWriterWriteComment(xmlTextWriterPtr writer, const xmlChar *content);
1675/// ```
1676///
1677/// # SAFETY
1678///
1679/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1680/// - `content` must be a valid null-terminated xmlChar string or NULL.
1681#[no_mangle]
1682pub unsafe extern "C" fn xmlTextWriterWriteComment(
1683    writer: *mut XmlTextWriter,
1684    content: *const xmlChar,
1685) -> c_int {
1686    let mut sum: c_int = 0;
1687    let ret = unsafe { xmlTextWriterStartComment(writer) };
1688    if ret < 0 {
1689        return -1;
1690    }
1691    sum += ret;
1692    let ret2 = unsafe { xmlTextWriterWriteString(writer, content) };
1693    if ret2 < 0 {
1694        return -1;
1695    }
1696    sum += ret2;
1697    let ret3 = unsafe { xmlTextWriterEndComment(writer) };
1698    if ret3 < 0 {
1699        return -1;
1700    }
1701    sum + ret3
1702}
1703
1704/// Start a comment.
1705///
1706/// # UPSTREAM-PARITY
1707///
1708/// ```c
1709/// int xmlTextWriterStartComment(xmlTextWriterPtr writer);
1710/// ```
1711///
1712/// # SAFETY
1713///
1714/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1715#[no_mangle]
1716pub unsafe extern "C" fn xmlTextWriterStartComment(writer: *mut XmlTextWriter) -> c_int {
1717    if writer.is_null() {
1718        return -1;
1719    }
1720    // SAFETY: writer is a valid XmlTextWriter.
1721    let w = unsafe { &mut *writer };
1722    let (closed, cnt) = w.close_start_tag();
1723    let mut sum: c_int = cnt;
1724    if closed && w.indent != 0 {
1725        sum += w.write_byte(b'\n');
1726    }
1727    sum += w.write_indent();
1728    sum += w.write_slice(b"<!--");
1729    w.state = WriterState::Comment;
1730    sum
1731}
1732
1733/// End a comment.
1734///
1735/// # UPSTREAM-PARITY
1736///
1737/// ```c
1738/// int xmlTextWriterEndComment(xmlTextWriterPtr writer);
1739/// ```
1740///
1741/// # SAFETY
1742///
1743/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1744#[no_mangle]
1745pub unsafe extern "C" fn xmlTextWriterEndComment(writer: *mut XmlTextWriter) -> c_int {
1746    if writer.is_null() {
1747        return -1;
1748    }
1749    // SAFETY: writer is a valid XmlTextWriter.
1750    let w = unsafe { &mut *writer };
1751    if w.state != WriterState::Comment {
1752        return -1;
1753    }
1754    let mut sum: c_int = w.write_slice(b"-->");
1755    if w.indent != 0 {
1756        sum += w.write_byte(b'\n');
1757    }
1758    w.state = WriterState::None;
1759    sum
1760}
1761
1762/// Write a processing instruction.
1763///
1764/// # UPSTREAM-PARITY
1765///
1766/// ```c
1767/// int xmlTextWriterWritePI(xmlTextWriterPtr writer,
1768///                           const xmlChar *target,
1769///                           const xmlChar *content);
1770/// ```
1771///
1772/// # SAFETY
1773///
1774/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1775/// - `target`, `content` must be valid null-terminated strings or NULL.
1776#[no_mangle]
1777pub unsafe extern "C" fn xmlTextWriterWritePI(
1778    writer: *mut XmlTextWriter,
1779    target: *const xmlChar,
1780    content: *const xmlChar,
1781) -> c_int {
1782    let mut sum: c_int = 0;
1783    let ret = unsafe { xmlTextWriterStartPI(writer, target) };
1784    if ret == -1 {
1785        return -1;
1786    }
1787    sum += ret;
1788    if !content.is_null() {
1789        let ret2 = unsafe { xmlTextWriterWriteString(writer, content) };
1790        if ret2 == -1 {
1791            return -1;
1792        }
1793        sum += ret2;
1794    }
1795    let ret3 = unsafe { xmlTextWriterEndPI(writer) };
1796    if ret3 == -1 {
1797        return -1;
1798    }
1799    sum + ret3
1800}
1801
1802/// Start a processing instruction.
1803///
1804/// # UPSTREAM-PARITY
1805///
1806/// ```c
1807/// int xmlTextWriterStartPI(xmlTextWriterPtr writer, const xmlChar *target);
1808/// ```
1809///
1810/// # SAFETY
1811///
1812/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1813/// - `target` must be a valid null-terminated string or NULL.
1814#[no_mangle]
1815pub unsafe extern "C" fn xmlTextWriterStartPI(
1816    writer: *mut XmlTextWriter,
1817    target: *const xmlChar,
1818) -> c_int {
1819    if writer.is_null() || target.is_null() || unsafe { *target } == 0 {
1820        return -1;
1821    }
1822    // SAFETY: writer is a valid XmlTextWriter.
1823    let w = unsafe { &mut *writer };
1824    let (closed, cnt) = w.close_start_tag();
1825    let mut sum: c_int = cnt;
1826    if closed && w.indent != 0 {
1827        sum += w.write_byte(b'\n');
1828    }
1829    sum += w.write_slice(b"<?");
1830    sum += w.write_str(target);
1831    // UPSTREAM-PARITY: no trailing space here — the first content write
1832    // emits the separator (xmlTextWriterHandleStateDependencies PI case).
1833    w.state = WriterState::PI;
1834    sum
1835}
1836
1837/// End a processing instruction.
1838///
1839/// # UPSTREAM-PARITY
1840///
1841/// ```c
1842/// int xmlTextWriterEndPI(xmlTextWriterPtr writer);
1843/// ```
1844///
1845/// # SAFETY
1846///
1847/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1848#[no_mangle]
1849pub unsafe extern "C" fn xmlTextWriterEndPI(writer: *mut XmlTextWriter) -> c_int {
1850    if writer.is_null() {
1851        return -1;
1852    }
1853    // SAFETY: writer is a valid XmlTextWriter.
1854    let w = unsafe { &mut *writer };
1855    if w.state != WriterState::PI {
1856        return -1;
1857    }
1858    let mut sum: c_int = w.write_slice(b"?>");
1859    if w.indent != 0 {
1860        sum += w.write_byte(b'\n');
1861    }
1862    w.state = WriterState::None;
1863    sum
1864}
1865
1866// ═══════════════════════════════════════════════════════════════════════════════
1867// DTD writing
1868// ═══════════════════════════════════════════════════════════════════════════════
1869
1870/// Write a DTD declaration.
1871///
1872/// # UPSTREAM-PARITY
1873///
1874/// ```c
1875/// int xmlTextWriterWriteDTD(xmlTextWriterPtr writer,
1876///                            const xmlChar *name,
1877///                            const xmlChar *pubid,
1878///                            const xmlChar *sysid,
1879///                            const xmlChar *subset);
1880/// ```
1881///
1882/// # SAFETY
1883///
1884/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1885/// - `name`, `pubid`, `sysid`, `subset` must be valid null-terminated strings or NULL.
1886#[no_mangle]
1887pub unsafe extern "C" fn xmlTextWriterWriteDTD(
1888    writer: *mut XmlTextWriter,
1889    name: *const xmlChar,
1890    pubid: *const xmlChar,
1891    sysid: *const xmlChar,
1892    subset: *const xmlChar,
1893) -> c_int {
1894    let mut sum: c_int = 0;
1895    let ret = unsafe { xmlTextWriterStartDTD(writer, name, pubid, sysid) };
1896    if ret == -1 {
1897        return ret;
1898    }
1899    sum += ret;
1900    if !subset.is_null() {
1901        let ret2 = unsafe { xmlTextWriterWriteString(writer, subset) };
1902        if ret2 == -1 {
1903            return ret2;
1904        }
1905        sum += ret2;
1906    }
1907    let ret3 = unsafe { xmlTextWriterEndDTD(writer) };
1908    if ret3 == -1 {
1909        return ret3;
1910    }
1911    sum + ret3
1912}
1913
1914/// Write a DTD element declaration.
1915///
1916/// # UPSTREAM-PARITY
1917///
1918/// ```c
1919/// int xmlTextWriterWriteDTDElement(xmlTextWriterPtr writer,
1920///                                   const xmlChar *name,
1921///                                   const xmlChar *content);
1922/// ```
1923///
1924/// # SAFETY
1925///
1926/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1927/// - `name`, `content` must be valid null-terminated strings or NULL.
1928#[no_mangle]
1929pub unsafe extern "C" fn xmlTextWriterWriteDTDElement(
1930    writer: *mut XmlTextWriter,
1931    name: *const xmlChar,
1932    content: *const xmlChar,
1933) -> c_int {
1934    if content.is_null() {
1935        return -1;
1936    }
1937    let mut sum: c_int = 0;
1938    let ret = unsafe { xmlTextWriterStartDTDElement(writer, name) };
1939    if ret == -1 {
1940        return ret;
1941    }
1942    sum += ret;
1943    let ret2 = unsafe { xmlTextWriterWriteString(writer, content) };
1944    if ret2 == -1 {
1945        return ret2;
1946    }
1947    sum += ret2;
1948    let ret3 = unsafe { xmlTextWriterEndDTDElement(writer) };
1949    if ret3 == -1 {
1950        return ret3;
1951    }
1952    sum + ret3
1953}
1954
1955/// Write a DTD attribute declaration.
1956///
1957/// # UPSTREAM-PARITY
1958///
1959/// ```c
1960/// int xmlTextWriterWriteDTDAttribute(xmlTextWriterPtr writer,
1961///                                     const xmlChar *name,
1962///                                     const xmlChar *content);
1963/// ```
1964///
1965/// # SAFETY
1966///
1967/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
1968/// - `name`, `content` must be valid null-terminated strings or NULL.
1969#[no_mangle]
1970pub unsafe extern "C" fn xmlTextWriterWriteDTDAttribute(
1971    writer: *mut XmlTextWriter,
1972    name: *const xmlChar,
1973    content: *const xmlChar,
1974) -> c_int {
1975    if content.is_null() {
1976        return -1;
1977    }
1978    // UPSTREAM-PARITY: upstream xmlTextWriterWriteDTDAttribute composes
1979    // StartDTDAttlist + WriteString + EndDTDAttlist (there is no separate
1980    // StartDTDAttribute API).
1981    let mut sum: c_int = 0;
1982    let ret = unsafe { xmlTextWriterStartDTDAttlist(writer, name) };
1983    if ret == -1 {
1984        return ret;
1985    }
1986    sum += ret;
1987    let ret2 = unsafe { xmlTextWriterWriteString(writer, content) };
1988    if ret2 == -1 {
1989        return ret2;
1990    }
1991    sum += ret2;
1992    let ret3 = unsafe { xmlTextWriterEndDTDAttlist(writer) };
1993    if ret3 == -1 {
1994        return ret3;
1995    }
1996    sum + ret3
1997}
1998
1999/// Write a DTD entity declaration.
2000///
2001/// # UPSTREAM-PARITY
2002///
2003/// ```c
2004/// int xmlTextWriterWriteDTDEntity(xmlTextWriterPtr writer,
2005///                                  const xmlChar *name,
2006///                                  const xmlChar *content);
2007/// ```
2008///
2009/// # SAFETY
2010///
2011/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2012/// - `name`, `content` must be valid null-terminated strings or NULL.
2013#[no_mangle]
2014pub unsafe extern "C" fn xmlTextWriterWriteDTDEntity(
2015    writer: *mut XmlTextWriter,
2016    pe: c_int,
2017    name: *const xmlChar,
2018    pubid: *const xmlChar,
2019    sysid: *const xmlChar,
2020    ndataid: *const xmlChar,
2021    content: *const xmlChar,
2022) -> c_int {
2023    if content.is_null() && pubid.is_null() && sysid.is_null() {
2024        return -1;
2025    }
2026    if pe != 0 && !ndataid.is_null() {
2027        return -1;
2028    }
2029    if pubid.is_null() && sysid.is_null() {
2030        return unsafe { xmlTextWriterWriteDTDInternalEntity(writer, pe, name, content) };
2031    }
2032    unsafe { xmlTextWriterWriteDTDExternalEntity(writer, pe, name, pubid, sysid, ndataid) }
2033}
2034
2035/// Write a DTD internal entity (StartDTDEntity + WriteString + EndDTDEntity).
2036///
2037/// # SAFETY
2038///
2039/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2040/// - `name`, `content` must be valid null-terminated strings or NULL.
2041#[no_mangle]
2042pub unsafe extern "C" fn xmlTextWriterWriteDTDInternalEntity(
2043    writer: *mut XmlTextWriter,
2044    pe: c_int,
2045    name: *const xmlChar,
2046    content: *const xmlChar,
2047) -> c_int {
2048    if name.is_null() || unsafe { *name } == 0 || content.is_null() {
2049        return -1;
2050    }
2051    let mut sum: c_int = 0;
2052    let ret = unsafe { xmlTextWriterStartDTDEntity(writer, pe, name) };
2053    if ret == -1 {
2054        return -1;
2055    }
2056    sum += ret;
2057    let ret2 = unsafe { xmlTextWriterWriteString(writer, content) };
2058    if ret2 == -1 {
2059        return -1;
2060    }
2061    sum += ret2;
2062    let ret3 = unsafe { xmlTextWriterEndDTDEntity(writer) };
2063    if ret3 == -1 {
2064        return -1;
2065    }
2066    sum + ret3
2067}
2068
2069/// Write a DTD external entity (StartDTDEntity + ExternalEntityContents + EndDTDEntity).
2070///
2071/// # SAFETY
2072///
2073/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2074/// - `name`, `pubid`, `sysid`, `ndataid` must be valid null-terminated
2075///   strings or NULL.
2076#[no_mangle]
2077pub unsafe extern "C" fn xmlTextWriterWriteDTDExternalEntity(
2078    writer: *mut XmlTextWriter,
2079    pe: c_int,
2080    name: *const xmlChar,
2081    pubid: *const xmlChar,
2082    sysid: *const xmlChar,
2083    ndataid: *const xmlChar,
2084) -> c_int {
2085    if pubid.is_null() && sysid.is_null() {
2086        return -1;
2087    }
2088    if pe != 0 && !ndataid.is_null() {
2089        return -1;
2090    }
2091    let mut sum: c_int = 0;
2092    let ret = unsafe { xmlTextWriterStartDTDEntity(writer, pe, name) };
2093    if ret == -1 {
2094        return -1;
2095    }
2096    sum += ret;
2097    let ret2 =
2098        unsafe { xmlTextWriterWriteDTDExternalEntityContents(writer, pubid, sysid, ndataid) };
2099    if ret2 < 0 {
2100        return -1;
2101    }
2102    sum += ret2;
2103    let ret3 = unsafe { xmlTextWriterEndDTDEntity(writer) };
2104    if ret3 == -1 {
2105        return -1;
2106    }
2107    sum + ret3
2108}
2109
2110/// Write the external-entity contents after `StartDTDEntity` (PUBLIC/SYSTEM
2111/// identifiers and NDATA).
2112///
2113/// # SAFETY
2114///
2115/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2116/// - `pubid`, `sysid`, `ndataid` must be valid null-terminated strings or NULL.
2117#[no_mangle]
2118pub unsafe extern "C" fn xmlTextWriterWriteDTDExternalEntityContents(
2119    writer: *mut XmlTextWriter,
2120    pubid: *const xmlChar,
2121    sysid: *const xmlChar,
2122    ndataid: *const xmlChar,
2123) -> c_int {
2124    if writer.is_null() {
2125        return -1;
2126    }
2127    let w = unsafe { &mut *writer };
2128    // UPSTREAM-PARITY: must be directly inside a StartDTDEntity declaration
2129    // (DTD_ENTY / DTD_PENT; content already written is rejected).
2130    if w.state != WriterState::DTDEntity {
2131        return -1;
2132    }
2133    if w.entity_pe && !ndataid.is_null() {
2134        // UPSTREAM-PARITY: notation not allowed with parameter entities.
2135        return -1;
2136    }
2137    let mut sum: c_int = 0;
2138    if !pubid.is_null() {
2139        if sysid.is_null() {
2140            return -1;
2141        }
2142        sum += w.write_slice(b" PUBLIC ");
2143        sum += w.write_byte(w.qchar);
2144        sum += w.write_str(pubid);
2145        sum += w.write_byte(w.qchar);
2146    }
2147    if !sysid.is_null() {
2148        if pubid.is_null() {
2149            sum += w.write_slice(b" SYSTEM");
2150        }
2151        sum += w.write_byte(b' ');
2152        sum += w.write_byte(w.qchar);
2153        sum += w.write_str(sysid);
2154        sum += w.write_byte(w.qchar);
2155    }
2156    if !ndataid.is_null() {
2157        sum += w.write_slice(b" NDATA ");
2158        sum += w.write_str(ndataid);
2159    }
2160    sum
2161}
2162
2163/// Write a DTD notation declaration.
2164///
2165/// # UPSTREAM-PARITY
2166///
2167/// ```c
2168/// int xmlTextWriterWriteDTDNotation(xmlTextWriterPtr writer,
2169///                                    const xmlChar *name,
2170///                                    const xmlChar *pubid,
2171///                                    const xmlChar *sysid);
2172/// ```
2173///
2174/// # SAFETY
2175///
2176/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2177/// - `name`, `pubid`, `sysid` must be valid null-terminated strings or NULL.
2178#[no_mangle]
2179pub unsafe extern "C" fn xmlTextWriterWriteDTDNotation(
2180    writer: *mut XmlTextWriter,
2181    name: *const xmlChar,
2182    pubid: *const xmlChar,
2183    sysid: *const xmlChar,
2184) -> c_int {
2185    if writer.is_null() || name.is_null() || unsafe { *name } == 0 {
2186        return -1;
2187    }
2188    let w = unsafe { &mut *writer };
2189    let mut sum: c_int = 0;
2190    if w.state == WriterState::DTD {
2191        // UPSTREAM-PARITY: first DTD child writes the internal-subset bracket.
2192        sum += w.write_slice(b" [");
2193        if w.indent != 0 {
2194            sum += w.write_byte(b'\n');
2195        }
2196        w.state = WriterState::DTDText;
2197    } else if w.state != WriterState::DTDText {
2198        return -1;
2199    }
2200    sum += w.write_indent();
2201    sum += w.write_slice(b"<!NOTATION ");
2202    sum += w.write_str(name);
2203    if !pubid.is_null() {
2204        sum += w.write_slice(b" PUBLIC ");
2205        sum += w.write_byte(w.qchar);
2206        sum += w.write_str(pubid);
2207        sum += w.write_byte(w.qchar);
2208    }
2209    if !sysid.is_null() {
2210        if pubid.is_null() {
2211            sum += w.write_slice(b" SYSTEM");
2212        }
2213        sum += w.write_byte(b' ');
2214        sum += w.write_byte(w.qchar);
2215        sum += w.write_str(sysid);
2216        sum += w.write_byte(w.qchar);
2217    }
2218    sum += w.write_byte(b'>');
2219    sum
2220}
2221
2222// ═══════════════════════════════════════════════════════════════════════════════
2223// Start/End DTD declaration
2224/// Start a DTD declaration.
2225///
2226/// # UPSTREAM-PARITY
2227///
2228/// ```c
2229/// int xmlTextWriterStartDTD(xmlTextWriterPtr writer,
2230///                            const xmlChar *name,
2231///                            const xmlChar *pubid,
2232///                            const xmlChar *sysid);
2233/// ```
2234///
2235/// # SAFETY
2236///
2237/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2238/// - `name`, `pubid`, `sysid` must be valid null-terminated strings or NULL.
2239#[no_mangle]
2240pub unsafe extern "C" fn xmlTextWriterStartDTD(
2241    writer: *mut XmlTextWriter,
2242    name: *const xmlChar,
2243    pubid: *const xmlChar,
2244    sysid: *const xmlChar,
2245) -> c_int {
2246    if writer.is_null() || name.is_null() || unsafe { *name } == 0 {
2247        return -1;
2248    }
2249    // SAFETY: writer is a valid XmlTextWriter.
2250    let w = unsafe { &mut *writer };
2251    if w.depth > 0 {
2252        // UPSTREAM-PARITY: DTD allowed only in the prolog (no open elements).
2253        return -1;
2254    }
2255
2256    let mut sum: c_int = 0;
2257    sum += w.write_slice(b"<!DOCTYPE ");
2258    sum += w.write_str(name);
2259
2260    if !pubid.is_null() {
2261        if sysid.is_null() {
2262            // UPSTREAM-PARITY: PUBLIC requires a system identifier.
2263            return -1;
2264        }
2265        if w.indent != 0 {
2266            sum += w.write_byte(b'\n');
2267        } else {
2268            sum += w.write_byte(b' ');
2269        }
2270        sum += w.write_slice(b"PUBLIC ");
2271        sum += w.write_byte(w.qchar);
2272        sum += w.write_str(pubid);
2273        sum += w.write_byte(w.qchar);
2274    }
2275    if !sysid.is_null() {
2276        if pubid.is_null() {
2277            if w.indent != 0 {
2278                sum += w.write_byte(b'\n');
2279            } else {
2280                sum += w.write_byte(b' ');
2281            }
2282            sum += w.write_slice(b"SYSTEM ");
2283        } else if w.indent != 0 {
2284            // UPSTREAM-PARITY: continuation line is indented 7 spaces.
2285            sum += w.write_slice(b"\n       ");
2286        } else {
2287            sum += w.write_byte(b' ');
2288        }
2289        sum += w.write_byte(w.qchar);
2290        sum += w.write_str(sysid);
2291        sum += w.write_byte(w.qchar);
2292    }
2293
2294    w.state = WriterState::DTD;
2295    sum
2296}
2297
2298/// End a DTD declaration.
2299///
2300/// # UPSTREAM-PARITY
2301///
2302/// ```c
2303/// int xmlTextWriterEndDTD(xmlTextWriterPtr writer);
2304/// ```
2305///
2306/// # SAFETY
2307///
2308/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2309#[no_mangle]
2310pub unsafe extern "C" fn xmlTextWriterEndDTD(writer: *mut XmlTextWriter) -> c_int {
2311    if writer.is_null() {
2312        return -1;
2313    }
2314    // SAFETY: writer is a valid XmlTextWriter.
2315    let w = unsafe { &mut *writer };
2316
2317    if w.state != WriterState::DTD && w.state != WriterState::DTDText {
2318        return -1;
2319    }
2320    let mut sum: c_int = 0;
2321    if w.state == WriterState::DTDText {
2322        sum += w.write_byte(b']');
2323    }
2324    sum += w.write_byte(b'>');
2325    if w.indent != 0 {
2326        sum += w.write_byte(b'\n');
2327    }
2328    w.state = WriterState::None;
2329    sum
2330}
2331
2332/// Internal: the ` [` (+ newline when indented) transition from the DTD state
2333/// used by all DTD child starts. Returns false when the state is not usable.
2334unsafe fn dtd_child_transition(w: &mut XmlTextWriter) -> bool {
2335    match w.state {
2336        WriterState::DTD => {
2337            w.write_slice(b" [");
2338            if w.indent != 0 {
2339                w.write_byte(b'\n');
2340            }
2341            w.state = WriterState::DTDText;
2342            true
2343        }
2344        WriterState::DTDText => true,
2345        _ => false,
2346    }
2347}
2348
2349/// Start a DTD element declaration.
2350///
2351/// # UPSTREAM-PARITY
2352///
2353/// ```c
2354/// int xmlTextWriterStartDTDElement(xmlTextWriterPtr writer, const xmlChar *name);
2355/// ```
2356///
2357/// # SAFETY
2358///
2359/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2360/// - `name` must be a valid null-terminated string or NULL.
2361#[no_mangle]
2362pub unsafe extern "C" fn xmlTextWriterStartDTDElement(
2363    writer: *mut XmlTextWriter,
2364    name: *const xmlChar,
2365) -> c_int {
2366    if writer.is_null() || name.is_null() || unsafe { *name } == 0 {
2367        return -1;
2368    }
2369    // SAFETY: writer is a valid XmlTextWriter.
2370    let w = unsafe { &mut *writer };
2371    if !unsafe { dtd_child_transition(w) } {
2372        return -1;
2373    }
2374    w.dtd_depth += 1;
2375    let mut sum: c_int = 0;
2376    sum += w.write_indent();
2377    sum += w.write_slice(b"<!ELEMENT ");
2378    sum += w.write_str(name);
2379    w.state = WriterState::DTDElem;
2380    sum
2381}
2382
2383/// End a DTD element declaration.
2384///
2385/// # UPSTREAM-PARITY
2386///
2387/// ```c
2388/// int xmlTextWriterEndDTDElement(xmlTextWriterPtr writer);
2389/// ```
2390///
2391/// # SAFETY
2392///
2393/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2394#[no_mangle]
2395pub unsafe extern "C" fn xmlTextWriterEndDTDElement(writer: *mut XmlTextWriter) -> c_int {
2396    if writer.is_null() {
2397        return -1;
2398    }
2399    // SAFETY: writer is a valid XmlTextWriter.
2400    let w = unsafe { &mut *writer };
2401    if w.state != WriterState::DTDElem && w.state != WriterState::DTDElemText {
2402        return -1;
2403    }
2404    let mut sum: c_int = w.write_byte(b'>');
2405    if w.indent != 0 {
2406        sum += w.write_byte(b'\n');
2407    }
2408    w.state = WriterState::DTDText;
2409    w.dtd_depth -= 1;
2410    sum
2411}
2412
2413/// Start a DTD attribute declaration.
2414///
2415/// # UPSTREAM-PARITY
2416///
2417/// ```c
2418/// int xmlTextWriterStartDTDAttribute(xmlTextWriterPtr writer, const xmlChar *name);
2419/// ```
2420///
2421/// # SAFETY
2422///
2423/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2424/// - `name` must be a valid null-terminated string or NULL.
2425#[no_mangle]
2426pub unsafe extern "C" fn xmlTextWriterStartDTDAttribute(
2427    writer: *mut XmlTextWriter,
2428    name: *const xmlChar,
2429) -> c_int {
2430    if writer.is_null() || name.is_null() || unsafe { *name } == 0 {
2431        return -1;
2432    }
2433    // SAFETY: writer is a valid XmlTextWriter.
2434    let w = unsafe { &mut *writer };
2435    if !unsafe { dtd_child_transition(w) } {
2436        return -1;
2437    }
2438    w.dtd_depth += 1;
2439    let mut sum: c_int = 0;
2440    sum += w.write_indent();
2441    sum += w.write_slice(b"<!ATTLIST ");
2442    sum += w.write_str(name);
2443    w.state = WriterState::DTDAttr;
2444    sum
2445}
2446
2447/// End a DTD attribute declaration.
2448///
2449/// # UPSTREAM-PARITY
2450///
2451/// ```c
2452/// int xmlTextWriterEndDTDAttribute(xmlTextWriterPtr writer);
2453/// ```
2454///
2455/// # SAFETY
2456///
2457/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2458#[no_mangle]
2459pub unsafe extern "C" fn xmlTextWriterEndDTDAttribute(writer: *mut XmlTextWriter) -> c_int {
2460    if writer.is_null() {
2461        return -1;
2462    }
2463    // SAFETY: writer is a valid XmlTextWriter.
2464    let w = unsafe { &mut *writer };
2465    if w.state != WriterState::DTDAttr && w.state != WriterState::DTDAttrText {
2466        return -1;
2467    }
2468    let mut sum: c_int = w.write_byte(b'>');
2469    if w.indent != 0 {
2470        sum += w.write_byte(b'\n');
2471    }
2472    w.state = WriterState::DTDText;
2473    w.dtd_depth -= 1;
2474    sum
2475}
2476
2477/// Start a DTD entity declaration.
2478///
2479/// # UPSTREAM-PARITY
2480///
2481/// ```c
2482/// int xmlTextWriterStartDTDEntity(xmlTextWriterPtr writer, const xmlChar *name);
2483/// ```
2484///
2485/// # SAFETY
2486///
2487/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2488/// - `name` must be a valid null-terminated string or NULL.
2489#[no_mangle]
2490pub unsafe extern "C" fn xmlTextWriterStartDTDEntity(
2491    writer: *mut XmlTextWriter,
2492    pe: c_int,
2493    name: *const xmlChar,
2494) -> c_int {
2495    if writer.is_null() || name.is_null() || unsafe { *name } == 0 {
2496        return -1;
2497    }
2498    // SAFETY: writer is a valid XmlTextWriter.
2499    let w = unsafe { &mut *writer };
2500    if !unsafe { dtd_child_transition(w) } {
2501        return -1;
2502    }
2503    w.dtd_depth += 1;
2504    let mut sum: c_int = 0;
2505    sum += w.write_indent();
2506    sum += w.write_slice(b"<!ENTITY ");
2507    if pe != 0 {
2508        sum += w.write_slice(b"% ");
2509    }
2510    sum += w.write_str(name);
2511    w.state = WriterState::DTDEntity;
2512    w.entity_pe = pe != 0;
2513    sum
2514}
2515
2516/// End a DTD entity declaration.
2517///
2518/// # UPSTREAM-PARITY
2519///
2520/// ```c
2521/// int xmlTextWriterEndDTDEntity(xmlTextWriterPtr writer);
2522/// ```
2523///
2524/// # SAFETY
2525///
2526/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2527#[no_mangle]
2528pub unsafe extern "C" fn xmlTextWriterEndDTDEntity(writer: *mut XmlTextWriter) -> c_int {
2529    if writer.is_null() {
2530        return -1;
2531    }
2532    // SAFETY: writer is a valid XmlTextWriter.
2533    let w = unsafe { &mut *writer };
2534    let mut sum: c_int = 0;
2535    if w.state == WriterState::DTDEntityText {
2536        sum += w.write_byte(w.qchar);
2537    } else if w.state != WriterState::DTDEntity {
2538        return -1;
2539    }
2540    sum += w.write_byte(b'>');
2541    if w.indent != 0 {
2542        sum += w.write_byte(b'\n');
2543    }
2544    w.state = WriterState::DTDText;
2545    w.entity_pe = false;
2546    w.dtd_depth -= 1;
2547    sum
2548}
2549
2550/// Start a DTD attribute-list declaration (`<!ATTLIST name`).
2551///
2552/// # SAFETY
2553///
2554/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2555/// - `name` must be a valid null-terminated string or NULL.
2556#[no_mangle]
2557pub unsafe extern "C" fn xmlTextWriterStartDTDAttlist(
2558    writer: *mut XmlTextWriter,
2559    name: *const xmlChar,
2560) -> c_int {
2561    if writer.is_null() || name.is_null() || unsafe { *name } == 0 {
2562        return -1;
2563    }
2564    // SAFETY: writer is a valid XmlTextWriter.
2565    let w = unsafe { &mut *writer };
2566    if !unsafe { dtd_child_transition(w) } {
2567        return -1;
2568    }
2569    w.dtd_depth += 1;
2570    let mut sum: c_int = 0;
2571    sum += w.write_indent();
2572    sum += w.write_slice(b"<!ATTLIST ");
2573    sum += w.write_str(name);
2574    w.state = WriterState::DTDAttr;
2575    sum
2576}
2577
2578/// End a DTD attribute-list declaration.
2579///
2580/// # SAFETY
2581///
2582/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2583#[no_mangle]
2584pub unsafe extern "C" fn xmlTextWriterEndDTDAttlist(writer: *mut XmlTextWriter) -> c_int {
2585    if writer.is_null() {
2586        return -1;
2587    }
2588    // SAFETY: writer is a valid XmlTextWriter.
2589    let w = unsafe { &mut *writer };
2590    if w.state != WriterState::DTDAttr && w.state != WriterState::DTDAttrText {
2591        return -1;
2592    }
2593    let mut sum: c_int = w.write_byte(b'>');
2594    if w.indent != 0 {
2595        sum += w.write_byte(b'\n');
2596    }
2597    w.state = WriterState::DTDText;
2598    w.dtd_depth -= 1;
2599    sum
2600}
2601
2602/// Write a DTD attribute-list declaration
2603/// (StartDTDAttlist + WriteString + EndDTDAttlist).
2604///
2605/// # SAFETY
2606///
2607/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2608/// - `name`, `content` must be valid null-terminated strings or NULL.
2609#[no_mangle]
2610pub unsafe extern "C" fn xmlTextWriterWriteDTDAttlist(
2611    writer: *mut XmlTextWriter,
2612    name: *const xmlChar,
2613    content: *const xmlChar,
2614) -> c_int {
2615    if content.is_null() {
2616        return -1;
2617    }
2618    let mut sum: c_int = 0;
2619    let ret = unsafe { xmlTextWriterStartDTDAttlist(writer, name) };
2620    if ret == -1 {
2621        return -1;
2622    }
2623    sum += ret;
2624    let ret2 = unsafe { xmlTextWriterWriteString(writer, content) };
2625    if ret2 == -1 {
2626        return -1;
2627    }
2628    sum += ret2;
2629    let ret3 = unsafe { xmlTextWriterEndDTDAttlist(writer) };
2630    if ret3 == -1 {
2631        return -1;
2632    }
2633    sum + ret3
2634}
2635
2636// ═══════════════════════════════════════════════════════════════════════════════
2637// Output management
2638// ═══════════════════════════════════════════════════════════════════════════════
2639
2640/// Flush the writer's output buffer.
2641///
2642/// # UPSTREAM-PARITY
2643///
2644/// ```c
2645/// int xmlTextWriterFlush(xmlTextWriterPtr writer);
2646/// ```
2647///
2648/// # SAFETY
2649///
2650/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2651#[no_mangle]
2652pub unsafe extern "C" fn xmlTextWriterFlush(writer: *mut XmlTextWriter) -> c_int {
2653    if writer.is_null() {
2654        return -1;
2655    }
2656    // SAFETY: writer is a valid XmlTextWriter.
2657    let w = unsafe { &mut *writer };
2658
2659    if w.output.is_null() {
2660        return -1;
2661    }
2662
2663    // Close any open start tag
2664    w.close_start_tag();
2665
2666    io::output_buffer_flush(w.output)
2667}
2668
2669/// Set indentation on/off.
2670///
2671/// # UPSTREAM-PARITY
2672///
2673/// ```c
2674/// int xmlTextWriterSetIndent(xmlTextWriterPtr writer, int indent);
2675/// ```
2676///
2677/// # SAFETY
2678///
2679/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2680#[no_mangle]
2681pub unsafe extern "C" fn xmlTextWriterSetIndent(
2682    writer: *mut XmlTextWriter,
2683    indent: c_int,
2684) -> c_int {
2685    if writer.is_null() {
2686        return -1;
2687    }
2688    // SAFETY: writer is a valid XmlTextWriter.
2689    unsafe { (*writer).indent = indent };
2690    0
2691}
2692
2693/// Set the indentation string.
2694///
2695/// # UPSTREAM-PARITY
2696///
2697/// ```c
2698/// int xmlTextWriterSetIndentString(xmlTextWriterPtr writer, const xmlChar *str);
2699/// ```
2700///
2701/// # SAFETY
2702///
2703/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2704/// - `str` must be a valid null-terminated xmlChar string or NULL.
2705#[no_mangle]
2706pub unsafe extern "C" fn xmlTextWriterSetIndentString(
2707    writer: *mut XmlTextWriter,
2708    str: *const xmlChar,
2709) -> c_int {
2710    if writer.is_null() || str.is_null() {
2711        return -1;
2712    }
2713    // SAFETY: writer is a valid XmlTextWriter.
2714    let w = unsafe { &mut *writer };
2715    w.indent_string = unsafe { c_str_to_vec(str) };
2716    0
2717}
2718
2719/// Set the quote character used for attribute and entity values.
2720///
2721/// # UPSTREAM-PARITY
2722///
2723/// ```c
2724/// int xmlTextWriterSetQuoteChar(xmlTextWriterPtr writer, xmlChar quotechar);
2725/// ```
2726///
2727/// Only `'` and `'\"'` are accepted; anything else returns -1.
2728#[no_mangle]
2729pub unsafe extern "C" fn xmlTextWriterSetQuoteChar(
2730    writer: *mut XmlTextWriter,
2731    quotechar: xmlChar,
2732) -> c_int {
2733    if writer.is_null() || (quotechar != b'\'' && quotechar != b'"') {
2734        return -1;
2735    }
2736    // SAFETY: writer is a valid XmlTextWriter.
2737    unsafe { (*writer).qchar = quotechar };
2738    0
2739}
2740
2741/// Close the writer's output buffer. The writer itself is NOT freed (upstream
2742/// contract: xmlFreeTextWriter does that). Returns XML_ERR_OK (0) on success,
2743/// XML_ERR_ARGUMENT (9) for a NULL writer or NULL output buffer.
2744///
2745/// # UPSTREAM-PARITY
2746///
2747/// ```c
2748/// int xmlTextWriterClose(xmlTextWriterPtr writer);
2749/// ```
2750///
2751/// # SAFETY
2752///
2753/// - `writer` must be a valid pointer to an `XmlTextWriter` or NULL.
2754#[no_mangle]
2755pub unsafe extern "C" fn xmlTextWriterClose(writer: *mut XmlTextWriter) -> c_int {
2756    if writer.is_null() {
2757        return crate::abi::types::XML_ERR_ARGUMENT as c_int;
2758    }
2759    let w = unsafe { &mut *writer };
2760    if w.output.is_null() {
2761        return crate::abi::types::XML_ERR_ARGUMENT as c_int;
2762    }
2763    let result = io::output_buffer_close(w.output);
2764    w.output = ptr::null_mut();
2765    if result >= 0 {
2766        crate::abi::types::XML_ERR_OK as c_int
2767    } else {
2768        -result
2769    }
2770}
2771
2772// ═══════════════════════════════════════════════════════════════════════════════
2773// Format / VFormat family
2774// ═══════════════════════════════════════════════════════════════════════════════
2775
2776/// The System V AMD64 `__va_list_tag` (24 bytes): gp_offset, fp_offset,
2777/// overflow_arg_area, reg_save_area. A C `va_list` parameter decays to a
2778/// pointer to this structure, which is exactly what the VFormat exports and
2779/// the Format shims exchange.
2780#[repr(C)]
2781#[derive(Clone, Copy)]
2782struct VaListTag {
2783    gp_offset: c_uint,
2784    fp_offset: c_uint,
2785    overflow_arg_area: *mut c_void,
2786    reg_save_area: *mut c_void,
2787}
2788
2789/// The platform `vsnprintf` (system libc — not an oracle dependency).
2790unsafe extern "C" {
2791    fn vsnprintf(s: *mut c_char, n: usize, format: *const c_char, ap: *mut VaListTag) -> c_int;
2792}
2793
2794/// Format a printf-style string with the given va_list into a fresh buffer,
2795/// mirroring upstream `xmlTextWriterVSprintf` (BUFSIZ start, doubling growth,
2796/// fresh va_copy per attempt).
2797///
2798/// Returns Err(()) on failure (unrepresentable output or absurd size).
2799///
2800/// # SAFETY
2801///
2802/// - `format` must be a valid printf format string.
2803/// - `args` must point to a valid va_list.
2804unsafe fn vformat_buf(format: *const c_char, args: *mut VaListTag) -> Result<Vec<u8>, ()> {
2805    let mut size: usize = 8192;
2806    loop {
2807        let mut buf = vec![0u8; size];
2808        // Fresh va_copy per attempt: vsnprintf consumes the va_list.
2809        // SAFETY: args points to a valid va_list; the bitwise copy is va_copy.
2810        let mut copy = unsafe { core::ptr::read(args) };
2811        let n = unsafe { vsnprintf(buf.as_mut_ptr() as *mut c_char, size, format, &mut copy) };
2812        if n >= 0 && (n as usize) < size {
2813            buf.truncate(n as usize);
2814            return Ok(buf);
2815        }
2816        if size >= (1 << 26) {
2817            return Err(());
2818        }
2819        size *= 2;
2820    }
2821}
2822
2823// The VFormat functions have heterogeneous fixed-arg lists, so each is written
2824// explicitly rather than through a macro (mirroring the upstream C).
2825
2826#[no_mangle]
2827pub unsafe extern "C" fn xmlTextWriterWriteVFormatRaw(
2828    writer: *mut XmlTextWriter,
2829    format: *const c_char,
2830    argptr: *mut VaListTag,
2831) -> c_int {
2832    if writer.is_null() {
2833        return -1;
2834    }
2835    let buf = match unsafe { vformat_buf(format, argptr) } {
2836        Ok(b) => b,
2837        Err(()) => return -1,
2838    };
2839    unsafe { xmlTextWriterWriteRaw(writer, buf.as_ptr() as *const xmlChar) }
2840}
2841
2842#[no_mangle]
2843pub unsafe extern "C" fn xmlTextWriterWriteVFormatString(
2844    writer: *mut XmlTextWriter,
2845    format: *const c_char,
2846    argptr: *mut VaListTag,
2847) -> c_int {
2848    if writer.is_null() || format.is_null() {
2849        return -1;
2850    }
2851    let buf = match unsafe { vformat_buf(format, argptr) } {
2852        Ok(b) => b,
2853        Err(()) => return -1,
2854    };
2855    unsafe { xmlTextWriterWriteString(writer, buf.as_ptr() as *const xmlChar) }
2856}
2857
2858#[no_mangle]
2859pub unsafe extern "C" fn xmlTextWriterWriteVFormatComment(
2860    writer: *mut XmlTextWriter,
2861    format: *const c_char,
2862    argptr: *mut VaListTag,
2863) -> c_int {
2864    if writer.is_null() {
2865        return -1;
2866    }
2867    let buf = match unsafe { vformat_buf(format, argptr) } {
2868        Ok(b) => b,
2869        Err(()) => return -1,
2870    };
2871    unsafe { xmlTextWriterWriteComment(writer, buf.as_ptr() as *const xmlChar) }
2872}
2873
2874#[no_mangle]
2875pub unsafe extern "C" fn xmlTextWriterWriteVFormatCDATA(
2876    writer: *mut XmlTextWriter,
2877    format: *const c_char,
2878    argptr: *mut VaListTag,
2879) -> c_int {
2880    if writer.is_null() {
2881        return -1;
2882    }
2883    let buf = match unsafe { vformat_buf(format, argptr) } {
2884        Ok(b) => b,
2885        Err(()) => return -1,
2886    };
2887    unsafe { xmlTextWriterWriteCDATA(writer, buf.as_ptr() as *const xmlChar) }
2888}
2889
2890#[no_mangle]
2891pub unsafe extern "C" fn xmlTextWriterWriteVFormatPI(
2892    writer: *mut XmlTextWriter,
2893    target: *const xmlChar,
2894    format: *const c_char,
2895    argptr: *mut VaListTag,
2896) -> c_int {
2897    if writer.is_null() {
2898        return -1;
2899    }
2900    let buf = match unsafe { vformat_buf(format, argptr) } {
2901        Ok(b) => b,
2902        Err(()) => return -1,
2903    };
2904    unsafe { xmlTextWriterWritePI(writer, target, buf.as_ptr() as *const xmlChar) }
2905}
2906
2907#[no_mangle]
2908pub unsafe extern "C" fn xmlTextWriterWriteVFormatElement(
2909    writer: *mut XmlTextWriter,
2910    name: *const xmlChar,
2911    format: *const c_char,
2912    argptr: *mut VaListTag,
2913) -> c_int {
2914    if writer.is_null() {
2915        return -1;
2916    }
2917    let buf = match unsafe { vformat_buf(format, argptr) } {
2918        Ok(b) => b,
2919        Err(()) => return -1,
2920    };
2921    unsafe { xmlTextWriterWriteElement(writer, name, buf.as_ptr() as *const xmlChar) }
2922}
2923
2924#[no_mangle]
2925pub unsafe extern "C" fn xmlTextWriterWriteVFormatElementNS(
2926    writer: *mut XmlTextWriter,
2927    prefix: *const xmlChar,
2928    name: *const xmlChar,
2929    namespaceURI: *const xmlChar,
2930    format: *const c_char,
2931    argptr: *mut VaListTag,
2932) -> c_int {
2933    if writer.is_null() {
2934        return -1;
2935    }
2936    let buf = match unsafe { vformat_buf(format, argptr) } {
2937        Ok(b) => b,
2938        Err(()) => return -1,
2939    };
2940    unsafe {
2941        xmlTextWriterWriteElementNS(
2942            writer,
2943            prefix,
2944            name,
2945            namespaceURI,
2946            buf.as_ptr() as *const xmlChar,
2947        )
2948    }
2949}
2950
2951#[no_mangle]
2952pub unsafe extern "C" fn xmlTextWriterWriteVFormatAttribute(
2953    writer: *mut XmlTextWriter,
2954    name: *const xmlChar,
2955    format: *const c_char,
2956    argptr: *mut VaListTag,
2957) -> c_int {
2958    if writer.is_null() {
2959        return -1;
2960    }
2961    let buf = match unsafe { vformat_buf(format, argptr) } {
2962        Ok(b) => b,
2963        Err(()) => return -1,
2964    };
2965    unsafe { xmlTextWriterWriteAttribute(writer, name, buf.as_ptr() as *const xmlChar) }
2966}
2967
2968#[no_mangle]
2969pub unsafe extern "C" fn xmlTextWriterWriteVFormatAttributeNS(
2970    writer: *mut XmlTextWriter,
2971    prefix: *const xmlChar,
2972    name: *const xmlChar,
2973    namespaceURI: *const xmlChar,
2974    format: *const c_char,
2975    argptr: *mut VaListTag,
2976) -> c_int {
2977    if writer.is_null() {
2978        return -1;
2979    }
2980    let buf = match unsafe { vformat_buf(format, argptr) } {
2981        Ok(b) => b,
2982        Err(()) => return -1,
2983    };
2984    unsafe {
2985        xmlTextWriterWriteAttributeNS(
2986            writer,
2987            prefix,
2988            name,
2989            namespaceURI,
2990            buf.as_ptr() as *const xmlChar,
2991        )
2992    }
2993}
2994
2995#[no_mangle]
2996pub unsafe extern "C" fn xmlTextWriterWriteVFormatDTD(
2997    writer: *mut XmlTextWriter,
2998    name: *const xmlChar,
2999    pubid: *const xmlChar,
3000    sysid: *const xmlChar,
3001    format: *const c_char,
3002    argptr: *mut VaListTag,
3003) -> c_int {
3004    if writer.is_null() {
3005        return -1;
3006    }
3007    let buf = match unsafe { vformat_buf(format, argptr) } {
3008        Ok(b) => b,
3009        Err(()) => return -1,
3010    };
3011    unsafe { xmlTextWriterWriteDTD(writer, name, pubid, sysid, buf.as_ptr() as *const xmlChar) }
3012}
3013
3014#[no_mangle]
3015pub unsafe extern "C" fn xmlTextWriterWriteVFormatDTDElement(
3016    writer: *mut XmlTextWriter,
3017    name: *const xmlChar,
3018    format: *const c_char,
3019    argptr: *mut VaListTag,
3020) -> c_int {
3021    if writer.is_null() {
3022        return -1;
3023    }
3024    let buf = match unsafe { vformat_buf(format, argptr) } {
3025        Ok(b) => b,
3026        Err(()) => return -1,
3027    };
3028    unsafe { xmlTextWriterWriteDTDElement(writer, name, buf.as_ptr() as *const xmlChar) }
3029}
3030
3031#[no_mangle]
3032pub unsafe extern "C" fn xmlTextWriterWriteVFormatDTDAttlist(
3033    writer: *mut XmlTextWriter,
3034    name: *const xmlChar,
3035    format: *const c_char,
3036    argptr: *mut VaListTag,
3037) -> c_int {
3038    if writer.is_null() {
3039        return -1;
3040    }
3041    let buf = match unsafe { vformat_buf(format, argptr) } {
3042        Ok(b) => b,
3043        Err(()) => return -1,
3044    };
3045    unsafe { xmlTextWriterWriteDTDAttlist(writer, name, buf.as_ptr() as *const xmlChar) }
3046}
3047
3048#[no_mangle]
3049pub unsafe extern "C" fn xmlTextWriterWriteVFormatDTDInternalEntity(
3050    writer: *mut XmlTextWriter,
3051    pe: c_int,
3052    name: *const xmlChar,
3053    format: *const c_char,
3054    argptr: *mut VaListTag,
3055) -> c_int {
3056    if writer.is_null() {
3057        return -1;
3058    }
3059    let buf = match unsafe { vformat_buf(format, argptr) } {
3060        Ok(b) => b,
3061        Err(()) => return -1,
3062    };
3063    unsafe { xmlTextWriterWriteDTDInternalEntity(writer, pe, name, buf.as_ptr() as *const xmlChar) }
3064}
3065
3066/// Assembly shims for the variadic `xmlTextWriterWriteFormat*` exports.
3067///
3068/// Stable Rust cannot define variadic `extern "C"` functions (c_variadic is
3069/// unstable), so each Format export is a #[no_mangle] function whose body is a
3070/// single `noreturn` inline-asm block: it captures the SysV x86-64 register
3071/// save area exactly like `va_start`, builds a `va_list`, forwards it to the
3072/// VFormat implementation, restores the stack and returns directly.
3073/// `#![no_mangle]` puts these exports into rustc's cdylib export list (a
3074/// version script localizes every other global).
3075///
3076/// Layout: reg_save_area = rsp+0 (6 GP + 8 SSE slots, 176 bytes); the va_list
3077/// struct lives at rsp+176 (gp_offset, fp_offset, overflow_arg_area,
3078/// reg_save_area); overflow varargs are above the return address.
3079///
3080/// NOTE on the frame: LLVM emits an 8-byte alignment `push` before the block
3081/// (verified for rustc 1.98.0 at opt-level 0); the block therefore uses a
3082/// 240-byte frame (≡ 0 mod 16, keeping the `call` 16-aligned), points the
3083/// overflow area at rsp+256 (= entry_rsp + 8) and pops the alignment push
3084/// before `ret`. The overflow-argument pointer is only dereferenced when more
3085/// than 6 general-purpose varargs are passed; WRITER-001 exercises that path.
3086/// This is native code with no dependency on any XML library.
3087#[cfg(target_arch = "x86_64")]
3088mod format_shims {
3089    use super::*;
3090
3091    /// `gp` is the gp_offset for the fixed-argument count (8 bytes each);
3092    /// `aptr` is the register receiving the va_list pointer for the VFormat
3093    /// call (rdx=2 fixed, rcx=3, r8=4, r9=5). The parameter list is types
3094    /// only — the values are read directly from registers inside the asm.
3095    macro_rules! vfmt_shim {
3096        ($name:ident, $vname:ident, $gp:literal, $aptr:tt, ($($pty:ty),*)) => {
3097            // No declared parameters: the C caller's fixed arguments arrive in
3098            // the ABI registers and are read directly inside the asm; with no
3099            // parameters and a noreturn body LLVM emits only an 8-byte
3100            // alignment push, which the block pops before `ret`.
3101            #[no_mangle]
3102            pub unsafe extern "C" fn $name() -> c_int {
3103                unsafe {
3104                    core::arch::asm!(
3105                        "sub rsp, 240",
3106                        "mov [rsp+0], rdi",
3107                        "mov [rsp+8], rsi",
3108                        "mov [rsp+16], rdx",
3109                        "mov [rsp+24], rcx",
3110                        "mov [rsp+32], r8",
3111                        "mov [rsp+40], r9",
3112                        "movaps [rsp+48], xmm0",
3113                        "movaps [rsp+64], xmm1",
3114                        "movaps [rsp+80], xmm2",
3115                        "movaps [rsp+96], xmm3",
3116                        "movaps [rsp+112], xmm4",
3117                        "movaps [rsp+128], xmm5",
3118                        "movaps [rsp+144], xmm6",
3119                        "movaps [rsp+160], xmm7",
3120                        concat!("mov dword ptr [rsp+176], ", $gp),
3121                        "mov dword ptr [rsp+180], 48",
3122                        "lea rax, [rsp+256]",
3123                        "mov [rsp+184], rax",
3124                        "lea rax, [rsp]",
3125                        "mov [rsp+192], rax",
3126                        concat!("lea ", stringify!($aptr), ", [rsp+176]"),
3127                        concat!("call ", stringify!($vname)),
3128                        "add rsp, 240",
3129                        "add rsp, 8",
3130                        "ret",
3131                        options(noreturn),
3132                    );
3133                }
3134            }
3135        };
3136    }
3137
3138    vfmt_shim!(
3139        xmlTextWriterWriteFormatRaw,
3140        xmlTextWriterWriteVFormatRaw,
3141        16,
3142        rdx,
3143        (*mut XmlTextWriter, *const c_char)
3144    );
3145    vfmt_shim!(
3146        xmlTextWriterWriteFormatString,
3147        xmlTextWriterWriteVFormatString,
3148        16,
3149        rdx,
3150        (*mut XmlTextWriter, *const c_char)
3151    );
3152    vfmt_shim!(
3153        xmlTextWriterWriteFormatComment,
3154        xmlTextWriterWriteVFormatComment,
3155        16,
3156        rdx,
3157        (*mut XmlTextWriter, *const c_char)
3158    );
3159    vfmt_shim!(
3160        xmlTextWriterWriteFormatCDATA,
3161        xmlTextWriterWriteVFormatCDATA,
3162        16,
3163        rdx,
3164        (*mut XmlTextWriter, *const c_char)
3165    );
3166    vfmt_shim!(
3167        xmlTextWriterWriteFormatPI,
3168        xmlTextWriterWriteVFormatPI,
3169        24,
3170        rcx,
3171        (*mut XmlTextWriter, *const xmlChar, *const c_char)
3172    );
3173    vfmt_shim!(
3174        xmlTextWriterWriteFormatElement,
3175        xmlTextWriterWriteVFormatElement,
3176        24,
3177        rcx,
3178        (*mut XmlTextWriter, *const xmlChar, *const c_char)
3179    );
3180    vfmt_shim!(
3181        xmlTextWriterWriteFormatAttribute,
3182        xmlTextWriterWriteVFormatAttribute,
3183        24,
3184        rcx,
3185        (*mut XmlTextWriter, *const xmlChar, *const c_char)
3186    );
3187    vfmt_shim!(
3188        xmlTextWriterWriteFormatDTDElement,
3189        xmlTextWriterWriteVFormatDTDElement,
3190        24,
3191        rcx,
3192        (*mut XmlTextWriter, *const xmlChar, *const c_char)
3193    );
3194    vfmt_shim!(
3195        xmlTextWriterWriteFormatDTDAttlist,
3196        xmlTextWriterWriteVFormatDTDAttlist,
3197        24,
3198        rcx,
3199        (*mut XmlTextWriter, *const xmlChar, *const c_char)
3200    );
3201    vfmt_shim!(
3202        xmlTextWriterWriteFormatDTDInternalEntity,
3203        xmlTextWriterWriteVFormatDTDInternalEntity,
3204        32,
3205        r8,
3206        (*mut XmlTextWriter, c_int, *const xmlChar, *const c_char)
3207    );
3208    vfmt_shim!(
3209        xmlTextWriterWriteFormatDTD,
3210        xmlTextWriterWriteVFormatDTD,
3211        40,
3212        r9,
3213        (
3214            *mut XmlTextWriter,
3215            *const xmlChar,
3216            *const xmlChar,
3217            *const xmlChar,
3218            *const c_char
3219        )
3220    );
3221    vfmt_shim!(
3222        xmlTextWriterWriteFormatElementNS,
3223        xmlTextWriterWriteVFormatElementNS,
3224        40,
3225        r9,
3226        (
3227            *mut XmlTextWriter,
3228            *const xmlChar,
3229            *const xmlChar,
3230            *const xmlChar,
3231            *const c_char
3232        )
3233    );
3234    vfmt_shim!(
3235        xmlTextWriterWriteFormatAttributeNS,
3236        xmlTextWriterWriteVFormatAttributeNS,
3237        40,
3238        r9,
3239        (
3240            *mut XmlTextWriter,
3241            *const xmlChar,
3242            *const xmlChar,
3243            *const xmlChar,
3244            *const c_char
3245        )
3246    );
3247}
3248
3249// Non-x86-64 fallback: honest stubs (the variadic ABI cannot be forwarded on
3250// stable Rust); the platform surface is documented as not yet executable there.
3251#[cfg(not(target_arch = "x86_64"))]
3252mod format_fallback {
3253    use super::*;
3254    macro_rules! fmt_stub {
3255        ($($name:ident),*) => {$(
3256            #[no_mangle]
3257            pub unsafe extern "C" fn $name(_writer: *mut XmlTextWriter, _format: *const c_char) -> c_int {
3258                -1
3259            }
3260        )*};
3261    }
3262    fmt_stub!(
3263        xmlTextWriterWriteFormatRaw,
3264        xmlTextWriterWriteFormatString,
3265        xmlTextWriterWriteFormatComment,
3266        xmlTextWriterWriteFormatCDATA,
3267        xmlTextWriterWriteFormatPI,
3268        xmlTextWriterWriteFormatElement,
3269        xmlTextWriterWriteFormatElementNS,
3270        xmlTextWriterWriteFormatAttribute,
3271        xmlTextWriterWriteFormatAttributeNS,
3272        xmlTextWriterWriteFormatDTD,
3273        xmlTextWriterWriteFormatDTDElement,
3274        xmlTextWriterWriteFormatDTDAttlist,
3275        xmlTextWriterWriteFormatDTDInternalEntity
3276    );
3277}
3278
3279// ═══════════════════════════════════════════════════════════════════════════════
3280// Internal helpers
3281// ═══════════════════════════════════════════════════════════════════════════════
3282
3283/// Convert a null-terminated C string to a Vec<u8> (including the null terminator).
3284///
3285/// # SAFETY
3286///
3287/// - `s` must be a valid pointer to a null-terminated string.
3288unsafe fn c_str_to_vec(s: *const u8) -> Vec<u8> {
3289    if s.is_null() {
3290        return Vec::new();
3291    }
3292    let len = tree::xml_strlen(s);
3293    let mut v = Vec::with_capacity(len as usize + 1);
3294    unsafe {
3295        for i in 0..len as isize {
3296            v.push(*s.offset(i));
3297        }
3298        v.push(0);
3299    }
3300    v
3301}
3302
3303/// Base64 encode a byte slice.
3304fn base64_encode(data: &[u8]) -> Vec<u8> {
3305    const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3306    let mut result = Vec::with_capacity((data.len() + 2) / 3 * 4);
3307    for chunk in data.chunks(3) {
3308        let b0 = chunk[0];
3309        let b1 = chunk.get(1).copied().unwrap_or(0);
3310        let b2 = chunk.get(2).copied().unwrap_or(0);
3311
3312        result.push(CHARS[((b0 >> 2) & 0x3F) as usize]);
3313        result.push(CHARS[(((b0 << 4) | (b1 >> 4)) & 0x3F) as usize]);
3314        result.push(if chunk.len() > 1 {
3315            CHARS[(((b1 << 2) | (b2 >> 6)) & 0x3F) as usize]
3316        } else {
3317            b'='
3318        });
3319        result.push(if chunk.len() > 2 {
3320            CHARS[(b2 & 0x3F) as usize]
3321        } else {
3322            b'='
3323        });
3324    }
3325    result
3326}
3327
3328/// Hex encode a byte slice (lowercase).
3329fn hex_encode(data: &[u8]) -> Vec<u8> {
3330    const CHARS: &[u8] = b"0123456789abcdef";
3331    let mut result = Vec::with_capacity(data.len() * 2);
3332    for &b in data {
3333        result.push(CHARS[((b >> 4) & 0x0F) as usize]);
3334        result.push(CHARS[(b & 0x0F) as usize]);
3335    }
3336    result
3337}
3338
3339// ═══════════════════════════════════════════════════════════════════════════════
3340// Tests
3341// ═══════════════════════════════════════════════════════════════════════════════
3342
3343#[cfg(test)]
3344mod tests {
3345    use super::*;
3346    use core::ptr;
3347
3348    /// Helper: create a memory buffer writer for testing.
3349    unsafe fn create_test_writer() -> (*mut XmlTextWriter, *mut _xmlBuffer) {
3350        let buf = io::buf_create(256);
3351        assert!(!buf.is_null(), "buf_create failed");
3352        let out = io::output_buffer_create_buffer(buf, ptr::null_mut());
3353        assert!(!out.is_null(), "output_buffer_create_buffer failed");
3354        let writer = xmlNewTextWriter(out);
3355        assert!(!writer.is_null(), "xmlNewTextWriter failed");
3356        (writer, buf)
3357    }
3358
3359    /// Helper: get the buffer content as a string.
3360    unsafe fn buf_to_string(buf: *mut _xmlBuffer) -> String {
3361        let content = io::buf_content(buf);
3362        let len = io::buf_length(buf);
3363        if content.is_null() || len <= 0 {
3364            return String::new();
3365        }
3366        let slice = unsafe { core::slice::from_raw_parts(content, len as usize) };
3367        String::from_utf8_lossy(slice).to_string()
3368    }
3369
3370    /// Helper: flush writer and return buffer content.
3371    unsafe fn flush_and_get(writer: *mut XmlTextWriter, buf: *mut _xmlBuffer) -> String {
3372        xmlTextWriterFlush(writer);
3373        buf_to_string(buf)
3374    }
3375
3376    // ═══════════════════════════════════════════════════════════════════════════
3377    // Test: Write a simple document
3378    // ═══════════════════════════════════════════════════════════════════════════
3379
3380    #[test]
3381    fn test_write_simple_document() {
3382        unsafe {
3383            let (writer, buf) = create_test_writer();
3384
3385            let r = xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
3386            assert_eq!(r, 0, "StartDocument failed");
3387
3388            let r = xmlTextWriterStartElement(writer, b"root\0" as *const u8);
3389            assert_eq!(r, 0, "StartElement(root) failed");
3390
3391            let r = xmlTextWriterWriteString(writer, b"Hello, World!\0" as *const u8);
3392            assert_eq!(r, 0, "WriteString failed");
3393
3394            let r = xmlTextWriterEndElement(writer);
3395            assert_eq!(r, 0, "EndElement failed");
3396
3397            let r = xmlTextWriterEndDocument(writer);
3398            assert!(r > 0, "EndDocument failed (rc={})", r);
3399
3400            let result = flush_and_get(writer, buf);
3401            assert!(
3402                result.contains("<?xml version=\"1.0\"?>"),
3403                "Missing XML declaration. Got: {}",
3404                result
3405            );
3406            assert!(
3407                result.contains("<root>"),
3408                "Missing <root> start tag. Got: {}",
3409                result
3410            );
3411            assert!(
3412                result.contains("Hello, World!"),
3413                "Missing content. Got: {}",
3414                result
3415            );
3416            assert!(
3417                result.contains("</root>"),
3418                "Missing </root> end tag. Got: {}",
3419                result
3420            );
3421
3422            xmlFreeTextWriter(writer);
3423            io::buf_free(buf);
3424        }
3425    }
3426
3427    // ═══════════════════════════════════════════════════════════════════════════
3428    // Test: Write elements with attributes
3429    // ═══════════════════════════════════════════════════════════════════════════
3430
3431    #[test]
3432    fn test_write_element_with_attributes() {
3433        unsafe {
3434            let (writer, buf) = create_test_writer();
3435
3436            xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
3437            xmlTextWriterStartElement(writer, b"root\0" as *const u8);
3438            xmlTextWriterWriteAttribute(writer, b"id\0" as *const u8, b"123\0" as *const u8);
3439            xmlTextWriterWriteAttribute(
3440                writer,
3441                b"name\0" as *const u8,
3442                b"test & demo\0" as *const u8,
3443            );
3444            xmlTextWriterEndElement(writer);
3445            xmlTextWriterEndDocument(writer);
3446
3447            let result = flush_and_get(writer, buf);
3448            assert!(
3449                result.contains("id=\"123\""),
3450                "Missing id attribute. Got: {}",
3451                result
3452            );
3453            assert!(
3454                result.contains("name=\"test &amp; demo\""),
3455                "Missing or improperly escaped name attribute. Got: {}",
3456                result
3457            );
3458            assert!(
3459                result.contains("<root"),
3460                "Missing root element. Got: {}",
3461                result
3462            );
3463
3464            xmlFreeTextWriter(writer);
3465            io::buf_free(buf);
3466        }
3467    }
3468
3469    // ═══════════════════════════════════════════════════════════════════════════
3470    // Test: Write with namespaces
3471    // ═══════════════════════════════════════════════════════════════════════════
3472
3473    #[test]
3474    fn test_write_with_namespaces() {
3475        unsafe {
3476            let (writer, buf) = create_test_writer();
3477
3478            xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
3479            xmlTextWriterStartElementNS(
3480                writer,
3481                b"ns\0" as *const u8,
3482                b"root\0" as *const u8,
3483                b"http://example.com/ns\0" as *const u8,
3484            );
3485            xmlTextWriterWriteAttributeNS(
3486                writer,
3487                ptr::null(),
3488                b"attr\0" as *const u8,
3489                ptr::null(),
3490                b"value\0" as *const u8,
3491            );
3492            xmlTextWriterEndElement(writer);
3493            xmlTextWriterEndDocument(writer);
3494
3495            let result = flush_and_get(writer, buf);
3496            assert!(
3497                result.contains("ns:root"),
3498                "Missing namespace prefix. Got: {}",
3499                result
3500            );
3501            assert!(
3502                result.contains("xmlns:ns=\"http://example.com/ns\""),
3503                "Missing xmlns declaration. Got: {}",
3504                result
3505            );
3506
3507            xmlFreeTextWriter(writer);
3508            io::buf_free(buf);
3509        }
3510    }
3511
3512    // ═══════════════════════════════════════════════════════════════════════════
3513    // Test: Write text, CDATA, comments, PIs
3514    // ═══════════════════════════════════════════════════════════════════════════
3515
3516    #[test]
3517    fn test_write_text_cdata_comment_pi() {
3518        unsafe {
3519            let (writer, buf) = create_test_writer();
3520
3521            xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
3522
3523            xmlTextWriterStartElement(writer, b"doc\0" as *const u8);
3524            xmlTextWriterWriteString(writer, b"text content\0" as *const u8);
3525            xmlTextWriterEndElement(writer);
3526
3527            xmlTextWriterWriteComment(writer, b"a comment\0" as *const u8);
3528
3529            xmlTextWriterWritePI(writer, b"target\0" as *const u8, b"data\0" as *const u8);
3530
3531            xmlTextWriterStartElement(writer, b"cdata\0" as *const u8);
3532            xmlTextWriterWriteCDATA(writer, b"<greeting>Hello</greeting>\0" as *const u8);
3533            xmlTextWriterEndElement(writer);
3534
3535            xmlTextWriterEndDocument(writer);
3536
3537            let result = flush_and_get(writer, buf);
3538            assert!(
3539                result.contains("text content"),
3540                "Missing text content. Got: {}",
3541                result
3542            );
3543            assert!(
3544                result.contains("<!--a comment-->"),
3545                "Missing comment. Got: {}",
3546                result
3547            );
3548            assert!(
3549                result.contains("<?target data?>"),
3550                "Missing PI. Got: {}",
3551                result
3552            );
3553            assert!(
3554                result.contains("<![CDATA["),
3555                "Missing CDATA start. Got: {}",
3556                result
3557            );
3558            assert!(
3559                result.contains("<greeting>Hello</greeting>"),
3560                "Missing CDATA content. Got: {}",
3561                result
3562            );
3563
3564            xmlFreeTextWriter(writer);
3565            io::buf_free(buf);
3566        }
3567    }
3568
3569    // ═══════════════════════════════════════════════════════════════════════════
3570    // Test: DTD writing
3571    // ═══════════════════════════════════════════════════════════════════════════
3572
3573    #[test]
3574    fn test_write_dtd() {
3575        unsafe {
3576            let (writer, buf) = create_test_writer();
3577
3578            xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
3579
3580            xmlTextWriterWriteDTD(
3581                writer,
3582                b"html\0" as *const u8,
3583                ptr::null(),
3584                b"http://www.w3.org/TR/html4/strict.dtd\0" as *const u8,
3585                ptr::null(),
3586            );
3587
3588            xmlTextWriterStartElement(writer, b"html\0" as *const u8);
3589            xmlTextWriterEndElement(writer);
3590            xmlTextWriterEndDocument(writer);
3591
3592            let result = flush_and_get(writer, buf);
3593            assert!(
3594                result.contains("<!DOCTYPE html SYSTEM"),
3595                "Missing DTD. Got: {}",
3596                result
3597            );
3598
3599            xmlFreeTextWriter(writer);
3600            io::buf_free(buf);
3601        }
3602    }
3603
3604    // ═══════════════════════════════════════════════════════════════════════════
3605    // Test: DTD with internal subset declarations
3606    // ═══════════════════════════════════════════════════════════════════════════
3607
3608    #[test]
3609    fn test_write_dtd_with_subset() {
3610        unsafe {
3611            let (writer, buf) = create_test_writer();
3612
3613            xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
3614
3615            xmlTextWriterStartDTD(writer, b"root\0" as *const u8, ptr::null(), ptr::null());
3616            xmlTextWriterWriteDTDElement(
3617                writer,
3618                b"child\0" as *const u8,
3619                b"(#PCDATA)\0" as *const u8,
3620            );
3621            xmlTextWriterWriteDTDAttribute(
3622                writer,
3623                b"child\0" as *const u8,
3624                b"id CDATA #IMPLIED\0" as *const u8,
3625            );
3626            xmlTextWriterWriteDTDEntity(
3627                writer,
3628                0, // pe
3629                b"copy\0" as *const u8,
3630                ptr::null(), // pubid
3631                ptr::null(), // sysid
3632                ptr::null(), // ndataid
3633                b"Copyright Me\0" as *const u8,
3634            );
3635            xmlTextWriterWriteDTDNotation(
3636                writer,
3637                b"note\0" as *const u8,
3638                b"PublicID\0" as *const u8,
3639                ptr::null(),
3640            );
3641            xmlTextWriterEndDTD(writer);
3642
3643            xmlTextWriterStartElement(writer, b"root\0" as *const u8);
3644            xmlTextWriterEndElement(writer);
3645            xmlTextWriterEndDocument(writer);
3646
3647            let result = flush_and_get(writer, buf);
3648            assert!(
3649                result.contains("<!DOCTYPE root"),
3650                "Missing DTD start. Got: {}",
3651                result
3652            );
3653            assert!(
3654                result.contains("<!ELEMENT child (#PCDATA)>"),
3655                "Missing DTD element. Got: {}",
3656                result
3657            );
3658            assert!(
3659                result.contains("<!ATTLIST child id CDATA #IMPLIED>"),
3660                "Missing DTD attribute. Got: {}",
3661                result
3662            );
3663            assert!(
3664                result.contains("<!ENTITY copy \"Copyright Me\">"),
3665                "Missing DTD entity. Got: {}",
3666                result
3667            );
3668            assert!(
3669                result.contains("<!NOTATION note PUBLIC \"PublicID\">"),
3670                "Missing DTD notation. Got: {}",
3671                result
3672            );
3673
3674            xmlFreeTextWriter(writer);
3675            io::buf_free(buf);
3676        }
3677    }
3678
3679    // ═══════════════════════════════════════════════════════════════════════════
3680    // Test: Indentation control
3681    // ═══════════════════════════════════════════════════════════════════════════
3682
3683    #[test]
3684    fn test_indentation_control() {
3685        unsafe {
3686            let (writer, buf) = create_test_writer();
3687
3688            // Enable indentation with tabs
3689            xmlTextWriterSetIndent(writer, 1);
3690            xmlTextWriterSetIndentString(writer, b"\t\0" as *const u8);
3691
3692            xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
3693            xmlTextWriterStartElement(writer, b"root\0" as *const u8);
3694            xmlTextWriterStartElement(writer, b"child\0" as *const u8);
3695            xmlTextWriterWriteString(writer, b"content\0" as *const u8);
3696            xmlTextWriterEndElement(writer);
3697            xmlTextWriterEndElement(writer);
3698            xmlTextWriterEndDocument(writer);
3699
3700            let result = flush_and_get(writer, buf);
3701
3702            // Check that we have indentation
3703            assert!(
3704                result.contains('\t'),
3705                "Expected tab indentation. Got: {}",
3706                result
3707            );
3708            // Check the XML declaration and elements are present
3709            assert!(result.contains("<root>"), "Missing root. Got: {}", result);
3710            assert!(result.contains("<child>"), "Missing child. Got: {}", result);
3711
3712            xmlFreeTextWriter(writer);
3713            io::buf_free(buf);
3714        }
3715    }
3716
3717    // ═══════════════════════════════════════════════════════════════════════════
3718    // Test: Memory output
3719    // ═══════════════════════════════════════════════════════════════════════════
3720
3721    #[test]
3722    fn test_memory_output() {
3723        unsafe {
3724            let buf = io::buf_create(256);
3725            assert!(!buf.is_null(), "buf_create failed");
3726
3727            let writer = xmlNewTextWriterMemory(buf, 0);
3728            assert!(!writer.is_null(), "xmlNewTextWriterMemory failed");
3729
3730            xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
3731            xmlTextWriterStartElement(writer, b"root\0" as *const u8);
3732            xmlTextWriterWriteString(writer, b"memory test\0" as *const u8);
3733            xmlTextWriterEndElement(writer);
3734            xmlTextWriterEndDocument(writer);
3735
3736            xmlTextWriterFlush(writer);
3737            let result = buf_to_string(buf);
3738            assert!(
3739                result.contains("memory test"),
3740                "Missing content in memory output. Got: {}",
3741                result
3742            );
3743
3744            xmlFreeTextWriter(writer);
3745            io::buf_free(buf);
3746        }
3747    }
3748
3749    // ═══════════════════════════════════════════════════════════════════════════
3750    // Test: Flush and close
3751    // ═══════════════════════════════════════════════════════════════════════════
3752
3753    #[test]
3754    fn test_flush_and_close() {
3755        unsafe {
3756            let (writer, buf) = create_test_writer();
3757
3758            xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
3759            xmlTextWriterStartElement(writer, b"root\0" as *const u8);
3760            xmlTextWriterWriteString(writer, b"flush me\0" as *const u8);
3761
3762            // Flush mid-document
3763            let r = xmlTextWriterFlush(writer);
3764            assert!(r >= 0, "Flush should return non-negative, got {}", r);
3765
3766            xmlTextWriterEndElement(writer);
3767            xmlTextWriterEndDocument(writer);
3768
3769            xmlFreeTextWriter(writer);
3770            io::buf_free(buf);
3771        }
3772    }
3773
3774    // ═══════════════════════════════════════════════════════════════════════════
3775    // Test: Edge cases — null writer, null parameters
3776    // ═══════════════════════════════════════════════════════════════════════════
3777
3778    #[test]
3779    fn test_null_handling() {
3780        unsafe {
3781            // All functions should gracefully handle NULL writer
3782            assert_eq!(
3783                xmlTextWriterStartDocument(ptr::null_mut(), ptr::null(), ptr::null(), ptr::null()),
3784                -1
3785            );
3786            assert_eq!(xmlTextWriterEndDocument(ptr::null_mut()), -1);
3787            assert_eq!(
3788                xmlTextWriterStartElement(ptr::null_mut(), b"x\0" as *const u8),
3789                -1
3790            );
3791            assert_eq!(xmlTextWriterEndElement(ptr::null_mut()), -1);
3792            assert_eq!(
3793                xmlTextWriterWriteString(ptr::null_mut(), b"x\0" as *const u8),
3794                -1
3795            );
3796            assert_eq!(
3797                xmlTextWriterWriteRaw(ptr::null_mut(), b"x\0" as *const u8),
3798                -1
3799            );
3800            assert_eq!(
3801                xmlTextWriterWriteCDATA(ptr::null_mut(), b"x\0" as *const u8),
3802                -1
3803            );
3804            assert_eq!(
3805                xmlTextWriterWriteComment(ptr::null_mut(), b"x\0" as *const u8),
3806                -1
3807            );
3808            assert_eq!(
3809                xmlTextWriterWritePI(ptr::null_mut(), b"x\0" as *const u8, ptr::null()),
3810                -1
3811            );
3812            assert_eq!(xmlTextWriterFlush(ptr::null_mut()), -1);
3813            assert_eq!(xmlTextWriterSetIndent(ptr::null_mut(), 1), -1);
3814            assert_eq!(
3815                xmlTextWriterSetIndentString(ptr::null_mut(), b"  \0" as *const u8),
3816                -1
3817            );
3818            assert_eq!(
3819                xmlTextWriterWriteAttribute(
3820                    ptr::null_mut(),
3821                    b"n\0" as *const u8,
3822                    b"v\0" as *const u8
3823                ),
3824                -1
3825            );
3826
3827            // Null writer should not crash free
3828            xmlFreeTextWriter(ptr::null_mut());
3829        }
3830    }
3831
3832    // ═══════════════════════════════════════════════════════════════════════════
3833    // Test: Nested elements
3834    // ═══════════════════════════════════════════════════════════════════════════
3835
3836    #[test]
3837    fn test_nested_elements() {
3838        unsafe {
3839            let (writer, buf) = create_test_writer();
3840
3841            xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
3842            xmlTextWriterStartElement(writer, b"a\0" as *const u8);
3843            xmlTextWriterStartElement(writer, b"b\0" as *const u8);
3844            xmlTextWriterStartElement(writer, b"c\0" as *const u8);
3845            xmlTextWriterWriteString(writer, b"deep\0" as *const u8);
3846            xmlTextWriterEndElement(writer);
3847            xmlTextWriterEndElement(writer);
3848            xmlTextWriterEndElement(writer);
3849            xmlTextWriterEndDocument(writer);
3850
3851            let result = flush_and_get(writer, buf);
3852            assert!(result.contains("<a>"), "Missing <a>. Got: {}", result);
3853            assert!(result.contains("<b>"), "Missing <b>. Got: {}", result);
3854            assert!(result.contains("<c>"), "Missing <c>. Got: {}", result);
3855            assert!(result.contains("</a>"), "Missing </a>. Got: {}", result);
3856            assert!(result.contains("</b>"), "Missing </b>. Got: {}", result);
3857            assert!(result.contains("</c>"), "Missing </c>. Got: {}", result);
3858
3859            xmlFreeTextWriter(writer);
3860            io::buf_free(buf);
3861        }
3862    }
3863
3864    // ═══════════════════════════════════════════════════════════════════════════
3865    // Test: Self-closing element (no content)
3866    // ═══════════════════════════════════════════════════════════════════════════
3867
3868    #[test]
3869    fn test_self_closing_element() {
3870        unsafe {
3871            let (writer, buf) = create_test_writer();
3872
3873            xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
3874            xmlTextWriterStartElement(writer, b"empty\0" as *const u8);
3875            xmlTextWriterEndElement(writer);
3876            xmlTextWriterEndDocument(writer);
3877
3878            let result = flush_and_get(writer, buf);
3879            assert!(
3880                result.contains("<empty/>"),
3881                "Expected self-closing <empty/>. Got: {}",
3882                result
3883            );
3884
3885            xmlFreeTextWriter(writer);
3886            io::buf_free(buf);
3887        }
3888    }
3889
3890    // ═══════════════════════════════════════════════════════════════════════════
3891    // Test: Full end element (not self-closing)
3892    // ═══════════════════════════════════════════════════════════════════════════
3893
3894    #[test]
3895    fn test_full_end_element() {
3896        unsafe {
3897            let (writer, buf) = create_test_writer();
3898
3899            xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
3900            xmlTextWriterStartElement(writer, b"container\0" as *const u8);
3901            xmlTextWriterFullEndElement(writer);
3902            xmlTextWriterEndDocument(writer);
3903
3904            let result = flush_and_get(writer, buf);
3905            assert!(
3906                result.contains("<container>"),
3907                "Missing <container>. Got: {}",
3908                result
3909            );
3910            assert!(
3911                result.contains("</container>"),
3912                "Missing </container>. Got: {}",
3913                result
3914            );
3915
3916            xmlFreeTextWriter(writer);
3917            io::buf_free(buf);
3918        }
3919    }
3920
3921    // ═══════════════════════════════════════════════════════════════════════════
3922    // Test: WriteElement (element with inline content)
3923    // ═══════════════════════════════════════════════════════════════════════════
3924
3925    #[test]
3926    fn test_write_element_inline() {
3927        unsafe {
3928            let (writer, buf) = create_test_writer();
3929
3930            xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
3931            xmlTextWriterWriteElement(writer, b"greeting\0" as *const u8, b"Hello\0" as *const u8);
3932            xmlTextWriterEndDocument(writer);
3933
3934            let result = flush_and_get(writer, buf);
3935            assert!(
3936                result.contains("<greeting>Hello</greeting>"),
3937                "Expected <greeting>Hello</greeting>. Got: {}",
3938                result
3939            );
3940
3941            xmlFreeTextWriter(writer);
3942            io::buf_free(buf);
3943        }
3944    }
3945
3946    // ═══════════════════════════════════════════════════════════════════════════
3947    // Test: XML escaping in text content
3948    // ═══════════════════════════════════════════════════════════════════════════
3949
3950    #[test]
3951    fn test_text_escaping() {
3952        unsafe {
3953            let (writer, buf) = create_test_writer();
3954
3955            xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
3956            xmlTextWriterStartElement(writer, b"esc\0" as *const u8);
3957            xmlTextWriterWriteString(writer, b"a < b & b > a\0" as *const u8);
3958            xmlTextWriterEndElement(writer);
3959            xmlTextWriterEndDocument(writer);
3960
3961            let result = flush_and_get(writer, buf);
3962            assert!(
3963                result.contains("a &lt; b &amp; b &gt; a"),
3964                "Expected escaped content. Got: {}",
3965                result
3966            );
3967
3968            xmlFreeTextWriter(writer);
3969            io::buf_free(buf);
3970        }
3971    }
3972
3973    // ═══════════════════════════════════════════════════════════════════════════
3974    // Test: Raw content (no escaping)
3975    // ═══════════════════════════════════════════════════════════════════════════
3976
3977    #[test]
3978    fn test_raw_content() {
3979        unsafe {
3980            let (writer, buf) = create_test_writer();
3981
3982            xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
3983            xmlTextWriterStartElement(writer, b"raw\0" as *const u8);
3984            xmlTextWriterWriteRaw(writer, b"<unencoded>&special;</unencoded>\0" as *const u8);
3985            xmlTextWriterEndElement(writer);
3986            xmlTextWriterEndDocument(writer);
3987
3988            let result = flush_and_get(writer, buf);
3989            assert!(
3990                result.contains("<unencoded>&special;</unencoded>"),
3991                "Expected raw unencoded content. Got: {}",
3992                result
3993            );
3994
3995            xmlFreeTextWriter(writer);
3996            io::buf_free(buf);
3997        }
3998    }
3999
4000    // ═══════════════════════════════════════════════════════════════════════════
4001    // Test: Base64 writing
4002    // ═══════════════════════════════════════════════════════════════════════════
4003
4004    #[test]
4005    fn test_base64_write() {
4006        unsafe {
4007            let (writer, buf) = create_test_writer();
4008
4009            xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
4010            xmlTextWriterStartElement(writer, b"data\0" as *const u8);
4011            let test_data = b"Hello, World!";
4012            xmlTextWriterWriteBase64(
4013                writer,
4014                test_data.as_ptr() as *const c_char,
4015                0,
4016                test_data.len() as c_int,
4017            );
4018            xmlTextWriterEndElement(writer);
4019            xmlTextWriterEndDocument(writer);
4020
4021            let result = flush_and_get(writer, buf);
4022            assert!(
4023                result.contains("SGVsbG8sIFdvcmxkIQ"),
4024                "Expected Base64-encoded content. Got: {}",
4025                result
4026            );
4027
4028            xmlFreeTextWriter(writer);
4029            io::buf_free(buf);
4030        }
4031    }
4032
4033    // ═══════════════════════════════════════════════════════════════════════════
4034    // Test: Incremental CDATA/comment/PI
4035    // ═══════════════════════════════════════════════════════════════════════════
4036
4037    #[test]
4038    fn test_incremental_cdata_comment_pi() {
4039        unsafe {
4040            let (writer, buf) = create_test_writer();
4041
4042            xmlTextWriterStartDocument(writer, ptr::null(), ptr::null(), ptr::null());
4043
4044            // Incremental CDATA
4045            xmlTextWriterStartElement(writer, b"inc\0" as *const u8);
4046            xmlTextWriterStartCDATA(writer);
4047            xmlTextWriterWriteString(writer, b"cdata content\0" as *const u8);
4048            xmlTextWriterEndCDATA(writer);
4049            xmlTextWriterEndElement(writer);
4050
4051            // Incremental comment
4052            xmlTextWriterStartComment(writer);
4053            xmlTextWriterWriteString(writer, b"comment text\0" as *const u8);
4054            xmlTextWriterEndComment(writer);
4055
4056            // Incremental PI
4057            xmlTextWriterStartPI(writer, b"xml-stylesheet\0" as *const u8);
4058            xmlTextWriterWriteString(
4059                writer,
4060                b"type=\"text/xsl\" href=\"style.xsl\"\0" as *const u8,
4061            );
4062            xmlTextWriterEndPI(writer);
4063
4064            xmlTextWriterEndDocument(writer);
4065
4066            let result = flush_and_get(writer, buf);
4067            assert!(
4068                result.contains("<![CDATA["),
4069                "Missing CDATA. Got: {}",
4070                result
4071            );
4072            assert!(
4073                result.contains("<!--comment text-->"),
4074                "Missing comment. Got: {}",
4075                result
4076            );
4077            assert!(
4078                result.contains("<?xml-stylesheet"),
4079                "Missing PI. Got: {}",
4080                result
4081            );
4082
4083            xmlFreeTextWriter(writer);
4084            io::buf_free(buf);
4085        }
4086    }
4087
4088    // ═══════════════════════════════════════════════════════════════════════════
4089    // Test: xmlNewTextWriterFilename returns NULL for NULL uri
4090    // ═══════════════════════════════════════════════════════════════════════════
4091
4092    #[test]
4093    fn test_new_writer_filename_null() {
4094        unsafe {
4095            let writer = xmlNewTextWriterFilename(ptr::null(), 0);
4096            assert!(writer.is_null(), "Expected NULL for null URI");
4097        }
4098    }
4099
4100    // ═══════════════════════════════════════════════════════════════════════════
4101    // Test: xmlNewTextWriter returns NULL for NULL output
4102    // ═══════════════════════════════════════════════════════════════════════════
4103
4104    #[test]
4105    fn test_new_writer_null_output() {
4106        unsafe {
4107            let writer = xmlNewTextWriter(ptr::null_mut());
4108            assert!(writer.is_null(), "Expected NULL for null output");
4109        }
4110    }
4111}