libxml_rs/abi/exports_buffer.rs
1//! C ABI exports for the buffer family — xmlBuf* / xmlBuffer* / xmlCharStr*
2//! (upstream buf.c, tree.c, xmlstring.c, 2.15.3).
3//!
4//! In 2.15 `xmlBuf` and `xmlBuffer` are the same C struct (typedef xmlBuffer
5//! xmlBuf), but the candidate mirrors them as two distinct structs
6//! (`_xmlBuffer` with `{content, use_, size, alloc, contentIO}` and `_xmlBuf`
7//! with `{content, use_, size, alloc, error, buffer, io}`). The `xmlBuf*`
8//! exports therefore operate on the `_xmlBuf` fields directly, allocating
9//! through the candidate allocator (`xmlMalloc`/`xmlFree`), exactly as
10//! upstream manipulates `buf->content`/`buf->use`/`buf->size`.
11//!
12//! # Upstream contract
13//!
14//! Parity target is upstream `buf.c` and `tree.c` (libxml2 2.15.3): the
15//! `xmlBuf*`/`xmlBuffer*`/`xmlCharStr*` entry points with the exact upstream
16//! signatures from `buf.h` and `tree.h`. R-000165 (11.1-O) closed the
17//! buffer-family gaps in the subsystem census.
18//!
19//! # Conceptual behavior
20//!
21//! This module implements the buffer ABI: content accessors, appends, shrinks,
22//! dumps, node-content extraction and the deprecated `xmlBuffer*` wrappers.
23//! In 2.15 `xmlBuf` and `xmlBuffer` are the same C struct upstream, but the
24//! candidate mirrors them as two distinct structs (`_xmlBuffer` vs `_xmlBuf`)
25//! and operates on the matching field set — see the header notes above.
26//!
27//! # Ownership & safety invariants
28//!
29//! Buffers are caller-owned: `xmlBufferCreate`/`xmlBufCreate` results are
30//! freed with `xmlBufferFree`/`xmlBufFree`; `xmlBufferDetach` transfers the
31//! content pointer to the caller (caller frees with `xmlFree`); returned
32//! content pointers are borrowed and valid until the next mutation
33//! (OWNERSHIP_ATLAS section 1). All internal allocation goes through
34//! `xmlMalloc`/`xmlFree` so the xml allocator domain holds.
35//!
36//! # Historical quirks & epochs
37//!
38//! `xmlBuffer` is the deprecated 1.x-era struct; `xmlBuf` superseded it in the
39//! 2.9 era (upstream keeps both in the ABI — HISTORY.md records the 2.0 ABI
40//! break and the modern 2.10+ epoch). R-000165: the buffer family was part of
41//! the 65-symbol export gap closed in 11.1-X.
42//!
43//! # Deliberate oddities
44//!
45//! The two-struct mirror (where upstream aliases one typedef) is a deliberate
46//! candidate-internal split: the field layouts differ (`_xmlBuf` carries
47//! error/buffer/io), and keeping them distinct keeps the Rust accessors honest
48//! about which fields exist.
49//!
50//! # Proving courts
51//!
52//! The OWNERSHIP and TREE-STRUCTURE court families exercise the buffer
53//! surface; the WRITER-001 probe (writer-family-probe.c) drives buffers
54//! through the writer and requires byte-identical output; DSO-LOADER resolves
55//! every export.
56//!
57//! # Tempting simplifications that would break parity
58//!
59//! A tempting simplification is to unify `_xmlBuffer` and `_xmlBuf` into one
60//! struct because upstream typedefs them together — but the candidate field
61//! sets genuinely differ, and conflating them would make the `xmlBuf*`
62//! accessors read the wrong offsets (the R-000129 layout-defect class).
63//! Another shortcut — returning raw byte counts from append calls — is the
64//! WRITER-001 lesson (R-000151): write returns are encoder-dependent.
65
66#![allow(
67 missing_docs,
68 non_snake_case,
69 non_camel_case_types,
70 non_upper_case_globals
71)]
72
73// SAFETY-SCOPE: EXPORT-BUFFER-MECHANICAL-001
74// (11.1-Z.3 proof scope, classified-generated) — this module is the
75// mechanical extern-"C" export surface: every `unsafe` block in it is
76// the documented indirection/registry-access pattern whose validity
77// rests on the upstream C contract, and the exported signatures are
78// machine-measured by the ABI-FUNCTION-SIGNATURE and DSO-LOADER
79// courts and the C-API differential probes. The safety contract of
80// each export is stated in its own doc comment; this scope covers the
81// mechanical wrappers' unsafe blocks.
82
83use core::ptr;
84use std::os::raw::{c_char, c_int, c_uint, c_void};
85
86use crate::abi::allocator::{xmlFreeImpl, xmlMallocImpl};
87use crate::abi::structs::{_xmlBuf, _xmlBuffer, _xmlDoc, _xmlNode, _xmlNs};
88use crate::abi::types::{xmlChar, xmlElementType, XML_ERR_ARGUMENT, XML_ERR_NO_MEMORY, XML_ERR_OK};
89use crate::xml::io;
90use crate::xml::tree;
91
92// ── libc FILE* plumbing for xmlBufferDump ───────────────────────────────
93//
94// The FILE* is opaque at the ABI boundary and is passed as *mut c_void.
95// fwrite(3) is declared here rather than pulled from the libc crate so the
96// dependency stays explicit; `stdout` is the libc data symbol used by
97// upstream's `if (file == NULL) file = stdout;` fallback.
98
99extern "C" {
100 /// libc `size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)`.
101 fn fwrite(ptr: *const c_void, size: usize, nmemb: usize, stream: *mut c_void) -> usize;
102 /// The libc `FILE *stdout` variable.
103 static mut stdout: *mut c_void;
104}
105
106// ═══════════════════════════════════════════════════════════════════════════════
107// 1. xmlBuf operations (modern replacement, tree.h)
108// ═══════════════════════════════════════════════════════════════════════════════
109
110/// Get pointer into buffer content.
111///
112/// # UPSTREAM-PARITY
113///
114/// ```c
115/// xmlChar *xmlBufContent(const xmlBuf *buf);
116/// ```
117///
118/// buf.c 2.15: returns `buf->content`, or NULL when `buf` is NULL or the
119/// buffer is in error state (`BUF_ERROR`).
120///
121/// # SAFETY
122///
123/// - `buf` must be a valid pointer to a `_xmlBuf` (or NULL).
124/// - The returned pointer is owned by `buf` and must not be freed by the
125/// caller.
126#[no_mangle]
127pub const unsafe extern "C" fn xmlBufContent(buf: *const _xmlBuf) -> *mut xmlChar {
128 if buf.is_null() || unsafe { (*buf).error != 0 } {
129 return ptr::null_mut();
130 }
131 unsafe { (*buf).content }
132}
133
134/// Return a pointer to the end of the buffer content.
135///
136/// # UPSTREAM-PARITY
137///
138/// ```c
139/// xmlChar *xmlBufEnd(xmlBuf *buf);
140/// ```
141///
142/// buf.c 2.15: returns `&buf->content[buf->use]`, or NULL when `buf` is NULL
143/// or the buffer is in error state.
144///
145/// # SAFETY
146///
147/// - `buf` must be a valid pointer to a `_xmlBuf` (or NULL).
148/// - The returned pointer is owned by `buf` and must not be freed by the
149/// caller.
150#[no_mangle]
151pub unsafe extern "C" fn xmlBufEnd(buf: *mut _xmlBuf) -> *mut xmlChar {
152 if buf.is_null() || unsafe { (*buf).error != 0 } {
153 return ptr::null_mut();
154 }
155 unsafe { (*buf).content.add((*buf).use_ as usize) }
156}
157
158/// Append the string value of a node to `buf`.
159///
160/// For text/CDATA/comment/PI nodes the string value is the node content;
161/// otherwise it is the concatenation of the string values of the node's
162/// descendants, with entity references substituted. Namespace declaration
163/// nodes contribute their href.
164///
165/// # UPSTREAM-PARITY
166///
167/// ```c
168/// int xmlBufGetNodeContent(xmlBuf *buf, const xmlNode *cur);
169/// ```
170///
171/// tree.c 2.15: returns -1 only when `cur` or `buf` is NULL; otherwise 0
172/// (append failures are ignored upstream and here, matching the void
173/// contract).
174///
175/// # SAFETY
176///
177/// - `buf` must be a valid pointer to a `_xmlBuf` (or NULL).
178/// - `cur` must be a valid pointer to a `_xmlNode`/`_xmlNs` (or NULL).
179#[no_mangle]
180pub unsafe extern "C" fn xmlBufGetNodeContent(buf: *mut _xmlBuf, cur: *const _xmlNode) -> c_int {
181 if cur.is_null() || buf.is_null() {
182 return -1;
183 }
184
185 // Upstream xmlBufGetNodeContent appends the namespace href for
186 // XML_NAMESPACE_DECL nodes; node_get_content has no arm for them.
187 if unsafe { (*cur).type_ } == xmlElementType::XML_NAMESPACE_DECL as c_int {
188 let ns = cur as *const _xmlNs;
189 if !unsafe { (*ns).href }.is_null() {
190 io::xml_buf_cat(buf, unsafe { (*ns).href });
191 }
192 return 0;
193 }
194
195 // node_get_content mirrors tree.c xmlNodeGetContent: recursive string
196 // value of the node (text descendants, entity expansion, attribute
197 // value, comment/PI content).
198 let content = unsafe { tree::node_get_content(cur as *mut _xmlNode) };
199 if content.is_null() {
200 // Allocation failure: nothing was appended; upstream still returns 0.
201 return 0;
202 }
203 io::xml_buf_cat(buf, content);
204 unsafe { xmlFreeImpl(content as *mut c_void) };
205 0
206}
207
208/// Serialize an XML node to an xmlBuf.
209///
210/// # UPSTREAM-PARITY
211///
212/// ```c
213/// size_t xmlBufNodeDump(xmlBuf *buf, xmlDoc *doc, xmlNode *cur, int level, int format);
214/// ```
215///
216/// xmlsave.c/tree.c 2.15: returns the number of bytes written to `buf`, or
217/// `(size_t)-1` when `buf`/`cur` is NULL or serialization fails. The level
218/// is clamped to [0, 100] (xmlNodeDumpOutput). `doc` is ignored, matching
219/// upstream's `(void) doc`.
220///
221/// # SAFETY
222///
223/// - `buf` must be a valid pointer to a `_xmlBuf` (or NULL).
224/// - `cur` must be a valid pointer to a `_xmlNode` (or NULL).
225/// - `doc` is unused and may be NULL.
226#[no_mangle]
227pub unsafe extern "C" fn xmlBufNodeDump(
228 buf: *mut _xmlBuf,
229 doc: *mut _xmlDoc,
230 cur: *mut _xmlNode,
231 level: c_int,
232 format: c_int,
233) -> usize {
234 let _ = doc;
235 if cur.is_null() || buf.is_null() {
236 return usize::MAX; // (size_t)-1
237 }
238 let level = level.clamp(0, 100);
239
240 // Serialize through a temporary xmlBuffer (the candidate serializer
241 // targets _xmlBuffer), then append the serialized bytes to the xmlBuf.
242 let tmp = io::buf_create(-1);
243 if tmp.is_null() {
244 return usize::MAX;
245 }
246 tree::serialize_node_opts(cur, tmp, format, level, ptr::null(), 0);
247
248 let len = io::buf_length(tmp);
249 let content = io::buf_content(tmp);
250 let written = if len > 0 && !content.is_null() {
251 io::xml_buf_add(buf, content, len)
252 } else {
253 0
254 };
255 io::buf_free(tmp);
256
257 if written < 0 {
258 usize::MAX
259 } else {
260 written as usize
261 }
262}
263
264/// Discard bytes at the start of a buffer.
265///
266/// # UPSTREAM-PARITY
267///
268/// ```c
269/// size_t xmlBufShrink(xmlBuf *buf, size_t len);
270/// ```
271///
272/// buf.c 2.15: removes `len` bytes from the front by advancing
273/// `buf->content` and decreasing `buf->use`/`buf->size`; returns the number
274/// of bytes removed, or 0 when `buf` is NULL, the buffer is in error state,
275/// `len` is 0, or `len` exceeds `buf->use`. Unlike `xmlBufferShrink`, errors
276/// return 0 rather than -1 (size_t return type).
277///
278/// # SAFETY
279///
280/// - `buf` must be a valid pointer to a `_xmlBuf` (or NULL).
281#[no_mangle]
282pub unsafe extern "C" fn xmlBufShrink(buf: *mut _xmlBuf, len: usize) -> usize {
283 if buf.is_null() || unsafe { (*buf).error != 0 } {
284 return 0;
285 }
286 if len == 0 {
287 return 0;
288 }
289 let b = unsafe { &mut *buf };
290 if len > b.use_ as usize {
291 return 0;
292 }
293 b.use_ -= len as c_uint;
294 b.content = unsafe { b.content.add(len) };
295 b.size -= len as c_uint;
296 len
297}
298
299/// Return the size of the buffer content.
300///
301/// # UPSTREAM-PARITY
302///
303/// ```c
304/// size_t xmlBufUse(xmlBuf *buf);
305/// ```
306///
307/// buf.c 2.15: returns `buf->use`, or 0 when `buf` is NULL or the buffer is
308/// in error state.
309///
310/// # SAFETY
311///
312/// - `buf` must be a valid pointer to a `_xmlBuf` (or NULL).
313#[no_mangle]
314pub unsafe extern "C" fn xmlBufUse(buf: *mut _xmlBuf) -> usize {
315 if buf.is_null() || unsafe { (*buf).error != 0 } {
316 return 0;
317 }
318 unsafe { (*buf).use_ as usize }
319}
320
321// ═══════════════════════════════════════════════════════════════════════════════
322// 2. xmlBuffer operations (deprecated, tree.h)
323// ═══════════════════════════════════════════════════════════════════════════════
324
325/// Append a zero-terminated C string to a buffer.
326///
327/// # UPSTREAM-PARITY
328///
329/// ```c
330/// int xmlBufferCCat(xmlBuffer *buf, const char *str);
331/// ```
332///
333/// buf.c 2.15: forwards to `xmlBufferAdd(buf, (const xmlChar *) str, -1)`,
334/// returning XML_ERR_ARGUMENT when `buf` or `str` is NULL, XML_ERR_OK on
335/// success (including the empty string), and XML_ERR_NO_MEMORY when growth
336/// fails.
337///
338/// # SAFETY
339///
340/// - `buf` must be a valid pointer to a `_xmlBuffer` (or NULL).
341/// - `str` must be a valid NUL-terminated C string (or NULL).
342#[no_mangle]
343pub unsafe extern "C" fn xmlBufferCCat(buf: *mut _xmlBuffer, str: *const c_char) -> c_int {
344 if buf.is_null() || str.is_null() {
345 return XML_ERR_ARGUMENT;
346 }
347 let ret = io::buf_cat(buf, str as *const xmlChar);
348 if ret < 0 {
349 XML_ERR_NO_MEMORY
350 } else {
351 XML_ERR_OK
352 }
353}
354
355/// Dump a buffer to a `FILE`.
356///
357/// # UPSTREAM-PARITY
358///
359/// ```c
360/// int xmlBufferDump(FILE *file, xmlBuffer *buf);
361/// ```
362///
363/// buf.c 2.15: returns 0 when `buf` is NULL or has no content, defaults a
364/// NULL `file` to stdout, and otherwise returns the `fwrite` byte count
365/// clamped to INT_MAX. Upstream never returns -1 from this function; write
366/// errors surface as a short count.
367///
368/// # SAFETY
369///
370/// - `file` must be a valid `FILE*` or NULL (then stdout is used).
371/// - `buf` must be a valid pointer to a `_xmlBuffer` (or NULL).
372#[no_mangle]
373pub unsafe extern "C" fn xmlBufferDump(file: *mut c_void, buf: *mut _xmlBuffer) -> c_int {
374 if buf.is_null() {
375 return 0;
376 }
377 let b = unsafe { &*buf };
378 if b.content.is_null() {
379 return 0;
380 }
381 let stream = if file.is_null() {
382 // Upstream: `if (file == NULL) file = stdout;`
383 unsafe { stdout }
384 } else {
385 file
386 };
387 let n = unsafe { fwrite(b.content as *const c_void, 1, b.use_ as usize, stream) };
388 if n > c_int::MAX as usize {
389 c_int::MAX
390 } else {
391 n as c_int
392 }
393}
394
395/// Resize a buffer to a minimum size.
396///
397/// # UPSTREAM-PARITY
398///
399/// ```c
400/// int xmlBufferResize(xmlBuffer *buf, unsigned int size);
401/// ```
402///
403/// buf.c 2.15: returns 0 when `buf` is NULL, 1 when `size` is below the
404/// current capacity, otherwise grows the buffer so its total capacity is at
405/// least `size` and returns 1 on success / 0 on allocation failure.
406///
407/// # SAFETY
408///
409/// - `buf` must be a valid pointer to a `_xmlBuffer` (or NULL).
410#[no_mangle]
411pub unsafe extern "C" fn xmlBufferResize(buf: *mut _xmlBuffer, size: c_uint) -> c_int {
412 if buf.is_null() {
413 return 0;
414 }
415 if size < unsafe { (*buf).size } {
416 return 1;
417 }
418 let res = io::buf_grow(buf, size);
419 if res < 0 {
420 0
421 } else {
422 1
423 }
424}
425
426/// Append a zero-terminated `xmlChar` string to a buffer.
427///
428/// # UPSTREAM-PARITY
429///
430/// ```c
431/// void xmlBufferWriteCHAR(xmlBuffer *buf, const xmlChar *string);
432/// ```
433///
434/// buf.c 2.15: `xmlBufferAdd(buf, string, -1)`, i.e. append the
435/// NUL-terminated string; failures are ignored (void return).
436///
437/// # SAFETY
438///
439/// - `buf` must be a valid pointer to a `_xmlBuffer` (or NULL).
440/// - `string` must be a valid NUL-terminated xmlChar string (or NULL).
441#[no_mangle]
442pub unsafe extern "C" fn xmlBufferWriteCHAR(buf: *mut _xmlBuffer, string: *const xmlChar) {
443 let _ = io::buf_cat(buf, string);
444}
445
446/// Append a zero-terminated C string to a buffer.
447///
448/// Same as `xmlBufferCCat`.
449///
450/// # UPSTREAM-PARITY
451///
452/// ```c
453/// void xmlBufferWriteChar(xmlBuffer *buf, const char *string);
454/// ```
455///
456/// buf.c 2.15: `xmlBufferAdd(buf, (const xmlChar *) string, -1)`; failures
457/// are ignored (void return).
458///
459/// # SAFETY
460///
461/// - `buf` must be a valid pointer to a `_xmlBuffer` (or NULL).
462/// - `string` must be a valid NUL-terminated C string (or NULL).
463#[no_mangle]
464pub unsafe extern "C" fn xmlBufferWriteChar(buf: *mut _xmlBuffer, string: *const c_char) {
465 let _ = io::buf_cat(buf, string as *const xmlChar);
466}
467
468/// Append a quoted string to a buffer.
469///
470/// Appends the string wrapped in quotes. If the string contains both single
471/// and double quotes, double quotes are escaped with `"` (upstream
472/// buf.c 2.15; no backslash or CR/LF escaping exists in this version).
473///
474/// # UPSTREAM-PARITY
475///
476/// ```c
477/// void xmlBufferWriteQuotedString(xmlBuffer *buf, const xmlChar *string);
478/// ```
479///
480/// buf.c 2.15: with a double-quote-containing string that also contains a
481/// single quote, emits `"` + string with `"` → `"` + `"`; with only
482/// double quotes emits `'` + string + `'`; otherwise emits `"` + string +
483/// `"`. A NULL string yields the bare quote pair, exactly like upstream
484/// (xmlStrchr(NULL) misses, xmlBufferCat(NULL) fails silently).
485///
486/// # SAFETY
487///
488/// - `buf` must be a valid pointer to a `_xmlBuffer` (or NULL).
489/// - `string` must be a valid NUL-terminated xmlChar string (or NULL).
490#[no_mangle]
491pub unsafe extern "C" fn xmlBufferWriteQuotedString(buf: *mut _xmlBuffer, string: *const xmlChar) {
492 if buf.is_null() {
493 return;
494 }
495 let has_dquote = !crate::abi::exports_xml2::xmlStrchr(string, b'"').is_null();
496 let has_squote = !crate::abi::exports_xml2::xmlStrchr(string, b'\'').is_null();
497 if has_dquote {
498 if has_squote {
499 // Escape every double quote with " inside double quotes.
500 io::buf_cat(buf, b"\"".as_ptr());
501 let mut base = string;
502 let mut cur = string;
503 while !cur.is_null() && unsafe { *cur } != 0 {
504 if unsafe { *cur } == b'"' {
505 if base != cur {
506 let n = cur.offset_from(base) as c_int;
507 io::buf_add(buf, base, n);
508 }
509 io::buf_cat(buf, b""".as_ptr());
510 cur = cur.add(1);
511 base = cur;
512 } else {
513 cur = cur.add(1);
514 }
515 }
516 if base != cur {
517 let n = cur.offset_from(base) as c_int;
518 io::buf_add(buf, base, n);
519 }
520 io::buf_cat(buf, b"\"".as_ptr());
521 } else {
522 // Only double quotes: single-quote the string.
523 io::buf_cat(buf, b"\'".as_ptr());
524 io::buf_cat(buf, string);
525 io::buf_cat(buf, b"\'".as_ptr());
526 }
527 } else {
528 io::buf_cat(buf, b"\"".as_ptr());
529 io::buf_cat(buf, string);
530 io::buf_cat(buf, b"\"".as_ptr());
531 }
532}
533
534// ═══════════════════════════════════════════════════════════════════════════════
535// 3. xmlChar string duplication (xmlstring.h)
536// ═══════════════════════════════════════════════════════════════════════════════
537
538/// Duplicate a `char *` string to a new `xmlChar *` string.
539///
540/// # UPSTREAM-PARITY
541///
542/// ```c
543/// xmlChar *xmlCharStrdup(const char *cur);
544/// ```
545///
546/// xmlstring.c 2.15: returns NULL for a NULL input, otherwise
547/// `xmlCharStrndup(cur, strlen(cur))`.
548///
549/// # SAFETY
550///
551/// - `cur` must be a valid NUL-terminated C string or NULL.
552/// - The returned pointer is allocated with `xmlMalloc` and must be freed
553/// with `xmlFree`.
554#[no_mangle]
555pub unsafe extern "C" fn xmlCharStrdup(cur: *const c_char) -> *mut xmlChar {
556 if cur.is_null() {
557 return ptr::null_mut();
558 }
559 let len = unsafe { libc::strlen(cur) } as c_int;
560 unsafe { xmlCharStrndup(cur, len) }
561}
562
563/// Duplicate `len` bytes of a `char *` string to a new `xmlChar *` string.
564///
565/// # UPSTREAM-PARITY
566///
567/// ```c
568/// xmlChar *xmlCharStrndup(const char *cur, int len);
569/// ```
570///
571/// xmlstring.c 2.15: returns NULL when `cur` is NULL or `len` is negative;
572/// otherwise allocates `len + 1` bytes, copies at most `len` bytes, stops
573/// early (and returns immediately) if an embedded NUL is copied, and always
574/// NUL-terminates.
575///
576/// # SAFETY
577///
578/// - `cur` must be a valid pointer to at least `len` readable bytes or NULL.
579/// - The returned pointer is allocated with `xmlMalloc` and must be freed
580/// with `xmlFree`.
581#[no_mangle]
582pub unsafe extern "C" fn xmlCharStrndup(cur: *const c_char, len: c_int) -> *mut xmlChar {
583 if cur.is_null() || len < 0 {
584 return ptr::null_mut();
585 }
586 let ret = unsafe { xmlMallocImpl(len as usize + 1) } as *mut xmlChar;
587 if ret.is_null() {
588 return ptr::null_mut();
589 }
590 unsafe {
591 let mut i: usize = 0;
592 while i < len as usize {
593 let byte = *cur.add(i) as xmlChar;
594 *ret.add(i) = byte;
595 if byte == 0 {
596 return ret; // Embedded NUL: already terminated.
597 }
598 i += 1;
599 }
600 *ret.add(len as usize) = 0;
601 }
602 ret
603}