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