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.
707///
708/// # Safety
709///
710/// - `name_bytes` must be a valid byte slice containing a NUL terminator;
711///   `xmlMemStrdupImpl` scans it as a C string.
712/// - The `xmlMallocImpl` result is NULL-checked before `ptr::write`
713///   initializes the handler; the written handler is inserted into the
714///   global registry, which keeps it alive for the process lifetime.
715fn register_handler(
716    name_bytes: &[u8],
717    _input_enc: xmlCharEncoding,
718    _output_enc: xmlCharEncoding,
719    input_func: Option<xmlCharEncodingInputFunc>,
720    output_func: Option<xmlCharEncodingOutputFunc>,
721) {
722    let name_raw =
723        unsafe { crate::abi::allocator::xmlMemStrdupImpl(name_bytes.as_ptr() as *const c_char) };
724    if name_raw.is_null() {
725        return;
726    }
727
728    let handler = unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) }
729        as *mut _xmlCharEncodingHandler;
730
731    if handler.is_null() {
732        unsafe { xmlFreeImpl(name_raw) };
733        return;
734    }
735
736    unsafe {
737        ptr::write(
738            handler,
739            _xmlCharEncodingHandler {
740                name: name_raw as *mut c_char,
741                input: EncodingInputUnion {
742                    legacyFunc: input_func,
743                },
744                output: EncodingOutputUnion {
745                    legacyFunc: output_func,
746                },
747                inputCtxt: ptr::null_mut(),
748                outputCtxt: ptr::null_mut(),
749                ctxtDtor: None,
750                flags: 0,
751            },
752        );
753    }
754
755    add_encoding_handler(handler);
756}
757
758/// Clean up encoding handlers.
759///
760/// Frees all registered handlers and resets the registry.
761///
762/// # Safety
763///
764/// - Every registered handler pointer must be NULL or a valid
765///   heap-allocated `_xmlCharEncodingHandler` whose `name` is NULL or a
766///   heap-allocated NUL-terminated string; each allocation is freed exactly
767///   once and must not be freed elsewhere.
768pub(crate) fn cleanup_encodings() {
769    let mut handlers = ENCODING_HANDLERS.write();
770    for &handler in handlers.iter() {
771        let ptr = handler.0;
772        if !ptr.is_null() {
773            unsafe {
774                if !(*ptr).name.is_null() {
775                    xmlFreeImpl((*ptr).name as *mut c_void);
776                }
777                xmlFreeImpl(ptr as *mut c_void);
778            }
779        }
780    }
781    handlers.clear();
782    ENCODING_INITIALIZED.store(false, Ordering::SeqCst);
783}
784
785/// Find an encoding handler by name.
786///
787/// Searches the global handler registry for a handler whose name matches
788/// (case-insensitive). Returns a pointer to the handler, or `ptr::null_mut()`
789/// if not found.
790///
791/// # Safety
792///
793/// - `name` must be NULL or a valid pointer to a NUL-terminated string.
794/// - Each registry entry must be NULL or a valid `_xmlCharEncodingHandler`
795///   whose `name` is NULL or a valid NUL-terminated string.
796pub(crate) fn find_encoding_handler(name: *const xmlChar) -> *mut _xmlCharEncodingHandler {
797    if name.is_null() {
798        return ptr::null_mut();
799    }
800
801    /* The upstream default-handler table is static and always present; the
802     * candidate's registry is populated lazily, so ensure it is initialized
803     * before any name-based lookup. Idempotent. */
804    init_encodings();
805
806    let name_str = unsafe {
807        match CStr::from_ptr(name as *const c_char).to_bytes() {
808            b"" => return ptr::null_mut(),
809            s => s,
810        }
811    };
812
813    let handlers = ENCODING_HANDLERS.read();
814    for &handler in handlers.iter() {
815        let ptr = handler.0;
816        if ptr.is_null() {
817            continue;
818        }
819        let h_name = unsafe {
820            if (*ptr).name.is_null() {
821                continue;
822            }
823            CStr::from_ptr((*ptr).name).to_bytes()
824        };
825
826        if name_str.eq_ignore_ascii_case(h_name) {
827            return ptr;
828        }
829    }
830
831    ptr::null_mut()
832}
833
834/// Add an encoding handler to the registry.
835///
836/// Returns 0 on success, -1 on failure (e.g., null pointer).
837pub(crate) fn add_encoding_handler(handler: *mut _xmlCharEncodingHandler) -> c_int {
838    if handler.is_null() {
839        return -1;
840    }
841
842    let mut handlers = ENCODING_HANDLERS.write();
843    handlers.push(HandlerPtr(handler));
844    0
845}
846
847// ═══════════════════════════════════════════════════════════════════════════════
848// 6. Encoding conversion functions
849// ═══════════════════════════════════════════════════════════════════════════════
850
851/// Input conversion: convert from handler's input encoding to UTF-8.
852///
853/// Calls the handler's `input.legacyFunc` callback. Returns bytes written or -1 on error.
854///
855/// # Safety
856///
857/// - `handler` must be NULL or a valid pointer to an initialized
858///   `_xmlCharEncodingHandler`; the stored `input.legacyFunc` callback, when
859///   present, must be a valid function pointer.
860/// - `out` must be a valid mutable byte slice and `in_data` a valid byte
861///   slice; both stay valid for the duration of the callback.
862#[allow(dead_code)]
863pub(crate) fn char_enc_in_func(
864    handler: *mut _xmlCharEncodingHandler,
865    out: &mut [u8],
866    in_data: &[u8],
867) -> c_int {
868    if handler.is_null() {
869        return -1;
870    }
871
872    let h = unsafe { &*handler };
873    let input_func = unsafe { h.input.legacyFunc };
874    let input_func = match input_func {
875        Some(f) => f,
876        None => return -1,
877    };
878
879    let mut outlen = out.len() as c_int;
880    let mut inlen = in_data.len() as c_int;
881
882    unsafe { input_func(out.as_mut_ptr(), &mut outlen, in_data.as_ptr(), &mut inlen) }
883}
884
885/// Output conversion: convert from UTF-8 to handler's output encoding.
886///
887/// Calls the handler's `output.legacyFunc` callback. Returns bytes written or -1 on error.
888///
889/// # Safety
890///
891/// - `handler` must be NULL or a valid pointer to an initialized
892///   `_xmlCharEncodingHandler`; the stored `output.legacyFunc` callback,
893///   when present, must be a valid function pointer.
894/// - `out` must be a valid mutable byte slice and `in_data` a valid byte
895///   slice; both stay valid for the duration of the callback.
896#[allow(dead_code)]
897pub(crate) fn char_enc_out_func(
898    handler: *mut _xmlCharEncodingHandler,
899    out: &mut [u8],
900    in_data: &[u8],
901) -> c_int {
902    if handler.is_null() {
903        return -1;
904    }
905
906    let h = unsafe { &*handler };
907    let output_func = unsafe { h.output.legacyFunc };
908    let output_func = match output_func {
909        Some(f) => f,
910        None => return -1,
911    };
912
913    let mut outlen = out.len() as c_int;
914    let mut inlen = in_data.len() as c_int;
915
916    unsafe { output_func(out.as_mut_ptr(), &mut outlen, in_data.as_ptr(), &mut inlen) }
917}
918
919/// Full input conversion (`xmlCharEncInFunc` equivalent).
920///
921/// Reads from the input `_xmlBuffer`, converts via the handler's `input.legacyFunc`,
922/// and appends the result to the output `_xmlBuffer`.
923///
924/// Returns the number of bytes written to the output buffer, or -1 on error.
925///
926/// # Safety
927///
928/// - `handler` must be NULL or a valid `_xmlCharEncodingHandler` whose
929///   `input.legacyFunc` callback is a valid function pointer.
930/// - `in_` and `out` must be NULL or valid `_xmlBuffer` pointers; `in_`'s
931///   `content` must be NULL or point to `use_` readable bytes, and `out`
932///   must stay valid while `append_to_xml_buffer` may reallocate its
933///   `content`.
934pub(crate) fn char_enc_in(
935    handler: *mut _xmlCharEncodingHandler,
936    out: *mut _xmlBuffer,
937    in_: *mut _xmlBuffer,
938) -> c_int {
939    if handler.is_null() || out.is_null() || in_.is_null() {
940        return -1;
941    }
942
943    let h = unsafe { &*handler };
944    let input_func = unsafe { h.input.legacyFunc };
945    let input_func = match input_func {
946        Some(f) => f,
947        None => return -1,
948    };
949
950    let in_buf = unsafe { &*in_ };
951    let out_buf = unsafe { &mut *out };
952
953    if in_buf.content.is_null() || in_buf.use_ == 0 {
954        return 0;
955    }
956
957    let in_data = unsafe { core::slice::from_raw_parts(in_buf.content, in_buf.use_ as usize) };
958
959    // Allocate an output buffer. A good heuristic is 2x input for UTF-16→UTF-8.
960    let out_capacity = (in_buf.use_ as usize).saturating_mul(3).max(256);
961    let mut out_vec = vec![0u8; out_capacity];
962    let mut out_len = out_capacity as c_int;
963    let mut in_len = in_buf.use_ as c_int;
964
965    let ret = unsafe {
966        input_func(
967            out_vec.as_mut_ptr(),
968            &mut out_len,
969            in_data.as_ptr(),
970            &mut in_len,
971        )
972    };
973
974    if ret < 0 {
975        return -1;
976    }
977
978    let written = ret as usize;
979
980    // Append to output buffer
981    append_to_xml_buffer(out_buf, &out_vec[..written]);
982
983    written as c_int
984}
985
986/// Full output conversion (`xmlCharEncOutFunc` equivalent).
987///
988/// Reads from the input `_xmlBuffer` (UTF-8), converts via the handler's
989/// `output.legacyFunc`, and appends the result to the output `_xmlBuffer`.
990///
991/// Returns the number of bytes written to the output buffer, or -1 on error.
992///
993/// # Safety
994///
995/// - `handler` must be NULL or a valid `_xmlCharEncodingHandler` whose
996///   `output.legacyFunc` callback is a valid function pointer.
997/// - `in_` and `out` must be NULL or valid `_xmlBuffer` pointers; `in_`'s
998///   `content` must be NULL or point to `use_` readable bytes, and `out`
999///   must stay valid while `append_to_xml_buffer` may reallocate its
1000///   `content`.
1001pub(crate) fn char_enc_out(
1002    handler: *mut _xmlCharEncodingHandler,
1003    out: *mut _xmlBuffer,
1004    in_: *mut _xmlBuffer,
1005) -> c_int {
1006    if handler.is_null() || out.is_null() || in_.is_null() {
1007        return -1;
1008    }
1009
1010    let h = unsafe { &*handler };
1011    let output_func = unsafe { h.output.legacyFunc };
1012    let output_func = match output_func {
1013        Some(f) => f,
1014        None => return -1,
1015    };
1016
1017    let in_buf = unsafe { &*in_ };
1018    let out_buf = unsafe { &mut *out };
1019
1020    if in_buf.content.is_null() || in_buf.use_ == 0 {
1021        return 0;
1022    }
1023
1024    let in_data = unsafe { core::slice::from_raw_parts(in_buf.content, in_buf.use_ as usize) };
1025
1026    let out_capacity = (in_buf.use_ as usize).saturating_mul(3).max(256);
1027    let mut out_vec = vec![0u8; out_capacity];
1028    let mut out_len = out_capacity as c_int;
1029    let mut in_len = in_buf.use_ as c_int;
1030
1031    let ret = unsafe {
1032        output_func(
1033            out_vec.as_mut_ptr(),
1034            &mut out_len,
1035            in_data.as_ptr(),
1036            &mut in_len,
1037        )
1038    };
1039
1040    if ret < 0 {
1041        return -1;
1042    }
1043
1044    let written = ret as usize;
1045
1046    // Append to output buffer
1047    append_to_xml_buffer(out_buf, &out_vec[..written]);
1048
1049    written as c_int
1050}
1051
1052/// Append bytes to an `_xmlBuffer`, reallocating if needed.
1053///
1054/// # Safety
1055///
1056/// - `buf` must be a valid `_xmlBuffer` whose `content` is NULL or points to
1057///   `size` allocated bytes; `buf.content` may be replaced by a fresh
1058///   `xmlReallocImpl` allocation when it must grow.
1059/// - `data` must be a valid byte slice; after the call, `buf.content` holds
1060///   `use_` initialized bytes.
1061fn append_to_xml_buffer(buf: &mut _xmlBuffer, data: &[u8]) {
1062    if data.is_empty() {
1063        return;
1064    }
1065
1066    let new_use = (buf.use_ as usize).saturating_add(data.len());
1067    if new_use > buf.size as usize {
1068        // Grow buffer: double or fit, whichever is larger
1069        let new_size = (buf.size as usize).saturating_mul(2).max(new_use).max(256);
1070        let new_content =
1071            unsafe { xmlReallocImpl(buf.content as *mut c_void, new_size) as *mut xmlChar };
1072        if new_content.is_null() {
1073            return; // Allocation failure — silently skip
1074        }
1075        buf.content = new_content;
1076        buf.size = new_size as c_uint;
1077    }
1078
1079    unsafe {
1080        ptr::copy_nonoverlapping(
1081            data.as_ptr(),
1082            buf.content.add(buf.use_ as usize),
1083            data.len(),
1084        );
1085    }
1086    buf.use_ = new_use as c_uint;
1087}
1088
1089// ═══════════════════════════════════════════════════════════════════════════════
1090// 7. Built-in encoding handler callbacks (extern "C")
1091// ═══════════════════════════════════════════════════════════════════════════════
1092
1093// ── UTF-8 (identity) ──────────────────────────────────────────────────────
1094
1095/// UTF-8 input function: identity (input is already UTF-8).
1096///
1097/// Simply copies bytes from input to output, up to the available space.
1098unsafe extern "C" fn utf8_input_func(
1099    out: *mut c_uchar,
1100    outlen: *mut c_int,
1101    in_: *const c_uchar,
1102    inlen: *mut c_int,
1103) -> c_int {
1104    let avail_out = *outlen as usize;
1105    let avail_in = *inlen as usize;
1106    let to_copy = avail_out.min(avail_in);
1107
1108    if to_copy > 0 {
1109        ptr::copy_nonoverlapping(in_, out, to_copy);
1110    }
1111
1112    *outlen = to_copy as c_int;
1113    *inlen = to_copy as c_int;
1114    to_copy as c_int
1115}
1116
1117/// UTF-8 output function: identity (output is already UTF-8).
1118unsafe extern "C" fn utf8_output_func(
1119    out: *mut c_uchar,
1120    outlen: *mut c_int,
1121    in_: *const c_uchar,
1122    inlen: *mut c_int,
1123) -> c_int {
1124    utf8_input_func(out, outlen, in_, inlen)
1125}
1126
1127// ── UTF-16LE ──────────────────────────────────────────────────────────────
1128
1129/// UTF-16LE input function: convert UTF-16LE to UTF-8.
1130unsafe extern "C" fn utf16le_input_func(
1131    out: *mut c_uchar,
1132    outlen: *mut c_int,
1133    in_: *const c_uchar,
1134    inlen: *mut c_int,
1135) -> c_int {
1136    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1137        return -1;
1138    }
1139
1140    let avail_in = *inlen as usize;
1141    let avail_out = *outlen as usize;
1142
1143    if avail_in == 0 || avail_out == 0 {
1144        *outlen = 0;
1145        *inlen = 0;
1146        return 0;
1147    }
1148
1149    let in_data = core::slice::from_raw_parts(in_, avail_in);
1150    let _out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1151
1152    // Use the safe wrapper
1153    let result = match utf16le_to_utf8(in_data) {
1154        Ok(v) => v,
1155        Err(()) => return -1,
1156    };
1157
1158    let written = result.len().min(avail_out);
1159    if written > 0 {
1160        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1161    }
1162
1163    *outlen = written as c_int;
1164    *inlen = avail_in as c_int; // All input consumed
1165    written as c_int
1166}
1167
1168/// UTF-16LE output function: convert UTF-8 to UTF-16LE.
1169unsafe extern "C" fn utf16le_output_func(
1170    out: *mut c_uchar,
1171    outlen: *mut c_int,
1172    in_: *const c_uchar,
1173    inlen: *mut c_int,
1174) -> c_int {
1175    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1176        return -1;
1177    }
1178
1179    let avail_in = *inlen as usize;
1180    let avail_out = *outlen as usize;
1181
1182    if avail_in == 0 || avail_out == 0 {
1183        *outlen = 0;
1184        *inlen = 0;
1185        return 0;
1186    }
1187
1188    let in_data = core::slice::from_raw_parts(in_, avail_in);
1189    let _out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1190
1191    let result = match utf8_to_utf16le(in_data) {
1192        Ok(v) => v,
1193        Err(()) => return -1,
1194    };
1195
1196    let written = result.len().min(avail_out);
1197    if written > 0 {
1198        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1199    }
1200
1201    *outlen = written as c_int;
1202    *inlen = avail_in as c_int;
1203    written as c_int
1204}
1205
1206// ── UTF-16BE ──────────────────────────────────────────────────────────────
1207
1208/// UTF-16BE input function: convert UTF-16BE to UTF-8.
1209unsafe extern "C" fn utf16be_input_func(
1210    out: *mut c_uchar,
1211    outlen: *mut c_int,
1212    in_: *const c_uchar,
1213    inlen: *mut c_int,
1214) -> c_int {
1215    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1216        return -1;
1217    }
1218
1219    let avail_in = *inlen as usize;
1220    let avail_out = *outlen as usize;
1221
1222    if avail_in == 0 || avail_out == 0 {
1223        *outlen = 0;
1224        *inlen = 0;
1225        return 0;
1226    }
1227
1228    let in_data = core::slice::from_raw_parts(in_, avail_in);
1229    let _out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1230
1231    let result = match utf16be_to_utf8(in_data) {
1232        Ok(v) => v,
1233        Err(()) => return -1,
1234    };
1235
1236    let written = result.len().min(avail_out);
1237    if written > 0 {
1238        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1239    }
1240
1241    *outlen = written as c_int;
1242    *inlen = avail_in as c_int;
1243    written as c_int
1244}
1245
1246/// UTF-16BE output function: convert UTF-8 to UTF-16BE.
1247unsafe extern "C" fn utf16be_output_func(
1248    out: *mut c_uchar,
1249    outlen: *mut c_int,
1250    in_: *const c_uchar,
1251    inlen: *mut c_int,
1252) -> c_int {
1253    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1254        return -1;
1255    }
1256
1257    let avail_in = *inlen as usize;
1258    let avail_out = *outlen as usize;
1259
1260    if avail_in == 0 || avail_out == 0 {
1261        *outlen = 0;
1262        *inlen = 0;
1263        return 0;
1264    }
1265
1266    let in_data = core::slice::from_raw_parts(in_, avail_in);
1267
1268    // First convert to UTF-16LE, then swap bytes
1269    let le_result = match utf8_to_utf16le(in_data) {
1270        Ok(v) => v,
1271        Err(()) => return -1,
1272    };
1273
1274    // Swap byte pairs to get UTF-16BE
1275    let mut result = le_result;
1276    for chunk in result.as_chunks_mut::<2>().0 {
1277        chunk.swap(0, 1);
1278    }
1279
1280    let written = result.len().min(avail_out);
1281    if written > 0 {
1282        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1283    }
1284
1285    *outlen = written as c_int;
1286    *inlen = avail_in as c_int;
1287    written as c_int
1288}
1289
1290// ── ISO-8859-1 (Latin-1) ─────────────────────────────────────────────────
1291
1292/// Latin-1 input function: convert ISO-8859-1 to UTF-8.
1293unsafe extern "C" fn latin1_input_func(
1294    out: *mut c_uchar,
1295    outlen: *mut c_int,
1296    in_: *const c_uchar,
1297    inlen: *mut c_int,
1298) -> c_int {
1299    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1300        return -1;
1301    }
1302
1303    let avail_in = *inlen as usize;
1304    let avail_out = *outlen as usize;
1305
1306    if avail_in == 0 || avail_out == 0 {
1307        *outlen = 0;
1308        *inlen = 0;
1309        return 0;
1310    }
1311
1312    let in_data = core::slice::from_raw_parts(in_, avail_in);
1313    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1314
1315    let mut in_pos = 0;
1316    let mut out_pos = 0;
1317
1318    while in_pos < avail_in && out_pos < avail_out {
1319        let byte = in_data[in_pos];
1320        in_pos += 1;
1321
1322        if byte < 0x80 {
1323            // Single byte UTF-8
1324            if out_pos < avail_out {
1325                out_slice[out_pos] = byte;
1326                out_pos += 1;
1327            } else {
1328                break;
1329            }
1330        } else {
1331            // Two byte UTF-8: 0xC0 | (byte >> 6), 0x80 | (byte & 0x3F)
1332            // For byte 0x80-0xFF, the encoding is 0xC2-0xC3 followed by continuation
1333            if out_pos + 1 < avail_out {
1334                out_slice[out_pos] = 0xC2 | (byte >> 6);
1335                out_slice[out_pos + 1] = 0x80 | (byte & 0x3F);
1336                out_pos += 2;
1337            } else {
1338                break;
1339            }
1340        }
1341    }
1342
1343    *outlen = out_pos as c_int;
1344    *inlen = in_pos as c_int;
1345    out_pos as c_int
1346}
1347
1348/// Latin-1 output function: convert UTF-8 to ISO-8859-1.
1349unsafe extern "C" fn latin1_output_func(
1350    out: *mut c_uchar,
1351    outlen: *mut c_int,
1352    in_: *const c_uchar,
1353    inlen: *mut c_int,
1354) -> c_int {
1355    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1356        return -1;
1357    }
1358
1359    let avail_in = *inlen as usize;
1360    let avail_out = *outlen as usize;
1361
1362    if avail_in == 0 || avail_out == 0 {
1363        *outlen = 0;
1364        *inlen = 0;
1365        return 0;
1366    }
1367
1368    let in_data = core::slice::from_raw_parts(in_, avail_in);
1369    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1370
1371    let mut in_pos = 0;
1372    let mut out_pos = 0;
1373
1374    while in_pos < avail_in && out_pos < avail_out {
1375        let byte = in_data[in_pos];
1376        in_pos += 1;
1377
1378        if byte < 0x80 {
1379            // ASCII — direct mapping
1380            out_slice[out_pos] = byte;
1381            out_pos += 1;
1382        } else if (0xC2..=0xC3).contains(&byte) {
1383            // Two-byte UTF-8 for codepoints U+0080–U+00FF
1384            if in_pos < avail_in {
1385                let second = in_data[in_pos];
1386                in_pos += 1;
1387                if second & 0xC0 != 0x80 {
1388                    return -1; // Invalid continuation byte
1389                }
1390                let cp = ((byte as u32 & 0x1F) << 6) | (second as u32 & 0x3F);
1391                if cp > 0xFF {
1392                    return -1; // Outside Latin-1 range
1393                }
1394                out_slice[out_pos] = cp as u8;
1395                out_pos += 1;
1396            } else {
1397                return -1; // Truncated
1398            }
1399        } else if (0x80..=0xBF).contains(&byte) {
1400            // Unexpected continuation byte
1401            return -1;
1402        } else {
1403            // Multi-byte sequence for codepoints > U+00FF
1404            // Skip the rest of the sequence and return error
1405            return -1;
1406        }
1407    }
1408
1409    *outlen = out_pos as c_int;
1410    *inlen = in_pos as c_int;
1411    out_pos as c_int
1412}
1413
1414// ── ASCII ─────────────────────────────────────────────────────────────────
1415
1416/// ASCII input function: verify and pass through ASCII data to UTF-8.
1417unsafe extern "C" fn ascii_input_func(
1418    out: *mut c_uchar,
1419    outlen: *mut c_int,
1420    in_: *const c_uchar,
1421    inlen: *mut c_int,
1422) -> c_int {
1423    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1424        return -1;
1425    }
1426
1427    let avail_in = *inlen as usize;
1428    let avail_out = *outlen as usize;
1429
1430    if avail_in == 0 || avail_out == 0 {
1431        *outlen = 0;
1432        *inlen = 0;
1433        return 0;
1434    }
1435
1436    let in_data = core::slice::from_raw_parts(in_, avail_in);
1437    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1438
1439    let mut pos = 0;
1440    while pos < avail_in && pos < avail_out {
1441        let byte = in_data[pos];
1442        if byte > 0x7F {
1443            return -1; // Not valid ASCII
1444        }
1445        out_slice[pos] = byte;
1446        pos += 1;
1447    }
1448
1449    *outlen = pos as c_int;
1450    *inlen = pos as c_int;
1451    pos as c_int
1452}
1453
1454/// ASCII output function: verify and pass through UTF-8 data that is ASCII.
1455unsafe extern "C" fn ascii_output_func(
1456    out: *mut c_uchar,
1457    outlen: *mut c_int,
1458    in_: *const c_uchar,
1459    inlen: *mut c_int,
1460) -> c_int {
1461    // For output, ASCII handler requires that input is already ASCII
1462    ascii_input_func(out, outlen, in_, inlen)
1463}
1464
1465// ═══════════════════════════════════════════════════════════════════════════════
1466// 8. ABI export functions (called from exports_xml2.rs)
1467// ═══════════════════════════════════════════════════════════════════════════════
1468
1469/// `xmlFindCharEncodingHandler` implementation.
1470///
1471/// Finds an encoding handler by name. Returns a pointer to the handler,
1472/// or `ptr::null_mut()` if not found.
1473pub(crate) fn xmlFindCharEncodingHandler(name: *const c_char) -> *mut _xmlCharEncodingHandler {
1474    if name.is_null() {
1475        return ptr::null_mut();
1476    }
1477    find_encoding_handler(name as *const xmlChar)
1478}
1479
1480/// `xmlGetCharEncodingName` implementation.
1481///
1482/// Returns the canonical name for an encoding, or `ptr::null()` if unknown.
1483pub(crate) const fn xmlGetCharEncodingName(enc: xmlCharEncoding) -> *const c_char {
1484    // Return null-terminated C strings using static CStr literals.
1485    // Mirrors upstream 2.15 xmlGetCharEncodingName: the UTF-16/UCS-4 pairs
1486    // return the W3C canonical names before the defaultHandlers table.
1487    match enc {
1488        xmlCharEncoding::XML_CHAR_ENCODING_UTF8 => c"UTF-8".as_ptr(),
1489        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE | xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE => {
1490            c"UTF-16".as_ptr()
1491        }
1492        xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE | xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE => {
1493            c"UCS-4".as_ptr()
1494        }
1495        xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC => c"IBM037".as_ptr(),
1496        xmlCharEncoding::XML_CHAR_ENCODING_UCS2 => c"UCS-2".as_ptr(),
1497        xmlCharEncoding::XML_CHAR_ENCODING_8859_1 => c"ISO-8859-1".as_ptr(),
1498        xmlCharEncoding::XML_CHAR_ENCODING_8859_2 => c"ISO-8859-2".as_ptr(),
1499        xmlCharEncoding::XML_CHAR_ENCODING_8859_3 => c"ISO-8859-3".as_ptr(),
1500        xmlCharEncoding::XML_CHAR_ENCODING_8859_4 => c"ISO-8859-4".as_ptr(),
1501        xmlCharEncoding::XML_CHAR_ENCODING_8859_5 => c"ISO-8859-5".as_ptr(),
1502        xmlCharEncoding::XML_CHAR_ENCODING_8859_6 => c"ISO-8859-6".as_ptr(),
1503        xmlCharEncoding::XML_CHAR_ENCODING_8859_7 => c"ISO-8859-7".as_ptr(),
1504        xmlCharEncoding::XML_CHAR_ENCODING_8859_8 => c"ISO-8859-8".as_ptr(),
1505        xmlCharEncoding::XML_CHAR_ENCODING_8859_9 => c"ISO-8859-9".as_ptr(),
1506        xmlCharEncoding::XML_CHAR_ENCODING_2022_JP => c"ISO-2022-JP".as_ptr(),
1507        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS => c"Shift_JIS".as_ptr(),
1508        xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP => c"EUC-JP".as_ptr(),
1509        // upstream defaultHandlers[22].name
1510        xmlCharEncoding::XML_CHAR_ENCODING_ASCII => c"US-ASCII".as_ptr(),
1511        _ => ptr::null(),
1512    }
1513}
1514
1515/// `xmlParseCharEncoding` implementation.
1516///
1517/// Parses an encoding name string to an `xmlCharEncoding` enum value,
1518/// returned as `c_int`.
1519///
1520/// # Safety
1521///
1522/// - `name` must be NULL or a valid pointer to a NUL-terminated string.
1523pub(crate) fn xmlParseCharEncoding(name: *const c_char) -> c_int {
1524    if name.is_null() {
1525        return xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int;
1526    }
1527    let bytes = unsafe { CStr::from_ptr(name).to_bytes() };
1528    encoding_from_name(bytes) as c_int
1529}
1530
1531// ── Encoding aliases (upstream encoding.c xmlAddEncodingAlias etc.) ──────────
1532//
1533// A global alias table maps alias names to canonical encoding names.
1534// Upstream keeps a static hash of aliases; the candidate uses a
1535// process-lifetime RwLock<HashMap>. Thread-safe; matches upstream's
1536// observable contract (add/del/get by name).
1537
1538static ENCODING_ALIASES: std::sync::OnceLock<
1539    parking_lot::RwLock<std::collections::HashMap<Vec<u8>, Vec<u8>>>,
1540> = std::sync::OnceLock::new();
1541
1542fn encoding_aliases() -> &'static parking_lot::RwLock<std::collections::HashMap<Vec<u8>, Vec<u8>>> {
1543    ENCODING_ALIASES.get_or_init(|| parking_lot::RwLock::new(std::collections::HashMap::new()))
1544}
1545
1546/// `xmlAddEncodingAlias` implementation: register `alias` for `name`.
1547/// Returns 0 on success, -1 on error (NULL arguments).
1548///
1549/// # Safety
1550///
1551/// - `name` and `alias` must be NULL or valid pointers to NUL-terminated
1552///   strings; both are copied before insertion into the alias table.
1553pub(crate) fn add_encoding_alias(name: *const c_char, alias: *const c_char) -> c_int {
1554    if name.is_null() || alias.is_null() {
1555        return -1;
1556    }
1557    let n = unsafe { CStr::from_ptr(name).to_bytes().to_vec() };
1558    let a = unsafe { CStr::from_ptr(alias).to_bytes().to_vec() };
1559    encoding_aliases().write().insert(a, n);
1560    0
1561}
1562
1563/// `xmlDelEncodingAlias` implementation: remove `alias`.
1564/// Returns 0 on success, -1 if the alias does not exist.
1565///
1566/// # Safety
1567///
1568/// - `alias` must be NULL or a valid pointer to a NUL-terminated string.
1569pub(crate) fn del_encoding_alias(alias: *const c_char) -> c_int {
1570    if alias.is_null() {
1571        return -1;
1572    }
1573    let a = unsafe { CStr::from_ptr(alias).to_bytes().to_vec() };
1574    if encoding_aliases().write().remove(&a).is_some() {
1575        0
1576    } else {
1577        -1
1578    }
1579}
1580
1581/// `xmlGetEncodingAlias` implementation: return the canonical name for
1582/// `alias`, or NULL when not registered.
1583///
1584/// # Safety
1585///
1586/// - `alias` must be NULL or a valid pointer to a NUL-terminated string.
1587/// - The returned pointer is a leaked, process-lifetime NUL-terminated
1588///   string, or NULL; the caller must not free it.
1589pub(crate) fn get_encoding_alias(alias: *const c_char) -> *const c_char {
1590    if alias.is_null() {
1591        return ptr::null();
1592    }
1593    let a = unsafe { CStr::from_ptr(alias).to_bytes().to_vec() };
1594    let guard = encoding_aliases().read();
1595    match guard.get(&a) {
1596        Some(v) => {
1597            // leak the canonical name: upstream returns a pointer valid for
1598            // the process lifetime (the alias hash owns the strings)
1599            let leaked: &'static [u8] = Box::leak(v.clone().into_boxed_slice());
1600            leaked.as_ptr() as *const c_char
1601        }
1602        None => ptr::null(),
1603    }
1604}
1605
1606/// `xmlCleanupEncodingAliases` implementation: drop all aliases.
1607pub(crate) fn cleanup_encoding_aliases() {
1608    encoding_aliases().write().clear();
1609}
1610
1611/// `xmlCharEncInFunc` implementation.
1612///
1613/// Converts the input buffer's encoding to UTF-8 using the given handler.
1614pub(crate) fn xmlCharEncInFunc(
1615    handler: *mut _xmlCharEncodingHandler,
1616    out: *mut _xmlBuffer,
1617    in_: *mut _xmlBuffer,
1618) -> c_int {
1619    char_enc_in(handler, out, in_)
1620}
1621
1622/// `xmlCharEncOutFunc` implementation.
1623///
1624/// Converts the input buffer from UTF-8 to the handler's output encoding.
1625pub(crate) fn xmlCharEncOutFunc(
1626    handler: *mut _xmlCharEncodingHandler,
1627    out: *mut _xmlBuffer,
1628    in_: *mut _xmlBuffer,
1629) -> c_int {
1630    char_enc_out(handler, out, in_)
1631}
1632
1633/// `xmlNewCharEncodingHandler` implementation.
1634///
1635/// Creates a new encoding handler with the given name and conversion functions.
1636/// The name string is duplicated. Returns a pointer to the new handler,
1637/// or `ptr::null_mut()` on allocation failure.
1638///
1639/// # Safety
1640///
1641/// - `name` must be NULL or a valid pointer to a NUL-terminated string that
1642///   stays valid until it is duplicated.
1643/// - `input` and `output` must be valid function pointers matching the
1644///   callback ABI; on success the returned handler owns a duplicated name
1645///   and must be released with `xmlDelEncodingHandler`.
1646pub(crate) fn xmlNewCharEncodingHandler(
1647    name: *const c_char,
1648    input: xmlCharEncodingInputFunc,
1649    output: xmlCharEncodingOutputFunc,
1650) -> *mut _xmlCharEncodingHandler {
1651    if name.is_null() {
1652        return ptr::null_mut();
1653    }
1654
1655    let name_raw = unsafe { crate::abi::allocator::xmlMemStrdupImpl(name) };
1656    if name_raw.is_null() {
1657        return ptr::null_mut();
1658    }
1659
1660    let handler = unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) }
1661        as *mut _xmlCharEncodingHandler;
1662
1663    if handler.is_null() {
1664        unsafe { xmlFreeImpl(name_raw) };
1665        return ptr::null_mut();
1666    }
1667
1668    unsafe {
1669        ptr::write(
1670            handler,
1671            _xmlCharEncodingHandler {
1672                name: name_raw as *mut c_char,
1673                input: EncodingInputUnion {
1674                    legacyFunc: Some(input),
1675                },
1676                output: EncodingOutputUnion {
1677                    legacyFunc: Some(output),
1678                },
1679                inputCtxt: ptr::null_mut(),
1680                outputCtxt: ptr::null_mut(),
1681                ctxtDtor: None,
1682                flags: 0,
1683            },
1684        );
1685    }
1686
1687    handler
1688}
1689
1690/// `xmlDelEncodingHandler` implementation.
1691///
1692/// Frees an encoding handler previously created with `xmlNewCharEncodingHandler`.
1693///
1694/// # Safety
1695///
1696/// - `handler` must be NULL or a valid heap-allocated
1697///   `_xmlCharEncodingHandler` whose `name` is NULL or a heap-allocated
1698///   NUL-terminated string; both allocations are freed exactly once, and the
1699///   handler must have been removed from the registry.
1700#[allow(dead_code)]
1701pub(crate) fn xmlDelEncodingHandler(handler: *mut _xmlCharEncodingHandler) {
1702    if handler.is_null() {
1703        return;
1704    }
1705
1706    // Remove from registry if present
1707    {
1708        let mut handlers = ENCODING_HANDLERS.write();
1709        handlers.retain(|&h| h.0 != handler);
1710    }
1711
1712    unsafe {
1713        if !(*handler).name.is_null() {
1714            xmlFreeImpl((*handler).name as *mut c_void);
1715        }
1716        xmlFreeImpl(handler as *mut c_void);
1717    }
1718}
1719
1720/// `xmlInitCharEncodingHandlers` implementation.
1721pub(crate) fn xmlInitCharEncodingHandlers() {
1722    init_encodings();
1723}
1724
1725/// `xmlCleanupCharEncodingHandlers` implementation.
1726pub(crate) fn xmlCleanupCharEncodingHandlers() {
1727    cleanup_encodings();
1728}
1729
1730// ═══════════════════════════════════════════════════════════════════════════════
1731// 7. Handler lookup / creation (upstream 2.13.0+ encoding.c)
1732// ═══════════════════════════════════════════════════════════════════════════════
1733//
1734// Upstream keeps a static `defaultHandlers[32]` table indexed by xmlCharEncoding
1735// plus iconv/ICU fallbacks. The candidate ships no iconv/ICU, so encodings whose
1736// upstream default handler carries a real converter (UTF-8, UTF-16LE, UTF-16BE,
1737// UTF-16, ISO-8859-1, US-ASCII) resolve to the registered built-in handlers;
1738// every other encoding reports XML_ERR_UNSUPPORTED_ENCODING exactly where
1739// upstream would fall through to iconv/ICU.
1740
1741/// `xmlLookupCharEncodingHandler` implementation (upstream encoding.c).
1742///
1743/// Mirrors the upstream control flow:
1744///  - `out == NULL`                     → XML_ERR_ARGUMENT (115)
1745///  - `enc <= 0 || enc >= 32`           → XML_ERR_UNSUPPORTED_ENCODING (32)
1746///  - UTF-8                             → XML_ERR_OK, `*out` stays NULL
1747///  - native built-in encoding          → XML_ERR_OK, `*out` = static handler
1748///  - iconv/ICU-only encoding           → XML_ERR_UNSUPPORTED_ENCODING
1749///
1750/// The returned handler is a static registry entry and must NOT be freed.
1751///
1752/// # Safety
1753///
1754/// - `out` must be a valid pointer to a `*mut c_void` out-parameter; it is
1755///   written with NULL or a pointer to a static registry handler that the
1756///   caller must not free.
1757pub(crate) fn xmlLookupCharEncodingHandler(enc: c_int, out: *mut *mut c_void) -> c_int {
1758    if out.is_null() {
1759        return crate::abi::types::XML_ERR_ARGUMENT;
1760    }
1761    unsafe {
1762        *out = ptr::null_mut();
1763    }
1764    if enc <= 0 || enc >= 32 {
1765        return crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING;
1766    }
1767    /* Return NULL handler for UTF-8 */
1768    if enc == xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int {
1769        return crate::abi::types::XML_ERR_OK;
1770    }
1771    let canonical: &[u8] = match enc {
1772        /* XML_CHAR_ENCODING_UTF16LE */
1773        2 => b"UTF-16LE\0",
1774        /* XML_CHAR_ENCODING_UTF16BE */
1775        3 => b"UTF-16BE\0",
1776        /* XML_CHAR_ENCODING_8859_1 */
1777        10 => b"ISO-8859-1\0",
1778        /* XML_CHAR_ENCODING_ASCII */
1779        22 => b"US-ASCII\0",
1780        /* XML_CHAR_ENCODING_UTF16 (not in the local enum) */
1781        23 => b"UTF-16\0",
1782        _ => return crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING,
1783    };
1784    let h = find_encoding_handler(canonical.as_ptr() as *const xmlChar);
1785    if h.is_null() {
1786        return crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING;
1787    }
1788    unsafe {
1789        *out = h as *mut c_void;
1790    }
1791    crate::abi::types::XML_ERR_OK
1792}
1793
1794/// `xmlGetCharEncodingHandler` implementation (deprecated upstream wrapper).
1795pub(crate) fn xmlGetCharEncodingHandler(enc: c_int) -> *mut c_void {
1796    let mut ret: *mut c_void = ptr::null_mut();
1797    let _rc = xmlLookupCharEncodingHandler(enc, &mut ret);
1798    ret
1799}
1800
1801/// `xmlCreateCharEncodingHandler` implementation (upstream 2.14.0+ encoding.c).
1802///
1803/// Flags: XML_ENC_INPUT = 1, XML_ENC_OUTPUT = 2, XML_ENC_HTML = 4.
1804/// Unlike upstream, no iconv/ICU backend exists, so encodings without a native
1805/// converter fall through to `find_extra_handler` (custom impl / deprecated
1806/// global registry) and otherwise report XML_ERR_UNSUPPORTED_ENCODING.
1807///
1808/// # Safety
1809///
1810/// - `out` must be a valid pointer to a `*mut c_void` out-parameter; it is
1811///   written with NULL or a heap-allocated handler copy the caller owns.
1812/// - `name` must be NULL or a valid pointer to a NUL-terminated string.
1813/// - `implCtxt` is an opaque context forwarded to `find_extra_handler` and
1814///   must be valid for the callback that consumes it.
1815pub(crate) fn xmlCreateCharEncodingHandler(
1816    name: *const c_char,
1817    flags: c_int,
1818    impl_: Option<xmlCharEncConvImpl>,
1819    implCtxt: *mut c_void,
1820    out: *mut *mut c_void,
1821) -> c_int {
1822    if out.is_null() {
1823        return crate::abi::types::XML_ERR_ARGUMENT;
1824    }
1825    unsafe {
1826        *out = ptr::null_mut();
1827    }
1828    if name.is_null() || flags == 0 {
1829        return crate::abi::types::XML_ERR_ARGUMENT;
1830    }
1831    let norig = unsafe { CStr::from_ptr(name).to_bytes() };
1832
1833    /* Alias resolution (upstream xmlGetEncodingAlias). */
1834    let mut eff: &[u8] = norig;
1835    let alias = get_encoding_alias(name);
1836    if !alias.is_null() {
1837        eff = unsafe { CStr::from_ptr(alias).to_bytes() };
1838    }
1839
1840    let enc = encoding_from_name(eff);
1841
1842    /* Return NULL handler for UTF-8 */
1843    if enc == xmlCharEncoding::XML_CHAR_ENCODING_UTF8 {
1844        return crate::abi::types::XML_ERR_OK;
1845    }
1846
1847    let canonical: &[u8] = match enc {
1848        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE => b"UTF-16LE\0",
1849        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE => b"UTF-16BE\0",
1850        xmlCharEncoding::XML_CHAR_ENCODING_8859_1 => b"ISO-8859-1\0",
1851        xmlCharEncoding::XML_CHAR_ENCODING_ASCII => b"US-ASCII\0",
1852        _ => {
1853            return find_extra_handler(norig, eff, flags, impl_, implCtxt, out);
1854        }
1855    };
1856    let h = find_encoding_handler(canonical.as_ptr() as *const xmlChar);
1857    if h.is_null() {
1858        return find_extra_handler(norig, eff, flags, impl_, implCtxt, out);
1859    }
1860    unsafe {
1861        let src = &*h;
1862        let has_in = (flags & 1) == 0 || !src.input.legacyFunc.is_none();
1863        let has_out = (flags & 2) == 0 || !src.output.legacyFunc.is_none();
1864        if !has_in || !has_out {
1865            return find_extra_handler(norig, eff, flags, impl_, implCtxt, out);
1866        }
1867        /*
1868         * Return a copy of the handler with the original name (upstream
1869         * "Return a copy of the handler with the original name").
1870         */
1871        let copy =
1872            xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) as *mut _xmlCharEncodingHandler;
1873        if copy.is_null() {
1874            return crate::abi::types::XML_ERR_NO_MEMORY;
1875        }
1876        let name_copy = crate::abi::allocator::xmlMemStrdupImpl(name) as *mut c_char;
1877        if name_copy.is_null() {
1878            xmlFreeImpl(copy as *mut c_void);
1879            return crate::abi::types::XML_ERR_NO_MEMORY;
1880        }
1881        ptr::write(
1882            copy,
1883            _xmlCharEncodingHandler {
1884                name: name_copy,
1885                input: EncodingInputUnion {
1886                    legacyFunc: src.input.legacyFunc,
1887                },
1888                output: EncodingOutputUnion {
1889                    legacyFunc: src.output.legacyFunc,
1890                },
1891                inputCtxt: src.inputCtxt,
1892                outputCtxt: src.outputCtxt,
1893                ctxtDtor: src.ctxtDtor,
1894                flags: src.flags,
1895            },
1896        );
1897        *out = copy as *mut c_void;
1898    }
1899    crate::abi::types::XML_ERR_OK
1900}
1901
1902/// Fallback path of `xmlCreateCharEncodingHandler` (upstream `xmlFindExtraHandler`).
1903///
1904/// Tries the caller-supplied custom implementation first, then the deprecated
1905/// global handler registry. iconv/ICU do not exist in the candidate, so the
1906/// final result is XML_ERR_UNSUPPORTED_ENCODING.
1907///
1908/// # Safety
1909///
1910/// - `norig` and `name` must be valid byte slices; NUL-terminated copies are
1911///   built from them for lookups and callbacks.
1912/// - `out` must be a valid out-parameter; it is written with NULL or a
1913///   registry handler pointer that must not be freed.
1914/// - `implCtxt` must be a valid context for the custom `impl_` callback when
1915///   one is supplied.
1916fn find_extra_handler(
1917    norig: &[u8],
1918    name: &[u8],
1919    flags: c_int,
1920    impl_: Option<xmlCharEncConvImpl>,
1921    implCtxt: *mut c_void,
1922    out: *mut *mut c_void,
1923) -> c_int {
1924    /* Custom implementation before deprecated global handlers. */
1925    if let Some(f) = impl_ {
1926        let mut n = norig.to_vec();
1927        n.push(0);
1928        let rc = unsafe {
1929            f(
1930                implCtxt,
1931                n.as_ptr() as *const c_char,
1932                flags,
1933                out as *mut *mut crate::abi::structs::_xmlCharEncodingHandler,
1934            )
1935        };
1936        return rc;
1937    }
1938    /* Deprecated global handlers registry (xmlRegisterCharEncodingHandler). */
1939    let mut n = name.to_vec();
1940    n.push(0);
1941    let h = find_encoding_handler(n.as_ptr() as *const xmlChar);
1942    if !h.is_null() {
1943        unsafe {
1944            let src = &*h;
1945            let has_in = (flags & 1) == 0 || !src.input.legacyFunc.is_none();
1946            let has_out = (flags & 2) == 0 || !src.output.legacyFunc.is_none();
1947            if has_in && has_out {
1948                *out = h as *mut c_void;
1949                return crate::abi::types::XML_ERR_OK;
1950            }
1951        }
1952    }
1953    crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING
1954}
1955
1956/// `xmlOpenCharEncodingHandler` implementation (upstream encoding.c).
1957pub(crate) fn xmlOpenCharEncodingHandler(
1958    name: *const c_char,
1959    output: c_int,
1960    out: *mut *mut c_void,
1961) -> c_int {
1962    /* XML_ENC_OUTPUT if output else XML_ENC_INPUT */
1963    let flags: c_int = if output != 0 { 2 } else { 1 };
1964    xmlCreateCharEncodingHandler(name, flags, None, ptr::null_mut(), out)
1965}
1966
1967/// `xmlCharEncNewCustomHandler` implementation (upstream 2.15.0+ encoding.c).
1968///
1969/// Creates a handler backed by modern `xmlCharEncConvFunc` callbacks (with
1970/// per-direction contexts and a context destructor). The handler must be
1971/// released with `xmlCharEncCloseFunc`.
1972///
1973/// # Safety
1974///
1975/// - `out` must be a valid pointer to a `*mut c_void` out-parameter.
1976/// - `name` must be NULL or a valid pointer to a NUL-terminated string.
1977/// - `input` and `output` must be valid `xmlCharEncConvFunc` callbacks;
1978///   `inputCtxt` and `outputCtxt` are opaque contexts consumed by them and
1979///   by `ctxtDtor`, which is invoked on each non-NULL context when
1980///   allocation fails (and later by `xmlCharEncCloseFunc`).
1981pub(crate) fn xmlCharEncNewCustomHandler(
1982    name: *const c_char,
1983    input: xmlCharEncConvFunc,
1984    output: xmlCharEncConvFunc,
1985    ctxtDtor: Option<xmlCharEncConvCtxtDtor>,
1986    inputCtxt: *mut c_void,
1987    outputCtxt: *mut c_void,
1988    out: *mut *mut c_void,
1989) -> c_int {
1990    if out.is_null() {
1991        return crate::abi::types::XML_ERR_ARGUMENT;
1992    }
1993    let handler = unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) }
1994        as *mut _xmlCharEncodingHandler;
1995    if handler.is_null() {
1996        unsafe {
1997            if let Some(d) = ctxtDtor {
1998                if !inputCtxt.is_null() {
1999                    d(inputCtxt);
2000                }
2001                if !outputCtxt.is_null() {
2002                    d(outputCtxt);
2003                }
2004            }
2005        }
2006        return crate::abi::types::XML_ERR_NO_MEMORY;
2007    }
2008    let name_copy = if name.is_null() {
2009        ptr::null_mut()
2010    } else {
2011        let nc = unsafe { crate::abi::allocator::xmlMemStrdupImpl(name) } as *mut c_char;
2012        if nc.is_null() {
2013            unsafe { xmlFreeImpl(handler as *mut c_void) };
2014            unsafe {
2015                if let Some(d) = ctxtDtor {
2016                    if !inputCtxt.is_null() {
2017                        d(inputCtxt);
2018                    }
2019                    if !outputCtxt.is_null() {
2020                        d(outputCtxt);
2021                    }
2022                }
2023            }
2024            return crate::abi::types::XML_ERR_NO_MEMORY;
2025        }
2026        nc
2027    };
2028    unsafe {
2029        ptr::write(
2030            handler,
2031            _xmlCharEncodingHandler {
2032                name: name_copy,
2033                input: EncodingInputUnion { func: Some(input) },
2034                output: EncodingOutputUnion { func: Some(output) },
2035                inputCtxt,
2036                outputCtxt,
2037                ctxtDtor,
2038                flags: 0,
2039            },
2040        );
2041        *out = handler as *mut c_void;
2042    }
2043    crate::abi::types::XML_ERR_OK
2044}
2045
2046// ═══════════════════════════════════════════════════════════════════════════════
2047// Tests
2048// ═══════════════════════════════════════════════════════════════════════════════
2049
2050#[cfg(test)]
2051mod tests {
2052    use super::*;
2053
2054    // ── BOM detection ──────────────────────────────────────────────────────
2055
2056    #[test]
2057    fn test_detect_bom_utf8() {
2058        let data = [0xEF, 0xBB, 0xBF, b'<', b'?', b'x', b'm', b'l'];
2059        assert_eq!(
2060            detect_encoding_from_bom(&data),
2061            xmlCharEncoding::XML_CHAR_ENCODING_UTF8
2062        );
2063    }
2064
2065    #[test]
2066    fn test_detect_bom_utf16le() {
2067        let data = [0xFF, 0xFE, 0x00, 0x01];
2068        assert_eq!(
2069            detect_encoding_from_bom(&data),
2070            xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
2071        );
2072    }
2073
2074    #[test]
2075    fn test_detect_bom_utf16be() {
2076        let data = [0xFE, 0xFF, 0x00, 0x01];
2077        assert_eq!(
2078            detect_encoding_from_bom(&data),
2079            xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE
2080        );
2081    }
2082
2083    #[test]
2084    fn test_detect_bom_none() {
2085        let data = b"<xml>";
2086        assert_eq!(
2087            detect_encoding_from_bom(data),
2088            xmlCharEncoding::XML_CHAR_ENCODING_NONE
2089        );
2090    }
2091
2092    #[test]
2093    fn test_detect_bom_empty() {
2094        assert_eq!(
2095            detect_encoding_from_bom(b""),
2096            xmlCharEncoding::XML_CHAR_ENCODING_NONE
2097        );
2098    }
2099
2100    // ── Encoding from declaration ──────────────────────────────────────────
2101
2102    #[test]
2103    fn test_detect_encoding_declaration_utf8() {
2104        let data = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
2105        let result = detect_encoding_from_declaration(data);
2106        assert_eq!(result, Some(b"utf-8".to_vec()));
2107    }
2108
2109    #[test]
2110    fn test_detect_encoding_declaration_iso() {
2111        let data = b"<?xml version='1.0' encoding='ISO-8859-1'?>";
2112        let result = detect_encoding_from_declaration(data);
2113        assert_eq!(result, Some(b"iso-8859-1".to_vec()));
2114    }
2115
2116    #[test]
2117    fn test_detect_encoding_declaration_none() {
2118        let data = b"<?xml version=\"1.0\"?>";
2119        let result = detect_encoding_from_declaration(data);
2120        assert!(result.is_none());
2121    }
2122
2123    #[test]
2124    fn test_detect_encoding_declaration_no_xml() {
2125        let data = b"<root>";
2126        let result = detect_encoding_from_declaration(data);
2127        assert!(result.is_none());
2128    }
2129
2130    #[test]
2131    fn test_detect_encoding_declaration_with_bom() {
2132        let mut data = vec![0xEF, 0xBB, 0xBF];
2133        data.extend_from_slice(b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
2134        let result = detect_encoding_from_declaration(&data);
2135        assert_eq!(result, Some(b"utf-8".to_vec()));
2136    }
2137
2138    // ── Encoding from name ─────────────────────────────────────────────────
2139
2140    #[test]
2141    fn test_encoding_from_name_utf8() {
2142        assert_eq!(
2143            encoding_from_name(b"UTF-8"),
2144            xmlCharEncoding::XML_CHAR_ENCODING_UTF8
2145        );
2146        assert_eq!(
2147            encoding_from_name(b"utf8"),
2148            xmlCharEncoding::XML_CHAR_ENCODING_UTF8
2149        );
2150    }
2151
2152    #[test]
2153    fn test_encoding_from_name_utf16() {
2154        assert_eq!(
2155            encoding_from_name(b"UTF-16LE"),
2156            xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
2157        );
2158        assert_eq!(
2159            encoding_from_name(b"UTF-16BE"),
2160            xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE
2161        );
2162        assert_eq!(
2163            encoding_from_name(b"utf-16"),
2164            xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
2165        );
2166    }
2167
2168    #[test]
2169    fn test_encoding_from_name_latin1() {
2170        assert_eq!(
2171            encoding_from_name(b"ISO-8859-1"),
2172            xmlCharEncoding::XML_CHAR_ENCODING_8859_1
2173        );
2174        assert_eq!(
2175            encoding_from_name(b"Latin1"),
2176            xmlCharEncoding::XML_CHAR_ENCODING_8859_1
2177        );
2178    }
2179
2180    #[test]
2181    fn test_encoding_from_name_ascii() {
2182        assert_eq!(
2183            encoding_from_name(b"ASCII"),
2184            xmlCharEncoding::XML_CHAR_ENCODING_ASCII
2185        );
2186        assert_eq!(
2187            encoding_from_name(b"US-ASCII"),
2188            xmlCharEncoding::XML_CHAR_ENCODING_ASCII
2189        );
2190    }
2191
2192    #[test]
2193    fn test_encoding_from_name_error() {
2194        assert_eq!(
2195            encoding_from_name(b"invalid-encoding"),
2196            xmlCharEncoding::XML_CHAR_ENCODING_ERROR
2197        );
2198    }
2199
2200    #[test]
2201    fn test_encoding_from_name_empty() {
2202        assert_eq!(
2203            encoding_from_name(b""),
2204            xmlCharEncoding::XML_CHAR_ENCODING_ERROR
2205        );
2206    }
2207
2208    // ── Encoding name ──────────────────────────────────────────────────────
2209
2210    #[test]
2211    fn test_encoding_name_utf8() {
2212        assert_eq!(
2213            encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_UTF8),
2214            Some(b"UTF-8" as &[u8])
2215        );
2216    }
2217
2218    #[test]
2219    fn test_encoding_name_utf16le() {
2220        assert_eq!(
2221            encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE),
2222            Some(b"UTF-16LE" as &[u8])
2223        );
2224    }
2225
2226    #[test]
2227    fn test_encoding_name_none() {
2228        assert!(encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_NONE).is_none());
2229    }
2230
2231    #[test]
2232    fn test_encoding_name_error() {
2233        assert!(encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_ERROR).is_none());
2234    }
2235
2236    // ── UTF-8 validation ───────────────────────────────────────────────────
2237
2238    #[test]
2239    fn test_utf8_valid_ascii() {
2240        assert!(utf8_valid(b"hello world"));
2241    }
2242
2243    #[test]
2244    fn test_utf8_valid_multi_byte() {
2245        assert!(utf8_valid("héllo wörld 🌍".as_bytes()));
2246    }
2247
2248    #[test]
2249    fn test_utf8_valid_empty() {
2250        assert!(utf8_valid(b""));
2251    }
2252
2253    #[test]
2254    fn test_utf8_invalid() {
2255        assert!(!utf8_valid(&[0xFF, 0xFE, 0x00]));
2256    }
2257
2258    // ── XML char validation ────────────────────────────────────────────────
2259
2260    #[test]
2261    fn test_valid_xml_chars() {
2262        assert!(is_valid_xml_char(0x9)); // Tab
2263        assert!(is_valid_xml_char(0xA)); // LF
2264        assert!(is_valid_xml_char(0xD)); // CR
2265        assert!(is_valid_xml_char(0x20)); // Space
2266        assert!(is_valid_xml_char(0x41)); // 'A'
2267        assert!(is_valid_xml_char(0xD7FF));
2268        assert!(is_valid_xml_char(0xE000));
2269        assert!(is_valid_xml_char(0xFFFD));
2270        assert!(is_valid_xml_char(0x10000));
2271        assert!(is_valid_xml_char(0x10FFFF));
2272    }
2273
2274    #[test]
2275    fn test_invalid_xml_chars() {
2276        assert!(!is_valid_xml_char(0x00));
2277        assert!(!is_valid_xml_char(0x08));
2278        assert!(!is_valid_xml_char(0x0B));
2279        assert!(!is_valid_xml_char(0x0C));
2280        assert!(!is_valid_xml_char(0x0E));
2281        assert!(!is_valid_xml_char(0x1F));
2282        assert!(!is_valid_xml_char(0xD800)); // Surrogate
2283        assert!(!is_valid_xml_char(0xDFFF)); // Surrogate
2284        assert!(!is_valid_xml_char(0xFFFE));
2285        assert!(!is_valid_xml_char(0xFFFF));
2286        assert!(!is_valid_xml_char(0x110000));
2287    }
2288
2289    // ── UTF-16LE to UTF-8 ──────────────────────────────────────────────────
2290
2291    #[test]
2292    fn test_utf16le_to_utf8_ascii() {
2293        // "AB" in UTF-16LE
2294        let data = [b'A', 0x00, b'B', 0x00];
2295        let result = utf16le_to_utf8(&data).unwrap();
2296        assert_eq!(result, b"AB");
2297    }
2298
2299    #[test]
2300    fn test_utf16le_to_utf8_bom() {
2301        let mut data = vec![0xFF, 0xFE]; // BOM
2302        data.extend_from_slice(&[b'A', 0x00, b'B', 0x00]);
2303        let result = utf16le_to_utf8(&data).unwrap();
2304        assert_eq!(result, b"AB");
2305    }
2306
2307    #[test]
2308    fn test_utf16le_to_utf8_bmp() {
2309        // U+00E9 (é) in UTF-16LE = 0xE9 0x00
2310        let data = [0xE9, 0x00];
2311        let result = utf16le_to_utf8(&data).unwrap();
2312        assert_eq!(result, "é".as_bytes());
2313    }
2314
2315    #[test]
2316    fn test_utf16le_to_utf8_supplementary() {
2317        // U+1F600 (😀) in UTF-16LE = 0x3D 0xD8 0x00 0xDE
2318        let data = [0x3D, 0xD8, 0x00, 0xDE];
2319        let result = utf16le_to_utf8(&data).unwrap();
2320        assert_eq!(result, "😀".as_bytes());
2321    }
2322
2323    #[test]
2324    fn test_utf16le_to_utf8_unpaired_surrogate() {
2325        let data = [0x00, 0xD8]; // High surrogate without low
2326        assert!(utf16le_to_utf8(&data).is_err());
2327    }
2328
2329    #[test]
2330    fn test_utf16le_to_utf8_truncated() {
2331        let data = [0x00]; // Odd length
2332        assert!(utf16le_to_utf8(&data).is_err());
2333    }
2334
2335    #[test]
2336    fn test_utf16le_to_utf8_empty() {
2337        let result = utf16le_to_utf8(b"").unwrap();
2338        assert!(result.is_empty());
2339    }
2340
2341    // ── UTF-16BE to UTF-8 ──────────────────────────────────────────────────
2342
2343    #[test]
2344    fn test_utf16be_to_utf8_ascii() {
2345        let data = [0x00, b'A', 0x00, b'B'];
2346        let result = utf16be_to_utf8(&data).unwrap();
2347        assert_eq!(result, b"AB");
2348    }
2349
2350    #[test]
2351    fn test_utf16be_to_utf8_bom() {
2352        let mut data = vec![0xFE, 0xFF]; // BOM
2353        data.extend_from_slice(&[0x00, b'A', 0x00, b'B']);
2354        let result = utf16be_to_utf8(&data).unwrap();
2355        assert_eq!(result, b"AB");
2356    }
2357
2358    #[test]
2359    fn test_utf16be_to_utf8_supplementary() {
2360        // U+1F600 (😀) in UTF-16BE = 0xD8 0x3D 0xDE 0x00
2361        let data = [0xD8, 0x3D, 0xDE, 0x00];
2362        let result = utf16be_to_utf8(&data).unwrap();
2363        assert_eq!(result, "😀".as_bytes());
2364    }
2365
2366    #[test]
2367    fn test_utf16be_to_utf8_empty() {
2368        let result = utf16be_to_utf8(b"").unwrap();
2369        assert!(result.is_empty());
2370    }
2371
2372    // ── UTF-8 to UTF-16LE ──────────────────────────────────────────────────
2373
2374    #[test]
2375    fn test_utf8_to_utf16le_ascii() {
2376        let result = utf8_to_utf16le(b"AB").unwrap();
2377        assert_eq!(result, [b'A', 0x00, b'B', 0x00]);
2378    }
2379
2380    #[test]
2381    fn test_utf8_to_utf16le_bmp() {
2382        let result = utf8_to_utf16le("é".as_bytes()).unwrap();
2383        assert_eq!(result, [0xE9, 0x00]);
2384    }
2385
2386    #[test]
2387    fn test_utf8_to_utf16le_supplementary() {
2388        let result = utf8_to_utf16le("😀".as_bytes()).unwrap();
2389        assert_eq!(result, [0x3D, 0xD8, 0x00, 0xDE]);
2390    }
2391
2392    #[test]
2393    fn test_utf8_to_utf16le_invalid_utf8() {
2394        assert!(utf8_to_utf16le(&[0xFF]).is_err());
2395    }
2396
2397    #[test]
2398    fn test_utf8_to_utf16le_empty() {
2399        let result = utf8_to_utf16le(b"").unwrap();
2400        assert!(result.is_empty());
2401    }
2402
2403    // ── Latin-1 to UTF-8 ───────────────────────────────────────────────────
2404
2405    #[test]
2406    fn test_latin1_to_utf8_ascii() {
2407        let result = latin1_to_utf8(b"ABC");
2408        assert_eq!(result, b"ABC");
2409    }
2410
2411    #[test]
2412    fn test_latin1_to_utf8_accented() {
2413        // 0xE9 = é in Latin-1
2414        let result = latin1_to_utf8(&[0xE9]);
2415        assert_eq!(result, "é".as_bytes());
2416    }
2417
2418    #[test]
2419    fn test_latin1_to_utf8_all_255() {
2420        let result = latin1_to_utf8(&[0xFF]);
2421        // U+00FF = ÿ, UTF-8: 0xC3 0xBF
2422        assert_eq!(result, [0xC3, 0xBF]);
2423    }
2424
2425    #[test]
2426    fn test_latin1_to_utf8_empty() {
2427        let result = latin1_to_utf8(b"");
2428        assert!(result.is_empty());
2429    }
2430
2431    #[test]
2432    fn test_latin1_to_utf8_mixed() {
2433        let result = latin1_to_utf8(b"caf\xE9");
2434        assert_eq!(result, "café".as_bytes());
2435    }
2436
2437    // ── UTF-8 to Latin-1 ───────────────────────────────────────────────────
2438
2439    #[test]
2440    fn test_utf8_to_latin1_ascii() {
2441        let result = utf8_to_latin1(b"ABC").unwrap();
2442        assert_eq!(result, b"ABC");
2443    }
2444
2445    #[test]
2446    fn test_utf8_to_latin1_accented() {
2447        let result = utf8_to_latin1("é".as_bytes()).unwrap();
2448        assert_eq!(result, [0xE9]);
2449    }
2450
2451    #[test]
2452    fn test_utf8_to_latin1_out_of_range() {
2453        assert!(utf8_to_latin1("€".as_bytes()).is_err()); // U+20AC not in Latin-1
2454    }
2455
2456    #[test]
2457    fn test_utf8_to_latin1_invalid_utf8() {
2458        assert!(utf8_to_latin1(&[0xFF]).is_err());
2459    }
2460
2461    #[test]
2462    fn test_utf8_to_latin1_empty() {
2463        let result = utf8_to_latin1(b"").unwrap();
2464        assert!(result.is_empty());
2465    }
2466
2467    // ── Encoding handler registry ──────────────────────────────────────────
2468
2469    #[test]
2470    fn test_init_and_find_encodings() {
2471        init_encodings();
2472
2473        let utf8_name: *const xmlChar = c"UTF-8".as_ptr() as *const xmlChar;
2474        assert!(!find_encoding_handler(utf8_name).is_null());
2475
2476        let utf16le_name: *const xmlChar = c"UTF-16LE".as_ptr() as *const xmlChar;
2477        assert!(!find_encoding_handler(utf16le_name).is_null());
2478
2479        let utf16be_name: *const xmlChar = c"UTF-16BE".as_ptr() as *const xmlChar;
2480        assert!(!find_encoding_handler(utf16be_name).is_null());
2481
2482        let latin1_name: *const xmlChar = c"ISO-8859-1".as_ptr() as *const xmlChar;
2483        assert!(!find_encoding_handler(latin1_name).is_null());
2484
2485        let ascii_name: *const xmlChar = c"ASCII".as_ptr() as *const xmlChar;
2486        assert!(!find_encoding_handler(ascii_name).is_null());
2487
2488        // Case insensitive
2489        let lower_name: *const xmlChar = c"utf-8".as_ptr() as *const xmlChar;
2490        assert!(!find_encoding_handler(lower_name).is_null());
2491    }
2492
2493    #[test]
2494    fn test_find_encoding_handler_not_found() {
2495        let name: *const xmlChar = c"NONEXISTENT".as_ptr() as *const xmlChar;
2496        assert!(find_encoding_handler(name).is_null());
2497    }
2498
2499    #[test]
2500    fn test_find_encoding_handler_null() {
2501        assert!(find_encoding_handler(ptr::null()).is_null());
2502    }
2503
2504    /// Verify registering a handler in the global registry and looking it
2505    /// up.
2506    ///
2507    /// # Safety
2508    ///
2509    /// - The `xmlMallocImpl` and `xmlMemStrdupImpl` results are NULL-checked
2510    ///   before `ptr::write` initializes the handler; the handler is removed
2511    ///   from the registry before its allocations are freed exactly once.
2512    #[test]
2513    fn test_add_encoding_handler() {
2514        let handler = unsafe {
2515            xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) as *mut _xmlCharEncodingHandler
2516        };
2517        assert!(!handler.is_null());
2518
2519        let name = unsafe {
2520            crate::abi::allocator::xmlMemStrdupImpl(c"TEST-ENC".as_ptr() as *const c_char)
2521        };
2522        unsafe {
2523            ptr::write(
2524                handler,
2525                _xmlCharEncodingHandler {
2526                    name: name as *mut c_char,
2527                    input: EncodingInputUnion { legacyFunc: None },
2528                    output: EncodingOutputUnion { legacyFunc: None },
2529                    inputCtxt: ptr::null_mut(),
2530                    outputCtxt: ptr::null_mut(),
2531                    ctxtDtor: None,
2532                    flags: 0,
2533                },
2534            );
2535        }
2536
2537        assert_eq!(add_encoding_handler(handler), 0);
2538
2539        let found = find_encoding_handler(c"TEST-ENC".as_ptr() as *const xmlChar);
2540        assert_eq!(found, handler);
2541
2542        // Remove from registry before freeing to avoid dangling pointers
2543        {
2544            let mut handlers = ENCODING_HANDLERS.write();
2545            handlers.retain(|&h| h.0 != handler);
2546        }
2547
2548        unsafe {
2549            xmlFreeImpl(name as *mut c_void);
2550            xmlFreeImpl(handler as *mut c_void);
2551        }
2552    }
2553
2554    // ── Conversion round-trips ─────────────────────────────────────────────
2555
2556    #[test]
2557    fn test_utf16le_roundtrip() {
2558        let original = b"Hello, World! UTF-16LE test: \xC3\xA9\xF0\x9F\x98\x80";
2559        let utf16 = utf8_to_utf16le(original).unwrap();
2560        let back = utf16le_to_utf8(&utf16).unwrap();
2561        assert_eq!(original.to_vec(), back);
2562    }
2563
2564    #[test]
2565    fn test_utf16be_roundtrip() {
2566        let original = b"Hello, World! UTF-16BE test: \xC3\xA9\xF0\x9F\x98\x80";
2567        let utf16le = utf8_to_utf16le(original).unwrap();
2568        // Convert LE to BE by swapping bytes
2569        let mut utf16be = utf16le.clone();
2570        for chunk in utf16be.as_chunks_mut::<2>().0 {
2571            chunk.swap(0, 1);
2572        }
2573        let back = utf16be_to_utf8(&utf16be).unwrap();
2574        assert_eq!(original.to_vec(), back);
2575    }
2576
2577    #[test]
2578    fn test_latin1_roundtrip() {
2579        let original: Vec<u8> = (0x00..=0xFF).collect();
2580        let utf8 = latin1_to_utf8(&original);
2581        let back = utf8_to_latin1(&utf8).unwrap();
2582        assert_eq!(original, back);
2583    }
2584
2585    // ── Built-in handler callbacks ─────────────────────────────────────────
2586
2587    /// Verify the UTF-8 identity callback copies bytes up to the smaller
2588    /// length.
2589    ///
2590    /// # Safety
2591    ///
2592    /// - `output` is a valid mutable 64-byte buffer and `input` a valid byte
2593    ///   slice; the callback writes at most the minimum of the two lengths.
2594    #[test]
2595    fn test_utf8_handler_identity() {
2596        let input = b"Hello, UTF-8!";
2597        let mut output = [0u8; 64];
2598        let mut outlen = output.len() as c_int;
2599        let mut inlen = input.len() as c_int;
2600
2601        let ret = unsafe {
2602            utf8_input_func(output.as_mut_ptr(), &mut outlen, input.as_ptr(), &mut inlen)
2603        };
2604
2605        assert_eq!(ret, input.len() as c_int);
2606        assert_eq!(&output[..ret as usize], input);
2607        assert_eq!(inlen, input.len() as c_int);
2608    }
2609
2610    /// Verify a UTF-16LE output/input callback round-trip.
2611    ///
2612    /// # Safety
2613    ///
2614    /// - The `utf16_buf` and `decoded` arrays are valid buffers of the given
2615    ///   lengths, and the input slices are valid; the callbacks write only
2616    ///   up to the advertised output length.
2617    #[test]
2618    fn test_utf16le_handler_roundtrip() {
2619        init_encodings();
2620
2621        let original = b"Hello UTF-16LE!";
2622        let mut utf16_buf = [0u8; 128];
2623        let mut outlen = utf16_buf.len() as c_int;
2624        let mut inlen = original.len() as c_int;
2625
2626        let written = unsafe {
2627            utf16le_output_func(
2628                utf16_buf.as_mut_ptr(),
2629                &mut outlen,
2630                original.as_ptr(),
2631                &mut inlen,
2632            )
2633        };
2634        assert!(written > 0);
2635
2636        // Now decode back
2637        let mut decoded = [0u8; 128];
2638        let mut outlen2 = decoded.len() as c_int;
2639        let mut inlen2 = written;
2640
2641        let written2 = unsafe {
2642            utf16le_input_func(
2643                decoded.as_mut_ptr(),
2644                &mut outlen2,
2645                utf16_buf.as_ptr(),
2646                &mut inlen2,
2647            )
2648        };
2649        assert_eq!(written2 as usize, original.len());
2650        assert_eq!(&decoded[..written2 as usize], original);
2651    }
2652
2653    // ── xmlBuffer operations ───────────────────────────────────────────────
2654
2655    /// Verify `append_to_xml_buffer` grows the buffer and copies bytes.
2656    ///
2657    /// # Safety
2658    ///
2659    /// - `content` is a valid 64-byte allocation owned by the test and freed
2660    ///   exactly once with `xmlFreeImpl`; `buf` keeps consistent `use_` and
2661    ///   `size` fields while `append_to_xml_buffer` may reallocate `content`.
2662    #[test]
2663    fn test_append_to_xml_buffer() {
2664        unsafe {
2665            let content = xmlMallocImpl(64) as *mut xmlChar;
2666            assert!(!content.is_null());
2667
2668            let mut buf = _xmlBuffer {
2669                content,
2670                use_: 0,
2671                size: 64,
2672                alloc: 0,
2673                contentIO: ptr::null_mut(),
2674            };
2675
2676            append_to_xml_buffer(&mut buf, b"Hello");
2677            assert_eq!(buf.use_, 5);
2678            let slice = core::slice::from_raw_parts(buf.content, 5);
2679            assert_eq!(slice, b"Hello");
2680
2681            append_to_xml_buffer(&mut buf, b" World");
2682            assert_eq!(buf.use_, 11);
2683            let slice = core::slice::from_raw_parts(buf.content, 11);
2684            assert_eq!(slice, b"Hello World");
2685
2686            xmlFreeImpl(buf.content as *mut c_void);
2687        }
2688    }
2689
2690    // ── ABI export functions ───────────────────────────────────────────────
2691
2692    #[test]
2693    fn test_xml_parse_char_encoding() {
2694        let name = c"UTF-8".as_ptr() as *const c_char;
2695        assert_eq!(
2696            xmlParseCharEncoding(name),
2697            xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int
2698        );
2699
2700        let name = c"ISO-8859-1".as_ptr() as *const c_char;
2701        assert_eq!(
2702            xmlParseCharEncoding(name),
2703            xmlCharEncoding::XML_CHAR_ENCODING_8859_1 as c_int
2704        );
2705
2706        assert_eq!(
2707            xmlParseCharEncoding(ptr::null()),
2708            xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int
2709        );
2710    }
2711
2712    /// Verify `xmlNewCharEncodingHandler` and `xmlDelEncodingHandler`
2713    /// round-trip.
2714    ///
2715    /// # Safety
2716    ///
2717    /// - `name` is a valid NUL-terminated string; the returned handler is
2718    ///   non-NULL, its `name` field is a valid NUL-terminated string, and it
2719    ///   is freed exactly once by `xmlDelEncodingHandler`.
2720    #[test]
2721    fn test_xml_new_and_del_encoding_handler() {
2722        let name = c"TestEnc".as_ptr() as *const c_char;
2723        let handler = xmlNewCharEncodingHandler(
2724            name,
2725            utf8_input_func as xmlCharEncodingInputFunc,
2726            utf8_output_func as xmlCharEncodingOutputFunc,
2727        );
2728        assert!(!handler.is_null());
2729
2730        unsafe {
2731            assert!(!(*handler).name.is_null());
2732            let cstr = CStr::from_ptr((*handler).name);
2733            assert_eq!(cstr.to_bytes(), b"TestEnc");
2734        }
2735
2736        xmlDelEncodingHandler(handler);
2737    }
2738
2739    #[test]
2740    fn test_xml_init_and_cleanup() {
2741        xmlInitCharEncodingHandlers();
2742
2743        let name: *const xmlChar = c"UTF-8".as_ptr() as *const xmlChar;
2744        assert!(!find_encoding_handler(name).is_null());
2745
2746        xmlCleanupCharEncodingHandlers();
2747        // After cleanup, handlers should be empty
2748    }
2749}