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