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