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