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