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