Skip to main content

libxml_rs/xml/io/
mod.rs

1//! Custom I/O and resource loaders (§59, §85 Phase 4).
2//!
3//! Input/output callback infrastructure: buffers, file I/O, encoding
4//! integration, input/output buffer management, and helper utilities.
5//!
6//! This module implements the libxml2 I/O subsystem in native Rust.
7//! No C dependencies beyond `libc` for file operations.
8
9#![allow(
10    clippy::cast_possible_truncation,
11    clippy::cast_sign_loss,
12    clippy::cast_ptr_alignment,
13    clippy::missing_safety_doc
14)]
15
16use std::ffi::CStr;
17use std::os::raw::{c_char, c_int, c_uint, c_ulong, c_void};
18use std::ptr;
19
20use libc;
21
22use crate::abi::allocator::{xmlFreeImpl, xmlMallocImpl, xmlReallocImpl};
23use crate::abi::callbacks::{
24    xmlInputCloseCallback, xmlInputReadCallback, xmlOutputCloseCallback, xmlOutputWriteCallback,
25};
26use crate::abi::structs::{
27    _xmlBuf, _xmlBuffer, _xmlCharEncodingHandler, _xmlOutputBuffer, _xmlParserInputBuffer,
28};
29use crate::abi::types::{xmlChar, xmlCharEncoding, xmlCharPtr};
30use crate::xml::encoding;
31
32// ═══════════════════════════════════════════════════════════════════════════════
33// Constants
34// ═══════════════════════════════════════════════════════════════════════════════
35
36/// Default buffer size for new buffers.
37pub(crate) const DEFAULT_BUFFER_SIZE: c_uint = 4000;
38
39/// Minimum buffer size.
40const MIN_BUFFER_SIZE: c_uint = 256;
41
42/// Buffer allocation scheme: double the size on growth.
43const XML_BUFFER_ALLOC_DOUBLEIT: c_int = 0;
44
45/// Buffer allocation scheme: exact size on growth.
46const XML_BUFFER_ALLOC_EXACT: c_int = 1;
47
48/// Buffer allocation scheme: immutable (no growth, no free of content).
49const XML_BUFFER_ALLOC_IMMUTABLE: c_int = 2;
50
51// ═══════════════════════════════════════════════════════════════════════════════
52// 1. xmlBuffer operations (deprecated)
53// ═══════════════════════════════════════════════════════════════════════════════
54
55/// Create a new xmlBuffer with the given initial size.
56///
57/// If `size` <= 0, a default buffer size is used.
58/// The buffer content is initialized to an empty null-terminated string.
59///
60/// Returns a pointer to the new buffer, or NULL on allocation failure.
61pub(crate) fn buf_create(size: c_int) -> *mut _xmlBuffer {
62    let buf_size = if size <= 0 {
63        DEFAULT_BUFFER_SIZE
64    } else {
65        size as c_uint
66    };
67
68    // Ensure minimum size
69    let buf_size = buf_size.max(MIN_BUFFER_SIZE);
70
71    let buf = unsafe { xmlMallocImpl(size_of::<_xmlBuffer>()) as *mut _xmlBuffer };
72    if buf.is_null() {
73        return ptr::null_mut();
74    }
75
76    let content = unsafe { xmlMallocImpl(buf_size as usize) as *mut xmlChar };
77    if content.is_null() {
78        unsafe { xmlFreeImpl(buf as *mut c_void) };
79        return ptr::null_mut();
80    }
81
82    // Initialize: null-terminate the empty buffer
83    unsafe {
84        ptr::write(content, 0);
85    }
86
87    unsafe {
88        ptr::write(
89            buf,
90            _xmlBuffer {
91                content,
92                use_: 0,
93                size: buf_size,
94                alloc: XML_BUFFER_ALLOC_DOUBLEIT,
95                contentIO: content, // Track original allocation for I/O mode
96            },
97        );
98    }
99
100    buf
101}
102
103/// Create a new xmlBuffer from a static string.
104///
105/// The buffer's content points directly to `str` (no copy is made).
106/// The buffer's allocation scheme is set to IMMUTABLE, meaning the
107/// content will not be freed when the buffer is freed.
108///
109/// If `size` <= 0, the length is determined by `xmlStrlen` (scanning for null).
110pub(crate) fn buf_create_static(str: *const xmlChar, size: c_int) -> *mut _xmlBuffer {
111    if str.is_null() {
112        return ptr::null_mut();
113    }
114
115    let len = if size <= 0 {
116        // Calculate length by scanning for null terminator
117        let mut len: c_uint = 0;
118        unsafe {
119            while *str.add(len as usize) != 0 {
120                len += 1;
121            }
122        }
123        len
124    } else {
125        size as c_uint
126    };
127
128    let buf = unsafe { xmlMallocImpl(size_of::<_xmlBuffer>()) as *mut _xmlBuffer };
129    if buf.is_null() {
130        return ptr::null_mut();
131    }
132
133    unsafe {
134        ptr::write(
135            buf,
136            _xmlBuffer {
137                content: str as *mut xmlChar,
138                use_: len,
139                size: len + 1, // Include space for null terminator
140                alloc: XML_BUFFER_ALLOC_IMMUTABLE,
141                contentIO: ptr::null_mut(),
142            },
143        );
144    }
145
146    buf
147}
148
149/// Free an xmlBuffer.
150///
151/// If the buffer's allocation scheme is not IMMUTABLE, the content is freed.
152/// The contentIO pointer (if non-NULL and different from content) is also freed.
153pub(crate) fn buf_free(buf: *mut _xmlBuffer) {
154    if buf.is_null() {
155        return;
156    }
157
158    unsafe {
159        let alloc = (*buf).alloc;
160        let content = (*buf).content;
161        let content_io = (*buf).contentIO;
162
163        if alloc != XML_BUFFER_ALLOC_IMMUTABLE {
164            // contentIO is the original allocation base (set in I/O mode).
165            // content may have been advanced during reads.
166            // Free the base pointer, not the possibly-advanced content.
167            let base = if !content_io.is_null() {
168                content_io
169            } else {
170                content
171            };
172            if !base.is_null() {
173                xmlFreeImpl(base as *mut c_void);
174            }
175        }
176
177        xmlFreeImpl(buf as *mut c_void);
178    }
179}
180
181/// Empty an xmlBuffer (reset `use_` to 0).
182///
183/// The content is kept allocated but the first byte is set to null terminator.
184pub(crate) fn buf_empty(buf: *mut _xmlBuffer) {
185    if buf.is_null() {
186        return;
187    }
188
189    unsafe {
190        (*buf).use_ = 0;
191        if !(*buf).content.is_null() {
192            ptr::write((*buf).content, 0);
193        }
194    }
195}
196
197/// Get the content of an xmlBuffer.
198pub(crate) fn buf_content(buf: *mut _xmlBuffer) -> *mut xmlChar {
199    if buf.is_null() {
200        return ptr::null_mut();
201    }
202    unsafe { (*buf).content }
203}
204
205/// Get the used length of an xmlBuffer.
206pub(crate) fn buf_length(buf: *mut _xmlBuffer) -> c_int {
207    if buf.is_null() {
208        return -1;
209    }
210    unsafe { (*buf).use_ as c_int }
211}
212
213/// Write `len` bytes from `str` to an xmlBuffer.
214///
215/// Grows the buffer if needed. Always maintains null termination.
216/// Returns the number of bytes written, or -1 on error.
217pub(crate) fn buf_add(buf: *mut _xmlBuffer, str: *const xmlChar, len: c_int) -> c_int {
218    if buf.is_null() || str.is_null() || len <= 0 {
219        return 0;
220    }
221
222    let len = len as c_uint;
223    let b = unsafe { &mut *buf };
224
225    // IMMUTABLE buffers cannot be written to
226    if b.alloc == XML_BUFFER_ALLOC_IMMUTABLE {
227        return -1;
228    }
229
230    // Ensure capacity: need use_ + len + 1 (for null terminator)
231    let needed = b.use_.saturating_add(len).saturating_add(1);
232    if needed > b.size {
233        // Grow buffer
234        let new_size = if b.alloc == XML_BUFFER_ALLOC_EXACT {
235            needed
236        } else {
237            // DOUBLEIT or default: double until big enough
238            let mut doubled = b.size.saturating_mul(2).max(MIN_BUFFER_SIZE);
239            while doubled < needed {
240                doubled = doubled.saturating_mul(2);
241            }
242            doubled
243        };
244
245        let new_content =
246            unsafe { xmlReallocImpl(b.content as *mut c_void, new_size as usize) as *mut xmlChar };
247        if new_content.is_null() {
248            return -1;
249        }
250        b.content = new_content;
251        b.contentIO = new_content; // Track reallocated base
252        b.size = new_size;
253    }
254
255    // Copy data
256    unsafe {
257        ptr::copy_nonoverlapping(str, b.content.add(b.use_ as usize), len as usize);
258    }
259    b.use_ = b.use_.saturating_add(len);
260
261    // Null-terminate
262    unsafe {
263        ptr::write(b.content.add(b.use_ as usize), 0);
264    }
265
266    len as c_int
267}
268
269/// Cat a null-terminated string to an xmlBuffer.
270pub(crate) fn buf_cat(buf: *mut _xmlBuffer, str: *const xmlChar) -> c_int {
271    if buf.is_null() || str.is_null() {
272        return -1;
273    }
274
275    // Calculate length of the null-terminated string
276    let len = unsafe {
277        let mut i: c_uint = 0;
278        while *str.add(i as usize) != 0 {
279            i += 1;
280        }
281        i
282    };
283
284    buf_add(buf, str, len as c_int)
285}
286
287/// Write a single character to an xmlBuffer.
288pub(crate) fn buf_ccat(buf: *mut _xmlBuffer, c: xmlChar) -> c_int {
289    buf_add(buf, &c as *const xmlChar, 1)
290}
291
292/// Shrink an xmlBuffer by `len` bytes from the end.
293///
294/// If `len` exceeds the used length, the buffer is emptied.
295/// Returns the new used length, or -1 on error.
296pub(crate) fn buf_shrink(buf: *mut _xmlBuffer, len: c_uint) -> c_int {
297    if buf.is_null() {
298        return -1;
299    }
300
301    let b = unsafe { &mut *buf };
302    if b.use_ == 0 {
303        return 0;
304    }
305
306    b.use_ = if len >= b.use_ { 0 } else { b.use_ - len };
307
308    // Null-terminate
309    unsafe {
310        ptr::write(b.content.add(b.use_ as usize), 0);
311    }
312
313    b.use_ as c_int
314}
315
316/// Grow an xmlBuffer to at least `size` bytes of capacity.
317///
318/// Returns 0 on success, -1 on failure.
319/// Add data at the head of a buffer.
320///
321/// Returns 0 on success, -1 on error.
322pub(crate) fn buf_add_head(buf: *mut _xmlBuffer, str: *const xmlChar, len: c_int) -> c_int {
323    if buf.is_null() || str.is_null() || len <= 0 {
324        return -1;
325    }
326    let len = len as c_uint;
327    unsafe {
328        let b = &mut *buf;
329        let needed = b.use_.saturating_add(len).saturating_add(1);
330        if needed > b.size {
331            let new_size = needed.saturating_mul(2).max(MIN_BUFFER_SIZE);
332            let new_content =
333                xmlReallocImpl(b.content as *mut c_void, new_size as usize) as *mut xmlChar;
334            if new_content.is_null() {
335                return -1;
336            }
337            b.content = new_content;
338            b.contentIO = new_content;
339            b.size = new_size;
340        }
341        // Shift existing content right by len bytes
342        if b.use_ > 0 {
343            core::ptr::copy(b.content, b.content.add(len as usize), b.use_ as usize);
344        }
345        // Copy new content to the beginning
346        core::ptr::copy_nonoverlapping(str, b.content, len as usize);
347        b.use_ = b.use_.saturating_add(len);
348        *b.content.add(b.use_ as usize) = 0;
349    }
350    0
351}
352
353pub(crate) fn buf_grow(buf: *mut _xmlBuffer, size: c_uint) -> c_int {
354    if buf.is_null() {
355        return -1;
356    }
357
358    let b = unsafe { &mut *buf };
359
360    if size <= b.size {
361        return 0; // Already big enough
362    }
363
364    let new_content =
365        unsafe { xmlReallocImpl(b.content as *mut c_void, size as usize) as *mut xmlChar };
366    if new_content.is_null() {
367        return -1;
368    }
369
370    b.content = new_content;
371    b.contentIO = new_content;
372    b.size = size;
373
374    0
375}
376
377// ═══════════════════════════════════════════════════════════════════════════════
378// 2. xmlBuf operations (modern replacement)
379// ═══════════════════════════════════════════════════════════════════════════════
380
381/// Create a new xmlBuf with the given initial size.
382///
383/// If `size` <= 0, a default buffer size is used.
384/// Returns a pointer to the new buffer, or NULL on allocation failure.
385pub(crate) fn xml_buf_create(size: c_int) -> *mut _xmlBuf {
386    let buf_size = if size <= 0 {
387        DEFAULT_BUFFER_SIZE
388    } else {
389        size as c_uint
390    };
391    let buf_size = buf_size.max(MIN_BUFFER_SIZE);
392
393    let buf = unsafe { xmlMallocImpl(size_of::<_xmlBuf>()) as *mut _xmlBuf };
394    if buf.is_null() {
395        return ptr::null_mut();
396    }
397
398    let content = unsafe { xmlMallocImpl(buf_size as usize) as *mut xmlChar };
399    if content.is_null() {
400        unsafe { xmlFreeImpl(buf as *mut c_void) };
401        return ptr::null_mut();
402    }
403
404    unsafe {
405        ptr::write(content, 0);
406    }
407
408    unsafe {
409        ptr::write(
410            buf,
411            _xmlBuf {
412                content,
413                use_: 0,
414                size: buf_size,
415                alloc: XML_BUFFER_ALLOC_DOUBLEIT,
416                error: 0,
417                buffer: 0,
418                io: 0,
419            },
420        );
421    }
422
423    buf
424}
425
426/// Free an xmlBuf.
427///
428/// Frees the content and the buffer struct itself.
429pub(crate) fn xml_buf_free(buf: *mut _xmlBuf) {
430    if buf.is_null() {
431        return;
432    }
433
434    unsafe {
435        if !(*buf).content.is_null() {
436            xmlFreeImpl((*buf).content as *mut c_void);
437        }
438        xmlFreeImpl(buf as *mut c_void);
439    }
440}
441
442/// Get the content of an xmlBuf.
443pub(crate) fn xml_buf_content(buf: *mut _xmlBuf) -> *mut xmlChar {
444    if buf.is_null() {
445        return ptr::null_mut();
446    }
447    unsafe { (*buf).content }
448}
449
450/// Get the used length of an xmlBuf.
451pub(crate) fn xml_buf_length(buf: *mut _xmlBuf) -> c_int {
452    if buf.is_null() {
453        return -1;
454    }
455    unsafe { (*buf).use_ as c_int }
456}
457
458/// Add `len` bytes from `str` to an xmlBuf.
459///
460/// Returns the number of bytes added, or -1 on error.
461pub(crate) fn xml_buf_add(buf: *mut _xmlBuf, str: *const xmlChar, len: c_int) -> c_int {
462    if buf.is_null() || str.is_null() || len <= 0 {
463        return 0;
464    }
465
466    let len = len as c_uint;
467    let b = unsafe { &mut *buf };
468
469    let needed = b.use_.saturating_add(len).saturating_add(1);
470    if needed > b.size {
471        let new_size = needed.saturating_mul(2).max(MIN_BUFFER_SIZE);
472        let new_content =
473            unsafe { xmlReallocImpl(b.content as *mut c_void, new_size as usize) as *mut xmlChar };
474        if new_content.is_null() {
475            return -1;
476        }
477        b.content = new_content;
478        b.size = new_size;
479    }
480
481    unsafe {
482        ptr::copy_nonoverlapping(str, b.content.add(b.use_ as usize), len as usize);
483    }
484    b.use_ = b.use_.saturating_add(len);
485
486    unsafe {
487        ptr::write(b.content.add(b.use_ as usize), 0);
488    }
489
490    len as c_int
491}
492
493/// Cat a null-terminated string to an xmlBuf.
494pub(crate) fn xml_buf_cat(buf: *mut _xmlBuf, str: *const xmlChar) -> c_int {
495    if buf.is_null() || str.is_null() {
496        return -1;
497    }
498
499    let len = unsafe {
500        let mut i: c_uint = 0;
501        while *str.add(i as usize) != 0 {
502            i += 1;
503        }
504        i
505    };
506
507    xml_buf_add(buf, str, len as c_int)
508}
509
510/// Grow an xmlBuf to at least `size` bytes of capacity.
511///
512/// Returns 0 on success, -1 on failure.
513pub(crate) fn xml_buf_grow(buf: *mut _xmlBuf, size: c_uint) -> c_int {
514    if buf.is_null() {
515        return -1;
516    }
517
518    let b = unsafe { &mut *buf };
519    if size <= b.size {
520        return 0;
521    }
522
523    let new_content =
524        unsafe { xmlReallocImpl(b.content as *mut c_void, size as usize) as *mut xmlChar };
525    if new_content.is_null() {
526        return -1;
527    }
528
529    b.content = new_content;
530    b.size = size;
531    0
532}
533
534/// Shrink an xmlBuf by `len` bytes from the end.
535///
536/// Returns the new used length, or -1 on error.
537pub(crate) fn xml_buf_shrink(buf: *mut _xmlBuf, len: c_uint) -> c_int {
538    if buf.is_null() {
539        return -1;
540    }
541
542    let b = unsafe { &mut *buf };
543    if b.use_ == 0 {
544        return 0;
545    }
546
547    b.use_ = if len >= b.use_ { 0 } else { b.use_ - len };
548
549    unsafe {
550        ptr::write(b.content.add(b.use_ as usize), 0);
551    }
552
553    b.use_ as c_int
554}
555
556// ═══════════════════════════════════════════════════════════════════════════════
557// 3. Input buffer operations
558// ═══════════════════════════════════════════════════════════════════════════════
559
560/// Convert a `c_int` encoding value to an `xmlCharEncoding` enum.
561fn encoding_from_int(enc: c_int) -> xmlCharEncoding {
562    match enc {
563        -1 => xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
564        0 => xmlCharEncoding::XML_CHAR_ENCODING_NONE,
565        1 => xmlCharEncoding::XML_CHAR_ENCODING_UTF8,
566        2 => xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
567        3 => xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE,
568        4 => xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE,
569        5 => xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE,
570        6 => xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC,
571        7 => xmlCharEncoding::XML_CHAR_ENCODING_UCS4_2143,
572        8 => xmlCharEncoding::XML_CHAR_ENCODING_UCS4_3412,
573        9 => xmlCharEncoding::XML_CHAR_ENCODING_UCS2,
574        10 => xmlCharEncoding::XML_CHAR_ENCODING_8859_1,
575        11 => xmlCharEncoding::XML_CHAR_ENCODING_8859_2,
576        12 => xmlCharEncoding::XML_CHAR_ENCODING_8859_3,
577        13 => xmlCharEncoding::XML_CHAR_ENCODING_8859_4,
578        14 => xmlCharEncoding::XML_CHAR_ENCODING_8859_5,
579        15 => xmlCharEncoding::XML_CHAR_ENCODING_8859_6,
580        16 => xmlCharEncoding::XML_CHAR_ENCODING_8859_7,
581        17 => xmlCharEncoding::XML_CHAR_ENCODING_8859_8,
582        18 => xmlCharEncoding::XML_CHAR_ENCODING_8859_9,
583        19 => xmlCharEncoding::XML_CHAR_ENCODING_2022_JP,
584        20 => xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS,
585        21 => xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP,
586        22 => xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
587        _ => xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
588    }
589}
590
591/// Find an encoding handler for the given encoding integer.
592///
593/// Returns a pointer to the handler, or NULL if not found or if the
594/// encoding is NONE or UTF-8 (which don't need conversion).
595fn find_handler_for_encoding(enc: c_int) -> *mut _xmlCharEncodingHandler {
596    let enc_enum = encoding_from_int(enc);
597    if enc_enum == xmlCharEncoding::XML_CHAR_ENCODING_NONE
598        || enc_enum == xmlCharEncoding::XML_CHAR_ENCODING_UTF8
599        || enc_enum == xmlCharEncoding::XML_CHAR_ENCODING_ERROR
600    {
601        return ptr::null_mut();
602    }
603
604    // Get the encoding name and create a null-terminated version for lookup
605    if let Some(name) = encoding::encoding_name(enc_enum) {
606        // Create a null-terminated copy on the stack if small, or heap
607        let mut name_nul = name.to_vec();
608        name_nul.push(0);
609        let handler = encoding::find_encoding_handler(name_nul.as_ptr() as *const xmlChar);
610        if !handler.is_null() {
611            return handler;
612        }
613    }
614
615    ptr::null_mut()
616}
617
618/// Internal helper: create an _xmlParserInputBuffer struct.
619///
620/// Allocates the struct and initializes all fields to zero/NULL.
621/// The caller is responsible for setting the specific fields.
622fn allocate_input_buffer() -> *mut _xmlParserInputBuffer {
623    let buf =
624        unsafe { xmlMallocImpl(size_of::<_xmlParserInputBuffer>()) as *mut _xmlParserInputBuffer };
625    if buf.is_null() {
626        return ptr::null_mut();
627    }
628
629    unsafe {
630        ptr::write(
631            buf,
632            _xmlParserInputBuffer {
633                context: ptr::null_mut(),
634                readcallback: None,
635                closecallback: None,
636                encoder: ptr::null_mut(),
637                buffer: ptr::null_mut(),
638                raw: ptr::null_mut(),
639                compressed: 0,
640                error: 0,
641                rawconsumed: 0,
642            },
643        );
644    }
645
646    buf
647}
648
649/// Create an input buffer from memory.
650///
651/// The data is copied into the input buffer's internal storage.
652/// If `enc` specifies a non-UTF-8 encoding, the data is converted to UTF-8.
653pub(crate) fn input_buffer_create_mem(
654    buffer: *const c_char,
655    size: c_int,
656    enc: c_int,
657) -> *mut _xmlParserInputBuffer {
658    if buffer.is_null() || size <= 0 {
659        return ptr::null_mut();
660    }
661
662    let buf = allocate_input_buffer();
663    if buf.is_null() {
664        return ptr::null_mut();
665    }
666
667    // Create the raw buffer containing the input data
668    let raw_buf = buf_create(size);
669    if raw_buf.is_null() {
670        unsafe { xmlFreeImpl(buf as *mut c_void) };
671        return ptr::null_mut();
672    }
673
674    // Copy data into the raw buffer
675    buf_add(raw_buf, buffer as *const xmlChar, size);
676
677    // Check if encoding conversion is needed
678    let handler = find_handler_for_encoding(enc);
679    if !handler.is_null() {
680        // Encoding conversion needed
681        // Create the output (UTF-8) buffer
682        let out_buf = buf_create((size as c_uint).saturating_mul(3).max(MIN_BUFFER_SIZE) as c_int);
683        if out_buf.is_null() {
684            buf_free(raw_buf);
685            unsafe { xmlFreeImpl(buf as *mut c_void) };
686            return ptr::null_mut();
687        }
688
689        // Convert raw data to UTF-8
690        let written = encoding::char_enc_in(handler, out_buf, raw_buf);
691        if written < 0 {
692            buf_free(raw_buf);
693            buf_free(out_buf);
694            unsafe { xmlFreeImpl(buf as *mut c_void) };
695            return ptr::null_mut();
696        }
697
698        unsafe {
699            (*buf).encoder = handler as *mut c_void;
700            (*buf).buffer = out_buf as *mut c_void;
701            (*buf).raw = raw_buf as *mut c_void;
702        }
703    } else {
704        // No encoding conversion needed — data is (or will be treated as) UTF-8
705        unsafe {
706            (*buf).buffer = raw_buf as *mut c_void;
707            (*buf).raw = raw_buf as *mut c_void;
708        }
709    }
710
711    buf
712}
713
714// ── File I/O callbacks ──────────────────────────────────────────────────────
715
716/// Read callback for file descriptor-based input.
717unsafe extern "C" fn file_read_callback(
718    context: *mut c_void,
719    buffer: *mut c_char,
720    len: c_int,
721) -> c_int {
722    if context.is_null() || buffer.is_null() || len <= 0 {
723        return -1;
724    }
725
726    let fd = context as c_int;
727    let ret = libc::read(fd, buffer as *mut c_void, len as usize);
728    if ret < 0 {
729        return -1;
730    }
731    ret as c_int
732}
733
734/// Close callback for file descriptor-based input.
735unsafe extern "C" fn file_close_callback(context: *mut c_void) -> c_int {
736    if context.is_null() {
737        return -1;
738    }
739
740    let fd = context as c_int;
741    libc::close(fd)
742}
743
744/// Create an input buffer from a file.
745///
746/// Opens the file, reads its contents into memory, and creates a memory-based
747/// input buffer. The file is closed after reading.
748pub(crate) fn input_buffer_create_file(
749    filename: *const c_char,
750    enc: c_int,
751) -> *mut _xmlParserInputBuffer {
752    if filename.is_null() {
753        return ptr::null_mut();
754    }
755
756    // Get filename as a Rust string
757    let filename_str = unsafe {
758        match CStr::from_ptr(filename).to_str() {
759            Ok(s) => s,
760            Err(_) => return ptr::null_mut(),
761        }
762    };
763
764    // Open the file
765    let fd = unsafe {
766        let path_c = std::ffi::CString::new(filename_str).unwrap_or_default();
767        libc::open(path_c.as_ptr(), libc::O_RDONLY)
768    };
769
770    if fd < 0 {
771        return ptr::null_mut();
772    }
773
774    // Stat the file to get its size
775    let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
776    let stat_ret = unsafe {
777        let path_c = std::ffi::CString::new(filename_str).unwrap_or_default();
778        libc::stat(path_c.as_ptr(), &mut stat_buf)
779    };
780
781    let file_size = if stat_ret == 0 {
782        stat_buf.st_size as usize
783    } else {
784        // Fall back to reading in chunks
785        0
786    };
787
788    // Read the file contents
789    let read_size = if file_size > 0 {
790        file_size
791    } else {
792        4096 // Default chunk
793    };
794
795    let mut data = vec![0u8; read_size];
796    let mut total_read: isize = 0;
797
798    loop {
799        let remaining = read_size.saturating_sub(total_read as usize);
800        if remaining == 0 {
801            // Grow buffer
802            let new_size = read_size.saturating_mul(2);
803            data.resize(new_size, 0u8);
804        }
805
806        let ret = unsafe {
807            libc::read(
808                fd,
809                data.as_mut_ptr().add(total_read as usize) as *mut c_void,
810                remaining,
811            )
812        };
813
814        if ret < 0 {
815            // Error
816            unsafe { libc::close(fd) };
817            return ptr::null_mut();
818        }
819
820        if ret == 0 {
821            // EOF
822            break;
823        }
824
825        total_read += ret as isize;
826    }
827
828    unsafe { libc::close(fd) };
829
830    data.truncate(total_read as usize);
831
832    if data.is_empty() {
833        return ptr::null_mut();
834    }
835
836    // Create a memory-based input buffer from the data
837    input_buffer_create_mem(data.as_ptr() as *const c_char, data.len() as c_int, enc)
838}
839
840/// Create an input buffer from I/O callbacks.
841///
842/// The `ioread` callback is called to fill the raw buffer.
843/// The `ioclose` callback is called when the buffer is freed.
844pub(crate) fn input_buffer_create_io(
845    ioread: Option<xmlInputReadCallback>,
846    ioclose: Option<xmlInputCloseCallback>,
847    ioctx: *mut c_void,
848    enc: c_int,
849) -> *mut _xmlParserInputBuffer {
850    let buf = allocate_input_buffer();
851    if buf.is_null() {
852        return ptr::null_mut();
853    }
854
855    // Create the raw buffer (used for reading from callback)
856    let raw_buf = buf_create(DEFAULT_BUFFER_SIZE as c_int);
857    if raw_buf.is_null() {
858        unsafe { xmlFreeImpl(buf as *mut c_void) };
859        return ptr::null_mut();
860    }
861
862    unsafe {
863        (*buf).context = ioctx;
864        (*buf).readcallback = ioread;
865        (*buf).closecallback = ioclose;
866        (*buf).raw = raw_buf as *mut c_void;
867    }
868
869    // Set up encoder if needed
870    let handler = find_handler_for_encoding(enc);
871    if !handler.is_null() {
872        let out_buf = buf_create(DEFAULT_BUFFER_SIZE as c_int);
873        if out_buf.is_null() {
874            buf_free(raw_buf);
875            unsafe { xmlFreeImpl(buf as *mut c_void) };
876            return ptr::null_mut();
877        }
878        unsafe {
879            (*buf).encoder = handler as *mut c_void;
880            (*buf).buffer = out_buf as *mut c_void;
881        }
882    } else {
883        unsafe {
884            (*buf).buffer = raw_buf as *mut c_void;
885        }
886    }
887
888    buf
889}
890
891/// Create an input buffer from a file descriptor.
892///
893/// The buffer uses read/close callbacks that wrap `libc::read` and `libc::close`.
894pub(crate) fn input_buffer_create_fd(fd: c_int, enc: c_int) -> *mut _xmlParserInputBuffer {
895    if fd < 0 {
896        return ptr::null_mut();
897    }
898
899    input_buffer_create_io(
900        Some(file_read_callback as xmlInputReadCallback),
901        Some(file_close_callback as xmlInputCloseCallback),
902        fd as *mut c_void,
903        enc,
904    )
905}
906
907/// Free an input buffer.
908///
909/// Calls the close callback if one is set, frees all internal buffers,
910/// then frees the input buffer struct itself.
911pub(crate) fn input_buffer_free(buf: *mut _xmlParserInputBuffer) {
912    if buf.is_null() {
913        return;
914    }
915
916    unsafe {
917        // Call the close callback if one is set
918        if let Some(close_cb) = (*buf).closecallback {
919            close_cb((*buf).context);
920        }
921
922        // Free the raw buffer
923        if !(*buf).raw.is_null() {
924            buf_free((*buf).raw as *mut _xmlBuffer);
925        }
926
927        // Free the (converted) buffer if different from raw
928        if !(*buf).buffer.is_null() && (*buf).buffer != (*buf).raw {
929            buf_free((*buf).buffer as *mut _xmlBuffer);
930        }
931
932        // Note: the encoder is owned by the encoding module, not by us.
933        // We do NOT free it here.
934
935        xmlFreeImpl(buf as *mut c_void);
936    }
937}
938
939/// Read from an input buffer.
940///
941/// If the buffer has a read callback, the callback is called to fill the raw
942/// buffer, then the data is converted (if an encoder is set) and copied to
943/// `buffer`. If no read callback is set (memory-based input), data is read
944/// directly from the internal buffer.
945///
946/// Returns the number of bytes read, or -1 on error.
947pub(crate) fn input_buffer_read(
948    buf: *mut _xmlParserInputBuffer,
949    buffer: *mut c_char,
950    len: c_int,
951) -> c_int {
952    if buf.is_null() || buffer.is_null() || len <= 0 {
953        return -1;
954    }
955
956    let b = unsafe { &mut *buf };
957
958    if b.error != 0 {
959        return -1;
960    }
961
962    if let Some(read_cb) = b.readcallback {
963        // Callback-based input: read into raw buffer, then convert
964        // Read a chunk
965        let raw_buf = b.raw as *mut _xmlBuffer;
966        if raw_buf.is_null() {
967            return -1;
968        }
969
970        // Read up to `len` bytes into a temporary buffer
971        let mut tmp = vec![0u8; len as usize];
972        let ret = unsafe { read_cb(b.context, tmp.as_mut_ptr() as *mut c_char, len) };
973
974        if ret < 0 {
975            b.error = 1;
976            return -1;
977        }
978
979        if ret == 0 {
980            // EOF
981            return 0;
982        }
983
984        // Add read data to raw buffer
985        buf_add(raw_buf, tmp.as_ptr() as *const xmlChar, ret);
986
987        // If encoder is set, convert raw -> buffer
988        if !b.encoder.is_null() {
989            let out_buf = b.buffer as *mut _xmlBuffer;
990            if out_buf.is_null() {
991                return -1;
992            }
993
994            let handler = b.encoder as *mut _xmlCharEncodingHandler;
995            let conv_ret = encoding::char_enc_in(handler, out_buf, raw_buf);
996            if conv_ret < 0 {
997                b.error = 1;
998                return -1;
999            }
1000
1001            // Read from the converted buffer
1002            let out_b = unsafe { &*out_buf };
1003            let to_copy = (out_b.use_ as c_int).min(len);
1004            if to_copy > 0 {
1005                unsafe {
1006                    ptr::copy_nonoverlapping(
1007                        out_b.content,
1008                        buffer as *mut xmlChar,
1009                        to_copy as usize,
1010                    );
1011                }
1012                // Remove the copied bytes from the output buffer
1013                buf_shrink(out_buf, to_copy as c_uint);
1014            }
1015            return to_copy;
1016        }
1017
1018        // No encoder: read from raw buffer directly
1019        let raw_b = unsafe { &*raw_buf };
1020        let to_copy = (raw_b.use_ as c_int).min(len);
1021        if to_copy > 0 {
1022            unsafe {
1023                ptr::copy_nonoverlapping(raw_b.content, buffer as *mut xmlChar, to_copy as usize);
1024            }
1025            buf_shrink(raw_buf, to_copy as c_uint);
1026        }
1027        return to_copy;
1028    }
1029
1030    // Memory-based input: read directly from the buffer
1031    let src_buf = b.buffer as *mut _xmlBuffer;
1032    if src_buf.is_null() {
1033        return -1;
1034    }
1035
1036    let src = unsafe { &mut *src_buf };
1037    if src.content.is_null() || src.use_ == 0 {
1038        return 0;
1039    }
1040
1041    let to_copy = (src.use_ as c_int).min(len);
1042    if to_copy > 0 {
1043        unsafe {
1044            ptr::copy_nonoverlapping(src.content, buffer as *mut xmlChar, to_copy as usize);
1045        }
1046        // Advance the content pointer and reduce use_
1047        unsafe {
1048            src.content = src.content.add(to_copy as usize);
1049        }
1050        src.use_ = src.use_.saturating_sub(to_copy as c_uint);
1051    }
1052
1053    to_copy
1054}
1055
1056/// Push data into an input buffer (for push parser).
1057///
1058/// The data is appended to the raw buffer and, if an encoder is set,
1059/// converted to UTF-8 in the buffer.
1060pub(crate) fn input_buffer_push(
1061    buf: *mut _xmlParserInputBuffer,
1062    buffer: *const c_char,
1063    len: c_int,
1064) -> c_int {
1065    if buf.is_null() || buffer.is_null() || len <= 0 {
1066        return -1;
1067    }
1068
1069    let b = unsafe { &mut *buf };
1070
1071    if b.error != 0 {
1072        return -1;
1073    }
1074
1075    // Append to raw buffer
1076    let raw_buf = b.raw as *mut _xmlBuffer;
1077    if raw_buf.is_null() {
1078        return -1;
1079    }
1080
1081    buf_add(raw_buf, buffer as *const xmlChar, len);
1082
1083    // If encoder is set, convert raw -> buffer
1084    if !b.encoder.is_null() {
1085        let out_buf = b.buffer as *mut _xmlBuffer;
1086        if out_buf.is_null() {
1087            return -1;
1088        }
1089
1090        let handler = b.encoder as *mut _xmlCharEncodingHandler;
1091        let ret = encoding::char_enc_in(handler, out_buf, raw_buf);
1092        if ret < 0 {
1093            b.error = 1;
1094            return -1;
1095        }
1096    }
1097
1098    len
1099}
1100
1101/// Update input buffer encoding.
1102///
1103/// Sets the encoder for an input buffer. The handler must already be
1104/// properly initialized.
1105pub(crate) fn input_buffer_set_encoder(
1106    buf: *mut _xmlParserInputBuffer,
1107    handler: *mut _xmlCharEncodingHandler,
1108) {
1109    if buf.is_null() {
1110        return;
1111    }
1112
1113    unsafe {
1114        (*buf).encoder = handler as *mut c_void;
1115    }
1116}
1117
1118// ═══════════════════════════════════════════════════════════════════════════════
1119// 4. Output buffer operations
1120// ═══════════════════════════════════════════════════════════════════════════════
1121
1122/// Write callback for file descriptor-based output.
1123unsafe extern "C" fn file_write_callback(
1124    context: *mut c_void,
1125    buffer: *const c_char,
1126    len: c_int,
1127) -> c_int {
1128    if context.is_null() || buffer.is_null() || len <= 0 {
1129        return -1;
1130    }
1131
1132    let fd = context as c_int;
1133    let ret = libc::write(fd, buffer as *const c_void, len as usize);
1134    if ret < 0 {
1135        return -1;
1136    }
1137    ret as c_int
1138}
1139
1140/// Close callback for file descriptor-based output.
1141unsafe extern "C" fn file_close_output_callback(context: *mut c_void) -> c_int {
1142    if context.is_null() {
1143        return -1;
1144    }
1145
1146    let fd = context as c_int;
1147    libc::close(fd)
1148}
1149
1150/// Write callback for buffer-based output (writes into an xmlBuffer).
1151unsafe extern "C" fn buffer_write_callback(
1152    context: *mut c_void,
1153    buffer: *const c_char,
1154    len: c_int,
1155) -> c_int {
1156    if context.is_null() || buffer.is_null() || len <= 0 {
1157        return -1;
1158    }
1159
1160    let target_buf = context as *mut _xmlBuffer;
1161    buf_add(target_buf, buffer as *const xmlChar, len)
1162}
1163
1164/// Internal helper: create an _xmlOutputBuffer struct.
1165///
1166/// Allocates the struct and initializes all fields to zero/NULL.
1167fn allocate_output_buffer() -> *mut _xmlOutputBuffer {
1168    let buf = unsafe { xmlMallocImpl(size_of::<_xmlOutputBuffer>()) as *mut _xmlOutputBuffer };
1169    if buf.is_null() {
1170        return ptr::null_mut();
1171    }
1172
1173    unsafe {
1174        ptr::write(
1175            buf,
1176            _xmlOutputBuffer {
1177                context: ptr::null_mut(),
1178                writecallback: None,
1179                closecallback: None,
1180                encoder: ptr::null_mut(),
1181                buffer: ptr::null_mut(),
1182                conv: ptr::null_mut(),
1183                written: 0,
1184                error: 0,
1185            },
1186        );
1187    }
1188
1189    buf
1190}
1191
1192/// Create an output buffer for a filename.
1193///
1194/// Opens the file for writing and sets up write/close callbacks.
1195/// If `compression` is nonzero, future versions may support compression.
1196pub(crate) fn output_buffer_create_filename(
1197    URI: *const c_char,
1198    encoder: *mut _xmlCharEncodingHandler,
1199    compression: c_int,
1200) -> *mut _xmlOutputBuffer {
1201    if URI.is_null() {
1202        return ptr::null_mut();
1203    }
1204
1205    let path_str = unsafe {
1206        match CStr::from_ptr(URI).to_str() {
1207            Ok(s) => s,
1208            Err(_) => return ptr::null_mut(),
1209        }
1210    };
1211
1212    let path_c = std::ffi::CString::new(path_str).unwrap_or_default();
1213
1214    // Open file for writing (create/truncate)
1215    let fd = unsafe {
1216        libc::open(
1217            path_c.as_ptr(),
1218            libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC,
1219            0o644,
1220        )
1221    };
1222
1223    if fd < 0 {
1224        return ptr::null_mut();
1225    }
1226
1227    let obuf = allocate_output_buffer();
1228    if obuf.is_null() {
1229        unsafe { libc::close(fd) };
1230        return ptr::null_mut();
1231    }
1232
1233    let buf = buf_create(DEFAULT_BUFFER_SIZE as c_int);
1234    if buf.is_null() {
1235        unsafe {
1236            libc::close(fd);
1237            xmlFreeImpl(obuf as *mut c_void);
1238        }
1239        return ptr::null_mut();
1240    }
1241
1242    let conv_buf = buf_create(DEFAULT_BUFFER_SIZE as c_int);
1243    if conv_buf.is_null() {
1244        unsafe {
1245            libc::close(fd);
1246            buf_free(buf);
1247            xmlFreeImpl(obuf as *mut c_void);
1248        }
1249        return ptr::null_mut();
1250    }
1251
1252    unsafe {
1253        (*obuf).context = fd as *mut c_void;
1254        (*obuf).writecallback = Some(file_write_callback as xmlOutputWriteCallback);
1255        (*obuf).closecallback = Some(file_close_output_callback as xmlOutputCloseCallback);
1256        (*obuf).encoder = encoder as *mut c_void;
1257        (*obuf).buffer = buf as *mut c_void;
1258        (*obuf).conv = conv_buf as *mut c_void;
1259        (*obuf).written = 0;
1260        (*obuf).error = 0;
1261    }
1262
1263    obuf
1264}
1265
1266/// Create an output buffer for a file descriptor.
1267pub(crate) fn output_buffer_create_fd(
1268    fd: c_int,
1269    encoder: *mut _xmlCharEncodingHandler,
1270) -> *mut _xmlOutputBuffer {
1271    if fd < 0 {
1272        return ptr::null_mut();
1273    }
1274
1275    let obuf = allocate_output_buffer();
1276    if obuf.is_null() {
1277        return ptr::null_mut();
1278    }
1279
1280    let buf = buf_create(DEFAULT_BUFFER_SIZE as c_int);
1281    if buf.is_null() {
1282        unsafe { xmlFreeImpl(obuf as *mut c_void) };
1283        return ptr::null_mut();
1284    }
1285
1286    let conv_buf = buf_create(DEFAULT_BUFFER_SIZE as c_int);
1287    if conv_buf.is_null() {
1288        unsafe {
1289            buf_free(buf);
1290            xmlFreeImpl(obuf as *mut c_void);
1291        }
1292        return ptr::null_mut();
1293    }
1294
1295    unsafe {
1296        (*obuf).context = fd as *mut c_void;
1297        (*obuf).writecallback = Some(file_write_callback as xmlOutputWriteCallback);
1298        (*obuf).closecallback = Some(file_close_output_callback as xmlOutputCloseCallback);
1299        (*obuf).encoder = encoder as *mut c_void;
1300        (*obuf).buffer = buf as *mut c_void;
1301        (*obuf).conv = conv_buf as *mut c_void;
1302        (*obuf).written = 0;
1303        (*obuf).error = 0;
1304    }
1305
1306    obuf
1307}
1308
1309/// Create an output buffer from I/O callbacks.
1310pub(crate) fn output_buffer_create_io(
1311    iowrite: Option<xmlOutputWriteCallback>,
1312    ioclose: Option<xmlOutputCloseCallback>,
1313    ioctx: *mut c_void,
1314    encoder: *mut _xmlCharEncodingHandler,
1315) -> *mut _xmlOutputBuffer {
1316    let obuf = allocate_output_buffer();
1317    if obuf.is_null() {
1318        return ptr::null_mut();
1319    }
1320
1321    let buf = buf_create(DEFAULT_BUFFER_SIZE as c_int);
1322    if buf.is_null() {
1323        unsafe { xmlFreeImpl(obuf as *mut c_void) };
1324        return ptr::null_mut();
1325    }
1326
1327    let conv_buf = buf_create(DEFAULT_BUFFER_SIZE as c_int);
1328    if conv_buf.is_null() {
1329        unsafe {
1330            buf_free(buf);
1331            xmlFreeImpl(obuf as *mut c_void);
1332        }
1333        return ptr::null_mut();
1334    }
1335
1336    unsafe {
1337        (*obuf).context = ioctx;
1338        (*obuf).writecallback = iowrite;
1339        (*obuf).closecallback = ioclose;
1340        (*obuf).encoder = encoder as *mut c_void;
1341        (*obuf).buffer = buf as *mut c_void;
1342        (*obuf).conv = conv_buf as *mut c_void;
1343        (*obuf).written = 0;
1344        (*obuf).error = 0;
1345    }
1346
1347    obuf
1348}
1349
1350/// Create an output buffer from a pre-existing xmlBuffer.
1351///
1352/// Writes to the output buffer will be appended to the given `_xmlBuffer`.
1353pub(crate) fn output_buffer_create_buffer(
1354    target_buf: *mut _xmlBuffer,
1355    encoder: *mut _xmlCharEncodingHandler,
1356) -> *mut _xmlOutputBuffer {
1357    if target_buf.is_null() {
1358        return ptr::null_mut();
1359    }
1360
1361    let obuf = allocate_output_buffer();
1362    if obuf.is_null() {
1363        return ptr::null_mut();
1364    }
1365
1366    // Internal buffer for buffering writes before flush
1367    let internal_buf = buf_create(DEFAULT_BUFFER_SIZE as c_int);
1368    if internal_buf.is_null() {
1369        unsafe { xmlFreeImpl(obuf as *mut c_void) };
1370        return ptr::null_mut();
1371    }
1372
1373    // Conversion buffer (used when encoder is present)
1374    let conv_buf = buf_create(DEFAULT_BUFFER_SIZE as c_int);
1375    if conv_buf.is_null() {
1376        unsafe {
1377            buf_free(internal_buf);
1378            xmlFreeImpl(obuf as *mut c_void);
1379        }
1380        return ptr::null_mut();
1381    }
1382
1383    unsafe {
1384        (*obuf).context = target_buf as *mut c_void;
1385        (*obuf).writecallback = Some(buffer_write_callback as xmlOutputWriteCallback);
1386        (*obuf).closecallback = None;
1387        (*obuf).encoder = encoder as *mut c_void;
1388        (*obuf).buffer = internal_buf as *mut c_void;
1389        (*obuf).conv = conv_buf as *mut c_void;
1390        (*obuf).written = 0;
1391        (*obuf).error = 0;
1392    }
1393
1394    obuf
1395}
1396
1397/// Flush an output buffer.
1398///
1399/// Encodes the buffered data (if an encoder is set) and writes it via the
1400/// write callback. Resets the internal buffer after writing.
1401///
1402/// Returns the number of bytes written, or -1 on error.
1403pub(crate) fn output_buffer_flush(out: *mut _xmlOutputBuffer) -> c_int {
1404    if out.is_null() {
1405        return -1;
1406    }
1407
1408    let ob = unsafe { &mut *out };
1409
1410    if ob.error != 0 {
1411        return -1;
1412    }
1413
1414    let buf = ob.buffer as *mut _xmlBuffer;
1415    if buf.is_null() {
1416        return 0;
1417    }
1418
1419    let b = unsafe { &*buf };
1420    if b.use_ == 0 {
1421        return 0;
1422    }
1423
1424    let write_cb = match ob.writecallback {
1425        Some(cb) => cb,
1426        None => {
1427            // No write callback — just clear the buffer
1428            buf_empty(buf);
1429            return 0;
1430        }
1431    };
1432
1433    let total_written = if !ob.encoder.is_null() {
1434        // Convert buffer content via encoder
1435        let handler = ob.encoder as *mut _xmlCharEncodingHandler;
1436        let conv = ob.conv as *mut _xmlBuffer;
1437
1438        // Ensure conv buffer is empty before converting
1439        buf_empty(conv);
1440
1441        let ret = encoding::char_enc_out(handler, conv, buf);
1442        if ret < 0 {
1443            ob.error = 1;
1444            return -1;
1445        }
1446
1447        // Write converted data via callback
1448        let conv_b = unsafe { &*conv };
1449        if conv_b.use_ > 0 {
1450            let written = unsafe {
1451                write_cb(
1452                    ob.context,
1453                    conv_b.content as *const c_char,
1454                    conv_b.use_ as c_int,
1455                )
1456            };
1457
1458            if written < 0 {
1459                ob.error = 1;
1460                return -1;
1461            }
1462
1463            ob.written = ob.written.saturating_add(written);
1464            buf_empty(conv);
1465            written
1466        } else {
1467            0
1468        }
1469    } else {
1470        // No encoder: write buffer content directly
1471        let written = unsafe { write_cb(ob.context, b.content as *const c_char, b.use_ as c_int) };
1472
1473        if written < 0 {
1474            ob.error = 1;
1475            return -1;
1476        }
1477
1478        ob.written = ob.written.saturating_add(written);
1479        written
1480    };
1481
1482    // Clear the buffer after writing
1483    buf_empty(buf);
1484
1485    total_written
1486}
1487
1488/// Free an output buffer.
1489///
1490/// Flushes any pending data, calls the close callback if set,
1491/// frees all internal buffers, then frees the output buffer struct.
1492pub(crate) fn output_buffer_close(out: *mut _xmlOutputBuffer) -> c_int {
1493    if out.is_null() {
1494        return -1;
1495    }
1496
1497    let ob = unsafe { &mut *out };
1498
1499    // Flush any pending data
1500    let flush_ret = output_buffer_flush(out);
1501
1502    // Call the close callback
1503    if let Some(close_cb) = ob.closecallback {
1504        unsafe {
1505            close_cb(ob.context);
1506        }
1507    }
1508
1509    // Free internal buffers
1510    if !ob.buffer.is_null() {
1511        buf_free(ob.buffer as *mut _xmlBuffer);
1512    }
1513    if !ob.conv.is_null() {
1514        buf_free(ob.conv as *mut _xmlBuffer);
1515    }
1516
1517    // Note: encoder is owned by the caller/encoding module, not by us
1518
1519    unsafe { xmlFreeImpl(out as *mut c_void) };
1520
1521    flush_ret
1522}
1523
1524/// Write to an output buffer.
1525///
1526/// The data is appended to the internal buffer; when a write callback is
1527/// installed and the buffered size reaches the upstream threshold
1528/// (`MINLEN` = 256), the buffered data is pushed through the callback.
1529///
1530/// # UPSTREAM-PARITY
1531///
1532/// Upstream 2.15 `xmlOutputBufferWrite` returns the number of bytes written
1533/// to the I/O channel **in this call** — 0 when the data merely landed in
1534/// the internal buffer (observable on the system DSO; verified by the
1535/// SAVE-001 differential court). With no write callback, `len` is returned.
1536///
1537/// Returns the bytes written through the callback (possibly 0), or -1 on error.
1538pub(crate) fn output_buffer_write(
1539    out: *mut _xmlOutputBuffer,
1540    len: c_int,
1541    data: *const c_char,
1542) -> c_int {
1543    if out.is_null() || data.is_null() || len <= 0 {
1544        return -1;
1545    }
1546
1547    let ob = unsafe { &mut *out };
1548
1549    if ob.error != 0 {
1550        return -1;
1551    }
1552
1553    let buf = ob.buffer as *mut _xmlBuffer;
1554    if buf.is_null() {
1555        return -1;
1556    }
1557
1558    let ret = buf_add(buf, data as *const xmlChar, len);
1559    if ret < 0 {
1560        ob.error = 1;
1561        return -1;
1562    }
1563
1564    if ob.writecallback.is_none() {
1565        return len; // no I/O channel: upstream returns len
1566    }
1567
1568    // Push buffered data through the callback once it reaches the upstream
1569    // MINLEN threshold; otherwise the write is only buffered and upstream
1570    // reports 0 bytes written in this call.
1571    let b = unsafe { &*buf };
1572    if b.use_ < MIN_BUFFER_SIZE {
1573        return 0;
1574    }
1575    output_buffer_flush(out)
1576}
1577
1578/// Write a null-terminated string to an output buffer.
1579pub(crate) fn output_buffer_write_string(out: *mut _xmlOutputBuffer, str: *const c_char) -> c_int {
1580    if out.is_null() || str.is_null() {
1581        return -1;
1582    }
1583
1584    let len = unsafe {
1585        let mut i: c_int = 0;
1586        while *str.add(i as usize) != 0 {
1587            i += 1;
1588        }
1589        i
1590    };
1591
1592    output_buffer_write(out, len, str)
1593}
1594
1595/// Write a single character to an output buffer.
1596pub(crate) fn output_buffer_write_char(out: *mut _xmlOutputBuffer, c: c_char) -> c_int {
1597    output_buffer_write(out, 1, &c as *const c_char)
1598}
1599
1600/// Get the content of an output buffer's internal buffer.
1601///
1602/// Returns a pointer to the internal buffer's content, or NULL on error.
1603pub(crate) fn output_buffer_get_content(out: *mut _xmlOutputBuffer) -> *const xmlChar {
1604    if out.is_null() {
1605        return ptr::null();
1606    }
1607
1608    let ob = unsafe { &*out };
1609    let buf = ob.buffer as *mut _xmlBuffer;
1610    if buf.is_null() {
1611        return ptr::null();
1612    }
1613
1614    buf_content(buf)
1615}
1616
1617/// Get the number of bytes currently buffered (upstream xmlOutputBufferGetSize).
1618pub(crate) fn output_buffer_get_size(out: *mut _xmlOutputBuffer) -> c_int {
1619    if out.is_null() {
1620        return -1;
1621    }
1622    let ob = unsafe { &*out };
1623    let buf = ob.buffer as *mut _xmlBuffer;
1624    if buf.is_null() {
1625        return -1;
1626    }
1627    buf_length(buf)
1628}
1629
1630/// Allocate an output buffer with no I/O target (upstream xmlAllocOutputBuffer):
1631/// a fresh internal buffer and no write/close callbacks.
1632pub(crate) fn output_buffer_create(
1633    _encoder: *mut crate::abi::structs::_xmlCharEncodingHandler,
1634) -> *mut _xmlOutputBuffer {
1635    let obuf = allocate_output_buffer();
1636    if obuf.is_null() {
1637        return ptr::null_mut();
1638    }
1639    let buf = buf_create(-1);
1640    if buf.is_null() {
1641        unsafe { xmlFreeImpl(obuf as *mut c_void) };
1642        return ptr::null_mut();
1643    }
1644    unsafe {
1645        (*obuf).buffer = buf as *mut c_void;
1646        (*obuf).encoder = _encoder as *mut c_void;
1647    }
1648    obuf
1649}
1650
1651/// Create an output buffer writing to a `FILE *` (upstream
1652/// xmlOutputBufferCreateFile): the FILE becomes the I/O context, writes go
1653/// through `fwrite`, close goes through `fflush` (upstream xmlFileWrite /
1654/// xmlFileFlush).
1655///
1656/// # SAFETY
1657///
1658/// - `file` must be a valid `FILE *` or NULL.
1659pub(crate) fn output_buffer_create_file(
1660    file: *mut libc::FILE,
1661    _encoder: *mut crate::abi::structs::_xmlCharEncodingHandler,
1662) -> *mut _xmlOutputBuffer {
1663    if file.is_null() {
1664        return ptr::null_mut();
1665    }
1666    unsafe extern "C" fn file_write(ctx: *mut c_void, buffer: *const c_char, len: c_int) -> c_int {
1667        let f = ctx as *mut libc::FILE;
1668        if f.is_null() || buffer.is_null() || len <= 0 {
1669            return 0;
1670        }
1671        let n = unsafe { libc::fwrite(buffer as *const libc::c_void, 1, len as usize, f) };
1672        n as c_int
1673    }
1674    unsafe extern "C" fn file_flush(ctx: *mut c_void) -> c_int {
1675        let f = ctx as *mut libc::FILE;
1676        if f.is_null() {
1677            return -1;
1678        }
1679        unsafe { libc::fflush(f) }
1680    }
1681    let obuf = output_buffer_create(_encoder);
1682    if obuf.is_null() {
1683        return ptr::null_mut();
1684    }
1685    unsafe {
1686        (*obuf).context = file as *mut c_void;
1687        (*obuf).writecallback = Some(file_write);
1688        (*obuf).closecallback = Some(file_flush);
1689    }
1690    obuf
1691}
1692
1693/// Write to an output buffer, applying an escape callback to the string
1694/// (upstream xmlOutputBufferWriteEscape).
1695///
1696/// # UPSTREAM-PARITY
1697///
1698/// With a NULL escape callback upstream runs the string through
1699/// `xmlEscapeText(str, 0)` (xmlIO.c 2.15, codegen/escape.inc) and then
1700/// `xmlOutputBufferWrite` — so `&`/`<`/`>` and CR become entities, tab/LF
1701/// and quotes are left verbatim, and the return value follows the write
1702/// path (0 while data is only buffered).
1703///
1704/// Returns the bytes written through the callback, or -1 on error.
1705pub(crate) fn output_buffer_write_escape(
1706    out: *mut _xmlOutputBuffer,
1707    str: *const xmlChar,
1708    escaping: Option<unsafe extern "C" fn(*mut u8, *mut c_int, *const u8, *mut c_int) -> c_int>,
1709) -> c_int {
1710    if out.is_null() || str.is_null() {
1711        return -1;
1712    }
1713    if escaping.is_none() {
1714        // Upstream xmlEscapeText(str, 0): only & < > and CR are escaped.
1715        // SAFETY: escape_text reads `str` and allocates a fresh copy.
1716        let escaped = unsafe { escape_text(str) };
1717        if escaped.is_null() {
1718            unsafe {
1719                let ob = &mut *out;
1720                ob.error = 1;
1721            }
1722            return -1;
1723        }
1724        let len = unsafe { libc::strlen(escaped as *const libc::c_char) as c_int };
1725        let ret = output_buffer_write(out, len, escaped as *const c_char);
1726        unsafe { libc::free(escaped as *mut libc::c_void) };
1727        return ret;
1728    }
1729    let ob = unsafe { &mut *out };
1730    if ob.error != 0 {
1731        return -1;
1732    }
1733    let buf = ob.buffer as *mut _xmlBuffer;
1734    if buf.is_null() {
1735        return -1;
1736    }
1737    let mut inlen = unsafe { libc::strlen(str as *const libc::c_char) as c_int };
1738    let mut inpos = 0i32;
1739    let mut total = 0i32;
1740    while inpos < inlen {
1741        let mut outbuf = [0u8; 1024];
1742        let mut outlen = 1024i32;
1743        let chunk_in = unsafe { str.add(inpos as usize) };
1744        let mut chunk_len = inlen - inpos;
1745        // SAFETY: escaping is a valid callback; buffers are valid for the call.
1746        let ret = unsafe {
1747            escaping.unwrap()(outbuf.as_mut_ptr(), &mut outlen, chunk_in, &mut chunk_len)
1748        };
1749        if ret < 0 || outlen < 0 {
1750            ob.error = 1;
1751            return -1;
1752        }
1753        if outlen > 0 {
1754            let r = output_buffer_write(out, outlen, outbuf.as_ptr() as *const c_char);
1755            if r < 0 {
1756                ob.error = 1;
1757                return -1;
1758            }
1759            total += r;
1760        }
1761        if chunk_len <= 0 {
1762            break; // escape consumed nothing: avoid an infinite loop
1763        }
1764        inpos += chunk_len;
1765        if inpos > inlen {
1766            break;
1767        }
1768    }
1769    total
1770}
1771
1772/// Upstream `xmlEscapeText(str, 0)` (xmlIO.c 2.15, codegen/escape.inc):
1773/// escapes `&`/`<`/`>` and CR; tab/LF/quotes pass through; multi-byte UTF-8
1774/// is copied verbatim (no XML_ESCAPE_NON_ASCII flag). Returns a
1775/// heap-allocated NUL-terminated string (caller frees).
1776unsafe fn escape_text(str: *const xmlChar) -> *mut xmlChar {
1777    if str.is_null() {
1778        return core::ptr::null_mut();
1779    }
1780    let mut out = Vec::<u8>::with_capacity(64);
1781    let mut cur = str;
1782    loop {
1783        let c = unsafe { *cur };
1784        if c == 0 {
1785            break;
1786        }
1787        match c {
1788            b'&' => out.extend_from_slice(b"&amp;"),
1789            b'<' => out.extend_from_slice(b"&lt;"),
1790            b'>' => out.extend_from_slice(b"&gt;"),
1791            0x0d => out.extend_from_slice(b"&#13;"),
1792            _ => {
1793                // Copy a whole UTF-8 sequence verbatim (upstream copies
1794                // bytes until the next escapable char).
1795                let len = utf8_seq_len(c);
1796                for _ in 0..len {
1797                    let b = unsafe { *cur };
1798                    if b == 0 {
1799                        break;
1800                    }
1801                    out.push(b);
1802                    cur = cur.add(1);
1803                }
1804                continue;
1805            }
1806        }
1807        cur = cur.add(1);
1808    }
1809    out.push(0);
1810    let p = libc::malloc(out.len()) as *mut xmlChar;
1811    if p.is_null() {
1812        return core::ptr::null_mut();
1813    }
1814    unsafe {
1815        libc::memcpy(
1816            p as *mut libc::c_void,
1817            out.as_ptr() as *const libc::c_void,
1818            out.len(),
1819        );
1820    }
1821    p
1822}
1823
1824/// Byte length of the UTF-8 sequence starting with `lead` (1 when invalid).
1825fn utf8_seq_len(lead: u8) -> usize {
1826    match lead {
1827        0x00..=0x7f => 1,
1828        0xc2..=0xdf => 2,
1829        0xe0..=0xef => 3,
1830        0xf0..=0xf4 => 4,
1831        _ => 1,
1832    }
1833}
1834
1835// ═══════════════════════════════════════════════════════════════════════════════
1836// 5. I/O helper functions
1837// ═══════════════════════════════════════════════════════════════════════════════
1838
1839/// Check if a file exists.
1840///
1841/// Returns 1 if the file exists, 0 if not, -1 on error.
1842pub(crate) fn check_file_exists(filename: *const c_char) -> c_int {
1843    if filename.is_null() {
1844        return -1;
1845    }
1846
1847    let path_str = unsafe {
1848        match CStr::from_ptr(filename).to_str() {
1849            Ok(s) => s,
1850            Err(_) => return -1,
1851        }
1852    };
1853
1854    let path_c = match std::ffi::CString::new(path_str) {
1855        Ok(c) => c,
1856        Err(_) => return -1,
1857    };
1858
1859    let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
1860    let ret = unsafe { libc::stat(path_c.as_ptr(), &mut stat_buf) };
1861
1862    if ret == 0 {
1863        1
1864    } else {
1865        0
1866    }
1867}
1868
1869/// Read a file into memory.
1870///
1871/// Reads the entire file contents into a newly allocated buffer.
1872/// Returns a pointer to the buffer, or NULL on failure.
1873/// The size of the buffer is stored in `size` if it's non-NULL.
1874///
1875/// The returned buffer must be freed with `xmlFree`.
1876pub(crate) fn read_file_to_memory(filename: *const c_char, size: *mut c_int) -> *mut c_char {
1877    if filename.is_null() {
1878        return ptr::null_mut();
1879    }
1880
1881    let path_str = unsafe {
1882        match CStr::from_ptr(filename).to_str() {
1883            Ok(s) => s,
1884            Err(_) => return ptr::null_mut(),
1885        }
1886    };
1887
1888    let path_c = match std::ffi::CString::new(path_str) {
1889        Ok(c) => c,
1890        Err(_) => return ptr::null_mut(),
1891    };
1892
1893    let fd = unsafe { libc::open(path_c.as_ptr(), libc::O_RDONLY) };
1894    if fd < 0 {
1895        return ptr::null_mut();
1896    }
1897
1898    // Stat to get file size
1899    let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
1900    let file_size = if unsafe { libc::stat(path_c.as_ptr(), &mut stat_buf) } == 0 {
1901        stat_buf.st_size as usize
1902    } else {
1903        0
1904    };
1905
1906    // Read in chunks
1907    let chunk_size = 4096usize;
1908    let initial_capacity = if file_size > 0 { file_size } else { chunk_size };
1909
1910    let mut data = Vec::with_capacity(initial_capacity);
1911    let mut buf = vec![0u8; chunk_size];
1912
1913    loop {
1914        let ret = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut c_void, chunk_size) };
1915
1916        if ret < 0 {
1917            unsafe { libc::close(fd) };
1918            return ptr::null_mut();
1919        }
1920
1921        if ret == 0 {
1922            break; // EOF
1923        }
1924
1925        data.extend_from_slice(&buf[..ret as usize]);
1926    }
1927
1928    unsafe { libc::close(fd) };
1929
1930    if data.is_empty() {
1931        return ptr::null_mut();
1932    }
1933
1934    // Allocate via xmlMalloc and copy
1935    let result = unsafe { xmlMallocImpl(data.len()) as *mut c_char };
1936    if result.is_null() {
1937        return ptr::null_mut();
1938    }
1939
1940    unsafe {
1941        ptr::copy_nonoverlapping(data.as_ptr(), result as *mut u8, data.len());
1942    }
1943
1944    if !size.is_null() {
1945        unsafe {
1946            *size = data.len() as c_int;
1947        }
1948    }
1949
1950    result
1951}
1952
1953/// Write memory to a file.
1954///
1955/// Creates or truncates the file and writes `size` bytes from `data`.
1956/// Returns 0 on success, -1 on error.
1957pub(crate) fn write_memory_to_file(
1958    filename: *const c_char,
1959    data: *const c_char,
1960    size: c_int,
1961) -> c_int {
1962    if filename.is_null() || data.is_null() || size <= 0 {
1963        return -1;
1964    }
1965
1966    let path_str = unsafe {
1967        match CStr::from_ptr(filename).to_str() {
1968            Ok(s) => s,
1969            Err(_) => return -1,
1970        }
1971    };
1972
1973    let path_c = match std::ffi::CString::new(path_str) {
1974        Ok(c) => c,
1975        Err(_) => return -1,
1976    };
1977
1978    let fd = unsafe {
1979        libc::open(
1980            path_c.as_ptr(),
1981            libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC,
1982            0o644,
1983        )
1984    };
1985
1986    if fd < 0 {
1987        return -1;
1988    }
1989
1990    let mut remaining = size as usize;
1991    let mut offset: usize = 0;
1992
1993    while remaining > 0 {
1994        let ret = unsafe {
1995            libc::write(
1996                fd,
1997                (data as *const u8).add(offset) as *const c_void,
1998                remaining,
1999            )
2000        };
2001
2002        if ret < 0 {
2003            unsafe { libc::close(fd) };
2004            return -1;
2005        }
2006
2007        let written = ret as usize;
2008        remaining -= written;
2009        offset += written;
2010    }
2011
2012    unsafe { libc::close(fd) };
2013    0
2014}
2015
2016/// Get the current working directory.
2017///
2018/// Returns a newly allocated null-terminated string, or NULL on failure.
2019/// The returned pointer must be freed with `xmlFree`.
2020pub(crate) fn get_cwd() -> *mut c_char {
2021    // Use a reasonable initial buffer size
2022    let mut size: usize = 1024;
2023
2024    loop {
2025        let buf = unsafe { xmlMallocImpl(size) as *mut c_char };
2026        if buf.is_null() {
2027            return ptr::null_mut();
2028        }
2029
2030        let ret = unsafe { libc::getcwd(buf as *mut c_char, size) };
2031        if !ret.is_null() {
2032            return buf;
2033        }
2034
2035        unsafe { xmlFreeImpl(buf as *mut c_void) };
2036
2037        // Check if the error was ERANGE (buffer too small)
2038        let err = std::io::Error::last_os_error();
2039        if err.raw_os_error() == Some(libc::ERANGE) {
2040            size = size.saturating_mul(2);
2041            if size > 65536 {
2042                return ptr::null_mut(); // Sanity cap
2043            }
2044        } else {
2045            return ptr::null_mut();
2046        }
2047    }
2048}
2049
2050// ═══════════════════════════════════════════════════════════════════════════════
2051// Tests
2052// ═══════════════════════════════════════════════════════════════════════════════
2053
2054#[cfg(test)]
2055mod tests {
2056    use super::*;
2057    use std::ffi::CString;
2058    use std::os::raw::c_char;
2059
2060    // ── Helpers ────────────────────────────────────────────────────────────
2061
2062    fn c(s: &str) -> CString {
2063        CString::new(s).unwrap()
2064    }
2065
2066    /// Create a CString from raw bytes (may contain non-ASCII).
2067    unsafe fn c_bytes(bytes: &[u8]) -> CString {
2068        CString::from_vec_unchecked(bytes.to_vec())
2069    }
2070
2071    /// Interpret a &[u8] as &[i8] for comparison with c_char buffers.
2072    fn i8_slice(s: &[u8]) -> &[i8] {
2073        unsafe { std::slice::from_raw_parts(s.as_ptr() as *const i8, s.len()) }
2074    }
2075
2076    // ── xmlBuffer tests ────────────────────────────────────────────────────
2077
2078    #[test]
2079    fn test_buf_create_free() {
2080        let buf = buf_create(100);
2081        assert!(!buf.is_null());
2082
2083        let b = unsafe { &*buf };
2084        assert!(!b.content.is_null());
2085        assert_eq!(b.use_, 0);
2086        assert!(b.size >= 100);
2087        assert_eq!(b.alloc, XML_BUFFER_ALLOC_DOUBLEIT);
2088
2089        // Content should be null-terminated empty string
2090        unsafe {
2091            assert_eq!(*b.content, 0);
2092        }
2093
2094        buf_free(buf);
2095    }
2096
2097    #[test]
2098    fn test_buf_create_default_size() {
2099        let buf = buf_create(0);
2100        assert!(!buf.is_null());
2101
2102        let b = unsafe { &*buf };
2103        assert!(b.size >= MIN_BUFFER_SIZE);
2104
2105        buf_free(buf);
2106    }
2107
2108    #[test]
2109    fn test_buf_create_static() {
2110        let s: &[u8] = b"hello\0";
2111        let buf = buf_create_static(s.as_ptr() as *const xmlChar, 5);
2112        assert!(!buf.is_null());
2113
2114        let b = unsafe { &*buf };
2115        assert_eq!(b.use_, 5);
2116        assert_eq!(b.alloc, XML_BUFFER_ALLOC_IMMUTABLE);
2117
2118        // Content should point to the original string
2119        unsafe {
2120            assert_eq!(*b.content.offset(0), b'h');
2121            assert_eq!(*b.content.offset(4), b'o');
2122            assert_eq!(*b.content.offset(5), 0);
2123        }
2124
2125        buf_free(buf); // Should not free the static content
2126    }
2127
2128    #[test]
2129    fn test_buf_add() {
2130        let buf = buf_create(10);
2131        assert!(!buf.is_null());
2132
2133        let s1: &[u8] = b"Hello\0";
2134        let ret = buf_add(buf, s1.as_ptr() as *const xmlChar, 5);
2135        assert_eq!(ret, 5);
2136
2137        let b = unsafe { &*buf };
2138        assert_eq!(b.use_, 5);
2139        unsafe {
2140            assert_eq!(*b.content.offset(0), b'H');
2141            assert_eq!(*b.content.offset(4), b'o');
2142            assert_eq!(*b.content.offset(5), 0); // null-terminated
2143        }
2144
2145        // Add more to trigger growth
2146        let s2: &[u8] = b" World!\0";
2147        let ret = buf_add(buf, s2.as_ptr() as *const xmlChar, 7);
2148        assert_eq!(ret, 7);
2149
2150        let b = unsafe { &*buf };
2151        assert_eq!(b.use_, 12);
2152        unsafe {
2153            assert_eq!(*b.content.offset(6), b'W');
2154            assert_eq!(*b.content.offset(11), b'!');
2155            assert_eq!(*b.content.offset(12), 0);
2156        }
2157
2158        buf_free(buf);
2159    }
2160
2161    #[test]
2162    fn test_buf_add_null() {
2163        let buf = buf_create(10);
2164        let ret = buf_add(buf, ptr::null(), 5);
2165        assert_eq!(ret, 0);
2166        buf_free(buf);
2167    }
2168
2169    #[test]
2170    fn test_buf_cat() {
2171        let buf = buf_create(10);
2172        let s: &[u8] = b"Hello\0";
2173        let ret = buf_cat(buf, s.as_ptr() as *const xmlChar);
2174        assert_eq!(ret, 5);
2175
2176        let b = unsafe { &*buf };
2177        assert_eq!(b.use_, 5);
2178
2179        buf_free(buf);
2180    }
2181
2182    #[test]
2183    fn test_buf_ccat() {
2184        let buf = buf_create(10);
2185        let ret = buf_ccat(buf, b'A' as xmlChar);
2186        assert_eq!(ret, 1);
2187
2188        let b = unsafe { &*buf };
2189        assert_eq!(b.use_, 1);
2190        unsafe {
2191            assert_eq!(*b.content, b'A');
2192        }
2193
2194        buf_free(buf);
2195    }
2196
2197    #[test]
2198    fn test_buf_empty() {
2199        let buf = buf_create(10);
2200        let s: &[u8] = b"Hello\0";
2201        buf_add(buf, s.as_ptr() as *const xmlChar, 5);
2202        assert_eq!(unsafe { &*buf }.use_, 5);
2203
2204        buf_empty(buf);
2205        let b = unsafe { &*buf };
2206        assert_eq!(b.use_, 0);
2207        unsafe {
2208            assert_eq!(*b.content, 0);
2209        }
2210
2211        buf_free(buf);
2212    }
2213
2214    #[test]
2215    fn test_buf_content() {
2216        let buf = buf_create(10);
2217        let content = buf_content(buf);
2218        assert!(!content.is_null());
2219        buf_free(buf);
2220    }
2221
2222    #[test]
2223    fn test_buf_length() {
2224        let buf = buf_create(10);
2225        assert_eq!(buf_length(buf), 0);
2226
2227        let s: &[u8] = b"Hi\0";
2228        buf_add(buf, s.as_ptr() as *const xmlChar, 2);
2229        assert_eq!(buf_length(buf), 2);
2230
2231        buf_free(buf);
2232    }
2233
2234    #[test]
2235    fn test_buf_shrink() {
2236        let buf = buf_create(10);
2237        let s: &[u8] = b"Hello World\0";
2238        buf_add(buf, s.as_ptr() as *const xmlChar, 11);
2239        assert_eq!(buf_length(buf), 11);
2240
2241        buf_shrink(buf, 5);
2242        assert_eq!(buf_length(buf), 6);
2243
2244        let b = unsafe { &*buf };
2245        unsafe {
2246            assert_eq!(*b.content.offset(6), 0); // null-terminated
2247        }
2248
2249        // Shrink more than available
2250        buf_shrink(buf, 100);
2251        assert_eq!(buf_length(buf), 0);
2252
2253        buf_free(buf);
2254    }
2255
2256    #[test]
2257    fn test_buf_grow() {
2258        let buf = buf_create(10);
2259        assert!(unsafe { &*buf }.size >= 10);
2260
2261        let ret = buf_grow(buf, 1000);
2262        assert_eq!(ret, 0);
2263        assert!(unsafe { &*buf }.size >= 1000);
2264
2265        buf_free(buf);
2266    }
2267
2268    #[test]
2269    fn test_buf_free_null() {
2270        buf_free(ptr::null_mut()); // Should not crash
2271    }
2272
2273    // ── xmlBuf tests ───────────────────────────────────────────────────────
2274
2275    #[test]
2276    fn test_xml_buf_create_free() {
2277        let buf = xml_buf_create(100);
2278        assert!(!buf.is_null());
2279
2280        let b = unsafe { &*buf };
2281        assert!(!b.content.is_null());
2282        assert_eq!(b.use_, 0);
2283        assert!(b.size >= 100);
2284        assert_eq!(b.error, 0);
2285        assert_eq!(b.buffer, 0);
2286        assert_eq!(b.io, 0);
2287
2288        xml_buf_free(buf);
2289    }
2290
2291    #[test]
2292    fn test_xml_buf_add() {
2293        let buf = xml_buf_create(10);
2294        let s: &[u8] = b"Hello\0";
2295        let ret = xml_buf_add(buf, s.as_ptr() as *const xmlChar, 5);
2296        assert_eq!(ret, 5);
2297
2298        let b = unsafe { &*buf };
2299        assert_eq!(b.use_, 5);
2300
2301        xml_buf_free(buf);
2302    }
2303
2304    #[test]
2305    fn test_xml_buf_cat() {
2306        let buf = xml_buf_create(10);
2307        let s: &[u8] = b"Hello\0";
2308        let ret = xml_buf_cat(buf, s.as_ptr() as *const xmlChar);
2309        assert_eq!(ret, 5);
2310
2311        xml_buf_free(buf);
2312    }
2313
2314    #[test]
2315    fn test_xml_buf_content() {
2316        let buf = xml_buf_create(10);
2317        let content = xml_buf_content(buf);
2318        assert!(!content.is_null());
2319        xml_buf_free(buf);
2320    }
2321
2322    #[test]
2323    fn test_xml_buf_length() {
2324        let buf = xml_buf_create(10);
2325        assert_eq!(xml_buf_length(buf), 0);
2326
2327        let s: &[u8] = b"Hi\0";
2328        xml_buf_add(buf, s.as_ptr() as *const xmlChar, 2);
2329        assert_eq!(xml_buf_length(buf), 2);
2330
2331        xml_buf_free(buf);
2332    }
2333
2334    #[test]
2335    fn test_xml_buf_grow() {
2336        let buf = xml_buf_create(10);
2337        let ret = xml_buf_grow(buf, 500);
2338        assert_eq!(ret, 0);
2339        assert!(unsafe { &*buf }.size >= 500);
2340
2341        xml_buf_free(buf);
2342    }
2343
2344    #[test]
2345    fn test_xml_buf_shrink() {
2346        let buf = xml_buf_create(10);
2347        let s: &[u8] = b"Hello\0";
2348        xml_buf_add(buf, s.as_ptr() as *const xmlChar, 5);
2349        assert_eq!(xml_buf_length(buf), 5);
2350
2351        xml_buf_shrink(buf, 3);
2352        assert_eq!(xml_buf_length(buf), 2);
2353
2354        xml_buf_free(buf);
2355    }
2356
2357    // ── Input buffer tests ─────────────────────────────────────────────────
2358
2359    #[test]
2360    fn test_input_buffer_create_mem() {
2361        let data = c("Hello XML");
2362        let buf = input_buffer_create_mem(data.as_ptr(), 9, 0); // NONE encoding
2363        assert!(!buf.is_null());
2364
2365        let b = unsafe { &*buf };
2366        assert!(b.readcallback.is_none());
2367        assert!(!b.buffer.is_null());
2368        assert_eq!(b.error, 0);
2369
2370        // Read back
2371        let mut out = [0i8; 16];
2372        let ret = input_buffer_read(buf, out.as_mut_ptr(), 16);
2373        assert_eq!(ret, 9);
2374        assert_eq!(&out[..9], i8_slice(b"Hello XML"));
2375
2376        input_buffer_free(buf);
2377    }
2378
2379    #[test]
2380    fn test_input_buffer_create_mem_empty() {
2381        let buf = input_buffer_create_mem(ptr::null(), 0, 0);
2382        assert!(buf.is_null());
2383    }
2384
2385    #[test]
2386    fn test_input_buffer_read_partial() {
2387        let data = c("Hello XML World");
2388        let buf = input_buffer_create_mem(data.as_ptr(), 15, 0);
2389        assert!(!buf.is_null());
2390
2391        // Read in two parts
2392        let mut out1 = [0i8; 5];
2393        let ret = input_buffer_read(buf, out1.as_mut_ptr(), 5);
2394        assert_eq!(ret, 5);
2395        assert_eq!(&out1[..5], i8_slice(b"Hello"));
2396
2397        let mut out2 = [0i8; 10];
2398        let ret = input_buffer_read(buf, out2.as_mut_ptr(), 10);
2399        assert_eq!(ret, 10);
2400        assert_eq!(&out2[..10], i8_slice(b" XML World"));
2401
2402        input_buffer_free(buf);
2403    }
2404
2405    #[test]
2406    fn test_input_buffer_push() {
2407        let buf = input_buffer_create_io(None, None, ptr::null_mut(), 0);
2408        assert!(!buf.is_null());
2409
2410        let data1 = c("<root>");
2411        let ret = input_buffer_push(buf, data1.as_ptr(), 6);
2412        assert_eq!(ret, 6);
2413
2414        let data2 = c("</root>");
2415        let ret = input_buffer_push(buf, data2.as_ptr(), 7);
2416        assert_eq!(ret, 7);
2417
2418        // Read back the pushed data
2419        let mut out = [0i8; 32];
2420        let ret = input_buffer_read(buf, out.as_mut_ptr(), 32);
2421        assert_eq!(ret, 13);
2422        assert_eq!(&out[..13], i8_slice(b"<root></root>"));
2423
2424        input_buffer_free(buf);
2425    }
2426
2427    #[test]
2428    fn test_input_buffer_set_encoder() {
2429        let buf = input_buffer_create_mem(ptr::null(), 0, 0);
2430        // Create a fresh buffer
2431        let data = c("test");
2432        let buf = input_buffer_create_mem(data.as_ptr(), 4, 0);
2433        assert!(!buf.is_null());
2434
2435        // Set encoder to null (no encoding)
2436        input_buffer_set_encoder(buf, ptr::null_mut());
2437        let b = unsafe { &*buf };
2438        assert!(b.encoder.is_null());
2439
2440        input_buffer_free(buf);
2441    }
2442
2443    // ── Output buffer tests ────────────────────────────────────────────────
2444
2445    #[test]
2446    fn test_output_buffer_create_buffer() {
2447        let internal_buf = buf_create(100);
2448        assert!(!internal_buf.is_null());
2449
2450        let obuf = output_buffer_create_buffer(internal_buf, ptr::null_mut());
2451        assert!(!obuf.is_null());
2452
2453        // Write data
2454        let data = c("Hello Output");
2455        // UPSTREAM-PARITY: with a write callback and < MINLEN buffered,
2456        // xmlOutputBufferWrite returns 0 (verified against the system DSO
2457        // by the SAVE-001 differential court).
2458        let ret = output_buffer_write(obuf, 12, data.as_ptr());
2459        assert_eq!(ret, 0);
2460
2461        // Flush - this writes buffered data via callback to the target buffer
2462        let flushed = output_buffer_flush(obuf);
2463        assert_eq!(flushed, 12);
2464
2465        // After flush, the internal buffer should be empty again
2466        let content = output_buffer_get_content(obuf);
2467        assert!(content.is_null() || unsafe { *content } == 0);
2468
2469        // The data was written via callback to the context buffer (internal_buf)
2470        let ctx = unsafe { (*obuf).context as *mut _xmlBuffer };
2471        let ctx_b = unsafe { &*ctx };
2472        assert_eq!(ctx_b.use_, 12);
2473        unsafe {
2474            assert_eq!(*ctx_b.content.offset(0), b'H' as xmlChar);
2475        }
2476
2477        // NOTE: output_buffer_close frees all internal buffers including
2478        // the internal_buf passed as target. Don't free it again here.
2479        output_buffer_close(obuf);
2480    }
2481
2482    #[test]
2483    fn test_output_buffer_write_string() {
2484        let internal_buf = buf_create(100);
2485        let obuf = output_buffer_create_buffer(internal_buf, ptr::null_mut());
2486        assert!(!obuf.is_null());
2487
2488        let s = c("Hello");
2489        // UPSTREAM-PARITY: buffered write returns 0 (see above).
2490        let ret = output_buffer_write_string(obuf, s.as_ptr());
2491        assert_eq!(ret, 0);
2492
2493        // output_buffer_close frees internal_buf via obuf.buffer
2494        output_buffer_close(obuf);
2495    }
2496
2497    #[test]
2498    fn test_output_buffer_write_char() {
2499        let internal_buf = buf_create(100);
2500        let obuf = output_buffer_create_buffer(internal_buf, ptr::null_mut());
2501        assert!(!obuf.is_null());
2502
2503        let ret = output_buffer_write_char(obuf, b'X' as c_char);
2504        // UPSTREAM-PARITY: buffered write returns 0 (see above).
2505        assert_eq!(ret, 0);
2506
2507        output_buffer_close(obuf);
2508    }
2509
2510    #[test]
2511    fn test_output_buffer_get_content() {
2512        let internal_buf = buf_create(100);
2513        let obuf = output_buffer_create_buffer(internal_buf, ptr::null_mut());
2514        assert!(!obuf.is_null());
2515
2516        let content = output_buffer_get_content(obuf);
2517        assert!(!content.is_null());
2518
2519        output_buffer_close(obuf);
2520    }
2521
2522    // ── File I/O tests ─────────────────────────────────────────────────────
2523
2524    #[test]
2525    fn test_check_file_exists() {
2526        // This file should exist
2527        let exists = check_file_exists(c("/dev/null").as_ptr());
2528        assert!(exists == 1);
2529
2530        // This file should not exist
2531        let not_exists = check_file_exists(c("/tmp/__nonexistent_file_xyz123__").as_ptr());
2532        assert!(not_exists == 0);
2533    }
2534
2535    #[test]
2536    fn test_read_write_file() {
2537        let tmpfile = c("/tmp/libxml_rs_test_io_file.txt");
2538
2539        // Write data to file
2540        let data = c("Hello File I/O!");
2541        let ret = write_memory_to_file(tmpfile.as_ptr(), data.as_ptr(), 15);
2542        assert_eq!(ret, 0);
2543
2544        // Check it exists
2545        assert!(check_file_exists(tmpfile.as_ptr()) == 1);
2546
2547        // Read it back
2548        let mut size: c_int = 0;
2549        let read_data = read_file_to_memory(tmpfile.as_ptr(), &mut size as *mut c_int);
2550        assert!(!read_data.is_null());
2551        assert_eq!(size, 15);
2552
2553        unsafe {
2554            let slice = std::slice::from_raw_parts(read_data as *const u8, size as usize);
2555            assert_eq!(slice, b"Hello File I/O!");
2556        }
2557
2558        unsafe { xmlFreeImpl(read_data as *mut c_void) };
2559
2560        // Clean up
2561        std::fs::remove_file("/tmp/libxml_rs_test_io_file.txt").ok();
2562    }
2563
2564    #[test]
2565    fn test_read_file_nonexistent() {
2566        let result = read_file_to_memory(
2567            c("/tmp/__nonexistent_file_xyz456__").as_ptr(),
2568            ptr::null_mut(),
2569        );
2570        assert!(result.is_null());
2571    }
2572
2573    #[test]
2574    fn test_write_file_null() {
2575        let ret = write_memory_to_file(ptr::null(), c("data").as_ptr(), 4);
2576        assert_eq!(ret, -1);
2577    }
2578
2579    #[test]
2580    fn test_get_cwd() {
2581        let cwd = get_cwd();
2582        assert!(!cwd.is_null());
2583        unsafe {
2584            let s = CStr::from_ptr(cwd);
2585            assert!(!s.to_bytes().is_empty());
2586            xmlFreeImpl(cwd as *mut c_void);
2587        }
2588    }
2589
2590    // ── Edge case tests ────────────────────────────────────────────────────
2591
2592    #[test]
2593    fn test_buf_add_large_data() {
2594        let buf = buf_create(10);
2595        let mut large_data = Vec::new();
2596        large_data.resize(5000, b'X');
2597        large_data.push(0);
2598
2599        let ret = buf_add(buf, large_data.as_ptr() as *const xmlChar, 5000);
2600        assert_eq!(ret, 5000);
2601
2602        let b = unsafe { &*buf };
2603        assert_eq!(b.use_, 5000);
2604        assert!(b.size >= 5001);
2605
2606        buf_free(buf);
2607    }
2608
2609    #[test]
2610    fn test_input_buffer_free_null() {
2611        input_buffer_free(ptr::null_mut()); // Should not crash
2612    }
2613
2614    #[test]
2615    fn test_output_buffer_close_null() {
2616        let ret = output_buffer_close(ptr::null_mut());
2617        assert_eq!(ret, -1);
2618    }
2619
2620    #[test]
2621    fn test_buf_add_to_immutable() {
2622        let s: &[u8] = b"static\0";
2623        let buf = buf_create_static(s.as_ptr() as *const xmlChar, 6);
2624        assert!(!buf.is_null());
2625
2626        // Try to add to immutable buffer
2627        let data: &[u8] = b"more\0";
2628        let ret = buf_add(buf, data.as_ptr() as *const xmlChar, 4);
2629        assert_eq!(ret, -1); // Should fail
2630
2631        buf_free(buf);
2632    }
2633
2634    // ── Encoding integration test ──────────────────────────────────────────
2635
2636    #[test]
2637    fn test_encoding_from_int() {
2638        assert_eq!(
2639            encoding_from_int(0),
2640            xmlCharEncoding::XML_CHAR_ENCODING_NONE
2641        );
2642        assert_eq!(
2643            encoding_from_int(1),
2644            xmlCharEncoding::XML_CHAR_ENCODING_UTF8
2645        );
2646        assert_eq!(
2647            encoding_from_int(10),
2648            xmlCharEncoding::XML_CHAR_ENCODING_8859_1
2649        );
2650        assert_eq!(
2651            encoding_from_int(22),
2652            xmlCharEncoding::XML_CHAR_ENCODING_ASCII
2653        );
2654        assert_eq!(
2655            encoding_from_int(999),
2656            xmlCharEncoding::XML_CHAR_ENCODING_ERROR
2657        );
2658    }
2659
2660    #[test]
2661    fn test_find_handler_for_encoding() {
2662        // UTF-8 should return null (no conversion needed)
2663        let handler = find_handler_for_encoding(1);
2664        assert!(handler.is_null());
2665
2666        // NONE should return null
2667        let handler = find_handler_for_encoding(0);
2668        assert!(handler.is_null());
2669
2670        // ERROR should return null
2671        let handler = find_handler_for_encoding(-1);
2672        assert!(handler.is_null());
2673    }
2674
2675    #[test]
2676    fn test_input_buffer_with_encoding_latin1() {
2677        // Initialize encodings
2678        encoding::init_encodings();
2679
2680        // Latin-1 byte 0xE9 = é in Latin-1, which is U+00E9 = 0xC3 0xA9 in UTF-8
2681        let latin1_data: &[u8] = &[0x48, 0x65, 0x6C, 0x6C, 0xF6, 0x00]; // "Hellö" in Latin-1
2682
2683        let buf = input_buffer_create_mem(
2684            latin1_data.as_ptr() as *const c_char,
2685            5,
2686            10, // ISO-8859-1
2687        );
2688        assert!(!buf.is_null());
2689
2690        // Read back as UTF-8
2691        let mut out = [0i8; 16];
2692        let ret = input_buffer_read(buf, out.as_mut_ptr(), 16);
2693        assert!(ret > 0);
2694
2695        // The output should be UTF-8 encoded "Hellö" = b"Hell\xC3\xB6"
2696        let expected = b"Hell\xC3\xB6";
2697        assert_eq!(&out[..ret as usize], i8_slice(expected));
2698
2699        input_buffer_free(buf);
2700    }
2701
2702    #[test]
2703    fn test_output_buffer_with_encoding() {
2704        // Initialize encodings
2705        encoding::init_encodings();
2706
2707        // Find Latin-1 encoder
2708        let enc_name: &[u8] = b"ISO-8859-1\0";
2709        let handler = encoding::find_encoding_handler(enc_name.as_ptr() as *const xmlChar);
2710        assert!(!handler.is_null(), "Latin-1 handler should be available");
2711
2712        let internal_buf = buf_create(100);
2713        let obuf = output_buffer_create_buffer(internal_buf, handler);
2714        assert!(!obuf.is_null());
2715
2716        // Write UTF-8 "Hellö" = [0x48, 0x65, 0x6C, 0x6C, 0xC3, 0xB6]
2717        let utf8_data = unsafe { c_bytes(&[0x48, 0x65, 0x6C, 0x6C, 0xC3, 0xB6]) };
2718        // UPSTREAM-PARITY: buffered write returns 0 (see above).
2719        let ret = output_buffer_write(obuf, 6, utf8_data.as_ptr());
2720        assert_eq!(ret, 0);
2721
2722        // Flush - this should convert via Latin-1 encoder
2723        let flushed = output_buffer_flush(obuf);
2724        assert!(flushed > 0);
2725
2726        // The context buffer should have the Latin-1 encoded data
2727        let ctx = unsafe { (*obuf).context as *mut _xmlBuffer };
2728        let ctx_b = unsafe { &*ctx };
2729        // Latin-1 "Hellö" = [0x48, 0x65, 0x6C, 0x6C, 0xF6]
2730        assert_eq!(ctx_b.use_, 5);
2731        unsafe {
2732            assert_eq!(*ctx_b.content.offset(0), 0x48); // 'H'
2733            assert_eq!(*ctx_b.content.offset(4), 0xF6); // 'ö'
2734        }
2735
2736        // output_buffer_close frees the internal buffer, but NOT the
2737        // context buffer (internal_buf) since that's the user's buffer.
2738        // Actually it frees obuf.buffer (a separate internal buffer) and
2739        // obuf.conv. The context buffer is NOT freed by output_buffer_close.
2740        // However, obuf.buffer was set to internal_buf in the OLD code.
2741        // With the fix, obuf.buffer is a separate internal buffer, so
2742        // we still need to free internal_buf ourselves.
2743        //
2744        // Wait -- let me check what output_buffer_close frees:
2745        // - ob.buffer: this is the internal buffer (SEPARATE from internal_buf)
2746        // - ob.conv: the conversion buffer
2747        // - The context (internal_buf) is NOT freed by output_buffer_close
2748        //
2749        // Actually, let me re-read the function...
2750        // output_buffer_close frees ob.buffer and ob.conv.
2751        // The context is the user's buffer (internal_buf), which is NOT freed.
2752        // So we DO need to free internal_buf here.
2753        output_buffer_close(obuf);
2754        buf_free(internal_buf);
2755    }
2756
2757    // ── Input buffer from fd (requires /dev/null) ──────────────────────────
2758
2759    #[test]
2760    fn test_input_buffer_create_fd() {
2761        // Open /dev/null and create an fd-based input buffer
2762        let fd =
2763            unsafe { libc::open(b"/dev/null\0" as *const u8 as *const c_char, libc::O_RDONLY) };
2764        assert!(fd >= 0);
2765
2766        let buf = input_buffer_create_fd(fd, 0);
2767        assert!(!buf.is_null());
2768
2769        // Reading from /dev/null should return 0 (EOF)
2770        let mut out = [0i8; 16];
2771        let ret = input_buffer_read(buf, out.as_mut_ptr(), 16);
2772        assert_eq!(ret, 0);
2773
2774        input_buffer_free(buf); // This will also close the fd via closecallback
2775    }
2776
2777    // ── Input buffer create_io ─────────────────────────────────────────────
2778
2779    #[test]
2780    fn test_input_buffer_create_io() {
2781        // Create a simple callback that provides data
2782        static mut TEST_DATA: &[u8] = b"Hello from callback!";
2783        static mut CALLED: bool = false;
2784
2785        unsafe extern "C" fn test_read(
2786            _ctx: *mut c_void,
2787            buffer: *mut c_char,
2788            len: c_int,
2789        ) -> c_int {
2790            if CALLED {
2791                return 0; // EOF on second call
2792            }
2793            CALLED = true;
2794            let data = TEST_DATA;
2795            let to_copy = (data.len() as c_int).min(len);
2796            if to_copy > 0 {
2797                std::ptr::copy_nonoverlapping(data.as_ptr(), buffer as *mut u8, to_copy as usize);
2798            }
2799            to_copy
2800        }
2801
2802        unsafe extern "C" fn test_close(_ctx: *mut c_void) -> c_int {
2803            0
2804        }
2805
2806        let buf = input_buffer_create_io(
2807            Some(test_read as xmlInputReadCallback),
2808            Some(test_close as xmlInputCloseCallback),
2809            ptr::null_mut(),
2810            0,
2811        );
2812        assert!(!buf.is_null());
2813
2814        let mut out = [0i8; 32];
2815        let ret = input_buffer_read(buf, out.as_mut_ptr(), 32);
2816        assert!(ret > 0);
2817
2818        input_buffer_free(buf);
2819    }
2820}