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