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