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