Skip to main content

libxml_rs/xml/encoding/
mod.rs

1//! Character encoding handling (§22, §85 Phase 4).
2//!
3//! Encoding detection, XML declaration encoding, BOM behavior, UTF-8/UTF-16
4//! validity, legacy encodings, conversion errors, output conversion,
5//! serializer fallback, custom encoding handlers.
6//!
7//! # Architecture
8//!
9//! ```text
10//! ABI exports (exports_xml2.rs)  ←  pub(crate) functions in this module
11//!                                           ↕
12//!                           Encoding handler registry (global RwLock)
13//!                                           ↕
14//!              Built-in handlers: UTF-8, UTF-16LE, UTF-16BE, Latin-1, ASCII
15//! ```
16//!
17//! The internal encoding is always UTF-8. All conversions go to/from UTF-8.
18//! The handler registry stores `_xmlCharEncodingHandler` structs that contain
19//! function pointers for input (→UTF-8) and output (UTF-8→) conversion.
20
21#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
22
23use std::ffi::CStr;
24use std::os::raw::{c_char, c_int, c_uchar, c_uint, c_void};
25use std::ptr;
26use std::sync::atomic::{AtomicBool, Ordering};
27
28use once_cell::sync::Lazy;
29use parking_lot::RwLock;
30
31use crate::abi::allocator::{xmlFreeImpl, xmlMallocImpl, xmlReallocImpl};
32use crate::abi::callbacks::{
33    xmlCharEncConvCtxtDtor, xmlCharEncConvFunc, xmlCharEncConvImpl, xmlCharEncodingInputFunc,
34    xmlCharEncodingOutputFunc,
35};
36use crate::abi::constants::XML_MAX_ENCODING_NAME_LEN;
37use crate::abi::structs::{
38    _xmlBuffer, _xmlCharEncodingHandler, EncodingInputUnion, EncodingOutputUnion,
39};
40use crate::abi::types::{xmlChar, xmlCharEncoding, xmlCharPtr};
41
42// ── Constants ──────────────────────────────────────────────────────────────
43
44/// Maximum bytes needed per character for any supported encoding.
45const MAX_CHAR_BYTES: usize = 6;
46
47/// UTF-8 BOM bytes.
48const UTF8_BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];
49
50/// UTF-16LE BOM bytes.
51const UTF16LE_BOM: [u8; 2] = [0xFF, 0xFE];
52
53/// UTF-16BE BOM bytes.
54const UTF16BE_BOM: [u8; 2] = [0xFE, 0xFF];
55
56// ── Global handler registry ────────────────────────────────────────────────
57
58/// A raw pointer wrapper that implements `Send` and `Sync`.
59///
60/// This is safe because all access to the global handler registry is
61/// serialized through the `RwLock`, and handlers are only accessed from
62/// trusted internal code.
63#[derive(Clone, Copy)]
64struct HandlerPtr(*mut _xmlCharEncodingHandler);
65
66unsafe impl Send for HandlerPtr {}
67unsafe impl Sync for HandlerPtr {}
68
69/// Global list of registered encoding handlers, protected by a read-write lock.
70static ENCODING_HANDLERS: Lazy<RwLock<Vec<HandlerPtr>>> = Lazy::new(|| RwLock::new(Vec::new()));
71
72/// Whether the built-in encoding handlers have been initialized.
73static ENCODING_INITIALIZED: AtomicBool = AtomicBool::new(false);
74
75/// Serializes first-time handler registration (see init_encodings).
76static ENCODING_INIT_MUTEX: parking_lot::Mutex<()> = parking_lot::Mutex::new(());
77
78// ═══════════════════════════════════════════════════════════════════════════════
79// 1. Encoding detection
80// ═══════════════════════════════════════════════════════════════════════════════
81
82/// Determine encoding from BOM bytes.
83///
84/// Returns `XML_CHAR_ENCODING_NONE` if no BOM is present, or if `data` is empty.
85/// Otherwise returns the matching encoding enum value.
86pub(crate) fn detect_encoding_from_bom(data: &[u8]) -> xmlCharEncoding {
87    if data.len() >= 3 && data[0..3] == UTF8_BOM {
88        xmlCharEncoding::XML_CHAR_ENCODING_UTF8
89    } else if data.len() >= 2 && data[0..2] == UTF16LE_BOM {
90        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
91    } else if data.len() >= 2 && data[0..2] == UTF16BE_BOM {
92        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE
93    } else {
94        xmlCharEncoding::XML_CHAR_ENCODING_NONE
95    }
96}
97
98/// Determine encoding from an XML declaration's `encoding` attribute.
99///
100/// Scans for `<?xml ... encoding="..." ?>` and returns the encoding name
101/// as a byte vector (lowercased), or `None` if not found.
102pub(crate) fn detect_encoding_from_declaration(data: &[u8]) -> Option<Vec<u8>> {
103    // Look for "<?xml" at the start (possibly after BOM)
104    let start = if data.len() >= 3 && data[0..3] == UTF8_BOM {
105        3
106    } else if data.len() >= 2 && (data[0..2] == UTF16LE_BOM || data[0..2] == UTF16BE_BOM) {
107        // For UTF-16, we can't easily scan the bytes; skip
108        return None;
109    } else {
110        0
111    };
112
113    let remaining = &data[start..];
114
115    // Must start with "<?xml"
116    if remaining.len() < 5 || !remaining[0..5].eq_ignore_ascii_case(b"<?xml") {
117        return None;
118    }
119
120    // Find the end of the PI: "?>"
121    let pi_end = remaining.windows(2).position(|w| w == b"?>")?;
122    let decl_content = &remaining[5..pi_end];
123
124    // Look for "encoding" attribute
125    let decl_str = core::str::from_utf8(decl_content).ok()?;
126    let lower = decl_str.to_ascii_lowercase();
127
128    // Find "encoding" keyword
129    let enc_pos = lower.find("encoding")?;
130
131    // After "encoding", expect optional whitespace and '='
132    let after_enc = &decl_content[enc_pos + 8..];
133    let after_enc_str = core::str::from_utf8(after_enc).ok()?;
134    let after_enc_trimmed = after_enc_str.trim_start();
135
136    if !after_enc_trimmed.starts_with('=') {
137        return None;
138    }
139
140    let after_eq = after_enc_trimmed[1..].trim_start();
141
142    // Expect quote character
143    let quote = after_eq.chars().next()?;
144    if quote != '"' && quote != '\'' {
145        return None;
146    }
147
148    // Find matching closing quote
149    let value_end = after_eq[1..].find(quote)?;
150    let encoding_value = &after_eq[1..=value_end];
151
152    Some(encoding_value.to_ascii_lowercase().as_bytes().to_vec())
153}
154
155/// Parse an encoding name string to an `xmlCharEncoding` enum value.
156///
157/// Matching is case-insensitive. Common aliases are recognized.
158/// Returns `XML_CHAR_ENCODING_ERROR` if the name is not recognized.
159pub(crate) fn encoding_from_name(name: &[u8]) -> xmlCharEncoding {
160    let s = core::str::from_utf8(name).unwrap_or("");
161    let s = s.trim().to_ascii_lowercase();
162
163    match s.as_str() {
164        // UTF-8
165        "utf-8" | "utf8" => xmlCharEncoding::XML_CHAR_ENCODING_UTF8,
166
167        // UTF-16
168        "utf-16" | "utf-16le" | "utf16le" => xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
169        "utf-16be" | "utf16be" => xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE,
170
171        // ISO-8859 variants
172        "iso-8859-1" | "iso_8859-1" | "latin1" | "latin-1" | "l1" | "cp819" | "ibm819"
173        | "iso-ir-100" | "iso_8859-1:1987" => xmlCharEncoding::XML_CHAR_ENCODING_8859_1,
174        "iso-8859-2" | "iso_8859-2" | "latin2" | "latin-2" | "l2" => {
175            xmlCharEncoding::XML_CHAR_ENCODING_8859_2
176        }
177        "iso-8859-3" | "iso_8859-3" | "latin3" | "latin-3" | "l3" => {
178            xmlCharEncoding::XML_CHAR_ENCODING_8859_3
179        }
180        "iso-8859-4" | "iso_8859-4" | "latin4" | "latin-4" | "l4" => {
181            xmlCharEncoding::XML_CHAR_ENCODING_8859_4
182        }
183        "iso-8859-5" | "iso_8859-5" | "cyrillic" => xmlCharEncoding::XML_CHAR_ENCODING_8859_5,
184        "iso-8859-6" | "iso_8859-6" | "arabic" => xmlCharEncoding::XML_CHAR_ENCODING_8859_6,
185        "iso-8859-7" | "iso_8859-7" | "greek" => xmlCharEncoding::XML_CHAR_ENCODING_8859_7,
186        "iso-8859-8" | "iso_8859-8" | "hebrew" => xmlCharEncoding::XML_CHAR_ENCODING_8859_8,
187        "iso-8859-9" | "iso_8859-9" | "latin5" | "latin-5" | "l5" | "turkish" => {
188            xmlCharEncoding::XML_CHAR_ENCODING_8859_9
189        }
190
191        // ASCII
192        "ascii" | "us-ascii" | "us" | "ansi_x3.4-1968" | "ansi_x3.4-1986" | "iso-ir-6"
193        | "iso_646.irv:1991" | "cp367" | "ibm367" => xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
194
195        // East Asian
196        "iso-2022-jp" | "iso2022-jp" => xmlCharEncoding::XML_CHAR_ENCODING_2022_JP,
197        "shift_jis" | "shift-jis" | "sjis" | "cp932" => {
198            xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS
199        }
200        "euc-jp" | "eucjp" => xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP,
201
202        // UCS/Unicode variants
203        "ucs-4" | "ucs4" => xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE,
204        "ucs-4le" | "ucs4le" => xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE,
205        "ucs-4be" | "ucs4be" => xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE,
206        "ucs-2" | "ucs2" => xmlCharEncoding::XML_CHAR_ENCODING_UCS2,
207
208        // EBCDIC
209        "ebcdic" | "cp037" | "ibm037" => xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC,
210
211        _ => xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
212    }
213}
214
215/// Get the canonical name for an encoding as a byte slice.
216///
217/// Returns `None` for `XML_CHAR_ENCODING_ERROR` and `XML_CHAR_ENCODING_NONE`.
218pub(crate) fn encoding_name(enc: xmlCharEncoding) -> Option<&'static [u8]> {
219    match enc {
220        xmlCharEncoding::XML_CHAR_ENCODING_UTF8 => Some(b"UTF-8" as &[u8]),
221        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE => Some(b"UTF-16LE" as &[u8]),
222        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE => Some(b"UTF-16BE" as &[u8]),
223        xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE => Some(b"UCS-4LE" as &[u8]),
224        xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE => Some(b"UCS-4BE" as &[u8]),
225        xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC => Some(b"EBCDIC" as &[u8]),
226        xmlCharEncoding::XML_CHAR_ENCODING_UCS4_2143 => Some(b"UCS-4-2143" as &[u8]),
227        xmlCharEncoding::XML_CHAR_ENCODING_UCS4_3412 => Some(b"UCS-4-3412" as &[u8]),
228        xmlCharEncoding::XML_CHAR_ENCODING_UCS2 => Some(b"UCS-2" as &[u8]),
229        xmlCharEncoding::XML_CHAR_ENCODING_8859_1 => Some(b"ISO-8859-1" as &[u8]),
230        xmlCharEncoding::XML_CHAR_ENCODING_8859_2 => Some(b"ISO-8859-2" as &[u8]),
231        xmlCharEncoding::XML_CHAR_ENCODING_8859_3 => Some(b"ISO-8859-3" as &[u8]),
232        xmlCharEncoding::XML_CHAR_ENCODING_8859_4 => Some(b"ISO-8859-4" as &[u8]),
233        xmlCharEncoding::XML_CHAR_ENCODING_8859_5 => Some(b"ISO-8859-5" as &[u8]),
234        xmlCharEncoding::XML_CHAR_ENCODING_8859_6 => Some(b"ISO-8859-6" as &[u8]),
235        xmlCharEncoding::XML_CHAR_ENCODING_8859_7 => Some(b"ISO-8859-7" as &[u8]),
236        xmlCharEncoding::XML_CHAR_ENCODING_8859_8 => Some(b"ISO-8859-8" as &[u8]),
237        xmlCharEncoding::XML_CHAR_ENCODING_8859_9 => Some(b"ISO-8859-9" as &[u8]),
238        xmlCharEncoding::XML_CHAR_ENCODING_2022_JP => Some(b"ISO-2022-JP" as &[u8]),
239        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS => Some(b"SHIFT_JIS" as &[u8]),
240        xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP => Some(b"EUC-JP" as &[u8]),
241        xmlCharEncoding::XML_CHAR_ENCODING_ASCII => Some(b"US-ASCII" as &[u8]),
242        _ => None,
243    }
244}
245
246// ═══════════════════════════════════════════════════════════════════════════════
247// 2. UTF-8 validation
248// ═══════════════════════════════════════════════════════════════════════════════
249
250/// Check if a byte sequence is valid UTF-8.
251///
252/// Returns `true` if the entire slice is valid UTF-8, `false` otherwise.
253pub(crate) fn utf8_valid(data: &[u8]) -> bool {
254    core::str::from_utf8(data).is_ok()
255}
256
257/// Check if a Unicode codepoint is a valid XML character.
258///
259/// Per XML 1.0 (Fifth Edition) §2.2, the valid character ranges are:
260/// - `#x9` (tab)
261/// - `#xA` (LF)
262/// - `#xD` (CR)
263/// - `#x20` – `#xD7FF`
264/// - `#xE000` – `#xFFFD`
265/// - `#x10000` – `#x10FFFF`
266///
267/// Excludes surrogate halves (`#xD800` – `#xDFFF`) and `#xFFFE`/`#xFFFF`.
268pub(crate) fn is_valid_xml_char(cp: u32) -> bool {
269    match cp {
270        0x9 | 0xA | 0xD => true,
271        0x20..=0xD7FF => true,
272        0xE000..=0xFFFD => true,
273        0x10000..=0x10FFFF => true,
274        _ => false,
275    }
276}
277
278// ═══════════════════════════════════════════════════════════════════════════════
279// 3. UTF-16 handling
280// ═══════════════════════════════════════════════════════════════════════════════
281
282/// Decode a single UTF-16LE code unit from two bytes.
283#[inline]
284fn read_utf16le_unit(data: &[u8]) -> Option<u16> {
285    if data.len() < 2 {
286        return None;
287    }
288    Some(u16::from_le_bytes([data[0], data[1]]))
289}
290
291/// Decode a single UTF-16BE code unit from two bytes.
292#[inline]
293fn read_utf16be_unit(data: &[u8]) -> Option<u16> {
294    if data.len() < 2 {
295        return None;
296    }
297    Some(u16::from_be_bytes([data[0], data[1]]))
298}
299
300/// Encode a Unicode codepoint as UTF-8 bytes.
301///
302/// Returns the number of bytes written (1–4), or 0 if the codepoint is invalid.
303fn encode_codepoint_to_utf8(cp: u32, out: &mut [u8]) -> usize {
304    if cp < 0x80 {
305        if !out.is_empty() {
306            out[0] = cp as u8;
307        }
308        1
309    } else if cp < 0x800 {
310        if out.len() < 2 {
311            return 0;
312        }
313        out[0] = 0xC0 | ((cp >> 6) as u8);
314        out[1] = 0x80 | (cp as u8 & 0x3F);
315        2
316    } else if cp < 0x10000 {
317        if out.len() < 3 {
318            return 0;
319        }
320        out[0] = 0xE0 | ((cp >> 12) as u8);
321        out[1] = 0x80 | ((cp >> 6) as u8 & 0x3F);
322        out[2] = 0x80 | (cp as u8 & 0x3F);
323        3
324    } else if cp < 0x110000 {
325        if out.len() < 4 {
326            return 0;
327        }
328        out[0] = 0xF0 | ((cp >> 18) as u8);
329        out[1] = 0x80 | ((cp >> 12) as u8 & 0x3F);
330        out[2] = 0x80 | ((cp >> 6) as u8 & 0x3F);
331        out[3] = 0x80 | (cp as u8 & 0x3F);
332        4
333    } else {
334        0
335    }
336}
337
338/// Convert UTF-16LE bytes to UTF-8.
339///
340/// Returns `Ok(converted_bytes)` on success, or `Err(())` on invalid input
341/// (e.g., unpaired surrogates, truncated data).
342pub(crate) fn utf16le_to_utf8(data: &[u8]) -> Result<Vec<u8>, ()> {
343    if data.is_empty() {
344        return Ok(Vec::new());
345    }
346
347    // Skip BOM if present
348    let offset = if data.len() >= 2 && data[0..2] == UTF16LE_BOM {
349        2
350    } else {
351        0
352    };
353
354    let mut result = Vec::with_capacity(data.len() / 2 + data.len() / 4);
355    let mut i = offset;
356
357    while i < data.len() {
358        let unit = read_utf16le_unit(&data[i..]).ok_or(())?;
359        i += 2;
360
361        if unit >= 0xD800 && unit <= 0xDBFF {
362            // High surrogate: expect a low surrogate
363            let low = read_utf16le_unit(&data[i..]).ok_or(())?;
364            i += 2;
365
366            if !(0xDC00..=0xDFFF).contains(&low) {
367                return Err(());
368            }
369
370            let cp = 0x10000 + ((unit as u32 - 0xD800) << 10) + (low as u32 - 0xDC00);
371            let mut buf = [0u8; 4];
372            let n = encode_codepoint_to_utf8(cp, &mut buf);
373            if n == 0 {
374                return Err(());
375            }
376            result.extend_from_slice(&buf[..n]);
377        } else if unit >= 0xDC00 && unit <= 0xDFFF {
378            // Unexpected low surrogate
379            return Err(());
380        } else {
381            let cp = unit as u32;
382            let mut buf = [0u8; 4];
383            let n = encode_codepoint_to_utf8(cp, &mut buf);
384            result.extend_from_slice(&buf[..n]);
385        }
386    }
387
388    Ok(result)
389}
390
391/// Convert UTF-16BE bytes to UTF-8.
392///
393/// Returns `Ok(converted_bytes)` on success, or `Err(())` on invalid input.
394pub(crate) fn utf16be_to_utf8(data: &[u8]) -> Result<Vec<u8>, ()> {
395    if data.is_empty() {
396        return Ok(Vec::new());
397    }
398
399    // Skip BOM if present
400    let offset = if data.len() >= 2 && data[0..2] == UTF16BE_BOM {
401        2
402    } else {
403        0
404    };
405
406    let mut result = Vec::with_capacity(data.len() / 2 + data.len() / 4);
407    let mut i = offset;
408
409    while i < data.len() {
410        let unit = read_utf16be_unit(&data[i..]).ok_or(())?;
411        i += 2;
412
413        if unit >= 0xD800 && unit <= 0xDBFF {
414            // High surrogate: expect a low surrogate
415            let low = read_utf16be_unit(&data[i..]).ok_or(())?;
416            i += 2;
417
418            if !(0xDC00..=0xDFFF).contains(&low) {
419                return Err(());
420            }
421
422            let cp = 0x10000 + ((unit as u32 - 0xD800) << 10) + (low as u32 - 0xDC00);
423            let mut buf = [0u8; 4];
424            let n = encode_codepoint_to_utf8(cp, &mut buf);
425            if n == 0 {
426                return Err(());
427            }
428            result.extend_from_slice(&buf[..n]);
429        } else if unit >= 0xDC00 && unit <= 0xDFFF {
430            // Unexpected low surrogate
431            return Err(());
432        } else {
433            let cp = unit as u32;
434            let mut buf = [0u8; 4];
435            let n = encode_codepoint_to_utf8(cp, &mut buf);
436            result.extend_from_slice(&buf[..n]);
437        }
438    }
439
440    Ok(result)
441}
442
443/// Encode a Unicode codepoint as UTF-16LE bytes.
444///
445/// Returns the number of bytes written (2 or 4), or 0 if the codepoint is invalid.
446fn encode_codepoint_to_utf16le(cp: u32, out: &mut [u8]) -> usize {
447    if cp < 0x10000 {
448        if out.len() < 2 {
449            return 0;
450        }
451        let u = cp as u16;
452        out[..2].copy_from_slice(&u.to_le_bytes());
453        2
454    } else if cp < 0x110000 {
455        if out.len() < 4 {
456            return 0;
457        }
458        let cp = cp - 0x10000;
459        let high = 0xD800 | ((cp >> 10) as u16);
460        let low = 0xDC00 | (cp as u16 & 0x3FF);
461        out[..2].copy_from_slice(&high.to_le_bytes());
462        out[2..4].copy_from_slice(&low.to_le_bytes());
463        4
464    } else {
465        0
466    }
467}
468
469/// Convert UTF-8 bytes to UTF-16LE.
470///
471/// Returns `Ok(converted_bytes)` on success, or `Err(())` on invalid UTF-8 input.
472pub(crate) fn utf8_to_utf16le(data: &[u8]) -> Result<Vec<u8>, ()> {
473    let s = core::str::from_utf8(data).map_err(|_| ())?;
474    let mut result = Vec::with_capacity(data.len() * 2);
475
476    for ch in s.chars() {
477        let cp = ch as u32;
478        let mut buf = [0u8; 4];
479        let n = encode_codepoint_to_utf16le(cp, &mut buf);
480        if n == 0 {
481            return Err(());
482        }
483        result.extend_from_slice(&buf[..n]);
484    }
485
486    Ok(result)
487}
488
489// ═══════════════════════════════════════════════════════════════════════════════
490// 4. ISO-8859-1 (Latin-1) handling
491// ═══════════════════════════════════════════════════════════════════════════════
492
493/// Convert Latin-1 (ISO-8859-1) bytes to UTF-8.
494///
495/// Latin-1 maps codepoints 0x00–0xFF directly to Unicode codepoints U+0000–U+00FF.
496/// Each input byte produces either 1 or 2 UTF-8 bytes.
497pub(crate) fn latin1_to_utf8(data: &[u8]) -> Vec<u8> {
498    let mut result = Vec::with_capacity(data.len() * 2);
499
500    for &byte in data {
501        let cp = byte as u32;
502        let mut buf = [0u8; 2];
503        let n = encode_codepoint_to_utf8(cp, &mut buf);
504        result.extend_from_slice(&buf[..n]);
505    }
506
507    result
508}
509
510/// Convert UTF-8 bytes to Latin-1 (ISO-8859-1).
511///
512/// Returns `Err(())` if the input is not valid UTF-8 or contains codepoints
513/// outside the Latin-1 range (U+0000–U+00FF).
514pub(crate) fn utf8_to_latin1(data: &[u8]) -> Result<Vec<u8>, ()> {
515    let s = core::str::from_utf8(data).map_err(|_| ())?;
516    let mut result = Vec::with_capacity(data.len());
517
518    for ch in s.chars() {
519        let cp = ch as u32;
520        if cp > 0xFF {
521            return Err(());
522        }
523        result.push(cp as u8);
524    }
525
526    Ok(result)
527}
528
529// ═══════════════════════════════════════════════════════════════════════════════
530// 5. Encoding handler registry
531// ═══════════════════════════════════════════════════════════════════════════════
532
533/// Initialize the built-in encoding handlers.
534///
535/// This function registers handlers for:
536/// - UTF-8 (identity/no conversion)
537/// - UTF-16LE
538/// - UTF-16BE
539/// - ISO-8859-1 (Latin-1)
540/// - ASCII
541///
542/// Safe to call multiple times — only the first call has an effect.
543pub(crate) fn init_encodings() {
544    if ENCODING_INITIALIZED.load(Ordering::SeqCst) {
545        return;
546    }
547    // Serialize first-time registration: without the mutex, a second thread
548    // can observe ENCODING_INITIALIZED == true and look up handlers while
549    // the first thread is still registering them (race found by the parallel
550    // test suite: xml::io test_output_buffer_with_encoding intermittently
551    // failed to find the Latin-1 handler).
552    let _guard = ENCODING_INIT_MUTEX.lock();
553    if ENCODING_INITIALIZED.load(Ordering::SeqCst) {
554        return;
555    }
556    register_builtin_handlers();
557    ENCODING_INITIALIZED.store(true, Ordering::SeqCst);
558}
559
560/// Register all built-in encoding handlers.
561fn register_builtin_handlers() {
562    // UTF-8 (identity handler — no conversion needed)
563    register_handler(
564        b"UTF-8\0",
565        xmlCharEncoding::XML_CHAR_ENCODING_UTF8,
566        xmlCharEncoding::XML_CHAR_ENCODING_UTF8,
567        Some(utf8_input_func as xmlCharEncodingInputFunc),
568        Some(utf8_output_func as xmlCharEncodingOutputFunc),
569    );
570
571    // UTF-16LE
572    register_handler(
573        b"UTF-16LE\0",
574        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
575        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
576        Some(utf16le_input_func as xmlCharEncodingInputFunc),
577        Some(utf16le_output_func as xmlCharEncodingOutputFunc),
578    );
579
580    // UTF-16BE
581    register_handler(
582        b"UTF-16BE\0",
583        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE,
584        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE,
585        Some(utf16be_input_func as xmlCharEncodingInputFunc),
586        Some(utf16be_output_func as xmlCharEncodingOutputFunc),
587    );
588
589    // ISO-8859-1 (Latin-1)
590    register_handler(
591        b"ISO-8859-1\0",
592        xmlCharEncoding::XML_CHAR_ENCODING_8859_1,
593        xmlCharEncoding::XML_CHAR_ENCODING_8859_1,
594        Some(latin1_input_func as xmlCharEncodingInputFunc),
595        Some(latin1_output_func as xmlCharEncodingOutputFunc),
596    );
597
598    // ASCII — upstream's static default handler (defaultHandlers[22]) is named
599    // "US-ASCII"; the name "ASCII" is registered as a second entry so name-based
600    // lookups (xmlFindCharEncodingHandler, the saver path) accept both spellings
601    // exactly like upstream's xmlParseCharEncodingInternal mapping.
602    register_handler(
603        b"US-ASCII\0",
604        xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
605        xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
606        Some(ascii_input_func as xmlCharEncodingInputFunc),
607        Some(ascii_output_func as xmlCharEncodingOutputFunc),
608    );
609    register_handler(
610        b"ASCII\0",
611        xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
612        xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
613        Some(ascii_input_func as xmlCharEncodingInputFunc),
614        Some(ascii_output_func as xmlCharEncodingOutputFunc),
615    );
616
617    // UTF-16 (default handler for enc == XML_CHAR_ENCODING_UTF16 == 23): the
618    // upstream converter is UTF16LEToUTF8/UTF8ToUTF16 (the latter emits the LE
619    // BOM on its init call). Our converter pair is the UTF-16LE pair; the BOM
620    // init protocol is not emitted (documented divergence, conversion only).
621    register_handler(
622        b"UTF-16\0",
623        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
624        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
625        Some(utf16le_input_func as xmlCharEncodingInputFunc),
626        Some(utf16le_output_func as xmlCharEncodingOutputFunc),
627    );
628}
629
630/// Helper to create and register an encoding handler.
631fn register_handler(
632    name_bytes: &[u8],
633    input_enc: xmlCharEncoding,
634    output_enc: xmlCharEncoding,
635    input_func: Option<xmlCharEncodingInputFunc>,
636    output_func: Option<xmlCharEncodingOutputFunc>,
637) {
638    let name_raw =
639        unsafe { crate::abi::allocator::xmlMemStrdupImpl(name_bytes.as_ptr() as *const c_char) };
640    if name_raw.is_null() {
641        return;
642    }
643
644    let handler =
645        unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) } as *mut _xmlCharEncodingHandler;
646
647    if handler.is_null() {
648        unsafe { xmlFreeImpl(name_raw) };
649        return;
650    }
651
652    unsafe {
653        ptr::write(
654            handler,
655            _xmlCharEncodingHandler {
656                name: name_raw as *mut c_char,
657                input: EncodingInputUnion {
658                    legacyFunc: input_func,
659                },
660                output: EncodingOutputUnion {
661                    legacyFunc: output_func,
662                },
663                inputCtxt: ptr::null_mut(),
664                outputCtxt: ptr::null_mut(),
665                ctxtDtor: None,
666                flags: 0,
667            },
668        );
669    }
670
671    add_encoding_handler(handler);
672}
673
674/// Clean up encoding handlers.
675///
676/// Frees all registered handlers and resets the registry.
677pub(crate) fn cleanup_encodings() {
678    let mut handlers = ENCODING_HANDLERS.write();
679    for &handler in handlers.iter() {
680        let ptr = handler.0;
681        if !ptr.is_null() {
682            unsafe {
683                if !(*ptr).name.is_null() {
684                    xmlFreeImpl((*ptr).name as *mut c_void);
685                }
686                xmlFreeImpl(ptr as *mut c_void);
687            }
688        }
689    }
690    handlers.clear();
691    ENCODING_INITIALIZED.store(false, Ordering::SeqCst);
692}
693
694/// Find an encoding handler by name.
695///
696/// Searches the global handler registry for a handler whose name matches
697/// (case-insensitive). Returns a pointer to the handler, or `ptr::null_mut()`
698/// if not found.
699pub(crate) fn find_encoding_handler(name: *const xmlChar) -> *mut _xmlCharEncodingHandler {
700    if name.is_null() {
701        return ptr::null_mut();
702    }
703
704    /* The upstream default-handler table is static and always present; the
705     * candidate's registry is populated lazily, so ensure it is initialized
706     * before any name-based lookup. Idempotent. */
707    init_encodings();
708
709    let name_str = unsafe {
710        match CStr::from_ptr(name as *const c_char).to_bytes() {
711            b"" => return ptr::null_mut(),
712            s => s,
713        }
714    };
715
716    let handlers = ENCODING_HANDLERS.read();
717    for &handler in handlers.iter() {
718        let ptr = handler.0;
719        if ptr.is_null() {
720            continue;
721        }
722        let h_name = unsafe {
723            if (*ptr).name.is_null() {
724                continue;
725            }
726            CStr::from_ptr((*ptr).name).to_bytes()
727        };
728
729        if name_str.eq_ignore_ascii_case(h_name) {
730            return ptr;
731        }
732    }
733
734    ptr::null_mut()
735}
736
737/// Add an encoding handler to the registry.
738///
739/// Returns 0 on success, -1 on failure (e.g., null pointer).
740pub(crate) fn add_encoding_handler(handler: *mut _xmlCharEncodingHandler) -> c_int {
741    if handler.is_null() {
742        return -1;
743    }
744
745    let mut handlers = ENCODING_HANDLERS.write();
746    handlers.push(HandlerPtr(handler));
747    0
748}
749
750// ═══════════════════════════════════════════════════════════════════════════════
751// 6. Encoding conversion functions
752// ═══════════════════════════════════════════════════════════════════════════════
753
754/// Input conversion: convert from handler's input encoding to UTF-8.
755///
756/// Calls the handler's `input.legacyFunc` callback. Returns bytes written or -1 on error.
757pub(crate) fn char_enc_in_func(
758    handler: *mut _xmlCharEncodingHandler,
759    out: &mut [u8],
760    in_data: &[u8],
761) -> c_int {
762    if handler.is_null() {
763        return -1;
764    }
765
766    let h = unsafe { &*handler };
767    let input_func = unsafe { h.input.legacyFunc };
768    let input_func = match input_func {
769        Some(f) => f,
770        None => return -1,
771    };
772
773    let mut outlen = out.len() as c_int;
774    let mut inlen = in_data.len() as c_int;
775
776    unsafe { input_func(out.as_mut_ptr(), &mut outlen, in_data.as_ptr(), &mut inlen) }
777}
778
779/// Output conversion: convert from UTF-8 to handler's output encoding.
780///
781/// Calls the handler's `output.legacyFunc` callback. Returns bytes written or -1 on error.
782pub(crate) fn char_enc_out_func(
783    handler: *mut _xmlCharEncodingHandler,
784    out: &mut [u8],
785    in_data: &[u8],
786) -> c_int {
787    if handler.is_null() {
788        return -1;
789    }
790
791    let h = unsafe { &*handler };
792    let output_func = unsafe { h.output.legacyFunc };
793    let output_func = match output_func {
794        Some(f) => f,
795        None => return -1,
796    };
797
798    let mut outlen = out.len() as c_int;
799    let mut inlen = in_data.len() as c_int;
800
801    unsafe { output_func(out.as_mut_ptr(), &mut outlen, in_data.as_ptr(), &mut inlen) }
802}
803
804/// Full input conversion (`xmlCharEncInFunc` equivalent).
805///
806/// Reads from the input `_xmlBuffer`, converts via the handler's `input.legacyFunc`,
807/// and appends the result to the output `_xmlBuffer`.
808///
809/// Returns the number of bytes written to the output buffer, or -1 on error.
810pub(crate) fn char_enc_in(
811    handler: *mut _xmlCharEncodingHandler,
812    out: *mut _xmlBuffer,
813    in_: *mut _xmlBuffer,
814) -> c_int {
815    if handler.is_null() || out.is_null() || in_.is_null() {
816        return -1;
817    }
818
819    let h = unsafe { &*handler };
820    let input_func = unsafe { h.input.legacyFunc };
821    let input_func = match input_func {
822        Some(f) => f,
823        None => return -1,
824    };
825
826    let in_buf = unsafe { &*in_ };
827    let out_buf = unsafe { &mut *out };
828
829    if in_buf.content.is_null() || in_buf.use_ == 0 {
830        return 0;
831    }
832
833    let in_data = unsafe { core::slice::from_raw_parts(in_buf.content, in_buf.use_ as usize) };
834
835    // Allocate an output buffer. A good heuristic is 2x input for UTF-16→UTF-8.
836    let out_capacity = (in_buf.use_ as usize).saturating_mul(3).max(256);
837    let mut out_vec = vec![0u8; out_capacity];
838    let mut out_len = out_capacity as c_int;
839    let mut in_len = in_buf.use_ as c_int;
840
841    let ret = unsafe {
842        input_func(
843            out_vec.as_mut_ptr(),
844            &mut out_len,
845            in_data.as_ptr(),
846            &mut in_len,
847        )
848    };
849
850    if ret < 0 {
851        return -1;
852    }
853
854    let written = ret as usize;
855
856    // Append to output buffer
857    append_to_xml_buffer(out_buf, &out_vec[..written]);
858
859    written as c_int
860}
861
862/// Full output conversion (`xmlCharEncOutFunc` equivalent).
863///
864/// Reads from the input `_xmlBuffer` (UTF-8), converts via the handler's
865/// `output.legacyFunc`, and appends the result to the output `_xmlBuffer`.
866///
867/// Returns the number of bytes written to the output buffer, or -1 on error.
868pub(crate) fn char_enc_out(
869    handler: *mut _xmlCharEncodingHandler,
870    out: *mut _xmlBuffer,
871    in_: *mut _xmlBuffer,
872) -> c_int {
873    if handler.is_null() || out.is_null() || in_.is_null() {
874        return -1;
875    }
876
877    let h = unsafe { &*handler };
878    let output_func = unsafe { h.output.legacyFunc };
879    let output_func = match output_func {
880        Some(f) => f,
881        None => return -1,
882    };
883
884    let in_buf = unsafe { &*in_ };
885    let out_buf = unsafe { &mut *out };
886
887    if in_buf.content.is_null() || in_buf.use_ == 0 {
888        return 0;
889    }
890
891    let in_data = unsafe { core::slice::from_raw_parts(in_buf.content, in_buf.use_ as usize) };
892
893    let out_capacity = (in_buf.use_ as usize).saturating_mul(3).max(256);
894    let mut out_vec = vec![0u8; out_capacity];
895    let mut out_len = out_capacity as c_int;
896    let mut in_len = in_buf.use_ as c_int;
897
898    let ret = unsafe {
899        output_func(
900            out_vec.as_mut_ptr(),
901            &mut out_len,
902            in_data.as_ptr(),
903            &mut in_len,
904        )
905    };
906
907    if ret < 0 {
908        return -1;
909    }
910
911    let written = ret as usize;
912
913    // Append to output buffer
914    append_to_xml_buffer(out_buf, &out_vec[..written]);
915
916    written as c_int
917}
918
919/// Append bytes to an `_xmlBuffer`, reallocating if needed.
920fn append_to_xml_buffer(buf: &mut _xmlBuffer, data: &[u8]) {
921    if data.is_empty() {
922        return;
923    }
924
925    let new_use = (buf.use_ as usize).saturating_add(data.len());
926    if new_use > buf.size as usize {
927        // Grow buffer: double or fit, whichever is larger
928        let new_size = (buf.size as usize).saturating_mul(2).max(new_use).max(256);
929        let new_content =
930            unsafe { xmlReallocImpl(buf.content as *mut c_void, new_size) as *mut xmlChar };
931        if new_content.is_null() {
932            return; // Allocation failure — silently skip
933        }
934        buf.content = new_content;
935        buf.size = new_size as c_uint;
936    }
937
938    unsafe {
939        ptr::copy_nonoverlapping(
940            data.as_ptr(),
941            buf.content.add(buf.use_ as usize),
942            data.len(),
943        );
944    }
945    buf.use_ = new_use as c_uint;
946}
947
948// ═══════════════════════════════════════════════════════════════════════════════
949// 7. Built-in encoding handler callbacks (extern "C")
950// ═══════════════════════════════════════════════════════════════════════════════
951
952// ── UTF-8 (identity) ──────────────────────────────────────────────────────
953
954/// UTF-8 input function: identity (input is already UTF-8).
955///
956/// Simply copies bytes from input to output, up to the available space.
957unsafe extern "C" fn utf8_input_func(
958    out: *mut c_uchar,
959    outlen: *mut c_int,
960    in_: *const c_uchar,
961    inlen: *mut c_int,
962) -> c_int {
963    let avail_out = *outlen as usize;
964    let avail_in = *inlen as usize;
965    let to_copy = avail_out.min(avail_in);
966
967    if to_copy > 0 {
968        ptr::copy_nonoverlapping(in_, out, to_copy);
969    }
970
971    *outlen = to_copy as c_int;
972    *inlen = to_copy as c_int;
973    to_copy as c_int
974}
975
976/// UTF-8 output function: identity (output is already UTF-8).
977unsafe extern "C" fn utf8_output_func(
978    out: *mut c_uchar,
979    outlen: *mut c_int,
980    in_: *const c_uchar,
981    inlen: *mut c_int,
982) -> c_int {
983    utf8_input_func(out, outlen, in_, inlen)
984}
985
986// ── UTF-16LE ──────────────────────────────────────────────────────────────
987
988/// UTF-16LE input function: convert UTF-16LE to UTF-8.
989unsafe extern "C" fn utf16le_input_func(
990    out: *mut c_uchar,
991    outlen: *mut c_int,
992    in_: *const c_uchar,
993    inlen: *mut c_int,
994) -> c_int {
995    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
996        return -1;
997    }
998
999    let avail_in = *inlen as usize;
1000    let avail_out = *outlen as usize;
1001
1002    if avail_in == 0 || avail_out == 0 {
1003        *outlen = 0;
1004        *inlen = 0;
1005        return 0;
1006    }
1007
1008    let in_data = core::slice::from_raw_parts(in_, avail_in);
1009    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1010
1011    // Use the safe wrapper
1012    let result = match utf16le_to_utf8(in_data) {
1013        Ok(v) => v,
1014        Err(()) => return -1,
1015    };
1016
1017    let written = result.len().min(avail_out);
1018    if written > 0 {
1019        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1020    }
1021
1022    *outlen = written as c_int;
1023    *inlen = avail_in as c_int; // All input consumed
1024    written as c_int
1025}
1026
1027/// UTF-16LE output function: convert UTF-8 to UTF-16LE.
1028unsafe extern "C" fn utf16le_output_func(
1029    out: *mut c_uchar,
1030    outlen: *mut c_int,
1031    in_: *const c_uchar,
1032    inlen: *mut c_int,
1033) -> c_int {
1034    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1035        return -1;
1036    }
1037
1038    let avail_in = *inlen as usize;
1039    let avail_out = *outlen as usize;
1040
1041    if avail_in == 0 || avail_out == 0 {
1042        *outlen = 0;
1043        *inlen = 0;
1044        return 0;
1045    }
1046
1047    let in_data = core::slice::from_raw_parts(in_, avail_in);
1048    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1049
1050    let result = match utf8_to_utf16le(in_data) {
1051        Ok(v) => v,
1052        Err(()) => return -1,
1053    };
1054
1055    let written = result.len().min(avail_out);
1056    if written > 0 {
1057        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1058    }
1059
1060    *outlen = written as c_int;
1061    *inlen = avail_in as c_int;
1062    written as c_int
1063}
1064
1065// ── UTF-16BE ──────────────────────────────────────────────────────────────
1066
1067/// UTF-16BE input function: convert UTF-16BE to UTF-8.
1068unsafe extern "C" fn utf16be_input_func(
1069    out: *mut c_uchar,
1070    outlen: *mut c_int,
1071    in_: *const c_uchar,
1072    inlen: *mut c_int,
1073) -> c_int {
1074    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1075        return -1;
1076    }
1077
1078    let avail_in = *inlen as usize;
1079    let avail_out = *outlen as usize;
1080
1081    if avail_in == 0 || avail_out == 0 {
1082        *outlen = 0;
1083        *inlen = 0;
1084        return 0;
1085    }
1086
1087    let in_data = core::slice::from_raw_parts(in_, avail_in);
1088    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1089
1090    let result = match utf16be_to_utf8(in_data) {
1091        Ok(v) => v,
1092        Err(()) => return -1,
1093    };
1094
1095    let written = result.len().min(avail_out);
1096    if written > 0 {
1097        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1098    }
1099
1100    *outlen = written as c_int;
1101    *inlen = avail_in as c_int;
1102    written as c_int
1103}
1104
1105/// UTF-16BE output function: convert UTF-8 to UTF-16BE.
1106unsafe extern "C" fn utf16be_output_func(
1107    out: *mut c_uchar,
1108    outlen: *mut c_int,
1109    in_: *const c_uchar,
1110    inlen: *mut c_int,
1111) -> c_int {
1112    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1113        return -1;
1114    }
1115
1116    let avail_in = *inlen as usize;
1117    let avail_out = *outlen as usize;
1118
1119    if avail_in == 0 || avail_out == 0 {
1120        *outlen = 0;
1121        *inlen = 0;
1122        return 0;
1123    }
1124
1125    let in_data = core::slice::from_raw_parts(in_, avail_in);
1126
1127    // First convert to UTF-16LE, then swap bytes
1128    let le_result = match utf8_to_utf16le(in_data) {
1129        Ok(v) => v,
1130        Err(()) => return -1,
1131    };
1132
1133    // Swap byte pairs to get UTF-16BE
1134    let mut result = le_result;
1135    for chunk in result.chunks_exact_mut(2) {
1136        chunk.swap(0, 1);
1137    }
1138
1139    let written = result.len().min(avail_out);
1140    if written > 0 {
1141        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1142    }
1143
1144    *outlen = written as c_int;
1145    *inlen = avail_in as c_int;
1146    written as c_int
1147}
1148
1149// ── ISO-8859-1 (Latin-1) ─────────────────────────────────────────────────
1150
1151/// Latin-1 input function: convert ISO-8859-1 to UTF-8.
1152unsafe extern "C" fn latin1_input_func(
1153    out: *mut c_uchar,
1154    outlen: *mut c_int,
1155    in_: *const c_uchar,
1156    inlen: *mut c_int,
1157) -> c_int {
1158    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1159        return -1;
1160    }
1161
1162    let avail_in = *inlen as usize;
1163    let avail_out = *outlen as usize;
1164
1165    if avail_in == 0 || avail_out == 0 {
1166        *outlen = 0;
1167        *inlen = 0;
1168        return 0;
1169    }
1170
1171    let in_data = core::slice::from_raw_parts(in_, avail_in);
1172    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1173
1174    let mut in_pos = 0;
1175    let mut out_pos = 0;
1176
1177    while in_pos < avail_in && out_pos < avail_out {
1178        let byte = in_data[in_pos];
1179        in_pos += 1;
1180
1181        if byte < 0x80 {
1182            // Single byte UTF-8
1183            if out_pos < avail_out {
1184                out_slice[out_pos] = byte;
1185                out_pos += 1;
1186            } else {
1187                break;
1188            }
1189        } else {
1190            // Two byte UTF-8: 0xC0 | (byte >> 6), 0x80 | (byte & 0x3F)
1191            // For byte 0x80-0xFF, the encoding is 0xC2-0xC3 followed by continuation
1192            if out_pos + 1 < avail_out {
1193                out_slice[out_pos] = 0xC2 | (byte >> 6);
1194                out_slice[out_pos + 1] = 0x80 | (byte & 0x3F);
1195                out_pos += 2;
1196            } else {
1197                break;
1198            }
1199        }
1200    }
1201
1202    *outlen = out_pos as c_int;
1203    *inlen = in_pos as c_int;
1204    out_pos as c_int
1205}
1206
1207/// Latin-1 output function: convert UTF-8 to ISO-8859-1.
1208unsafe extern "C" fn latin1_output_func(
1209    out: *mut c_uchar,
1210    outlen: *mut c_int,
1211    in_: *const c_uchar,
1212    inlen: *mut c_int,
1213) -> c_int {
1214    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1215        return -1;
1216    }
1217
1218    let avail_in = *inlen as usize;
1219    let avail_out = *outlen as usize;
1220
1221    if avail_in == 0 || avail_out == 0 {
1222        *outlen = 0;
1223        *inlen = 0;
1224        return 0;
1225    }
1226
1227    let in_data = core::slice::from_raw_parts(in_, avail_in);
1228    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1229
1230    let mut in_pos = 0;
1231    let mut out_pos = 0;
1232
1233    while in_pos < avail_in && out_pos < avail_out {
1234        let byte = in_data[in_pos];
1235        in_pos += 1;
1236
1237        if byte < 0x80 {
1238            // ASCII — direct mapping
1239            out_slice[out_pos] = byte;
1240            out_pos += 1;
1241        } else if byte >= 0xC2 && byte <= 0xC3 {
1242            // Two-byte UTF-8 for codepoints U+0080–U+00FF
1243            if in_pos < avail_in {
1244                let second = in_data[in_pos];
1245                in_pos += 1;
1246                if second & 0xC0 != 0x80 {
1247                    return -1; // Invalid continuation byte
1248                }
1249                let cp = ((byte as u32 & 0x1F) << 6) | (second as u32 & 0x3F);
1250                if cp > 0xFF {
1251                    return -1; // Outside Latin-1 range
1252                }
1253                out_slice[out_pos] = cp as u8;
1254                out_pos += 1;
1255            } else {
1256                return -1; // Truncated
1257            }
1258        } else if byte >= 0x80 && byte <= 0xBF {
1259            // Unexpected continuation byte
1260            return -1;
1261        } else {
1262            // Multi-byte sequence for codepoints > U+00FF
1263            // Skip the rest of the sequence and return error
1264            return -1;
1265        }
1266    }
1267
1268    *outlen = out_pos as c_int;
1269    *inlen = in_pos as c_int;
1270    out_pos as c_int
1271}
1272
1273// ── ASCII ─────────────────────────────────────────────────────────────────
1274
1275/// ASCII input function: verify and pass through ASCII data to UTF-8.
1276unsafe extern "C" fn ascii_input_func(
1277    out: *mut c_uchar,
1278    outlen: *mut c_int,
1279    in_: *const c_uchar,
1280    inlen: *mut c_int,
1281) -> c_int {
1282    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1283        return -1;
1284    }
1285
1286    let avail_in = *inlen as usize;
1287    let avail_out = *outlen as usize;
1288
1289    if avail_in == 0 || avail_out == 0 {
1290        *outlen = 0;
1291        *inlen = 0;
1292        return 0;
1293    }
1294
1295    let in_data = core::slice::from_raw_parts(in_, avail_in);
1296    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1297
1298    let mut pos = 0;
1299    while pos < avail_in && pos < avail_out {
1300        let byte = in_data[pos];
1301        if byte > 0x7F {
1302            return -1; // Not valid ASCII
1303        }
1304        out_slice[pos] = byte;
1305        pos += 1;
1306    }
1307
1308    *outlen = pos as c_int;
1309    *inlen = pos as c_int;
1310    pos as c_int
1311}
1312
1313/// ASCII output function: verify and pass through UTF-8 data that is ASCII.
1314unsafe extern "C" fn ascii_output_func(
1315    out: *mut c_uchar,
1316    outlen: *mut c_int,
1317    in_: *const c_uchar,
1318    inlen: *mut c_int,
1319) -> c_int {
1320    // For output, ASCII handler requires that input is already ASCII
1321    ascii_input_func(out, outlen, in_, inlen)
1322}
1323
1324// ═══════════════════════════════════════════════════════════════════════════════
1325// 8. ABI export functions (called from exports_xml2.rs)
1326// ═══════════════════════════════════════════════════════════════════════════════
1327
1328/// `xmlFindCharEncodingHandler` implementation.
1329///
1330/// Finds an encoding handler by name. Returns a pointer to the handler,
1331/// or `ptr::null_mut()` if not found.
1332pub(crate) fn xmlFindCharEncodingHandler(name: *const c_char) -> *mut _xmlCharEncodingHandler {
1333    if name.is_null() {
1334        return ptr::null_mut();
1335    }
1336    find_encoding_handler(name as *const xmlChar)
1337}
1338
1339/// `xmlGetCharEncodingName` implementation.
1340///
1341/// Returns the canonical name for an encoding, or `ptr::null()` if unknown.
1342pub(crate) fn xmlGetCharEncodingName(enc: xmlCharEncoding) -> *const c_char {
1343    // Return null-terminated C strings using static CStr literals.
1344    // Mirrors upstream 2.15 xmlGetCharEncodingName: the UTF-16/UCS-4 pairs
1345    // return the W3C canonical names before the defaultHandlers table.
1346    match enc {
1347        xmlCharEncoding::XML_CHAR_ENCODING_UTF8 => c"UTF-8".as_ptr(),
1348        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE | xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE => {
1349            c"UTF-16".as_ptr()
1350        }
1351        xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE | xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE => {
1352            c"UCS-4".as_ptr()
1353        }
1354        xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC => c"IBM037".as_ptr(),
1355        xmlCharEncoding::XML_CHAR_ENCODING_UCS2 => c"UCS-2".as_ptr(),
1356        xmlCharEncoding::XML_CHAR_ENCODING_8859_1 => c"ISO-8859-1".as_ptr(),
1357        xmlCharEncoding::XML_CHAR_ENCODING_8859_2 => c"ISO-8859-2".as_ptr(),
1358        xmlCharEncoding::XML_CHAR_ENCODING_8859_3 => c"ISO-8859-3".as_ptr(),
1359        xmlCharEncoding::XML_CHAR_ENCODING_8859_4 => c"ISO-8859-4".as_ptr(),
1360        xmlCharEncoding::XML_CHAR_ENCODING_8859_5 => c"ISO-8859-5".as_ptr(),
1361        xmlCharEncoding::XML_CHAR_ENCODING_8859_6 => c"ISO-8859-6".as_ptr(),
1362        xmlCharEncoding::XML_CHAR_ENCODING_8859_7 => c"ISO-8859-7".as_ptr(),
1363        xmlCharEncoding::XML_CHAR_ENCODING_8859_8 => c"ISO-8859-8".as_ptr(),
1364        xmlCharEncoding::XML_CHAR_ENCODING_8859_9 => c"ISO-8859-9".as_ptr(),
1365        xmlCharEncoding::XML_CHAR_ENCODING_2022_JP => c"ISO-2022-JP".as_ptr(),
1366        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS => c"Shift_JIS".as_ptr(),
1367        xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP => c"EUC-JP".as_ptr(),
1368        // upstream defaultHandlers[22].name
1369        xmlCharEncoding::XML_CHAR_ENCODING_ASCII => c"US-ASCII".as_ptr(),
1370        _ => ptr::null(),
1371    }
1372}
1373
1374/// `xmlParseCharEncoding` implementation.
1375///
1376/// Parses an encoding name string to an `xmlCharEncoding` enum value,
1377/// returned as `c_int`.
1378pub(crate) fn xmlParseCharEncoding(name: *const c_char) -> c_int {
1379    if name.is_null() {
1380        return xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int;
1381    }
1382    let bytes = unsafe { CStr::from_ptr(name).to_bytes() };
1383    encoding_from_name(bytes) as c_int
1384}
1385
1386// ── Encoding aliases (upstream encoding.c xmlAddEncodingAlias etc.) ──────────
1387//
1388// A global alias table maps alias names to canonical encoding names.
1389// Upstream keeps a static hash of aliases; the candidate uses a
1390// process-lifetime RwLock<HashMap>. Thread-safe; matches upstream's
1391// observable contract (add/del/get by name).
1392
1393static ENCODING_ALIASES: std::sync::OnceLock<
1394    parking_lot::RwLock<std::collections::HashMap<Vec<u8>, Vec<u8>>>,
1395> = std::sync::OnceLock::new();
1396
1397fn encoding_aliases() -> &'static parking_lot::RwLock<std::collections::HashMap<Vec<u8>, Vec<u8>>> {
1398    ENCODING_ALIASES.get_or_init(|| {
1399        let m = parking_lot::RwLock::new(std::collections::HashMap::new());
1400        m
1401    })
1402}
1403
1404/// `xmlAddEncodingAlias` implementation: register `alias` for `name`.
1405/// Returns 0 on success, -1 on error (NULL arguments).
1406pub(crate) fn add_encoding_alias(name: *const c_char, alias: *const c_char) -> c_int {
1407    if name.is_null() || alias.is_null() {
1408        return -1;
1409    }
1410    let n = unsafe { CStr::from_ptr(name).to_bytes().to_vec() };
1411    let a = unsafe { CStr::from_ptr(alias).to_bytes().to_vec() };
1412    encoding_aliases().write().insert(a, n);
1413    0
1414}
1415
1416/// `xmlDelEncodingAlias` implementation: remove `alias`.
1417/// Returns 0 on success, -1 if the alias does not exist.
1418pub(crate) fn del_encoding_alias(alias: *const c_char) -> c_int {
1419    if alias.is_null() {
1420        return -1;
1421    }
1422    let a = unsafe { CStr::from_ptr(alias).to_bytes().to_vec() };
1423    if encoding_aliases().write().remove(&a).is_some() {
1424        0
1425    } else {
1426        -1
1427    }
1428}
1429
1430/// `xmlGetEncodingAlias` implementation: return the canonical name for
1431/// `alias`, or NULL when not registered.
1432pub(crate) fn get_encoding_alias(alias: *const c_char) -> *const c_char {
1433    if alias.is_null() {
1434        return ptr::null();
1435    }
1436    let a = unsafe { CStr::from_ptr(alias).to_bytes().to_vec() };
1437    let guard = encoding_aliases().read();
1438    match guard.get(&a) {
1439        Some(v) => {
1440            // leak the canonical name: upstream returns a pointer valid for
1441            // the process lifetime (the alias hash owns the strings)
1442            let leaked: &'static [u8] = Box::leak(v.clone().into_boxed_slice());
1443            leaked.as_ptr() as *const c_char
1444        }
1445        None => ptr::null(),
1446    }
1447}
1448
1449/// `xmlCleanupEncodingAliases` implementation: drop all aliases.
1450pub(crate) fn cleanup_encoding_aliases() {
1451    encoding_aliases().write().clear();
1452}
1453
1454/// `xmlCharEncInFunc` implementation.
1455///
1456/// Converts the input buffer's encoding to UTF-8 using the given handler.
1457pub(crate) fn xmlCharEncInFunc(
1458    handler: *mut _xmlCharEncodingHandler,
1459    out: *mut _xmlBuffer,
1460    in_: *mut _xmlBuffer,
1461) -> c_int {
1462    char_enc_in(handler, out, in_)
1463}
1464
1465/// `xmlCharEncOutFunc` implementation.
1466///
1467/// Converts the input buffer from UTF-8 to the handler's output encoding.
1468pub(crate) fn xmlCharEncOutFunc(
1469    handler: *mut _xmlCharEncodingHandler,
1470    out: *mut _xmlBuffer,
1471    in_: *mut _xmlBuffer,
1472) -> c_int {
1473    char_enc_out(handler, out, in_)
1474}
1475
1476/// `xmlNewCharEncodingHandler` implementation.
1477///
1478/// Creates a new encoding handler with the given name and conversion functions.
1479/// The name string is duplicated. Returns a pointer to the new handler,
1480/// or `ptr::null_mut()` on allocation failure.
1481pub(crate) fn xmlNewCharEncodingHandler(
1482    name: *const c_char,
1483    input: xmlCharEncodingInputFunc,
1484    output: xmlCharEncodingOutputFunc,
1485) -> *mut _xmlCharEncodingHandler {
1486    if name.is_null() {
1487        return ptr::null_mut();
1488    }
1489
1490    let name_raw = unsafe { crate::abi::allocator::xmlMemStrdupImpl(name) };
1491    if name_raw.is_null() {
1492        return ptr::null_mut();
1493    }
1494
1495    let handler =
1496        unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) } as *mut _xmlCharEncodingHandler;
1497
1498    if handler.is_null() {
1499        unsafe { xmlFreeImpl(name_raw) };
1500        return ptr::null_mut();
1501    }
1502
1503    unsafe {
1504        ptr::write(
1505            handler,
1506            _xmlCharEncodingHandler {
1507                name: name_raw as *mut c_char,
1508                input: EncodingInputUnion {
1509                    legacyFunc: Some(input),
1510                },
1511                output: EncodingOutputUnion {
1512                    legacyFunc: Some(output),
1513                },
1514                inputCtxt: ptr::null_mut(),
1515                outputCtxt: ptr::null_mut(),
1516                ctxtDtor: None,
1517                flags: 0,
1518            },
1519        );
1520    }
1521
1522    handler
1523}
1524
1525/// `xmlDelEncodingHandler` implementation.
1526///
1527/// Frees an encoding handler previously created with `xmlNewCharEncodingHandler`.
1528pub(crate) fn xmlDelEncodingHandler(handler: *mut _xmlCharEncodingHandler) {
1529    if handler.is_null() {
1530        return;
1531    }
1532
1533    // Remove from registry if present
1534    {
1535        let mut handlers = ENCODING_HANDLERS.write();
1536        handlers.retain(|&h| h.0 != handler);
1537    }
1538
1539    unsafe {
1540        if !(*handler).name.is_null() {
1541            xmlFreeImpl((*handler).name as *mut c_void);
1542        }
1543        xmlFreeImpl(handler as *mut c_void);
1544    }
1545}
1546
1547/// `xmlInitCharEncodingHandlers` implementation.
1548pub(crate) fn xmlInitCharEncodingHandlers() {
1549    init_encodings();
1550}
1551
1552/// `xmlCleanupCharEncodingHandlers` implementation.
1553pub(crate) fn xmlCleanupCharEncodingHandlers() {
1554    cleanup_encodings();
1555}
1556
1557// ═══════════════════════════════════════════════════════════════════════════════
1558// 7. Handler lookup / creation (upstream 2.13.0+ encoding.c)
1559// ═══════════════════════════════════════════════════════════════════════════════
1560//
1561// Upstream keeps a static `defaultHandlers[32]` table indexed by xmlCharEncoding
1562// plus iconv/ICU fallbacks. The candidate ships no iconv/ICU, so encodings whose
1563// upstream default handler carries a real converter (UTF-8, UTF-16LE, UTF-16BE,
1564// UTF-16, ISO-8859-1, US-ASCII) resolve to the registered built-in handlers;
1565// every other encoding reports XML_ERR_UNSUPPORTED_ENCODING exactly where
1566// upstream would fall through to iconv/ICU.
1567
1568/// `xmlLookupCharEncodingHandler` implementation (upstream encoding.c).
1569///
1570/// Mirrors the upstream control flow:
1571///  - `out == NULL`                     → XML_ERR_ARGUMENT (115)
1572///  - `enc <= 0 || enc >= 32`           → XML_ERR_UNSUPPORTED_ENCODING (32)
1573///  - UTF-8                             → XML_ERR_OK, `*out` stays NULL
1574///  - native built-in encoding          → XML_ERR_OK, `*out` = static handler
1575///  - iconv/ICU-only encoding           → XML_ERR_UNSUPPORTED_ENCODING
1576///
1577/// The returned handler is a static registry entry and must NOT be freed.
1578pub(crate) fn xmlLookupCharEncodingHandler(enc: c_int, out: *mut *mut c_void) -> c_int {
1579    if out.is_null() {
1580        return crate::abi::types::XML_ERR_ARGUMENT;
1581    }
1582    unsafe {
1583        *out = ptr::null_mut();
1584    }
1585    if enc <= 0 || enc >= 32 {
1586        return crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING;
1587    }
1588    /* Return NULL handler for UTF-8 */
1589    if enc == xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int {
1590        return crate::abi::types::XML_ERR_OK;
1591    }
1592    let canonical: &[u8] = match enc {
1593        /* XML_CHAR_ENCODING_UTF16LE */
1594        2 => b"UTF-16LE\0",
1595        /* XML_CHAR_ENCODING_UTF16BE */
1596        3 => b"UTF-16BE\0",
1597        /* XML_CHAR_ENCODING_8859_1 */
1598        10 => b"ISO-8859-1\0",
1599        /* XML_CHAR_ENCODING_ASCII */
1600        22 => b"US-ASCII\0",
1601        /* XML_CHAR_ENCODING_UTF16 (not in the local enum) */
1602        23 => b"UTF-16\0",
1603        _ => return crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING,
1604    };
1605    let h = find_encoding_handler(canonical.as_ptr() as *const xmlChar);
1606    if h.is_null() {
1607        return crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING;
1608    }
1609    unsafe {
1610        *out = h as *mut c_void;
1611    }
1612    crate::abi::types::XML_ERR_OK
1613}
1614
1615/// `xmlGetCharEncodingHandler` implementation (deprecated upstream wrapper).
1616pub(crate) fn xmlGetCharEncodingHandler(enc: c_int) -> *mut c_void {
1617    let mut ret: *mut c_void = ptr::null_mut();
1618    let _rc = xmlLookupCharEncodingHandler(enc, &mut ret);
1619    ret
1620}
1621
1622/// `xmlCreateCharEncodingHandler` implementation (upstream 2.14.0+ encoding.c).
1623///
1624/// Flags: XML_ENC_INPUT = 1, XML_ENC_OUTPUT = 2, XML_ENC_HTML = 4.
1625/// Unlike upstream, no iconv/ICU backend exists, so encodings without a native
1626/// converter fall through to `find_extra_handler` (custom impl / deprecated
1627/// global registry) and otherwise report XML_ERR_UNSUPPORTED_ENCODING.
1628pub(crate) fn xmlCreateCharEncodingHandler(
1629    name: *const c_char,
1630    flags: c_int,
1631    impl_: Option<xmlCharEncConvImpl>,
1632    implCtxt: *mut c_void,
1633    out: *mut *mut c_void,
1634) -> c_int {
1635    if out.is_null() {
1636        return crate::abi::types::XML_ERR_ARGUMENT;
1637    }
1638    unsafe {
1639        *out = ptr::null_mut();
1640    }
1641    if name.is_null() || flags == 0 {
1642        return crate::abi::types::XML_ERR_ARGUMENT;
1643    }
1644    let norig = unsafe { CStr::from_ptr(name).to_bytes() };
1645
1646    /* Alias resolution (upstream xmlGetEncodingAlias). */
1647    let mut eff: &[u8] = norig;
1648    let alias = get_encoding_alias(name);
1649    if !alias.is_null() {
1650        eff = unsafe { CStr::from_ptr(alias).to_bytes() };
1651    }
1652
1653    let enc = encoding_from_name(eff);
1654
1655    /* Return NULL handler for UTF-8 */
1656    if enc == xmlCharEncoding::XML_CHAR_ENCODING_UTF8 {
1657        return crate::abi::types::XML_ERR_OK;
1658    }
1659
1660    let canonical: &[u8] = match enc {
1661        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE => b"UTF-16LE\0",
1662        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE => b"UTF-16BE\0",
1663        xmlCharEncoding::XML_CHAR_ENCODING_8859_1 => b"ISO-8859-1\0",
1664        xmlCharEncoding::XML_CHAR_ENCODING_ASCII => b"US-ASCII\0",
1665        _ => {
1666            return find_extra_handler(norig, eff, flags, impl_, implCtxt, out);
1667        }
1668    };
1669    let h = find_encoding_handler(canonical.as_ptr() as *const xmlChar);
1670    if h.is_null() {
1671        return find_extra_handler(norig, eff, flags, impl_, implCtxt, out);
1672    }
1673    unsafe {
1674        let src = &*h;
1675        let has_in = (flags & 1) == 0 || !src.input.legacyFunc.is_none();
1676        let has_out = (flags & 2) == 0 || !src.output.legacyFunc.is_none();
1677        if !has_in || !has_out {
1678            return find_extra_handler(norig, eff, flags, impl_, implCtxt, out);
1679        }
1680        /*
1681         * Return a copy of the handler with the original name (upstream
1682         * "Return a copy of the handler with the original name").
1683         */
1684        let copy = xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) as *mut _xmlCharEncodingHandler;
1685        if copy.is_null() {
1686            return crate::abi::types::XML_ERR_NO_MEMORY;
1687        }
1688        let name_copy = crate::abi::allocator::xmlMemStrdupImpl(name) as *mut c_char;
1689        if name_copy.is_null() {
1690            xmlFreeImpl(copy as *mut c_void);
1691            return crate::abi::types::XML_ERR_NO_MEMORY;
1692        }
1693        ptr::write(
1694            copy,
1695            _xmlCharEncodingHandler {
1696                name: name_copy,
1697                input: EncodingInputUnion {
1698                    legacyFunc: src.input.legacyFunc,
1699                },
1700                output: EncodingOutputUnion {
1701                    legacyFunc: src.output.legacyFunc,
1702                },
1703                inputCtxt: src.inputCtxt,
1704                outputCtxt: src.outputCtxt,
1705                ctxtDtor: src.ctxtDtor,
1706                flags: src.flags,
1707            },
1708        );
1709        *out = copy as *mut c_void;
1710    }
1711    crate::abi::types::XML_ERR_OK
1712}
1713
1714/// Fallback path of `xmlCreateCharEncodingHandler` (upstream `xmlFindExtraHandler`).
1715///
1716/// Tries the caller-supplied custom implementation first, then the deprecated
1717/// global handler registry. iconv/ICU do not exist in the candidate, so the
1718/// final result is XML_ERR_UNSUPPORTED_ENCODING.
1719fn find_extra_handler(
1720    norig: &[u8],
1721    name: &[u8],
1722    flags: c_int,
1723    impl_: Option<xmlCharEncConvImpl>,
1724    implCtxt: *mut c_void,
1725    out: *mut *mut c_void,
1726) -> c_int {
1727    /* Custom implementation before deprecated global handlers. */
1728    if let Some(f) = impl_ {
1729        let mut n = norig.to_vec();
1730        n.push(0);
1731        let rc = unsafe {
1732            f(
1733                implCtxt,
1734                n.as_ptr() as *const c_char,
1735                flags,
1736                out as *mut *mut crate::abi::structs::_xmlCharEncodingHandler,
1737            )
1738        };
1739        return rc;
1740    }
1741    /* Deprecated global handlers registry (xmlRegisterCharEncodingHandler). */
1742    let mut n = name.to_vec();
1743    n.push(0);
1744    let h = find_encoding_handler(n.as_ptr() as *const xmlChar);
1745    if !h.is_null() {
1746        unsafe {
1747            let src = &*h;
1748            let has_in = (flags & 1) == 0 || !src.input.legacyFunc.is_none();
1749            let has_out = (flags & 2) == 0 || !src.output.legacyFunc.is_none();
1750            if has_in && has_out {
1751                *out = h as *mut c_void;
1752                return crate::abi::types::XML_ERR_OK;
1753            }
1754        }
1755    }
1756    crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING
1757}
1758
1759/// `xmlOpenCharEncodingHandler` implementation (upstream encoding.c).
1760pub(crate) fn xmlOpenCharEncodingHandler(
1761    name: *const c_char,
1762    output: c_int,
1763    out: *mut *mut c_void,
1764) -> c_int {
1765    /* XML_ENC_OUTPUT if output else XML_ENC_INPUT */
1766    let flags: c_int = if output != 0 { 2 } else { 1 };
1767    xmlCreateCharEncodingHandler(name, flags, None, ptr::null_mut(), out)
1768}
1769
1770/// `xmlCharEncNewCustomHandler` implementation (upstream 2.15.0+ encoding.c).
1771///
1772/// Creates a handler backed by modern `xmlCharEncConvFunc` callbacks (with
1773/// per-direction contexts and a context destructor). The handler must be
1774/// released with `xmlCharEncCloseFunc`.
1775pub(crate) fn xmlCharEncNewCustomHandler(
1776    name: *const c_char,
1777    input: xmlCharEncConvFunc,
1778    output: xmlCharEncConvFunc,
1779    ctxtDtor: Option<xmlCharEncConvCtxtDtor>,
1780    inputCtxt: *mut c_void,
1781    outputCtxt: *mut c_void,
1782    out: *mut *mut c_void,
1783) -> c_int {
1784    if out.is_null() {
1785        return crate::abi::types::XML_ERR_ARGUMENT;
1786    }
1787    let handler =
1788        unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) } as *mut _xmlCharEncodingHandler;
1789    if handler.is_null() {
1790        unsafe {
1791            if let Some(d) = ctxtDtor {
1792                if !inputCtxt.is_null() {
1793                    d(inputCtxt);
1794                }
1795                if !outputCtxt.is_null() {
1796                    d(outputCtxt);
1797                }
1798            }
1799        }
1800        return crate::abi::types::XML_ERR_NO_MEMORY;
1801    }
1802    let name_copy = if name.is_null() {
1803        ptr::null_mut()
1804    } else {
1805        let nc = unsafe { crate::abi::allocator::xmlMemStrdupImpl(name) } as *mut c_char;
1806        if nc.is_null() {
1807            unsafe { xmlFreeImpl(handler as *mut c_void) };
1808            unsafe {
1809                if let Some(d) = ctxtDtor {
1810                    if !inputCtxt.is_null() {
1811                        d(inputCtxt);
1812                    }
1813                    if !outputCtxt.is_null() {
1814                        d(outputCtxt);
1815                    }
1816                }
1817            }
1818            return crate::abi::types::XML_ERR_NO_MEMORY;
1819        }
1820        nc
1821    };
1822    unsafe {
1823        ptr::write(
1824            handler,
1825            _xmlCharEncodingHandler {
1826                name: name_copy,
1827                input: EncodingInputUnion { func: Some(input) },
1828                output: EncodingOutputUnion { func: Some(output) },
1829                inputCtxt,
1830                outputCtxt,
1831                ctxtDtor,
1832                flags: 0,
1833            },
1834        );
1835        *out = handler as *mut c_void;
1836    }
1837    crate::abi::types::XML_ERR_OK
1838}
1839
1840// ═══════════════════════════════════════════════════════════════════════════════
1841// Tests
1842// ═══════════════════════════════════════════════════════════════════════════════
1843
1844#[cfg(test)]
1845mod tests {
1846    use super::*;
1847
1848    // ── BOM detection ──────────────────────────────────────────────────────
1849
1850    #[test]
1851    fn test_detect_bom_utf8() {
1852        let data = [0xEF, 0xBB, 0xBF, b'<', b'?', b'x', b'm', b'l'];
1853        assert_eq!(
1854            detect_encoding_from_bom(&data),
1855            xmlCharEncoding::XML_CHAR_ENCODING_UTF8
1856        );
1857    }
1858
1859    #[test]
1860    fn test_detect_bom_utf16le() {
1861        let data = [0xFF, 0xFE, 0x00, 0x01];
1862        assert_eq!(
1863            detect_encoding_from_bom(&data),
1864            xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
1865        );
1866    }
1867
1868    #[test]
1869    fn test_detect_bom_utf16be() {
1870        let data = [0xFE, 0xFF, 0x00, 0x01];
1871        assert_eq!(
1872            detect_encoding_from_bom(&data),
1873            xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE
1874        );
1875    }
1876
1877    #[test]
1878    fn test_detect_bom_none() {
1879        let data = b"<xml>";
1880        assert_eq!(
1881            detect_encoding_from_bom(data),
1882            xmlCharEncoding::XML_CHAR_ENCODING_NONE
1883        );
1884    }
1885
1886    #[test]
1887    fn test_detect_bom_empty() {
1888        assert_eq!(
1889            detect_encoding_from_bom(b""),
1890            xmlCharEncoding::XML_CHAR_ENCODING_NONE
1891        );
1892    }
1893
1894    // ── Encoding from declaration ──────────────────────────────────────────
1895
1896    #[test]
1897    fn test_detect_encoding_declaration_utf8() {
1898        let data = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
1899        let result = detect_encoding_from_declaration(data);
1900        assert_eq!(result, Some(b"utf-8".to_vec()));
1901    }
1902
1903    #[test]
1904    fn test_detect_encoding_declaration_iso() {
1905        let data = b"<?xml version='1.0' encoding='ISO-8859-1'?>";
1906        let result = detect_encoding_from_declaration(data);
1907        assert_eq!(result, Some(b"iso-8859-1".to_vec()));
1908    }
1909
1910    #[test]
1911    fn test_detect_encoding_declaration_none() {
1912        let data = b"<?xml version=\"1.0\"?>";
1913        let result = detect_encoding_from_declaration(data);
1914        assert!(result.is_none());
1915    }
1916
1917    #[test]
1918    fn test_detect_encoding_declaration_no_xml() {
1919        let data = b"<root>";
1920        let result = detect_encoding_from_declaration(data);
1921        assert!(result.is_none());
1922    }
1923
1924    #[test]
1925    fn test_detect_encoding_declaration_with_bom() {
1926        let mut data = vec![0xEF, 0xBB, 0xBF];
1927        data.extend_from_slice(b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
1928        let result = detect_encoding_from_declaration(&data);
1929        assert_eq!(result, Some(b"utf-8".to_vec()));
1930    }
1931
1932    // ── Encoding from name ─────────────────────────────────────────────────
1933
1934    #[test]
1935    fn test_encoding_from_name_utf8() {
1936        assert_eq!(
1937            encoding_from_name(b"UTF-8"),
1938            xmlCharEncoding::XML_CHAR_ENCODING_UTF8
1939        );
1940        assert_eq!(
1941            encoding_from_name(b"utf8"),
1942            xmlCharEncoding::XML_CHAR_ENCODING_UTF8
1943        );
1944    }
1945
1946    #[test]
1947    fn test_encoding_from_name_utf16() {
1948        assert_eq!(
1949            encoding_from_name(b"UTF-16LE"),
1950            xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
1951        );
1952        assert_eq!(
1953            encoding_from_name(b"UTF-16BE"),
1954            xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE
1955        );
1956        assert_eq!(
1957            encoding_from_name(b"utf-16"),
1958            xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
1959        );
1960    }
1961
1962    #[test]
1963    fn test_encoding_from_name_latin1() {
1964        assert_eq!(
1965            encoding_from_name(b"ISO-8859-1"),
1966            xmlCharEncoding::XML_CHAR_ENCODING_8859_1
1967        );
1968        assert_eq!(
1969            encoding_from_name(b"Latin1"),
1970            xmlCharEncoding::XML_CHAR_ENCODING_8859_1
1971        );
1972    }
1973
1974    #[test]
1975    fn test_encoding_from_name_ascii() {
1976        assert_eq!(
1977            encoding_from_name(b"ASCII"),
1978            xmlCharEncoding::XML_CHAR_ENCODING_ASCII
1979        );
1980        assert_eq!(
1981            encoding_from_name(b"US-ASCII"),
1982            xmlCharEncoding::XML_CHAR_ENCODING_ASCII
1983        );
1984    }
1985
1986    #[test]
1987    fn test_encoding_from_name_error() {
1988        assert_eq!(
1989            encoding_from_name(b"invalid-encoding"),
1990            xmlCharEncoding::XML_CHAR_ENCODING_ERROR
1991        );
1992    }
1993
1994    #[test]
1995    fn test_encoding_from_name_empty() {
1996        assert_eq!(
1997            encoding_from_name(b""),
1998            xmlCharEncoding::XML_CHAR_ENCODING_ERROR
1999        );
2000    }
2001
2002    // ── Encoding name ──────────────────────────────────────────────────────
2003
2004    #[test]
2005    fn test_encoding_name_utf8() {
2006        assert_eq!(
2007            encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_UTF8),
2008            Some(b"UTF-8" as &[u8])
2009        );
2010    }
2011
2012    #[test]
2013    fn test_encoding_name_utf16le() {
2014        assert_eq!(
2015            encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE),
2016            Some(b"UTF-16LE" as &[u8])
2017        );
2018    }
2019
2020    #[test]
2021    fn test_encoding_name_none() {
2022        assert!(encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_NONE).is_none());
2023    }
2024
2025    #[test]
2026    fn test_encoding_name_error() {
2027        assert!(encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_ERROR).is_none());
2028    }
2029
2030    // ── UTF-8 validation ───────────────────────────────────────────────────
2031
2032    #[test]
2033    fn test_utf8_valid_ascii() {
2034        assert!(utf8_valid(b"hello world"));
2035    }
2036
2037    #[test]
2038    fn test_utf8_valid_multi_byte() {
2039        assert!(utf8_valid("héllo wörld 🌍".as_bytes()));
2040    }
2041
2042    #[test]
2043    fn test_utf8_valid_empty() {
2044        assert!(utf8_valid(b""));
2045    }
2046
2047    #[test]
2048    fn test_utf8_invalid() {
2049        assert!(!utf8_valid(&[0xFF, 0xFE, 0x00]));
2050    }
2051
2052    // ── XML char validation ────────────────────────────────────────────────
2053
2054    #[test]
2055    fn test_valid_xml_chars() {
2056        assert!(is_valid_xml_char(0x9)); // Tab
2057        assert!(is_valid_xml_char(0xA)); // LF
2058        assert!(is_valid_xml_char(0xD)); // CR
2059        assert!(is_valid_xml_char(0x20)); // Space
2060        assert!(is_valid_xml_char(0x41)); // 'A'
2061        assert!(is_valid_xml_char(0xD7FF));
2062        assert!(is_valid_xml_char(0xE000));
2063        assert!(is_valid_xml_char(0xFFFD));
2064        assert!(is_valid_xml_char(0x10000));
2065        assert!(is_valid_xml_char(0x10FFFF));
2066    }
2067
2068    #[test]
2069    fn test_invalid_xml_chars() {
2070        assert!(!is_valid_xml_char(0x00));
2071        assert!(!is_valid_xml_char(0x08));
2072        assert!(!is_valid_xml_char(0x0B));
2073        assert!(!is_valid_xml_char(0x0C));
2074        assert!(!is_valid_xml_char(0x0E));
2075        assert!(!is_valid_xml_char(0x1F));
2076        assert!(!is_valid_xml_char(0xD800)); // Surrogate
2077        assert!(!is_valid_xml_char(0xDFFF)); // Surrogate
2078        assert!(!is_valid_xml_char(0xFFFE));
2079        assert!(!is_valid_xml_char(0xFFFF));
2080        assert!(!is_valid_xml_char(0x110000));
2081    }
2082
2083    // ── UTF-16LE to UTF-8 ──────────────────────────────────────────────────
2084
2085    #[test]
2086    fn test_utf16le_to_utf8_ascii() {
2087        // "AB" in UTF-16LE
2088        let data = [b'A', 0x00, b'B', 0x00];
2089        let result = utf16le_to_utf8(&data).unwrap();
2090        assert_eq!(result, b"AB");
2091    }
2092
2093    #[test]
2094    fn test_utf16le_to_utf8_bom() {
2095        let mut data = vec![0xFF, 0xFE]; // BOM
2096        data.extend_from_slice(&[b'A', 0x00, b'B', 0x00]);
2097        let result = utf16le_to_utf8(&data).unwrap();
2098        assert_eq!(result, b"AB");
2099    }
2100
2101    #[test]
2102    fn test_utf16le_to_utf8_bmp() {
2103        // U+00E9 (é) in UTF-16LE = 0xE9 0x00
2104        let data = [0xE9, 0x00];
2105        let result = utf16le_to_utf8(&data).unwrap();
2106        assert_eq!(result, "é".as_bytes());
2107    }
2108
2109    #[test]
2110    fn test_utf16le_to_utf8_supplementary() {
2111        // U+1F600 (😀) in UTF-16LE = 0x3D 0xD8 0x00 0xDE
2112        let data = [0x3D, 0xD8, 0x00, 0xDE];
2113        let result = utf16le_to_utf8(&data).unwrap();
2114        assert_eq!(result, "😀".as_bytes());
2115    }
2116
2117    #[test]
2118    fn test_utf16le_to_utf8_unpaired_surrogate() {
2119        let data = [0x00, 0xD8]; // High surrogate without low
2120        assert!(utf16le_to_utf8(&data).is_err());
2121    }
2122
2123    #[test]
2124    fn test_utf16le_to_utf8_truncated() {
2125        let data = [0x00]; // Odd length
2126        assert!(utf16le_to_utf8(&data).is_err());
2127    }
2128
2129    #[test]
2130    fn test_utf16le_to_utf8_empty() {
2131        let result = utf16le_to_utf8(b"").unwrap();
2132        assert!(result.is_empty());
2133    }
2134
2135    // ── UTF-16BE to UTF-8 ──────────────────────────────────────────────────
2136
2137    #[test]
2138    fn test_utf16be_to_utf8_ascii() {
2139        let data = [0x00, b'A', 0x00, b'B'];
2140        let result = utf16be_to_utf8(&data).unwrap();
2141        assert_eq!(result, b"AB");
2142    }
2143
2144    #[test]
2145    fn test_utf16be_to_utf8_bom() {
2146        let mut data = vec![0xFE, 0xFF]; // BOM
2147        data.extend_from_slice(&[0x00, b'A', 0x00, b'B']);
2148        let result = utf16be_to_utf8(&data).unwrap();
2149        assert_eq!(result, b"AB");
2150    }
2151
2152    #[test]
2153    fn test_utf16be_to_utf8_supplementary() {
2154        // U+1F600 (😀) in UTF-16BE = 0xD8 0x3D 0xDE 0x00
2155        let data = [0xD8, 0x3D, 0xDE, 0x00];
2156        let result = utf16be_to_utf8(&data).unwrap();
2157        assert_eq!(result, "😀".as_bytes());
2158    }
2159
2160    #[test]
2161    fn test_utf16be_to_utf8_empty() {
2162        let result = utf16be_to_utf8(b"").unwrap();
2163        assert!(result.is_empty());
2164    }
2165
2166    // ── UTF-8 to UTF-16LE ──────────────────────────────────────────────────
2167
2168    #[test]
2169    fn test_utf8_to_utf16le_ascii() {
2170        let result = utf8_to_utf16le(b"AB").unwrap();
2171        assert_eq!(result, [b'A', 0x00, b'B', 0x00]);
2172    }
2173
2174    #[test]
2175    fn test_utf8_to_utf16le_bmp() {
2176        let result = utf8_to_utf16le("é".as_bytes()).unwrap();
2177        assert_eq!(result, [0xE9, 0x00]);
2178    }
2179
2180    #[test]
2181    fn test_utf8_to_utf16le_supplementary() {
2182        let result = utf8_to_utf16le("😀".as_bytes()).unwrap();
2183        assert_eq!(result, [0x3D, 0xD8, 0x00, 0xDE]);
2184    }
2185
2186    #[test]
2187    fn test_utf8_to_utf16le_invalid_utf8() {
2188        assert!(utf8_to_utf16le(&[0xFF]).is_err());
2189    }
2190
2191    #[test]
2192    fn test_utf8_to_utf16le_empty() {
2193        let result = utf8_to_utf16le(b"").unwrap();
2194        assert!(result.is_empty());
2195    }
2196
2197    // ── Latin-1 to UTF-8 ───────────────────────────────────────────────────
2198
2199    #[test]
2200    fn test_latin1_to_utf8_ascii() {
2201        let result = latin1_to_utf8(b"ABC");
2202        assert_eq!(result, b"ABC");
2203    }
2204
2205    #[test]
2206    fn test_latin1_to_utf8_accented() {
2207        // 0xE9 = é in Latin-1
2208        let result = latin1_to_utf8(&[0xE9]);
2209        assert_eq!(result, "é".as_bytes());
2210    }
2211
2212    #[test]
2213    fn test_latin1_to_utf8_all_255() {
2214        let result = latin1_to_utf8(&[0xFF]);
2215        // U+00FF = ÿ, UTF-8: 0xC3 0xBF
2216        assert_eq!(result, [0xC3, 0xBF]);
2217    }
2218
2219    #[test]
2220    fn test_latin1_to_utf8_empty() {
2221        let result = latin1_to_utf8(b"");
2222        assert!(result.is_empty());
2223    }
2224
2225    #[test]
2226    fn test_latin1_to_utf8_mixed() {
2227        let result = latin1_to_utf8(b"caf\xE9");
2228        assert_eq!(result, "café".as_bytes());
2229    }
2230
2231    // ── UTF-8 to Latin-1 ───────────────────────────────────────────────────
2232
2233    #[test]
2234    fn test_utf8_to_latin1_ascii() {
2235        let result = utf8_to_latin1(b"ABC").unwrap();
2236        assert_eq!(result, b"ABC");
2237    }
2238
2239    #[test]
2240    fn test_utf8_to_latin1_accented() {
2241        let result = utf8_to_latin1("é".as_bytes()).unwrap();
2242        assert_eq!(result, [0xE9]);
2243    }
2244
2245    #[test]
2246    fn test_utf8_to_latin1_out_of_range() {
2247        assert!(utf8_to_latin1("€".as_bytes()).is_err()); // U+20AC not in Latin-1
2248    }
2249
2250    #[test]
2251    fn test_utf8_to_latin1_invalid_utf8() {
2252        assert!(utf8_to_latin1(&[0xFF]).is_err());
2253    }
2254
2255    #[test]
2256    fn test_utf8_to_latin1_empty() {
2257        let result = utf8_to_latin1(b"").unwrap();
2258        assert!(result.is_empty());
2259    }
2260
2261    // ── Encoding handler registry ──────────────────────────────────────────
2262
2263    #[test]
2264    fn test_init_and_find_encodings() {
2265        init_encodings();
2266
2267        let utf8_name: *const xmlChar = b"UTF-8\0".as_ptr();
2268        assert!(!find_encoding_handler(utf8_name).is_null());
2269
2270        let utf16le_name: *const xmlChar = b"UTF-16LE\0".as_ptr();
2271        assert!(!find_encoding_handler(utf16le_name).is_null());
2272
2273        let utf16be_name: *const xmlChar = b"UTF-16BE\0".as_ptr();
2274        assert!(!find_encoding_handler(utf16be_name).is_null());
2275
2276        let latin1_name: *const xmlChar = b"ISO-8859-1\0".as_ptr();
2277        assert!(!find_encoding_handler(latin1_name).is_null());
2278
2279        let ascii_name: *const xmlChar = b"ASCII\0".as_ptr();
2280        assert!(!find_encoding_handler(ascii_name).is_null());
2281
2282        // Case insensitive
2283        let lower_name: *const xmlChar = b"utf-8\0".as_ptr();
2284        assert!(!find_encoding_handler(lower_name).is_null());
2285    }
2286
2287    #[test]
2288    fn test_find_encoding_handler_not_found() {
2289        let name: *const xmlChar = b"NONEXISTENT\0".as_ptr();
2290        assert!(find_encoding_handler(name).is_null());
2291    }
2292
2293    #[test]
2294    fn test_find_encoding_handler_null() {
2295        assert!(find_encoding_handler(ptr::null()).is_null());
2296    }
2297
2298    #[test]
2299    fn test_add_encoding_handler() {
2300        let handler = unsafe {
2301            xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) as *mut _xmlCharEncodingHandler
2302        };
2303        assert!(!handler.is_null());
2304
2305        let name =
2306            unsafe { crate::abi::allocator::xmlMemStrdupImpl(b"TEST-ENC\0".as_ptr() as *const c_char) };
2307        unsafe {
2308            ptr::write(
2309                handler,
2310                _xmlCharEncodingHandler {
2311                    name: name as *mut c_char,
2312                    input: EncodingInputUnion { legacyFunc: None },
2313                    output: EncodingOutputUnion { legacyFunc: None },
2314                    inputCtxt: ptr::null_mut(),
2315                    outputCtxt: ptr::null_mut(),
2316                    ctxtDtor: None,
2317                    flags: 0,
2318                },
2319            );
2320        }
2321
2322        assert_eq!(add_encoding_handler(handler), 0);
2323
2324        let found = find_encoding_handler(b"TEST-ENC\0".as_ptr() as *const xmlChar);
2325        assert_eq!(found, handler);
2326
2327        // Remove from registry before freeing to avoid dangling pointers
2328        {
2329            let mut handlers = ENCODING_HANDLERS.write();
2330            handlers.retain(|&h| h.0 != handler);
2331        }
2332
2333        unsafe {
2334            xmlFreeImpl(name as *mut c_void);
2335            xmlFreeImpl(handler as *mut c_void);
2336        }
2337    }
2338
2339    // ── Conversion round-trips ─────────────────────────────────────────────
2340
2341    #[test]
2342    fn test_utf16le_roundtrip() {
2343        let original = b"Hello, World! UTF-16LE test: \xC3\xA9\xF0\x9F\x98\x80";
2344        let utf16 = utf8_to_utf16le(original).unwrap();
2345        let back = utf16le_to_utf8(&utf16).unwrap();
2346        assert_eq!(original.to_vec(), back);
2347    }
2348
2349    #[test]
2350    fn test_utf16be_roundtrip() {
2351        let original = b"Hello, World! UTF-16BE test: \xC3\xA9\xF0\x9F\x98\x80";
2352        let utf16le = utf8_to_utf16le(original).unwrap();
2353        // Convert LE to BE by swapping bytes
2354        let mut utf16be = utf16le.clone();
2355        for chunk in utf16be.chunks_exact_mut(2) {
2356            chunk.swap(0, 1);
2357        }
2358        let back = utf16be_to_utf8(&utf16be).unwrap();
2359        assert_eq!(original.to_vec(), back);
2360    }
2361
2362    #[test]
2363    fn test_latin1_roundtrip() {
2364        let original: Vec<u8> = (0x00..=0xFF).collect();
2365        let utf8 = latin1_to_utf8(&original);
2366        let back = utf8_to_latin1(&utf8).unwrap();
2367        assert_eq!(original, back);
2368    }
2369
2370    // ── Built-in handler callbacks ─────────────────────────────────────────
2371
2372    #[test]
2373    fn test_utf8_handler_identity() {
2374        let input = b"Hello, UTF-8!";
2375        let mut output = [0u8; 64];
2376        let mut outlen = output.len() as c_int;
2377        let mut inlen = input.len() as c_int;
2378
2379        let ret = unsafe {
2380            utf8_input_func(output.as_mut_ptr(), &mut outlen, input.as_ptr(), &mut inlen)
2381        };
2382
2383        assert_eq!(ret, input.len() as c_int);
2384        assert_eq!(&output[..ret as usize], input);
2385        assert_eq!(inlen, input.len() as c_int);
2386    }
2387
2388    #[test]
2389    fn test_utf16le_handler_roundtrip() {
2390        init_encodings();
2391
2392        let original = b"Hello UTF-16LE!";
2393        let mut utf16_buf = [0u8; 128];
2394        let mut outlen = utf16_buf.len() as c_int;
2395        let mut inlen = original.len() as c_int;
2396
2397        let written = unsafe {
2398            utf16le_output_func(
2399                utf16_buf.as_mut_ptr(),
2400                &mut outlen,
2401                original.as_ptr(),
2402                &mut inlen,
2403            )
2404        };
2405        assert!(written > 0);
2406
2407        // Now decode back
2408        let mut decoded = [0u8; 128];
2409        let mut outlen2 = decoded.len() as c_int;
2410        let mut inlen2 = written;
2411
2412        let written2 = unsafe {
2413            utf16le_input_func(
2414                decoded.as_mut_ptr(),
2415                &mut outlen2,
2416                utf16_buf.as_ptr(),
2417                &mut inlen2,
2418            )
2419        };
2420        assert_eq!(written2 as usize, original.len());
2421        assert_eq!(&decoded[..written2 as usize], original);
2422    }
2423
2424    // ── xmlBuffer operations ───────────────────────────────────────────────
2425
2426    #[test]
2427    fn test_append_to_xml_buffer() {
2428        unsafe {
2429            let content = xmlMallocImpl(64) as *mut xmlChar;
2430            assert!(!content.is_null());
2431
2432            let mut buf = _xmlBuffer {
2433                content,
2434                use_: 0,
2435                size: 64,
2436                alloc: 0,
2437                contentIO: ptr::null_mut(),
2438            };
2439
2440            append_to_xml_buffer(&mut buf, b"Hello");
2441            assert_eq!(buf.use_, 5);
2442            let slice = core::slice::from_raw_parts(buf.content, 5);
2443            assert_eq!(slice, b"Hello");
2444
2445            append_to_xml_buffer(&mut buf, b" World");
2446            assert_eq!(buf.use_, 11);
2447            let slice = core::slice::from_raw_parts(buf.content, 11);
2448            assert_eq!(slice, b"Hello World");
2449
2450            xmlFreeImpl(buf.content as *mut c_void);
2451        }
2452    }
2453
2454    // ── ABI export functions ───────────────────────────────────────────────
2455
2456    #[test]
2457    fn test_xml_parse_char_encoding() {
2458        let name = b"UTF-8\0".as_ptr() as *const c_char;
2459        assert_eq!(
2460            xmlParseCharEncoding(name),
2461            xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int
2462        );
2463
2464        let name = b"ISO-8859-1\0".as_ptr() as *const c_char;
2465        assert_eq!(
2466            xmlParseCharEncoding(name),
2467            xmlCharEncoding::XML_CHAR_ENCODING_8859_1 as c_int
2468        );
2469
2470        assert_eq!(
2471            xmlParseCharEncoding(ptr::null()),
2472            xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int
2473        );
2474    }
2475
2476    #[test]
2477    fn test_xml_new_and_del_encoding_handler() {
2478        let name = b"TestEnc\0".as_ptr() as *const c_char;
2479        let handler = xmlNewCharEncodingHandler(
2480            name,
2481            utf8_input_func as xmlCharEncodingInputFunc,
2482            utf8_output_func as xmlCharEncodingOutputFunc,
2483        );
2484        assert!(!handler.is_null());
2485
2486        unsafe {
2487            assert!(!(*handler).name.is_null());
2488            let cstr = CStr::from_ptr((*handler).name);
2489            assert_eq!(cstr.to_bytes(), b"TestEnc");
2490        }
2491
2492        xmlDelEncodingHandler(handler);
2493    }
2494
2495    #[test]
2496    fn test_xml_init_and_cleanup() {
2497        xmlInitCharEncodingHandlers();
2498
2499        let name: *const xmlChar = b"UTF-8\0".as_ptr();
2500        assert!(!find_encoding_handler(name).is_null());
2501
2502        xmlCleanupCharEncodingHandlers();
2503        // After cleanup, handlers should be empty
2504    }
2505}