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    // Windows-1252 (CP1252): served by iconv on the oracle; native converter
675    // here (R-000157 partial closure). Registered under both the canonical
676    // spelling and the cp1252 alias; lookup is case-insensitive so
677    // "Windows-1252" (the Dom\XMLDocument overrideEncoding the PHP court
678    // passes verbatim) resolves to the same entry.
679    register_handler(
680        b"windows-1252\0",
681        xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
682        xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
683        Some(cp1252_input_func as xmlCharEncodingInputFunc),
684        Some(cp1252_output_func as xmlCharEncodingOutputFunc),
685    );
686    register_handler(
687        b"cp1252\0",
688        xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
689        xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
690        Some(cp1252_input_func as xmlCharEncodingInputFunc),
691        Some(cp1252_output_func as xmlCharEncodingOutputFunc),
692    );
693
694    // ASCII — upstream's static default handler (defaultHandlers[22]) is named
695    // "US-ASCII"; the name "ASCII" is registered as a second entry so name-based
696    // lookups (xmlFindCharEncodingHandler, the saver path) accept both spellings
697    // exactly like upstream's xmlParseCharEncodingInternal mapping.
698    register_handler(
699        b"US-ASCII\0",
700        xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
701        xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
702        Some(ascii_input_func as xmlCharEncodingInputFunc),
703        Some(ascii_output_func as xmlCharEncodingOutputFunc),
704    );
705    register_handler(
706        b"ASCII\0",
707        xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
708        xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
709        Some(ascii_input_func as xmlCharEncodingInputFunc),
710        Some(ascii_output_func as xmlCharEncodingOutputFunc),
711    );
712
713    // UTF-16 (default handler for enc == XML_CHAR_ENCODING_UTF16 == 23): the
714    // upstream converter is UTF16LEToUTF8/UTF8ToUTF16 (the latter emits the LE
715    // BOM on its init call). Our converter pair is the UTF-16LE pair; the BOM
716    // init protocol is not emitted (documented divergence, conversion only).
717    register_handler(
718        b"UTF-16\0",
719        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
720        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
721        Some(utf16le_input_func as xmlCharEncodingInputFunc),
722        Some(utf16le_output_func as xmlCharEncodingOutputFunc),
723    );
724
725    // Shift_JIS + EUC-JP — encoding_rs-backed converters (R-000157 closure
726    // slice, Phase 14.27). Upstream serves these through iconv on the
727    // executed oracle (2.15.3, Iconv+ICU); the crate ships no iconv/ICU
728    // backend, so the converters are implemented natively over WHATWG
729    // Shift_JIS (a CP932-compatible superset) / EUC-JP, which byte-match
730    // glibc iconv on the shared JIS X 0208 repertoire the php suite and
731    // byte-parity probes exercise (the WHATWG/CP932 extension differences
732    // are residualized in R-000157). Registered under the canonical
733    // spellings + the aliases upstream's name path accepts (registry lookup
734    // is case-insensitive).
735    register_handler(
736        b"SHIFT_JIS\0",
737        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS,
738        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS,
739        Some(shift_jis_input_func as xmlCharEncodingInputFunc),
740        Some(shift_jis_output_func as xmlCharEncodingOutputFunc),
741    );
742    register_handler(
743        b"SJIS\0",
744        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS,
745        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS,
746        Some(shift_jis_input_func as xmlCharEncodingInputFunc),
747        Some(shift_jis_output_func as xmlCharEncodingOutputFunc),
748    );
749    register_handler(
750        b"CP932\0",
751        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS,
752        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS,
753        Some(shift_jis_input_func as xmlCharEncodingInputFunc),
754        Some(shift_jis_output_func as xmlCharEncodingOutputFunc),
755    );
756    register_handler(
757        b"EUC-JP\0",
758        xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP,
759        xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP,
760        Some(euc_jp_input_func as xmlCharEncodingInputFunc),
761        Some(euc_jp_output_func as xmlCharEncodingOutputFunc),
762    );
763    register_handler(
764        b"EUCJP\0",
765        xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP,
766        xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP,
767        Some(euc_jp_input_func as xmlCharEncodingInputFunc),
768        Some(euc_jp_output_func as xmlCharEncodingOutputFunc),
769    );
770}
771
772/// Helper to create and register an encoding handler.
773///
774/// # Safety
775///
776/// - `name_bytes` must be a valid byte slice containing a NUL terminator;
777///   `xmlMemStrdupImpl` scans it as a C string.
778/// - The `xmlMallocImpl` result is NULL-checked before `ptr::write`
779///   initializes the handler; the written handler is inserted into the
780///   global registry, which keeps it alive for the process lifetime.
781fn register_handler(
782    name_bytes: &[u8],
783    _input_enc: xmlCharEncoding,
784    _output_enc: xmlCharEncoding,
785    input_func: Option<xmlCharEncodingInputFunc>,
786    output_func: Option<xmlCharEncodingOutputFunc>,
787) {
788    let name_raw =
789        unsafe { crate::abi::allocator::xmlMemStrdupImpl(name_bytes.as_ptr() as *const c_char) };
790    if name_raw.is_null() {
791        return;
792    }
793
794    let handler = unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) }
795        as *mut _xmlCharEncodingHandler;
796
797    if handler.is_null() {
798        unsafe { xmlFreeImpl(name_raw) };
799        return;
800    }
801
802    unsafe {
803        ptr::write(
804            handler,
805            _xmlCharEncodingHandler {
806                name: name_raw as *mut c_char,
807                input: EncodingInputUnion {
808                    legacyFunc: input_func,
809                },
810                output: EncodingOutputUnion {
811                    legacyFunc: output_func,
812                },
813                inputCtxt: ptr::null_mut(),
814                outputCtxt: ptr::null_mut(),
815                ctxtDtor: None,
816                flags: 0,
817            },
818        );
819    }
820
821    add_encoding_handler(handler);
822}
823
824/// Clean up encoding handlers.
825///
826/// Frees all registered handlers and resets the registry.
827///
828/// # Safety
829///
830/// - Every registered handler pointer must be NULL or a valid
831///   heap-allocated `_xmlCharEncodingHandler` whose `name` is NULL or a
832///   heap-allocated NUL-terminated string; each allocation is freed exactly
833///   once and must not be freed elsewhere.
834pub(crate) fn cleanup_encodings() {
835    let mut handlers = ENCODING_HANDLERS.write();
836    for &handler in handlers.iter() {
837        let ptr = handler.0;
838        if !ptr.is_null() {
839            unsafe {
840                if !(*ptr).name.is_null() {
841                    xmlFreeImpl((*ptr).name as *mut c_void);
842                }
843                xmlFreeImpl(ptr as *mut c_void);
844            }
845        }
846    }
847    handlers.clear();
848    // Re-allow registration on the next init_encodings()/cleanup round-trip so
849    // a caller that cleans up and then (re)initializes in another thread does
850    // not observe a stale "already initialized" registry that stays empty.
851    // (ENCODING_INITIALIZED/ENCODING_INIT_MUTEX are separate statics.)
852    drop(handlers);
853    ENCODING_INITIALIZED.store(false, Ordering::SeqCst);
854}
855
856/// Find an encoding handler by name.
857///
858/// Searches the global handler registry for a handler whose name matches
859/// (case-insensitive). Returns a pointer to the handler, or `ptr::null_mut()`
860/// if not found.
861///
862/// # Safety
863///
864/// - `name` must be NULL or a valid pointer to a NUL-terminated string.
865/// - Each registry entry must be NULL or a valid `_xmlCharEncodingHandler`
866///   whose `name` is NULL or a valid NUL-terminated string.
867pub(crate) fn find_encoding_handler(name: *const xmlChar) -> *mut _xmlCharEncodingHandler {
868    if name.is_null() {
869        return ptr::null_mut();
870    }
871
872    /* The upstream default-handler table is static and always present; the
873     * candidate's registry is populated lazily, so ensure it is initialized
874     * before any name-based lookup. Idempotent. */
875    init_encodings();
876
877    let name_str = unsafe {
878        match CStr::from_ptr(name as *const c_char).to_bytes() {
879            b"" => return ptr::null_mut(),
880            s => s,
881        }
882    };
883
884    let handlers = ENCODING_HANDLERS.read();
885    for &handler in handlers.iter() {
886        let ptr = handler.0;
887        if ptr.is_null() {
888            continue;
889        }
890        let h_name = unsafe {
891            if (*ptr).name.is_null() {
892                continue;
893            }
894            CStr::from_ptr((*ptr).name).to_bytes()
895        };
896
897        if name_str.eq_ignore_ascii_case(h_name) {
898            return ptr;
899        }
900    }
901
902    ptr::null_mut()
903}
904
905/// Build an owned, caller-freed copy of a registered encoding handler.
906///
907/// Upstream's `xmlFindCharEncodingHandler` hands the caller a handler it owns
908/// and is expected to release with `xmlCharEncCloseFunc` after use (Phase 14
909/// PHP court: `dom_document_encoding_write` finds a handler then closes it for
910/// every write). Returning the persistent registry pointer directly would let
911/// the exported close free the registry entry out from under later lookups
912/// (a use-after-free seen as `DOMDocument::$encoding = 'UTF-16'` corrupting the
913/// handler registry and crashing the next `find_encoding_handler`).
914///
915/// The copy duplicates the name with `xmlMemStrdupImpl` so the caller may free
916/// it; the conversion unions and context pointers are shared with the original
917/// registry entry. All built-in registry handlers the find path serves are
918/// stateless (`ctxtDtor` is None, contexts NULL), so `xmlCharEncCloseFunc` on
919/// the copy only releases the duplicated name and the struct.
920///
921/// Returns the new handler or `ptr::null_mut()` when `src` is NULL/alloc fails.
922pub(crate) fn clone_encoding_handler_for_find(
923    src: *mut _xmlCharEncodingHandler,
924) -> *mut _xmlCharEncodingHandler {
925    if src.is_null() {
926        return ptr::null_mut();
927    }
928    let name_raw = unsafe {
929        let nm = (*src).name;
930        if nm.is_null() {
931            ptr::null_mut()
932        } else {
933            crate::abi::allocator::xmlMemStrdupImpl(nm)
934        }
935    };
936    let handler = unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) }
937        as *mut _xmlCharEncodingHandler;
938    if handler.is_null() {
939        if !name_raw.is_null() {
940            unsafe { crate::abi::allocator::xmlFreeImpl(name_raw) };
941        }
942        return ptr::null_mut();
943    }
944    unsafe {
945        ptr::write(
946            handler,
947            _xmlCharEncodingHandler {
948                name: name_raw as *mut c_char,
949                input: ptr::read(&(*src).input),
950                output: ptr::read(&(*src).output),
951                inputCtxt: (*src).inputCtxt,
952                outputCtxt: (*src).outputCtxt,
953                ctxtDtor: (*src).ctxtDtor,
954                flags: (*src).flags,
955            },
956        );
957    }
958    handler
959}
960
961/// Upstream handler `flags` marker: the handler lives for the process lifetime
962/// (upstream `encoding.c` `{"UTF-8", ... , XML_HANDLER_STATIC}`) and
963/// `xmlCharEncCloseFunc` must therefore not release it.
964pub(crate) const XML_HANDLER_STATIC: c_int = 0x01;
965
966/// ABI `xmlFindCharEncodingHandler` mirror (upstream libxml2 2.15 encoding.c
967/// `xmlFindCharEncodingHandler`).
968///
969/// Upstream returns an OWNED handler the caller releases with
970/// `xmlCharEncCloseFunc`, except for UTF-8/UTF8 where it returns the static
971/// `defaultHandlers[XML_CHAR_ENCODING_UTF8]` (has `XML_HANDLER_STATIC`, so
972/// `xmlCharEncCloseFunc` is a no-op). Phase 14 PHP court:
973/// `dom_document_encoding_write` finds a handler for every `$dom->encoding=
974/// write and closes it — so returning the persistent registry pointer for a
975/// non-UTF-8 encoding let the caller's close free the registry entry (the
976/// use-after-free behind `DOMDocument::$encoding = 'UTF-16'` crashing the next
977/// `find_encoding_handler`).
978///
979/// Returns an owned heap copy for non-UTF-8 encodings, the flagged-static
980/// registry UTF-8 handler for UTF-8, or `ptr::null_mut()` when `name` is NULL
981/// or no handler is registered.
982pub(crate) fn xmlFindCharEncodingHandler_owned(
983    name: *const xmlChar,
984) -> *mut _xmlCharEncodingHandler {
985    if name.is_null() {
986        return ptr::null_mut();
987    }
988    let name_bytes = unsafe {
989        let len = libc::strlen(name as *const c_char);
990        core::slice::from_raw_parts(name as *const u8, len)
991    };
992
993    // UTF-8 / UTF8 special case (upstream returns the static handler).
994    if encoding_from_name(name_bytes) == xmlCharEncoding::XML_CHAR_ENCODING_UTF8 {
995        let utf8 = find_encoding_handler(c"UTF-8".as_ptr() as *const xmlChar);
996        if utf8.is_null() {
997            return ptr::null_mut();
998        }
999        // Flag it static so xmlCharEncCloseFunc does not free the registry entry.
1000        unsafe {
1001            (*utf8).flags |= XML_HANDLER_STATIC;
1002        }
1003        return utf8;
1004    }
1005
1006    // Non-UTF-8: resolve the registry entry, preferring a canonical lookup when
1007    // the raw spelling is not itself a registered key (mirrors the canonical
1008    // re-lookup upstream performs in xmlCreateCharEncodingHandler).
1009    let mut entry = find_encoding_handler(name as *const xmlChar);
1010    if entry.is_null() {
1011        if let Some(canon) = encoding_name(encoding_from_name(name_bytes)) {
1012            entry = find_encoding_handler(canon.as_ptr() as *const xmlChar);
1013        }
1014    }
1015    // Return an OWNED copy of the registry entry (never the entry itself), so
1016    // the caller's xmlCharEncCloseFunc releases only the copy.
1017    clone_encoding_handler_for_find(entry)
1018}
1019
1020/// Add an encoding handler to the registry.
1021///
1022/// Returns 0 on success, -1 on failure (e.g., null pointer).
1023pub(crate) fn add_encoding_handler(handler: *mut _xmlCharEncodingHandler) -> c_int {
1024    if handler.is_null() {
1025        return -1;
1026    }
1027
1028    let mut handlers = ENCODING_HANDLERS.write();
1029    handlers.push(HandlerPtr(handler));
1030    0
1031}
1032
1033// ═══════════════════════════════════════════════════════════════════════════════
1034// 6. Encoding conversion functions
1035// ═══════════════════════════════════════════════════════════════════════════════
1036
1037/// Input conversion: convert from handler's input encoding to UTF-8.
1038///
1039/// Calls the handler's `input.legacyFunc` callback. Returns bytes written or -1 on error.
1040///
1041/// # Safety
1042///
1043/// - `handler` must be NULL or a valid pointer to an initialized
1044///   `_xmlCharEncodingHandler`; the stored `input.legacyFunc` callback, when
1045///   present, must be a valid function pointer.
1046/// - `out` must be a valid mutable byte slice and `in_data` a valid byte
1047///   slice; both stay valid for the duration of the callback.
1048#[allow(dead_code)]
1049pub(crate) fn char_enc_in_func(
1050    handler: *mut _xmlCharEncodingHandler,
1051    out: &mut [u8],
1052    in_data: &[u8],
1053) -> c_int {
1054    if handler.is_null() {
1055        return -1;
1056    }
1057
1058    let h = unsafe { &*handler };
1059    let input_func = unsafe { h.input.legacyFunc };
1060    let input_func = match input_func {
1061        Some(f) => f,
1062        None => return -1,
1063    };
1064
1065    let mut outlen = out.len() as c_int;
1066    let mut inlen = in_data.len() as c_int;
1067
1068    unsafe { input_func(out.as_mut_ptr(), &mut outlen, in_data.as_ptr(), &mut inlen) }
1069}
1070
1071/// Output conversion: convert from UTF-8 to handler's output encoding.
1072///
1073/// Calls the handler's `output.legacyFunc` callback. Returns bytes written or -1 on error.
1074///
1075/// # Safety
1076///
1077/// - `handler` must be NULL or a valid pointer to an initialized
1078///   `_xmlCharEncodingHandler`; the stored `output.legacyFunc` callback,
1079///   when present, must be a valid function pointer.
1080/// - `out` must be a valid mutable byte slice and `in_data` a valid byte
1081///   slice; both stay valid for the duration of the callback.
1082#[allow(dead_code)]
1083pub(crate) fn char_enc_out_func(
1084    handler: *mut _xmlCharEncodingHandler,
1085    out: &mut [u8],
1086    in_data: &[u8],
1087) -> c_int {
1088    if handler.is_null() {
1089        return -1;
1090    }
1091
1092    let h = unsafe { &*handler };
1093    let output_func = unsafe { h.output.legacyFunc };
1094    let output_func = match output_func {
1095        Some(f) => f,
1096        None => return -1,
1097    };
1098
1099    let mut outlen = out.len() as c_int;
1100    let mut inlen = in_data.len() as c_int;
1101
1102    unsafe { output_func(out.as_mut_ptr(), &mut outlen, in_data.as_ptr(), &mut inlen) }
1103}
1104
1105/// Full input conversion (`xmlCharEncInFunc` equivalent).
1106///
1107/// Reads from the input `_xmlBuffer`, converts via the handler's `input.legacyFunc`,
1108/// and appends the result to the output `_xmlBuffer`.
1109///
1110/// Returns the number of bytes written to the output buffer, or -1 on error.
1111///
1112/// # Safety
1113///
1114/// - `handler` must be NULL or a valid `_xmlCharEncodingHandler` whose
1115///   `input.legacyFunc` callback is a valid function pointer.
1116/// - `in_` and `out` must be NULL or valid `_xmlBuffer` pointers; `in_`'s
1117///   `content` must be NULL or point to `use_` readable bytes, and `out`
1118///   must stay valid while `append_to_xml_buffer` may reallocate its
1119///   `content`.
1120pub(crate) fn char_enc_in(
1121    handler: *mut _xmlCharEncodingHandler,
1122    out: *mut _xmlBuffer,
1123    in_: *mut _xmlBuffer,
1124) -> c_int {
1125    if handler.is_null() || out.is_null() || in_.is_null() {
1126        return -1;
1127    }
1128
1129    let h = unsafe { &*handler };
1130    let input_func = unsafe { h.input.legacyFunc };
1131    let input_func = match input_func {
1132        Some(f) => f,
1133        None => return -1,
1134    };
1135
1136    let in_buf = unsafe { &*in_ };
1137    let out_buf = unsafe { &mut *out };
1138
1139    if in_buf.content.is_null() || in_buf.use_ == 0 {
1140        return 0;
1141    }
1142
1143    let in_data = unsafe { core::slice::from_raw_parts(in_buf.content, in_buf.use_ as usize) };
1144
1145    // Allocate an output buffer. A good heuristic is 2x input for UTF-16→UTF-8.
1146    let out_capacity = (in_buf.use_ as usize).saturating_mul(3).max(256);
1147    let mut out_vec = vec![0u8; out_capacity];
1148    let mut out_len = out_capacity as c_int;
1149    let mut in_len = in_buf.use_ as c_int;
1150
1151    let ret = unsafe {
1152        input_func(
1153            out_vec.as_mut_ptr(),
1154            &mut out_len,
1155            in_data.as_ptr(),
1156            &mut in_len,
1157        )
1158    };
1159
1160    if ret < 0 {
1161        return -1;
1162    }
1163
1164    let written = ret as usize;
1165
1166    // Append to output buffer
1167    append_to_xml_buffer(out_buf, &out_vec[..written]);
1168
1169    written as c_int
1170}
1171
1172/// Full output conversion (`xmlCharEncOutFunc` equivalent).
1173///
1174/// Reads from the input `_xmlBuffer` (UTF-8), converts via the handler's
1175/// `output.legacyFunc`, and appends the result to the output `_xmlBuffer`.
1176///
1177/// Returns the number of bytes written to the output buffer, or -1 on error.
1178///
1179/// # Safety
1180///
1181/// - `handler` must be NULL or a valid `_xmlCharEncodingHandler` whose
1182///   `output.legacyFunc` callback is a valid function pointer.
1183/// - `in_` and `out` must be NULL or valid `_xmlBuffer` pointers; `in_`'s
1184///   `content` must be NULL or point to `use_` readable bytes, and `out`
1185///   must stay valid while `append_to_xml_buffer` may reallocate its
1186///   `content`.
1187pub(crate) fn char_enc_out(
1188    handler: *mut _xmlCharEncodingHandler,
1189    out: *mut _xmlBuffer,
1190    in_: *mut _xmlBuffer,
1191) -> c_int {
1192    if handler.is_null() || out.is_null() || in_.is_null() {
1193        return -1;
1194    }
1195
1196    let h = unsafe { &*handler };
1197    let output_func = unsafe { h.output.legacyFunc };
1198    let output_func = match output_func {
1199        Some(f) => f,
1200        None => return -1,
1201    };
1202
1203    let in_buf = unsafe { &*in_ };
1204    let out_buf = unsafe { &mut *out };
1205
1206    if in_buf.content.is_null() || in_buf.use_ == 0 {
1207        return 0;
1208    }
1209
1210    let mut in_data = unsafe { core::slice::from_raw_parts(in_buf.content, in_buf.use_ as usize) };
1211
1212    // UPSTREAM-PARITY (encoding.c xmlCharEncOutput): the output conversion
1213    // runs in a loop; when the converter reports an INPUT error (a character
1214    // not representable in the output encoding — the ASCII handler stops at
1215    // the first byte >= 0x80), the offending UTF-8 character is decoded and
1216    // replaced by a DECIMAL character reference (&#NNN;), then conversion
1217    // continues. This is how libxml2 serializes non-ASCII text into an
1218    // ASCII output buffer (lxml's default `tostring` encoding, which
1219    // produces `&#195;&#169;` for the mojibake case).
1220    const ENC_INPUT_ERROR: c_int = -2;
1221    let mut total_written: usize = 0;
1222    loop {
1223        let out_capacity = (in_data.len().saturating_mul(3)).max(64) + 16;
1224        let mut out_vec = vec![0u8; out_capacity];
1225        let mut out_len = out_capacity as c_int;
1226        let mut in_len = in_data.len() as c_int;
1227        let ret = unsafe {
1228            output_func(
1229                out_vec.as_mut_ptr(),
1230                &mut out_len,
1231                in_data.as_ptr(),
1232                &mut in_len,
1233            )
1234        };
1235        let written = out_len.max(0) as usize;
1236        if written > 0 {
1237            append_to_xml_buffer(out_buf, &out_vec[..written]);
1238            total_written += written;
1239        }
1240        let consumed = in_len.max(0) as usize;
1241        if ret == ENC_INPUT_ERROR && consumed < in_data.len() {
1242            // Decode the UTF-8 character at the offending position and emit
1243            // a decimal character reference (upstream xmlSerializeDecCharRef).
1244            let mut clen: c_int = 4;
1245            let cp = unsafe {
1246                crate::abi::exports_misc::xmlGetUTF8Char(in_data[consumed..].as_ptr(), &mut clen)
1247            };
1248            if cp <= 0 || clen <= 0 || (consumed + clen as usize) > in_data.len() {
1249                return -1;
1250            }
1251            let ref_str = format!("&#{};", cp);
1252            append_to_xml_buffer(out_buf, ref_str.as_bytes());
1253            total_written += ref_str.len();
1254            in_data = &in_data[consumed + clen as usize..];
1255            if in_data.is_empty() {
1256                break;
1257            }
1258            continue;
1259        }
1260        if ret < 0 {
1261            return -1;
1262        }
1263        break;
1264    }
1265
1266    total_written as c_int
1267}
1268
1269/// Append bytes to an `_xmlBuffer`, reallocating if needed.
1270///
1271/// # Safety
1272///
1273/// - `buf` must be a valid `_xmlBuffer` whose `content` is NULL or points to
1274///   `size` allocated bytes; `buf.content` may be replaced by a fresh
1275///   `xmlReallocImpl` allocation when it must grow.
1276/// - `data` must be a valid byte slice; after the call, `buf.content` holds
1277///   `use_` initialized bytes.
1278fn append_to_xml_buffer(buf: &mut _xmlBuffer, data: &[u8]) {
1279    if data.is_empty() {
1280        return;
1281    }
1282
1283    let new_use = (buf.use_ as usize).saturating_add(data.len());
1284    if new_use > buf.size as usize {
1285        // Grow buffer: double or fit, whichever is larger
1286        let new_size = (buf.size as usize).saturating_mul(2).max(new_use).max(256);
1287        let new_content =
1288            unsafe { xmlReallocImpl(buf.content as *mut c_void, new_size) as *mut xmlChar };
1289        if new_content.is_null() {
1290            return; // Allocation failure — silently skip
1291        }
1292        buf.content = new_content;
1293        // UPSTREAM-PARITY (io/mod.rs buf_add realloc paths): when the buffer
1294        // grows, contentIO tracks the CURRENT allocation base — buf_free
1295        // frees contentIO, so a stale contentIO (the pre-realloc block) would
1296        // cause a double-free on buffers that grew through this conversion
1297        // path (nokogiri HTML4/HTML5 UTF-8 serialization).
1298        buf.contentIO = new_content;
1299        buf.size = new_size as c_uint;
1300    }
1301
1302    unsafe {
1303        ptr::copy_nonoverlapping(
1304            data.as_ptr(),
1305            buf.content.add(buf.use_ as usize),
1306            data.len(),
1307        );
1308    }
1309    buf.use_ = new_use as c_uint;
1310}
1311
1312// ═══════════════════════════════════════════════════════════════════════════════
1313// 7. Built-in encoding handler callbacks (extern "C")
1314// ═══════════════════════════════════════════════════════════════════════════════
1315
1316// ── UTF-8 (identity) ──────────────────────────────────────────────────────
1317
1318/// UTF-8 input function: identity (input is already UTF-8).
1319///
1320/// Simply copies bytes from input to output, up to the available space.
1321unsafe extern "C" fn utf8_input_func(
1322    out: *mut c_uchar,
1323    outlen: *mut c_int,
1324    in_: *const c_uchar,
1325    inlen: *mut c_int,
1326) -> c_int {
1327    let avail_out = *outlen as usize;
1328    let avail_in = *inlen as usize;
1329    let to_copy = avail_out.min(avail_in);
1330
1331    if to_copy > 0 {
1332        ptr::copy_nonoverlapping(in_, out, to_copy);
1333    }
1334
1335    *outlen = to_copy as c_int;
1336    *inlen = to_copy as c_int;
1337    to_copy as c_int
1338}
1339
1340/// UTF-8 output function: identity (output is already UTF-8).
1341unsafe extern "C" fn utf8_output_func(
1342    out: *mut c_uchar,
1343    outlen: *mut c_int,
1344    in_: *const c_uchar,
1345    inlen: *mut c_int,
1346) -> c_int {
1347    utf8_input_func(out, outlen, in_, inlen)
1348}
1349
1350// ── UTF-16LE ──────────────────────────────────────────────────────────────
1351
1352/// UTF-16LE input function: convert UTF-16LE to UTF-8.
1353unsafe extern "C" fn utf16le_input_func(
1354    out: *mut c_uchar,
1355    outlen: *mut c_int,
1356    in_: *const c_uchar,
1357    inlen: *mut c_int,
1358) -> c_int {
1359    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1360        return -1;
1361    }
1362
1363    let avail_in = *inlen as usize;
1364    let avail_out = *outlen as usize;
1365
1366    if avail_in == 0 || avail_out == 0 {
1367        *outlen = 0;
1368        *inlen = 0;
1369        return 0;
1370    }
1371
1372    let in_data = core::slice::from_raw_parts(in_, avail_in);
1373    let _out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1374
1375    // Use the safe wrapper
1376    let result = match utf16le_to_utf8(in_data) {
1377        Ok(v) => v,
1378        Err(()) => return -1,
1379    };
1380
1381    let written = result.len().min(avail_out);
1382    if written > 0 {
1383        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1384    }
1385
1386    *outlen = written as c_int;
1387    *inlen = avail_in as c_int; // All input consumed
1388    written as c_int
1389}
1390
1391/// UTF-16LE output function: convert UTF-8 to UTF-16LE.
1392unsafe extern "C" fn utf16le_output_func(
1393    out: *mut c_uchar,
1394    outlen: *mut c_int,
1395    in_: *const c_uchar,
1396    inlen: *mut c_int,
1397) -> c_int {
1398    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1399        return -1;
1400    }
1401
1402    let avail_in = *inlen as usize;
1403    let avail_out = *outlen as usize;
1404
1405    if avail_in == 0 || avail_out == 0 {
1406        *outlen = 0;
1407        *inlen = 0;
1408        return 0;
1409    }
1410
1411    let in_data = core::slice::from_raw_parts(in_, avail_in);
1412    let _out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1413
1414    let result = match utf8_to_utf16le(in_data) {
1415        Ok(v) => v,
1416        Err(()) => return -1,
1417    };
1418
1419    let written = result.len().min(avail_out);
1420    if written > 0 {
1421        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1422    }
1423
1424    *outlen = written as c_int;
1425    *inlen = avail_in as c_int;
1426    written as c_int
1427}
1428
1429// ── UTF-16BE ──────────────────────────────────────────────────────────────
1430
1431/// UTF-16BE input function: convert UTF-16BE to UTF-8.
1432unsafe extern "C" fn utf16be_input_func(
1433    out: *mut c_uchar,
1434    outlen: *mut c_int,
1435    in_: *const c_uchar,
1436    inlen: *mut c_int,
1437) -> c_int {
1438    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1439        return -1;
1440    }
1441
1442    let avail_in = *inlen as usize;
1443    let avail_out = *outlen as usize;
1444
1445    if avail_in == 0 || avail_out == 0 {
1446        *outlen = 0;
1447        *inlen = 0;
1448        return 0;
1449    }
1450
1451    let in_data = core::slice::from_raw_parts(in_, avail_in);
1452    let _out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1453
1454    let result = match utf16be_to_utf8(in_data) {
1455        Ok(v) => v,
1456        Err(()) => return -1,
1457    };
1458
1459    let written = result.len().min(avail_out);
1460    if written > 0 {
1461        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1462    }
1463
1464    *outlen = written as c_int;
1465    *inlen = avail_in as c_int;
1466    written as c_int
1467}
1468
1469/// UTF-16BE output function: convert UTF-8 to UTF-16BE.
1470unsafe extern "C" fn utf16be_output_func(
1471    out: *mut c_uchar,
1472    outlen: *mut c_int,
1473    in_: *const c_uchar,
1474    inlen: *mut c_int,
1475) -> c_int {
1476    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1477        return -1;
1478    }
1479
1480    let avail_in = *inlen as usize;
1481    let avail_out = *outlen as usize;
1482
1483    if avail_in == 0 || avail_out == 0 {
1484        *outlen = 0;
1485        *inlen = 0;
1486        return 0;
1487    }
1488
1489    let in_data = core::slice::from_raw_parts(in_, avail_in);
1490
1491    // First convert to UTF-16LE, then swap bytes
1492    let le_result = match utf8_to_utf16le(in_data) {
1493        Ok(v) => v,
1494        Err(()) => return -1,
1495    };
1496
1497    // Swap byte pairs to get UTF-16BE
1498    let mut result = le_result;
1499    for chunk in result.as_chunks_mut::<2>().0 {
1500        chunk.swap(0, 1);
1501    }
1502
1503    let written = result.len().min(avail_out);
1504    if written > 0 {
1505        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1506    }
1507
1508    *outlen = written as c_int;
1509    *inlen = avail_in as c_int;
1510    written as c_int
1511}
1512
1513// ── ISO-8859-1 (Latin-1) ─────────────────────────────────────────────────
1514
1515/// Latin-1 input function: convert ISO-8859-1 to UTF-8.
1516unsafe extern "C" fn latin1_input_func(
1517    out: *mut c_uchar,
1518    outlen: *mut c_int,
1519    in_: *const c_uchar,
1520    inlen: *mut c_int,
1521) -> c_int {
1522    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1523        return -1;
1524    }
1525
1526    let avail_in = *inlen as usize;
1527    let avail_out = *outlen as usize;
1528
1529    if avail_in == 0 || avail_out == 0 {
1530        *outlen = 0;
1531        *inlen = 0;
1532        return 0;
1533    }
1534
1535    let in_data = core::slice::from_raw_parts(in_, avail_in);
1536    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1537
1538    let mut in_pos = 0;
1539    let mut out_pos = 0;
1540
1541    while in_pos < avail_in && out_pos < avail_out {
1542        let byte = in_data[in_pos];
1543        in_pos += 1;
1544
1545        if byte < 0x80 {
1546            // Single byte UTF-8
1547            if out_pos < avail_out {
1548                out_slice[out_pos] = byte;
1549                out_pos += 1;
1550            } else {
1551                break;
1552            }
1553        } else {
1554            // Two byte UTF-8: 0xC0 | (byte >> 6), 0x80 | (byte & 0x3F)
1555            // For byte 0x80-0xFF, the encoding is 0xC2-0xC3 followed by continuation
1556            if out_pos + 1 < avail_out {
1557                out_slice[out_pos] = 0xC2 | (byte >> 6);
1558                out_slice[out_pos + 1] = 0x80 | (byte & 0x3F);
1559                out_pos += 2;
1560            } else {
1561                break;
1562            }
1563        }
1564    }
1565
1566    *outlen = out_pos as c_int;
1567    *inlen = in_pos as c_int;
1568    out_pos as c_int
1569}
1570
1571/// Latin-1 output function: convert UTF-8 to ISO-8859-1.
1572unsafe extern "C" fn latin1_output_func(
1573    out: *mut c_uchar,
1574    outlen: *mut c_int,
1575    in_: *const c_uchar,
1576    inlen: *mut c_int,
1577) -> c_int {
1578    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1579        return -1;
1580    }
1581
1582    let avail_in = *inlen as usize;
1583    let avail_out = *outlen as usize;
1584
1585    if avail_in == 0 || avail_out == 0 {
1586        *outlen = 0;
1587        *inlen = 0;
1588        return 0;
1589    }
1590
1591    let in_data = core::slice::from_raw_parts(in_, avail_in);
1592    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1593
1594    let mut in_pos = 0;
1595    let mut out_pos = 0;
1596
1597    while in_pos < avail_in && out_pos < avail_out {
1598        let byte = in_data[in_pos];
1599        in_pos += 1;
1600
1601        if byte < 0x80 {
1602            // ASCII — direct mapping
1603            out_slice[out_pos] = byte;
1604            out_pos += 1;
1605        } else if (0xC2..=0xC3).contains(&byte) {
1606            // Two-byte UTF-8 for codepoints U+0080–U+00FF
1607            if in_pos < avail_in {
1608                let second = in_data[in_pos];
1609                in_pos += 1;
1610                if second & 0xC0 != 0x80 {
1611                    return -1; // Invalid continuation byte
1612                }
1613                let cp = ((byte as u32 & 0x1F) << 6) | (second as u32 & 0x3F);
1614                if cp > 0xFF {
1615                    return -1; // Outside Latin-1 range
1616                }
1617                out_slice[out_pos] = cp as u8;
1618                out_pos += 1;
1619            } else {
1620                return -1; // Truncated
1621            }
1622        } else if (0x80..=0xBF).contains(&byte) {
1623            // Unexpected continuation byte
1624            return -1;
1625        } else {
1626            // Multi-byte sequence for codepoints > U+00FF
1627            // Skip the rest of the sequence and return error
1628            return -1;
1629        }
1630    }
1631
1632    *outlen = out_pos as c_int;
1633    *inlen = in_pos as c_int;
1634    out_pos as c_int
1635}
1636
1637// ── Windows-1252 (CP1252) ────────────────────────────────────────────────
1638
1639/// Windows-1252 mapping for bytes 0x80..=0xFF (WHATWG windows-1252 == glibc
1640/// iconv CP1252). Bytes 0x81, 0x8D, 0x8F, 0x90, 0x9D are UNDEFINED in the
1641/// encoding (iconv raises EILSEQ on them). 0x00..=0x7F are ASCII and 0xA0..=
1642/// 0xFF are the Latin-1 supplement, so only 0x80..=0x9F need the table below
1643/// (indexed by `byte - 0x80`, U+FFFF = undefined).
1644///
1645/// R-000157 closure (partial): the oracle serves windows-1252 through iconv;
1646/// the candidate now ships a native converter for this single-byte set.
1647const CP1252_C1: [u16; 32] = [
1648    0x20AC, 0xFFFF, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, // 80..87
1649    0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0xFFFF, 0x017D, 0xFFFF, // 88..8F
1650    0xFFFF, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, // 90..97
1651    0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0xFFFF, 0x017E, 0x0178, // 98..9F
1652];
1653
1654/// Map a Windows-1252 byte to its Unicode codepoint; `None` for the five
1655/// undefined C1 bytes.
1656#[allow(dead_code)]
1657pub(crate) const fn cp1252_byte_to_cp(byte: u8) -> Option<u32> {
1658    match byte {
1659        0x00..=0x7F => Some(byte as u32),
1660        0x80..=0x9F => {
1661            let cp = CP1252_C1[(byte - 0x80) as usize];
1662            if cp == 0xFFFF {
1663                None
1664            } else {
1665                Some(cp as u32)
1666            }
1667        }
1668        _ => Some(byte as u32), // 0xA0..=0xFF = Latin-1 supplement
1669    }
1670}
1671
1672/// Map a Unicode codepoint back to its Windows-1252 byte; `None` when the
1673/// codepoint is not representable in windows-1252.
1674#[allow(dead_code)]
1675pub(crate) const fn cp_to_cp1252_byte(cp: u32) -> Option<u8> {
1676    if cp < 0x80 || (cp >= 0xA0 && cp <= 0xFF) {
1677        Some(cp as u8)
1678    } else if cp >= 0x80 && cp <= 0x9F {
1679        // Reverse scan of the C1 table (32 entries; called per character on
1680        // output conversion only).
1681        let mut i = 0;
1682        while i < 32 {
1683            if CP1252_C1[i] == cp as u16 {
1684                return Some(0x80 + i as u8);
1685            }
1686            i += 1;
1687        }
1688        None
1689    } else {
1690        None
1691    }
1692}
1693
1694/// Convert a single UTF-8 character starting at `data[in_pos]` to its
1695/// codepoint. Returns `(cp, bytes_consumed)` or `None` on invalid UTF-8.
1696fn decode_utf8_char(data: &[u8], in_pos: usize) -> Option<(u32, usize)> {
1697    let b0 = *data.get(in_pos)?;
1698    if b0 < 0x80 {
1699        return Some((u32::from(b0), 1));
1700    }
1701    let (len, cp0) = match b0 {
1702        0xC2..=0xDF => (2, u32::from(b0 & 0x1F)),
1703        0xE0..=0xEF => (3, u32::from(b0 & 0x0F)),
1704        0xF0..=0xF4 => (4, u32::from(b0 & 0x07)),
1705        _ => return None,
1706    };
1707    if in_pos + len > data.len() {
1708        return None;
1709    }
1710    let mut cp = cp0;
1711    for k in 1..len {
1712        let b = data[in_pos + k];
1713        if b & 0xC0 != 0x80 {
1714            return None;
1715        }
1716        cp = (cp << 6) | u32::from(b & 0x3F);
1717    }
1718    Some((cp, len))
1719}
1720
1721/// Convert a whole CP1252 byte slice to UTF-8.
1722///
1723/// Returns `Err(())` when a byte has no windows-1252 mapping (the five
1724/// undefined C1 bytes 0x81/0x8D/0x8F/0x90/0x9D — iconv raises EILSEQ).
1725pub(crate) fn cp1252_to_utf8(data: &[u8]) -> Result<Vec<u8>, ()> {
1726    let mut result = Vec::with_capacity(data.len() * 2);
1727    for &byte in data {
1728        let cp = match cp1252_byte_to_cp(byte) {
1729            None => return Err(()),
1730            Some(cp) => cp,
1731        };
1732        let mut buf = [0u8; 4];
1733        let n = encode_codepoint_to_utf8(cp, &mut buf);
1734        result.extend_from_slice(&buf[..n]);
1735    }
1736    Ok(result)
1737}
1738
1739/// Convert UTF-8 bytes to CP1252 (used by whole-buffer output paths).
1740///
1741/// Returns `Err(())` on invalid UTF-8 or an unrepresentable codepoint.
1742#[allow(dead_code)]
1743pub(crate) fn utf8_to_cp1252(data: &[u8]) -> Result<Vec<u8>, ()> {
1744    let mut result = Vec::with_capacity(data.len());
1745    let mut pos = 0;
1746    while pos < data.len() {
1747        let (cp, consumed) = match decode_utf8_char(data, pos) {
1748            None => return Err(()),
1749            Some(v) => v,
1750        };
1751        let byte = match cp_to_cp1252_byte(cp) {
1752            None => return Err(()),
1753            Some(b) => b,
1754        };
1755        result.push(byte);
1756        pos += consumed;
1757    }
1758    Ok(result)
1759}
1760
1761/// Windows-1252 input function: convert CP1252 bytes to UTF-8.
1762unsafe extern "C" fn cp1252_input_func(
1763    out: *mut c_uchar,
1764    outlen: *mut c_int,
1765    in_: *const c_uchar,
1766    inlen: *mut c_int,
1767) -> c_int {
1768    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1769        return -1;
1770    }
1771
1772    let avail_in = *inlen as usize;
1773    let avail_out = *outlen as usize;
1774
1775    if avail_in == 0 || avail_out == 0 {
1776        *outlen = 0;
1777        *inlen = 0;
1778        return 0;
1779    }
1780
1781    let in_data = core::slice::from_raw_parts(in_, avail_in);
1782    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1783
1784    let mut in_pos = 0;
1785    let mut out_pos = 0;
1786
1787    while in_pos < avail_in && out_pos < avail_out {
1788        let byte = in_data[in_pos];
1789        let cp = match cp1252_byte_to_cp(byte) {
1790            // Undefined byte (0x81/0x8D/0x8F/0x90/0x9D): EILSEQ like iconv.
1791            None => {
1792                *outlen = out_pos as c_int;
1793                *inlen = in_pos as c_int;
1794                return -1;
1795            }
1796            Some(cp) => cp,
1797        };
1798        let mut buf = [0u8; 4];
1799        let n = encode_codepoint_to_utf8(cp, &mut buf);
1800        if out_pos + n > avail_out {
1801            break;
1802        }
1803        out_slice[out_pos..out_pos + n].copy_from_slice(&buf[..n]);
1804        out_pos += n;
1805        in_pos += 1;
1806    }
1807
1808    *outlen = out_pos as c_int;
1809    *inlen = in_pos as c_int;
1810    out_pos as c_int
1811}
1812
1813/// Windows-1252 output function: convert UTF-8 to CP1252 bytes.
1814unsafe extern "C" fn cp1252_output_func(
1815    out: *mut c_uchar,
1816    outlen: *mut c_int,
1817    in_: *const c_uchar,
1818    inlen: *mut c_int,
1819) -> c_int {
1820    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1821        return -1;
1822    }
1823
1824    let avail_in = *inlen as usize;
1825    let avail_out = *outlen as usize;
1826
1827    if avail_in == 0 || avail_out == 0 {
1828        *outlen = 0;
1829        *inlen = 0;
1830        return 0;
1831    }
1832
1833    let in_data = core::slice::from_raw_parts(in_, avail_in);
1834    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1835
1836    let mut in_pos = 0;
1837    let mut out_pos = 0;
1838
1839    while in_pos < avail_in && out_pos < avail_out {
1840        let (cp, consumed) = match decode_utf8_char(in_data, in_pos) {
1841            None => {
1842                *outlen = out_pos as c_int;
1843                *inlen = in_pos as c_int;
1844                return -1;
1845            }
1846            Some(v) => v,
1847        };
1848        let byte = match cp_to_cp1252_byte(cp) {
1849            None => {
1850                // Not representable in windows-1252: EILSEQ like iconv.
1851                *outlen = out_pos as c_int;
1852                *inlen = in_pos as c_int;
1853                return -1;
1854            }
1855            Some(b) => b,
1856        };
1857        out_slice[out_pos] = byte;
1858        out_pos += 1;
1859        in_pos += consumed;
1860    }
1861
1862    *outlen = out_pos as c_int;
1863    *inlen = in_pos as c_int;
1864    out_pos as c_int
1865}
1866
1867// ── ASCII ─────────────────────────────────────────────────────────────────
1868
1869/// ASCII input function: verify and pass through ASCII data to UTF-8.
1870///
1871/// Returns the number of bytes written, `-1` on invalid arguments, or
1872/// `-2` (the candidate's input-error code) when a byte >= 0x80 is reached
1873/// — in that case `*inlen`/`*outlen` hold the bytes consumed/written before
1874/// the offending character, so the output converter (`char_enc_out`) can
1875/// decode the UTF-8 character and replace it with a decimal character
1876/// reference (upstream `asciiToAscii` returns XML_ENC_ERR_INPUT).
1877unsafe extern "C" fn ascii_input_func(
1878    out: *mut c_uchar,
1879    outlen: *mut c_int,
1880    in_: *const c_uchar,
1881    inlen: *mut c_int,
1882) -> c_int {
1883    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1884        return -1;
1885    }
1886
1887    let avail_in = *inlen as usize;
1888    let avail_out = *outlen as usize;
1889
1890    if avail_in == 0 || avail_out == 0 {
1891        *outlen = 0;
1892        *inlen = 0;
1893        return 0;
1894    }
1895
1896    let in_data = core::slice::from_raw_parts(in_, avail_in);
1897    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1898
1899    let mut pos = 0;
1900    while pos < avail_in && pos < avail_out {
1901        let byte = in_data[pos];
1902        if byte > 0x7F {
1903            // Not valid ASCII: report how much was consumed so the caller
1904            // can substitute a character reference and retry.
1905            *outlen = pos as c_int;
1906            *inlen = pos as c_int;
1907            return -2;
1908        }
1909        out_slice[pos] = byte;
1910        pos += 1;
1911    }
1912
1913    *outlen = pos as c_int;
1914    *inlen = pos as c_int;
1915    pos as c_int
1916}
1917
1918/// ASCII output function: verify and pass through UTF-8 data that is ASCII.
1919unsafe extern "C" fn ascii_output_func(
1920    out: *mut c_uchar,
1921    outlen: *mut c_int,
1922    in_: *const c_uchar,
1923    inlen: *mut c_int,
1924) -> c_int {
1925    // For output, ASCII handler requires that input is already ASCII
1926    ascii_input_func(out, outlen, in_, inlen)
1927}
1928
1929// ── Shift_JIS / EUC-JP (encoding_rs-backed; R-000157 closure slice) ────────
1930
1931/// Module-level input-error code: a converter reports the character at
1932/// `*inlen` as unrepresentable and `char_enc_out` substitutes the upstream
1933/// decimal character reference (&#NNN;) before retrying (encoding.c
1934/// xmlCharEncOutput XML_ENC_ERR_INPUT path).
1935const ENC_INPUT_ERROR: c_int = -2;
1936
1937/// Shared output conversion for the encoding_rs-backed East-Asian handlers
1938/// (UTF-8 → `target`). House func contract (see cp1252): complete UTF-8
1939/// characters are converted while output space lasts; the first character
1940/// `target` cannot represent stops the conversion and is reported with the
1941/// -2 input-error convention (so `char_enc_out` emits the decimal character
1942/// reference and retries); invalid UTF-8 (or an incomplete trailing
1943/// sequence) reports -1 with the bytes before the error in `*inlen`. No
1944/// charref expansion happens inside the func, and Shift_JIS/EUC-JP output is
1945/// at most 1:1 with the UTF-8 input on the representable repertoire, so the
1946/// caller's >= 3x scratch can never overflow.
1947unsafe fn enc_rs_output(
1948    target: &'static encoding_rs::Encoding,
1949    out: *mut c_uchar,
1950    outlen: *mut c_int,
1951    in_: *const c_uchar,
1952    inlen: *mut c_int,
1953) -> c_int {
1954    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1955        return -1;
1956    }
1957    let avail_in = *inlen as usize;
1958    let avail_out = *outlen as usize;
1959
1960    if avail_in == 0 || avail_out == 0 {
1961        *outlen = 0;
1962        *inlen = 0;
1963        return 0;
1964    }
1965
1966    let in_data = core::slice::from_raw_parts(in_, avail_in);
1967    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1968
1969    // Convert only complete UTF-8 characters. On invalid bytes (or an
1970    // incomplete trailing sequence) the valid prefix is converted and the
1971    // error is reported at the first offending byte (upstream iconv EILSEQ;
1972    // the xmlCharEncOutput error path decodes the UTF-8 character there).
1973    let (s, error_at) = match core::str::from_utf8(in_data) {
1974        Ok(s) => (s, None),
1975        Err(e) => {
1976            let valid = e.valid_up_to();
1977            if valid == 0 {
1978                *outlen = 0;
1979                *inlen = 0;
1980                return -1;
1981            }
1982            // SAFETY: `valid` is a UTF-8 boundary (from_utf8 guarantees the
1983            // valid prefix ends on a character boundary).
1984            (
1985                unsafe { core::str::from_utf8_unchecked(&in_data[..valid]) },
1986                Some(valid),
1987            )
1988        }
1989    };
1990
1991    let mut encoder = target.new_encoder();
1992    let mut in_pos: usize = 0;
1993    let mut out_pos: usize = 0;
1994    while in_pos < s.len() && out_pos < avail_out {
1995        let dst = &mut out_slice[out_pos..];
1996        let (res, read, written) =
1997            encoder.encode_from_utf8_without_replacement(&s[in_pos..], dst, true);
1998        out_pos += written;
1999        in_pos += read;
2000        match res {
2001            encoding_rs::EncoderResult::InputEmpty => break,
2002            encoding_rs::EncoderResult::OutputFull => {
2003                // Output exhausted: report the partial conversion (with the
2004                // caller's >= 3x scratch this is unreachable for these
2005                // encodings on complete input).
2006                break;
2007            }
2008            encoding_rs::EncoderResult::Unmappable(c) => {
2009                // The encoder consumed the unrepresentable character `c`
2010                // (its UTF-8 bytes are the last len_utf8() bytes of the
2011                // consumed prefix), so rewind *inlen to point AT it:
2012                // char_enc_out substitutes the decimal character reference
2013                // for the character there and retries the remainder.
2014                *outlen = out_pos as c_int;
2015                *inlen = (in_pos - c.len_utf8()) as c_int;
2016                return ENC_INPUT_ERROR;
2017            }
2018        }
2019    }
2020
2021    if let Some(err) = error_at {
2022        if in_pos == s.len() {
2023            // The whole convertible prefix was converted; report the UTF-8
2024            // error at the offending byte (the trailing partial is not
2025            // converted).
2026            *outlen = out_pos as c_int;
2027            *inlen = err as c_int;
2028            return -1;
2029        }
2030    }
2031    *outlen = out_pos as c_int;
2032    *inlen = in_pos as c_int;
2033    out_pos as c_int
2034}
2035
2036/// Shared input conversion for the encoding_rs-backed East-Asian handlers
2037/// (`source` → UTF-8). Converts complete characters while output space
2038/// lasts; an undefined byte or an incomplete trailing sequence reports -1
2039/// with the bytes before the error in `*inlen` (iconv EILSEQ semantics —
2040/// deterministic and loop-free for the caller).
2041unsafe fn enc_rs_input(
2042    source: &'static encoding_rs::Encoding,
2043    out: *mut c_uchar,
2044    outlen: *mut c_int,
2045    in_: *const c_uchar,
2046    inlen: *mut c_int,
2047) -> c_int {
2048    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
2049        return -1;
2050    }
2051    let avail_in = *inlen as usize;
2052    let avail_out = *outlen as usize;
2053
2054    if avail_in == 0 || avail_out == 0 {
2055        *outlen = 0;
2056        *inlen = 0;
2057        return 0;
2058    }
2059
2060    let in_data = core::slice::from_raw_parts(in_, avail_in);
2061    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
2062
2063    let mut decoder = source.new_decoder_without_bom_handling();
2064    let mut in_pos: usize = 0;
2065    let mut out_pos: usize = 0;
2066    while in_pos < avail_in && out_pos < avail_out {
2067        let (res, read, written) = decoder.decode_to_utf8_without_replacement(
2068            &in_data[in_pos..],
2069            &mut out_slice[out_pos..],
2070            true,
2071        );
2072        out_pos += written;
2073        in_pos += read;
2074        match res {
2075            encoding_rs::DecoderResult::InputEmpty => break,
2076            encoding_rs::DecoderResult::OutputFull => break,
2077            encoding_rs::DecoderResult::Malformed(..) => {
2078                // Undefined byte or incomplete tail: hard error after the
2079                // complete prefix (iconv EILSEQ).
2080                *outlen = out_pos as c_int;
2081                *inlen = in_pos as c_int;
2082                return -1;
2083            }
2084        }
2085    }
2086
2087    *outlen = out_pos as c_int;
2088    *inlen = in_pos as c_int;
2089    out_pos as c_int
2090}
2091
2092/// Shift_JIS input function (CP932-compatible WHATWG Shift_JIS → UTF-8).
2093unsafe extern "C" fn shift_jis_input_func(
2094    out: *mut c_uchar,
2095    outlen: *mut c_int,
2096    in_: *const c_uchar,
2097    inlen: *mut c_int,
2098) -> c_int {
2099    enc_rs_input(encoding_rs::SHIFT_JIS, out, outlen, in_, inlen)
2100}
2101
2102/// Shift_JIS output function (UTF-8 → CP932-compatible WHATWG Shift_JIS).
2103unsafe extern "C" fn shift_jis_output_func(
2104    out: *mut c_uchar,
2105    outlen: *mut c_int,
2106    in_: *const c_uchar,
2107    inlen: *mut c_int,
2108) -> c_int {
2109    enc_rs_output(encoding_rs::SHIFT_JIS, out, outlen, in_, inlen)
2110}
2111
2112/// EUC-JP input function (EUC-JP → UTF-8).
2113unsafe extern "C" fn euc_jp_input_func(
2114    out: *mut c_uchar,
2115    outlen: *mut c_int,
2116    in_: *const c_uchar,
2117    inlen: *mut c_int,
2118) -> c_int {
2119    enc_rs_input(encoding_rs::EUC_JP, out, outlen, in_, inlen)
2120}
2121
2122/// EUC-JP output function (UTF-8 → EUC-JP).
2123unsafe extern "C" fn euc_jp_output_func(
2124    out: *mut c_uchar,
2125    outlen: *mut c_int,
2126    in_: *const c_uchar,
2127    inlen: *mut c_int,
2128) -> c_int {
2129    enc_rs_output(encoding_rs::EUC_JP, out, outlen, in_, inlen)
2130}
2131
2132// ═══════════════════════════════════════════════════════════════════════════════
2133// 8. ABI export functions (called from exports_xml2.rs)
2134// ═══════════════════════════════════════════════════════════════════════════════
2135
2136/// `xmlFindCharEncodingHandler` implementation.
2137///
2138/// Finds an encoding handler by name. Returns a pointer to the handler,
2139/// or `ptr::null_mut()` if not found.
2140pub(crate) fn xmlFindCharEncodingHandler(name: *const c_char) -> *mut _xmlCharEncodingHandler {
2141    if name.is_null() {
2142        return ptr::null_mut();
2143    }
2144    find_encoding_handler(name as *const xmlChar)
2145}
2146
2147/// `xmlGetCharEncodingName` implementation.
2148///
2149/// Returns the canonical name for an encoding, or `ptr::null()` if unknown.
2150pub(crate) const fn xmlGetCharEncodingName(enc: xmlCharEncoding) -> *const c_char {
2151    // Return null-terminated C strings using static CStr literals.
2152    // Mirrors upstream 2.15 xmlGetCharEncodingName: the UTF-16/UCS-4 pairs
2153    // return the W3C canonical names before the defaultHandlers table.
2154    match enc {
2155        xmlCharEncoding::XML_CHAR_ENCODING_UTF8 => c"UTF-8".as_ptr(),
2156        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE | xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE => {
2157            c"UTF-16".as_ptr()
2158        }
2159        xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE | xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE => {
2160            c"UCS-4".as_ptr()
2161        }
2162        xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC => c"IBM037".as_ptr(),
2163        xmlCharEncoding::XML_CHAR_ENCODING_UCS2 => c"UCS-2".as_ptr(),
2164        xmlCharEncoding::XML_CHAR_ENCODING_8859_1 => c"ISO-8859-1".as_ptr(),
2165        xmlCharEncoding::XML_CHAR_ENCODING_8859_2 => c"ISO-8859-2".as_ptr(),
2166        xmlCharEncoding::XML_CHAR_ENCODING_8859_3 => c"ISO-8859-3".as_ptr(),
2167        xmlCharEncoding::XML_CHAR_ENCODING_8859_4 => c"ISO-8859-4".as_ptr(),
2168        xmlCharEncoding::XML_CHAR_ENCODING_8859_5 => c"ISO-8859-5".as_ptr(),
2169        xmlCharEncoding::XML_CHAR_ENCODING_8859_6 => c"ISO-8859-6".as_ptr(),
2170        xmlCharEncoding::XML_CHAR_ENCODING_8859_7 => c"ISO-8859-7".as_ptr(),
2171        xmlCharEncoding::XML_CHAR_ENCODING_8859_8 => c"ISO-8859-8".as_ptr(),
2172        xmlCharEncoding::XML_CHAR_ENCODING_8859_9 => c"ISO-8859-9".as_ptr(),
2173        xmlCharEncoding::XML_CHAR_ENCODING_2022_JP => c"ISO-2022-JP".as_ptr(),
2174        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS => c"Shift_JIS".as_ptr(),
2175        xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP => c"EUC-JP".as_ptr(),
2176        // upstream defaultHandlers[22].name
2177        xmlCharEncoding::XML_CHAR_ENCODING_ASCII => c"US-ASCII".as_ptr(),
2178        _ => ptr::null(),
2179    }
2180}
2181
2182/// `xmlParseCharEncoding` implementation.
2183///
2184/// Parses an encoding name string to an `xmlCharEncoding` enum value,
2185/// returned as `c_int`.
2186///
2187/// # Safety
2188///
2189/// - `name` must be NULL or a valid pointer to a NUL-terminated string.
2190pub(crate) fn xmlParseCharEncoding(name: *const c_char) -> c_int {
2191    if name.is_null() {
2192        return xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int;
2193    }
2194    let bytes = unsafe { CStr::from_ptr(name).to_bytes() };
2195    encoding_from_name(bytes) as c_int
2196}
2197
2198// ── Encoding aliases (upstream encoding.c xmlAddEncodingAlias etc.) ──────────
2199//
2200// A global alias table maps alias names to canonical encoding names.
2201// Upstream keeps a static hash of aliases; the candidate uses a
2202// process-lifetime RwLock<HashMap>. Thread-safe; matches upstream's
2203// observable contract (add/del/get by name).
2204
2205static ENCODING_ALIASES: std::sync::OnceLock<
2206    parking_lot::RwLock<std::collections::HashMap<Vec<u8>, Vec<u8>>>,
2207> = std::sync::OnceLock::new();
2208
2209fn encoding_aliases() -> &'static parking_lot::RwLock<std::collections::HashMap<Vec<u8>, Vec<u8>>> {
2210    ENCODING_ALIASES.get_or_init(|| parking_lot::RwLock::new(std::collections::HashMap::new()))
2211}
2212
2213/// `xmlAddEncodingAlias` implementation: register `alias` for `name`.
2214/// Returns 0 on success, -1 on error (NULL arguments).
2215///
2216/// # Safety
2217///
2218/// - `name` and `alias` must be NULL or valid pointers to NUL-terminated
2219///   strings; both are copied before insertion into the alias table.
2220pub(crate) fn add_encoding_alias(name: *const c_char, alias: *const c_char) -> c_int {
2221    if name.is_null() || alias.is_null() {
2222        return -1;
2223    }
2224    let n = unsafe { CStr::from_ptr(name).to_bytes().to_vec() };
2225    let a = unsafe { CStr::from_ptr(alias).to_bytes().to_vec() };
2226    encoding_aliases().write().insert(a, n);
2227    0
2228}
2229
2230/// `xmlDelEncodingAlias` implementation: remove `alias`.
2231/// Returns 0 on success, -1 if the alias does not exist.
2232///
2233/// # Safety
2234///
2235/// - `alias` must be NULL or a valid pointer to a NUL-terminated string.
2236pub(crate) fn del_encoding_alias(alias: *const c_char) -> c_int {
2237    if alias.is_null() {
2238        return -1;
2239    }
2240    let a = unsafe { CStr::from_ptr(alias).to_bytes().to_vec() };
2241    if encoding_aliases().write().remove(&a).is_some() {
2242        0
2243    } else {
2244        -1
2245    }
2246}
2247
2248/// `xmlGetEncodingAlias` implementation: return the canonical name for
2249/// `alias`, or NULL when not registered.
2250///
2251/// # Safety
2252///
2253/// - `alias` must be NULL or a valid pointer to a NUL-terminated string.
2254/// - The returned pointer is a leaked, process-lifetime NUL-terminated
2255///   string, or NULL; the caller must not free it.
2256pub(crate) fn get_encoding_alias(alias: *const c_char) -> *const c_char {
2257    if alias.is_null() {
2258        return ptr::null();
2259    }
2260    let a = unsafe { CStr::from_ptr(alias).to_bytes().to_vec() };
2261    let guard = encoding_aliases().read();
2262    match guard.get(&a) {
2263        Some(v) => {
2264            // leak the canonical name: upstream returns a pointer valid for
2265            // the process lifetime (the alias hash owns the strings)
2266            let leaked: &'static [u8] = Box::leak(v.clone().into_boxed_slice());
2267            leaked.as_ptr() as *const c_char
2268        }
2269        None => ptr::null(),
2270    }
2271}
2272
2273/// `xmlCleanupEncodingAliases` implementation: drop all aliases.
2274pub(crate) fn cleanup_encoding_aliases() {
2275    encoding_aliases().write().clear();
2276}
2277
2278/// `xmlCharEncInFunc` implementation.
2279///
2280/// Converts the input buffer's encoding to UTF-8 using the given handler.
2281pub(crate) fn xmlCharEncInFunc(
2282    handler: *mut _xmlCharEncodingHandler,
2283    out: *mut _xmlBuffer,
2284    in_: *mut _xmlBuffer,
2285) -> c_int {
2286    char_enc_in(handler, out, in_)
2287}
2288
2289/// `xmlCharEncOutFunc` implementation.
2290///
2291/// Converts the input buffer from UTF-8 to the handler's output encoding.
2292pub(crate) fn xmlCharEncOutFunc(
2293    handler: *mut _xmlCharEncodingHandler,
2294    out: *mut _xmlBuffer,
2295    in_: *mut _xmlBuffer,
2296) -> c_int {
2297    char_enc_out(handler, out, in_)
2298}
2299
2300/// `xmlNewCharEncodingHandler` implementation.
2301///
2302/// Creates a new encoding handler with the given name and conversion functions.
2303/// The name string is duplicated. Returns a pointer to the new handler,
2304/// or `ptr::null_mut()` on allocation failure.
2305///
2306/// # Safety
2307///
2308/// - `name` must be NULL or a valid pointer to a NUL-terminated string that
2309///   stays valid until it is duplicated.
2310/// - `input` and `output` must be valid function pointers matching the
2311///   callback ABI; on success the returned handler owns a duplicated name
2312///   and must be released with `xmlDelEncodingHandler`.
2313pub(crate) fn xmlNewCharEncodingHandler(
2314    name: *const c_char,
2315    input: xmlCharEncodingInputFunc,
2316    output: xmlCharEncodingOutputFunc,
2317) -> *mut _xmlCharEncodingHandler {
2318    if name.is_null() {
2319        return ptr::null_mut();
2320    }
2321
2322    let name_raw = unsafe { crate::abi::allocator::xmlMemStrdupImpl(name) };
2323    if name_raw.is_null() {
2324        return ptr::null_mut();
2325    }
2326
2327    let handler = unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) }
2328        as *mut _xmlCharEncodingHandler;
2329
2330    if handler.is_null() {
2331        unsafe { xmlFreeImpl(name_raw) };
2332        return ptr::null_mut();
2333    }
2334
2335    unsafe {
2336        ptr::write(
2337            handler,
2338            _xmlCharEncodingHandler {
2339                name: name_raw as *mut c_char,
2340                input: EncodingInputUnion {
2341                    legacyFunc: Some(input),
2342                },
2343                output: EncodingOutputUnion {
2344                    legacyFunc: Some(output),
2345                },
2346                inputCtxt: ptr::null_mut(),
2347                outputCtxt: ptr::null_mut(),
2348                ctxtDtor: None,
2349                flags: 0,
2350            },
2351        );
2352    }
2353
2354    handler
2355}
2356
2357/// `xmlDelEncodingHandler` implementation.
2358///
2359/// Frees an encoding handler previously created with `xmlNewCharEncodingHandler`.
2360///
2361/// # Safety
2362///
2363/// - `handler` must be NULL or a valid heap-allocated
2364///   `_xmlCharEncodingHandler` whose `name` is NULL or a heap-allocated
2365///   NUL-terminated string; both allocations are freed exactly once, and the
2366///   handler must have been removed from the registry.
2367#[allow(dead_code)]
2368pub(crate) fn xmlDelEncodingHandler(handler: *mut _xmlCharEncodingHandler) {
2369    if handler.is_null() {
2370        return;
2371    }
2372
2373    // Remove from registry if present
2374    {
2375        let mut handlers = ENCODING_HANDLERS.write();
2376        handlers.retain(|&h| h.0 != handler);
2377    }
2378
2379    unsafe {
2380        if !(*handler).name.is_null() {
2381            xmlFreeImpl((*handler).name as *mut c_void);
2382        }
2383        xmlFreeImpl(handler as *mut c_void);
2384    }
2385}
2386
2387/// `xmlInitCharEncodingHandlers` implementation.
2388pub(crate) fn xmlInitCharEncodingHandlers() {
2389    init_encodings();
2390}
2391
2392/// `xmlCleanupCharEncodingHandlers` implementation.
2393pub(crate) fn xmlCleanupCharEncodingHandlers() {
2394    cleanup_encodings();
2395}
2396
2397// ═══════════════════════════════════════════════════════════════════════════════
2398// 7. Handler lookup / creation (upstream 2.13.0+ encoding.c)
2399// ═══════════════════════════════════════════════════════════════════════════════
2400//
2401// Upstream keeps a static `defaultHandlers[32]` table indexed by xmlCharEncoding
2402// plus iconv/ICU fallbacks. The candidate ships no iconv/ICU, so encodings whose
2403// upstream default handler carries a real converter (UTF-8, UTF-16LE, UTF-16BE,
2404// UTF-16, ISO-8859-1, US-ASCII) resolve to the registered built-in handlers;
2405// every other encoding reports XML_ERR_UNSUPPORTED_ENCODING exactly where
2406// upstream would fall through to iconv/ICU.
2407
2408/// `xmlLookupCharEncodingHandler` implementation (upstream encoding.c).
2409///
2410/// Mirrors the upstream control flow:
2411///  - `out == NULL`                     → XML_ERR_ARGUMENT (115)
2412///  - `enc <= 0 || enc >= 32`           → XML_ERR_UNSUPPORTED_ENCODING (32)
2413///  - UTF-8                             → XML_ERR_OK, `*out` stays NULL
2414///  - native built-in encoding          → XML_ERR_OK, `*out` = static handler
2415///  - iconv/ICU-only encoding           → XML_ERR_UNSUPPORTED_ENCODING
2416///
2417/// The returned handler is a static registry entry and must NOT be freed.
2418///
2419/// # Safety
2420///
2421/// - `out` must be a valid pointer to a `*mut c_void` out-parameter; it is
2422///   written with NULL or a pointer to a static registry handler that the
2423///   caller must not free.
2424pub(crate) fn xmlLookupCharEncodingHandler(enc: c_int, out: *mut *mut c_void) -> c_int {
2425    if out.is_null() {
2426        return crate::abi::types::XML_ERR_ARGUMENT;
2427    }
2428    unsafe {
2429        *out = ptr::null_mut();
2430    }
2431    if enc <= 0 || enc >= 32 {
2432        return crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING;
2433    }
2434    /* Return NULL handler for UTF-8 */
2435    if enc == xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int {
2436        return crate::abi::types::XML_ERR_OK;
2437    }
2438    let canonical: &[u8] = match enc {
2439        /* XML_CHAR_ENCODING_UTF16LE */
2440        2 => b"UTF-16LE\0",
2441        /* XML_CHAR_ENCODING_UTF16BE */
2442        3 => b"UTF-16BE\0",
2443        /* XML_CHAR_ENCODING_8859_1 */
2444        10 => b"ISO-8859-1\0",
2445        /* XML_CHAR_ENCODING_ASCII */
2446        22 => b"US-ASCII\0",
2447        /* XML_CHAR_ENCODING_UTF16 (not in the local enum) */
2448        23 => b"UTF-16\0",
2449        _ => return crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING,
2450    };
2451    let h = find_encoding_handler(canonical.as_ptr() as *const xmlChar);
2452    if h.is_null() {
2453        return crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING;
2454    }
2455    unsafe {
2456        *out = h as *mut c_void;
2457    }
2458    crate::abi::types::XML_ERR_OK
2459}
2460
2461/// `xmlGetCharEncodingHandler` implementation (deprecated upstream wrapper).
2462pub(crate) fn xmlGetCharEncodingHandler(enc: c_int) -> *mut c_void {
2463    let mut ret: *mut c_void = ptr::null_mut();
2464    let _rc = xmlLookupCharEncodingHandler(enc, &mut ret);
2465    ret
2466}
2467
2468/// `xmlCreateCharEncodingHandler` implementation (upstream 2.14.0+ encoding.c).
2469///
2470/// Flags: XML_ENC_INPUT = 1, XML_ENC_OUTPUT = 2, XML_ENC_HTML = 4.
2471/// Unlike upstream, no iconv/ICU backend exists, so encodings without a native
2472/// converter fall through to `find_extra_handler` (custom impl / deprecated
2473/// global registry) and otherwise report XML_ERR_UNSUPPORTED_ENCODING.
2474///
2475/// # Safety
2476///
2477/// - `out` must be a valid pointer to a `*mut c_void` out-parameter; it is
2478///   written with NULL or a heap-allocated handler copy the caller owns.
2479/// - `name` must be NULL or a valid pointer to a NUL-terminated string.
2480/// - `implCtxt` is an opaque context forwarded to `find_extra_handler` and
2481///   must be valid for the callback that consumes it.
2482pub(crate) fn xmlCreateCharEncodingHandler(
2483    name: *const c_char,
2484    flags: c_int,
2485    impl_: Option<xmlCharEncConvImpl>,
2486    implCtxt: *mut c_void,
2487    out: *mut *mut c_void,
2488) -> c_int {
2489    if out.is_null() {
2490        return crate::abi::types::XML_ERR_ARGUMENT;
2491    }
2492    unsafe {
2493        *out = ptr::null_mut();
2494    }
2495    if name.is_null() || flags == 0 {
2496        return crate::abi::types::XML_ERR_ARGUMENT;
2497    }
2498    let norig = unsafe { CStr::from_ptr(name).to_bytes() };
2499
2500    /* Alias resolution (upstream xmlGetEncodingAlias). */
2501    let mut eff: &[u8] = norig;
2502    let alias = get_encoding_alias(name);
2503    if !alias.is_null() {
2504        eff = unsafe { CStr::from_ptr(alias).to_bytes() };
2505    }
2506
2507    let enc = encoding_from_name(eff);
2508
2509    /* Return NULL handler for UTF-8 */
2510    if enc == xmlCharEncoding::XML_CHAR_ENCODING_UTF8 {
2511        return crate::abi::types::XML_ERR_OK;
2512    }
2513
2514    let canonical: &[u8] = match enc {
2515        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE => b"UTF-16LE\0",
2516        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE => b"UTF-16BE\0",
2517        xmlCharEncoding::XML_CHAR_ENCODING_8859_1 => b"ISO-8859-1\0",
2518        xmlCharEncoding::XML_CHAR_ENCODING_ASCII => b"US-ASCII\0",
2519        _ => {
2520            return find_extra_handler(norig, eff, flags, impl_, implCtxt, out);
2521        }
2522    };
2523    let h = find_encoding_handler(canonical.as_ptr() as *const xmlChar);
2524    if h.is_null() {
2525        return find_extra_handler(norig, eff, flags, impl_, implCtxt, out);
2526    }
2527    unsafe {
2528        let src = &*h;
2529        let has_in = (flags & 1) == 0 || !src.input.legacyFunc.is_none();
2530        let has_out = (flags & 2) == 0 || !src.output.legacyFunc.is_none();
2531        if !has_in || !has_out {
2532            return find_extra_handler(norig, eff, flags, impl_, implCtxt, out);
2533        }
2534        /*
2535         * Return a copy of the handler with the original name (upstream
2536         * "Return a copy of the handler with the original name").
2537         */
2538        let copy =
2539            xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) as *mut _xmlCharEncodingHandler;
2540        if copy.is_null() {
2541            return crate::abi::types::XML_ERR_NO_MEMORY;
2542        }
2543        let name_copy = crate::abi::allocator::xmlMemStrdupImpl(name) as *mut c_char;
2544        if name_copy.is_null() {
2545            xmlFreeImpl(copy as *mut c_void);
2546            return crate::abi::types::XML_ERR_NO_MEMORY;
2547        }
2548        ptr::write(
2549            copy,
2550            _xmlCharEncodingHandler {
2551                name: name_copy,
2552                input: EncodingInputUnion {
2553                    legacyFunc: src.input.legacyFunc,
2554                },
2555                output: EncodingOutputUnion {
2556                    legacyFunc: src.output.legacyFunc,
2557                },
2558                inputCtxt: src.inputCtxt,
2559                outputCtxt: src.outputCtxt,
2560                ctxtDtor: src.ctxtDtor,
2561                flags: src.flags,
2562            },
2563        );
2564        *out = copy as *mut c_void;
2565    }
2566    crate::abi::types::XML_ERR_OK
2567}
2568
2569/// Fallback path of `xmlCreateCharEncodingHandler` (upstream `xmlFindExtraHandler`).
2570///
2571/// Tries the caller-supplied custom implementation first, then the deprecated
2572/// global handler registry. iconv/ICU do not exist in the candidate, so the
2573/// final result is XML_ERR_UNSUPPORTED_ENCODING.
2574///
2575/// # Safety
2576///
2577/// - `norig` and `name` must be valid byte slices; NUL-terminated copies are
2578///   built from them for lookups and callbacks.
2579/// - `out` must be a valid out-parameter; it is written with NULL or a
2580///   registry handler pointer that must not be freed.
2581/// - `implCtxt` must be a valid context for the custom `impl_` callback when
2582///   one is supplied.
2583fn find_extra_handler(
2584    norig: &[u8],
2585    name: &[u8],
2586    flags: c_int,
2587    impl_: Option<xmlCharEncConvImpl>,
2588    implCtxt: *mut c_void,
2589    out: *mut *mut c_void,
2590) -> c_int {
2591    /* Custom implementation before deprecated global handlers. */
2592    if let Some(f) = impl_ {
2593        let mut n = norig.to_vec();
2594        n.push(0);
2595        let rc = unsafe {
2596            f(
2597                implCtxt,
2598                n.as_ptr() as *const c_char,
2599                flags,
2600                out as *mut *mut crate::abi::structs::_xmlCharEncodingHandler,
2601            )
2602        };
2603        return rc;
2604    }
2605    /* Deprecated global handlers registry (xmlRegisterCharEncodingHandler). */
2606    let mut n = name.to_vec();
2607    n.push(0);
2608    let h = find_encoding_handler(n.as_ptr() as *const xmlChar);
2609    if !h.is_null() {
2610        unsafe {
2611            let src = &*h;
2612            let has_in = (flags & 1) == 0 || !src.input.legacyFunc.is_none();
2613            let has_out = (flags & 2) == 0 || !src.output.legacyFunc.is_none();
2614            if has_in && has_out {
2615                *out = h as *mut c_void;
2616                return crate::abi::types::XML_ERR_OK;
2617            }
2618        }
2619    }
2620    crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING
2621}
2622
2623/// `xmlOpenCharEncodingHandler` implementation (upstream encoding.c).
2624pub(crate) fn xmlOpenCharEncodingHandler(
2625    name: *const c_char,
2626    output: c_int,
2627    out: *mut *mut c_void,
2628) -> c_int {
2629    /* XML_ENC_OUTPUT if output else XML_ENC_INPUT */
2630    let flags: c_int = if output != 0 { 2 } else { 1 };
2631    xmlCreateCharEncodingHandler(name, flags, None, ptr::null_mut(), out)
2632}
2633
2634/// `xmlCharEncNewCustomHandler` implementation (upstream 2.15.0+ encoding.c).
2635///
2636/// Creates a handler backed by modern `xmlCharEncConvFunc` callbacks (with
2637/// per-direction contexts and a context destructor). The handler must be
2638/// released with `xmlCharEncCloseFunc`.
2639///
2640/// # Safety
2641///
2642/// - `out` must be a valid pointer to a `*mut c_void` out-parameter.
2643/// - `name` must be NULL or a valid pointer to a NUL-terminated string.
2644/// - `input` and `output` must be valid `xmlCharEncConvFunc` callbacks;
2645///   `inputCtxt` and `outputCtxt` are opaque contexts consumed by them and
2646///   by `ctxtDtor`, which is invoked on each non-NULL context when
2647///   allocation fails (and later by `xmlCharEncCloseFunc`).
2648pub(crate) fn xmlCharEncNewCustomHandler(
2649    name: *const c_char,
2650    input: xmlCharEncConvFunc,
2651    output: xmlCharEncConvFunc,
2652    ctxtDtor: Option<xmlCharEncConvCtxtDtor>,
2653    inputCtxt: *mut c_void,
2654    outputCtxt: *mut c_void,
2655    out: *mut *mut c_void,
2656) -> c_int {
2657    if out.is_null() {
2658        return crate::abi::types::XML_ERR_ARGUMENT;
2659    }
2660    let handler = unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) }
2661        as *mut _xmlCharEncodingHandler;
2662    if handler.is_null() {
2663        unsafe {
2664            if let Some(d) = ctxtDtor {
2665                if !inputCtxt.is_null() {
2666                    d(inputCtxt);
2667                }
2668                if !outputCtxt.is_null() {
2669                    d(outputCtxt);
2670                }
2671            }
2672        }
2673        return crate::abi::types::XML_ERR_NO_MEMORY;
2674    }
2675    let name_copy = if name.is_null() {
2676        ptr::null_mut()
2677    } else {
2678        let nc = unsafe { crate::abi::allocator::xmlMemStrdupImpl(name) } as *mut c_char;
2679        if nc.is_null() {
2680            unsafe { xmlFreeImpl(handler as *mut c_void) };
2681            unsafe {
2682                if let Some(d) = ctxtDtor {
2683                    if !inputCtxt.is_null() {
2684                        d(inputCtxt);
2685                    }
2686                    if !outputCtxt.is_null() {
2687                        d(outputCtxt);
2688                    }
2689                }
2690            }
2691            return crate::abi::types::XML_ERR_NO_MEMORY;
2692        }
2693        nc
2694    };
2695    unsafe {
2696        ptr::write(
2697            handler,
2698            _xmlCharEncodingHandler {
2699                name: name_copy,
2700                input: EncodingInputUnion { func: Some(input) },
2701                output: EncodingOutputUnion { func: Some(output) },
2702                inputCtxt,
2703                outputCtxt,
2704                ctxtDtor,
2705                flags: 0,
2706            },
2707        );
2708        *out = handler as *mut c_void;
2709    }
2710    crate::abi::types::XML_ERR_OK
2711}
2712
2713// ═══════════════════════════════════════════════════════════════════════════════
2714// Tests
2715// ═══════════════════════════════════════════════════════════════════════════════
2716
2717#[cfg(test)]
2718mod tests {
2719    use super::*;
2720
2721    // ── BOM detection ──────────────────────────────────────────────────────
2722
2723    #[test]
2724    fn test_detect_bom_utf8() {
2725        let data = [0xEF, 0xBB, 0xBF, b'<', b'?', b'x', b'm', b'l'];
2726        assert_eq!(
2727            detect_encoding_from_bom(&data),
2728            xmlCharEncoding::XML_CHAR_ENCODING_UTF8
2729        );
2730    }
2731
2732    #[test]
2733    fn test_detect_bom_utf16le() {
2734        let data = [0xFF, 0xFE, 0x00, 0x01];
2735        assert_eq!(
2736            detect_encoding_from_bom(&data),
2737            xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
2738        );
2739    }
2740
2741    #[test]
2742    fn test_detect_bom_utf16be() {
2743        let data = [0xFE, 0xFF, 0x00, 0x01];
2744        assert_eq!(
2745            detect_encoding_from_bom(&data),
2746            xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE
2747        );
2748    }
2749
2750    #[test]
2751    fn test_detect_bom_none() {
2752        let data = b"<xml>";
2753        assert_eq!(
2754            detect_encoding_from_bom(data),
2755            xmlCharEncoding::XML_CHAR_ENCODING_NONE
2756        );
2757    }
2758
2759    #[test]
2760    fn test_detect_bom_empty() {
2761        assert_eq!(
2762            detect_encoding_from_bom(b""),
2763            xmlCharEncoding::XML_CHAR_ENCODING_NONE
2764        );
2765    }
2766
2767    // ── Encoding from declaration ──────────────────────────────────────────
2768
2769    #[test]
2770    fn test_detect_encoding_declaration_utf8() {
2771        let data = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
2772        let result = detect_encoding_from_declaration(data);
2773        assert_eq!(result, Some(b"utf-8".to_vec()));
2774    }
2775
2776    #[test]
2777    fn test_detect_encoding_declaration_iso() {
2778        let data = b"<?xml version='1.0' encoding='ISO-8859-1'?>";
2779        let result = detect_encoding_from_declaration(data);
2780        assert_eq!(result, Some(b"iso-8859-1".to_vec()));
2781    }
2782
2783    #[test]
2784    fn test_detect_encoding_declaration_none() {
2785        let data = b"<?xml version=\"1.0\"?>";
2786        let result = detect_encoding_from_declaration(data);
2787        assert!(result.is_none());
2788    }
2789
2790    #[test]
2791    fn test_detect_encoding_declaration_no_xml() {
2792        let data = b"<root>";
2793        let result = detect_encoding_from_declaration(data);
2794        assert!(result.is_none());
2795    }
2796
2797    #[test]
2798    fn test_detect_encoding_declaration_with_bom() {
2799        let mut data = vec![0xEF, 0xBB, 0xBF];
2800        data.extend_from_slice(b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
2801        let result = detect_encoding_from_declaration(&data);
2802        assert_eq!(result, Some(b"utf-8".to_vec()));
2803    }
2804
2805    // ── Encoding from name ─────────────────────────────────────────────────
2806
2807    #[test]
2808    fn test_encoding_from_name_utf8() {
2809        assert_eq!(
2810            encoding_from_name(b"UTF-8"),
2811            xmlCharEncoding::XML_CHAR_ENCODING_UTF8
2812        );
2813        assert_eq!(
2814            encoding_from_name(b"utf8"),
2815            xmlCharEncoding::XML_CHAR_ENCODING_UTF8
2816        );
2817    }
2818
2819    #[test]
2820    fn test_encoding_from_name_utf16() {
2821        assert_eq!(
2822            encoding_from_name(b"UTF-16LE"),
2823            xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
2824        );
2825        assert_eq!(
2826            encoding_from_name(b"UTF-16BE"),
2827            xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE
2828        );
2829        assert_eq!(
2830            encoding_from_name(b"utf-16"),
2831            xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
2832        );
2833    }
2834
2835    #[test]
2836    fn test_encoding_from_name_latin1() {
2837        assert_eq!(
2838            encoding_from_name(b"ISO-8859-1"),
2839            xmlCharEncoding::XML_CHAR_ENCODING_8859_1
2840        );
2841        assert_eq!(
2842            encoding_from_name(b"Latin1"),
2843            xmlCharEncoding::XML_CHAR_ENCODING_8859_1
2844        );
2845    }
2846
2847    #[test]
2848    fn test_encoding_from_name_ascii() {
2849        assert_eq!(
2850            encoding_from_name(b"ASCII"),
2851            xmlCharEncoding::XML_CHAR_ENCODING_ASCII
2852        );
2853        assert_eq!(
2854            encoding_from_name(b"US-ASCII"),
2855            xmlCharEncoding::XML_CHAR_ENCODING_ASCII
2856        );
2857    }
2858
2859    #[test]
2860    fn test_encoding_from_name_error() {
2861        assert_eq!(
2862            encoding_from_name(b"invalid-encoding"),
2863            xmlCharEncoding::XML_CHAR_ENCODING_ERROR
2864        );
2865    }
2866
2867    #[test]
2868    fn test_encoding_from_name_empty() {
2869        assert_eq!(
2870            encoding_from_name(b""),
2871            xmlCharEncoding::XML_CHAR_ENCODING_ERROR
2872        );
2873    }
2874
2875    // ── Encoding name ──────────────────────────────────────────────────────
2876
2877    #[test]
2878    fn test_encoding_name_utf8() {
2879        assert_eq!(
2880            encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_UTF8),
2881            Some(b"UTF-8" as &[u8])
2882        );
2883    }
2884
2885    #[test]
2886    fn test_encoding_name_utf16le() {
2887        assert_eq!(
2888            encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE),
2889            Some(b"UTF-16LE" as &[u8])
2890        );
2891    }
2892
2893    #[test]
2894    fn test_encoding_name_none() {
2895        assert!(encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_NONE).is_none());
2896    }
2897
2898    #[test]
2899    fn test_encoding_name_error() {
2900        assert!(encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_ERROR).is_none());
2901    }
2902
2903    // ── UTF-8 validation ───────────────────────────────────────────────────
2904
2905    #[test]
2906    fn test_utf8_valid_ascii() {
2907        assert!(utf8_valid(b"hello world"));
2908    }
2909
2910    #[test]
2911    fn test_utf8_valid_multi_byte() {
2912        assert!(utf8_valid("héllo wörld 🌍".as_bytes()));
2913    }
2914
2915    #[test]
2916    fn test_utf8_valid_empty() {
2917        assert!(utf8_valid(b""));
2918    }
2919
2920    #[test]
2921    fn test_utf8_invalid() {
2922        assert!(!utf8_valid(&[0xFF, 0xFE, 0x00]));
2923    }
2924
2925    // ── XML char validation ────────────────────────────────────────────────
2926
2927    #[test]
2928    fn test_valid_xml_chars() {
2929        assert!(is_valid_xml_char(0x9)); // Tab
2930        assert!(is_valid_xml_char(0xA)); // LF
2931        assert!(is_valid_xml_char(0xD)); // CR
2932        assert!(is_valid_xml_char(0x20)); // Space
2933        assert!(is_valid_xml_char(0x41)); // 'A'
2934        assert!(is_valid_xml_char(0xD7FF));
2935        assert!(is_valid_xml_char(0xE000));
2936        assert!(is_valid_xml_char(0xFFFD));
2937        assert!(is_valid_xml_char(0x10000));
2938        assert!(is_valid_xml_char(0x10FFFF));
2939    }
2940
2941    #[test]
2942    fn test_invalid_xml_chars() {
2943        assert!(!is_valid_xml_char(0x00));
2944        assert!(!is_valid_xml_char(0x08));
2945        assert!(!is_valid_xml_char(0x0B));
2946        assert!(!is_valid_xml_char(0x0C));
2947        assert!(!is_valid_xml_char(0x0E));
2948        assert!(!is_valid_xml_char(0x1F));
2949        assert!(!is_valid_xml_char(0xD800)); // Surrogate
2950        assert!(!is_valid_xml_char(0xDFFF)); // Surrogate
2951        assert!(!is_valid_xml_char(0xFFFE));
2952        assert!(!is_valid_xml_char(0xFFFF));
2953        assert!(!is_valid_xml_char(0x110000));
2954    }
2955
2956    // ── UTF-16LE to UTF-8 ──────────────────────────────────────────────────
2957
2958    #[test]
2959    fn test_utf16le_to_utf8_ascii() {
2960        // "AB" in UTF-16LE
2961        let data = [b'A', 0x00, b'B', 0x00];
2962        let result = utf16le_to_utf8(&data).unwrap();
2963        assert_eq!(result, b"AB");
2964    }
2965
2966    #[test]
2967    fn test_utf16le_to_utf8_bom() {
2968        let mut data = vec![0xFF, 0xFE]; // BOM
2969        data.extend_from_slice(&[b'A', 0x00, b'B', 0x00]);
2970        let result = utf16le_to_utf8(&data).unwrap();
2971        assert_eq!(result, b"AB");
2972    }
2973
2974    #[test]
2975    fn test_utf16le_to_utf8_bmp() {
2976        // U+00E9 (é) in UTF-16LE = 0xE9 0x00
2977        let data = [0xE9, 0x00];
2978        let result = utf16le_to_utf8(&data).unwrap();
2979        assert_eq!(result, "é".as_bytes());
2980    }
2981
2982    #[test]
2983    fn test_utf16le_to_utf8_supplementary() {
2984        // U+1F600 (😀) in UTF-16LE = 0x3D 0xD8 0x00 0xDE
2985        let data = [0x3D, 0xD8, 0x00, 0xDE];
2986        let result = utf16le_to_utf8(&data).unwrap();
2987        assert_eq!(result, "😀".as_bytes());
2988    }
2989
2990    #[test]
2991    fn test_utf16le_to_utf8_unpaired_surrogate() {
2992        let data = [0x00, 0xD8]; // High surrogate without low
2993        assert!(utf16le_to_utf8(&data).is_err());
2994    }
2995
2996    #[test]
2997    fn test_utf16le_to_utf8_truncated() {
2998        let data = [0x00]; // Odd length
2999        assert!(utf16le_to_utf8(&data).is_err());
3000    }
3001
3002    #[test]
3003    fn test_utf16le_to_utf8_empty() {
3004        let result = utf16le_to_utf8(b"").unwrap();
3005        assert!(result.is_empty());
3006    }
3007
3008    // ── UTF-16BE to UTF-8 ──────────────────────────────────────────────────
3009
3010    #[test]
3011    fn test_utf16be_to_utf8_ascii() {
3012        let data = [0x00, b'A', 0x00, b'B'];
3013        let result = utf16be_to_utf8(&data).unwrap();
3014        assert_eq!(result, b"AB");
3015    }
3016
3017    #[test]
3018    fn test_utf16be_to_utf8_bom() {
3019        let mut data = vec![0xFE, 0xFF]; // BOM
3020        data.extend_from_slice(&[0x00, b'A', 0x00, b'B']);
3021        let result = utf16be_to_utf8(&data).unwrap();
3022        assert_eq!(result, b"AB");
3023    }
3024
3025    #[test]
3026    fn test_utf16be_to_utf8_supplementary() {
3027        // U+1F600 (😀) in UTF-16BE = 0xD8 0x3D 0xDE 0x00
3028        let data = [0xD8, 0x3D, 0xDE, 0x00];
3029        let result = utf16be_to_utf8(&data).unwrap();
3030        assert_eq!(result, "😀".as_bytes());
3031    }
3032
3033    #[test]
3034    fn test_utf16be_to_utf8_empty() {
3035        let result = utf16be_to_utf8(b"").unwrap();
3036        assert!(result.is_empty());
3037    }
3038
3039    // ── UTF-8 to UTF-16LE ──────────────────────────────────────────────────
3040
3041    #[test]
3042    fn test_utf8_to_utf16le_ascii() {
3043        let result = utf8_to_utf16le(b"AB").unwrap();
3044        assert_eq!(result, [b'A', 0x00, b'B', 0x00]);
3045    }
3046
3047    #[test]
3048    fn test_utf8_to_utf16le_bmp() {
3049        let result = utf8_to_utf16le("é".as_bytes()).unwrap();
3050        assert_eq!(result, [0xE9, 0x00]);
3051    }
3052
3053    #[test]
3054    fn test_utf8_to_utf16le_supplementary() {
3055        let result = utf8_to_utf16le("😀".as_bytes()).unwrap();
3056        assert_eq!(result, [0x3D, 0xD8, 0x00, 0xDE]);
3057    }
3058
3059    #[test]
3060    fn test_utf8_to_utf16le_invalid_utf8() {
3061        assert!(utf8_to_utf16le(&[0xFF]).is_err());
3062    }
3063
3064    #[test]
3065    fn test_utf8_to_utf16le_empty() {
3066        let result = utf8_to_utf16le(b"").unwrap();
3067        assert!(result.is_empty());
3068    }
3069
3070    // ── Latin-1 to UTF-8 ───────────────────────────────────────────────────
3071
3072    #[test]
3073    fn test_latin1_to_utf8_ascii() {
3074        let result = latin1_to_utf8(b"ABC");
3075        assert_eq!(result, b"ABC");
3076    }
3077
3078    #[test]
3079    fn test_latin1_to_utf8_accented() {
3080        // 0xE9 = é in Latin-1
3081        let result = latin1_to_utf8(&[0xE9]);
3082        assert_eq!(result, "é".as_bytes());
3083    }
3084
3085    #[test]
3086    fn test_latin1_to_utf8_all_255() {
3087        let result = latin1_to_utf8(&[0xFF]);
3088        // U+00FF = ÿ, UTF-8: 0xC3 0xBF
3089        assert_eq!(result, [0xC3, 0xBF]);
3090    }
3091
3092    #[test]
3093    fn test_latin1_to_utf8_empty() {
3094        let result = latin1_to_utf8(b"");
3095        assert!(result.is_empty());
3096    }
3097
3098    #[test]
3099    fn test_latin1_to_utf8_mixed() {
3100        let result = latin1_to_utf8(b"caf\xE9");
3101        assert_eq!(result, "café".as_bytes());
3102    }
3103
3104    // ── UTF-8 to Latin-1 ───────────────────────────────────────────────────
3105
3106    #[test]
3107    fn test_utf8_to_latin1_ascii() {
3108        let result = utf8_to_latin1(b"ABC").unwrap();
3109        assert_eq!(result, b"ABC");
3110    }
3111
3112    #[test]
3113    fn test_utf8_to_latin1_accented() {
3114        let result = utf8_to_latin1("é".as_bytes()).unwrap();
3115        assert_eq!(result, [0xE9]);
3116    }
3117
3118    #[test]
3119    fn test_utf8_to_latin1_out_of_range() {
3120        assert!(utf8_to_latin1("€".as_bytes()).is_err()); // U+20AC not in Latin-1
3121    }
3122
3123    #[test]
3124    fn test_utf8_to_latin1_invalid_utf8() {
3125        assert!(utf8_to_latin1(&[0xFF]).is_err());
3126    }
3127
3128    #[test]
3129    fn test_utf8_to_latin1_empty() {
3130        let result = utf8_to_latin1(b"").unwrap();
3131        assert!(result.is_empty());
3132    }
3133
3134    // ── Encoding handler registry ──────────────────────────────────────────
3135
3136    #[test]
3137    fn test_init_and_find_encodings() {
3138        init_encodings();
3139
3140        let utf8_name: *const xmlChar = c"UTF-8".as_ptr() as *const xmlChar;
3141        assert!(!find_encoding_handler(utf8_name).is_null());
3142
3143        let utf16le_name: *const xmlChar = c"UTF-16LE".as_ptr() as *const xmlChar;
3144        assert!(!find_encoding_handler(utf16le_name).is_null());
3145
3146        let utf16be_name: *const xmlChar = c"UTF-16BE".as_ptr() as *const xmlChar;
3147        assert!(!find_encoding_handler(utf16be_name).is_null());
3148
3149        let latin1_name: *const xmlChar = c"ISO-8859-1".as_ptr() as *const xmlChar;
3150        assert!(!find_encoding_handler(latin1_name).is_null());
3151
3152        let ascii_name: *const xmlChar = c"ASCII".as_ptr() as *const xmlChar;
3153        assert!(!find_encoding_handler(ascii_name).is_null());
3154
3155        // Case insensitive
3156        let lower_name: *const xmlChar = c"utf-8".as_ptr() as *const xmlChar;
3157        assert!(!find_encoding_handler(lower_name).is_null());
3158    }
3159
3160    /// Phase 14 PHP court regression: the ABI `xmlFindCharEncodingHandler`
3161    /// hands the caller an OWNED handler that it may release with
3162    /// `xmlCharEncCloseFunc` — except UTF-8, where upstream returns a static
3163    /// handler that close must not release (so the registry is never freed
3164    /// out from under subsequent lookups). Closing a returned non-UTF-8
3165    /// handler must not free the persistent registry entry.
3166    ///
3167    /// # Safety
3168    ///
3169    /// - The handler returned by `xmlFindCharEncodingHandler_owned` is owned by
3170    ///   the caller and released here with the allocator, mirroring the export
3171    ///   `xmlCharEncCloseFunc` (which, for these stateless built-in handlers,
3172    ///   frees `name` and the struct without invoking any context destructor).
3173    #[test]
3174    fn test_find_owned_close_keeps_registry_intact() {
3175        init_encodings();
3176        let name: *const xmlChar = c"ISO-8859-1".as_ptr() as *const xmlChar;
3177
3178        // The registry entry is a long-lived borrow.
3179        let registry = find_encoding_handler(name);
3180        assert!(!registry.is_null());
3181        // First retrieval returns an OWNED copy, distinct from the registry entry.
3182        let h1 = xmlFindCharEncodingHandler_owned(name);
3183        assert!(!h1.is_null());
3184        assert_ne!(h1 as *const c_void, registry as *const c_void);
3185
3186        // Closing h1 (simulate xmlCharEncCloseFunc on a non-static handler):
3187        // frees its name + struct but NOT the registry entry.
3188        unsafe {
3189            if !(*h1).name.is_null() {
3190                crate::abi::allocator::xmlFreeImpl((*h1).name as *mut c_void);
3191            }
3192            xmlFreeImpl(h1 as *mut c_void);
3193        }
3194
3195        // The registry entry must survive the close of a previous result with
3196        // its name intact (the PHP `$dom->encoding='UTF-16'` crash was the
3197        // registry entry itself being freed by this very close, so the next
3198        // lookup returned freed memory).
3199        let registry2 = find_encoding_handler(name);
3200        assert_eq!(registry2 as *const c_void, registry as *const c_void);
3201        assert!(!unsafe { (*registry2).name }.is_null());
3202        let reg_name = unsafe { CStr::from_ptr((*registry2).name as *const c_char) };
3203        assert_eq!(reg_name.to_bytes(), b"ISO-8859-1");
3204
3205        // A second owned retrieval still works and is usable.
3206        let h2 = xmlFindCharEncodingHandler_owned(name);
3207        assert!(!h2.is_null());
3208        assert_ne!(h2 as *const c_void, registry as *const c_void);
3209        unsafe {
3210            if !(*h2).name.is_null() {
3211                crate::abi::allocator::xmlFreeImpl((*h2).name as *mut c_void);
3212            }
3213            xmlFreeImpl(h2 as *mut c_void);
3214        }
3215    }
3216
3217    /// Phase 14 PHP court regression (UTF-8 subset): retrieval for UTF-8/UTF8
3218    /// returns the persistent static handler, and referencing it from a second
3219    /// caller must yield the same live pointer (the registry entry is never
3220    /// freed by a close — `xmlCharEncCloseFunc` on XML_HANDLER_STATIC is a
3221    /// no-op).
3222    #[test]
3223    fn test_find_owned_utf8_static_and_persistent() {
3224        init_encodings();
3225        let name: *const xmlChar = c"UTF-8".as_ptr() as *const xmlChar;
3226        let u1 = xmlFindCharEncodingHandler_owned(name);
3227        assert!(!u1.is_null());
3228        // Static: close must not release it, so a second find returns the same
3229        // live registry handler.
3230        let u2 = xmlFindCharEncodingHandler_owned(c"utf8".as_ptr() as *const xmlChar);
3231        assert_eq!(u1, u2);
3232        assert_eq!(
3233            unsafe { (*u1).flags } & XML_HANDLER_STATIC,
3234            XML_HANDLER_STATIC
3235        );
3236    }
3237
3238    #[test]
3239    fn test_find_encoding_handler_not_found() {
3240        let name: *const xmlChar = c"NONEXISTENT".as_ptr() as *const xmlChar;
3241        assert!(find_encoding_handler(name).is_null());
3242    }
3243
3244    #[test]
3245    fn test_find_encoding_handler_null() {
3246        assert!(find_encoding_handler(ptr::null()).is_null());
3247    }
3248
3249    /// Verify registering a handler in the global registry and looking it
3250    /// up.
3251    ///
3252    /// # Safety
3253    ///
3254    /// - The `xmlMallocImpl` and `xmlMemStrdupImpl` results are NULL-checked
3255    ///   before `ptr::write` initializes the handler; the handler is removed
3256    ///   from the registry before its allocations are freed exactly once.
3257    #[test]
3258    fn test_add_encoding_handler() {
3259        let handler = unsafe {
3260            xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) as *mut _xmlCharEncodingHandler
3261        };
3262        assert!(!handler.is_null());
3263
3264        let name = unsafe {
3265            crate::abi::allocator::xmlMemStrdupImpl(c"TEST-ENC".as_ptr() as *const c_char)
3266        };
3267        unsafe {
3268            ptr::write(
3269                handler,
3270                _xmlCharEncodingHandler {
3271                    name: name as *mut c_char,
3272                    input: EncodingInputUnion { legacyFunc: None },
3273                    output: EncodingOutputUnion { legacyFunc: None },
3274                    inputCtxt: ptr::null_mut(),
3275                    outputCtxt: ptr::null_mut(),
3276                    ctxtDtor: None,
3277                    flags: 0,
3278                },
3279            );
3280        }
3281
3282        assert_eq!(add_encoding_handler(handler), 0);
3283
3284        let found = find_encoding_handler(c"TEST-ENC".as_ptr() as *const xmlChar);
3285        assert_eq!(found, handler);
3286
3287        // Remove from registry before freeing to avoid dangling pointers
3288        {
3289            let mut handlers = ENCODING_HANDLERS.write();
3290            handlers.retain(|&h| h.0 != handler);
3291        }
3292
3293        unsafe {
3294            xmlFreeImpl(name as *mut c_void);
3295            xmlFreeImpl(handler as *mut c_void);
3296        }
3297    }
3298
3299    // ── Conversion round-trips ─────────────────────────────────────────────
3300
3301    #[test]
3302    fn test_utf16le_roundtrip() {
3303        let original = b"Hello, World! UTF-16LE test: \xC3\xA9\xF0\x9F\x98\x80";
3304        let utf16 = utf8_to_utf16le(original).unwrap();
3305        let back = utf16le_to_utf8(&utf16).unwrap();
3306        assert_eq!(original.to_vec(), back);
3307    }
3308
3309    #[test]
3310    fn test_utf16be_roundtrip() {
3311        let original = b"Hello, World! UTF-16BE test: \xC3\xA9\xF0\x9F\x98\x80";
3312        let utf16le = utf8_to_utf16le(original).unwrap();
3313        // Convert LE to BE by swapping bytes
3314        let mut utf16be = utf16le.clone();
3315        for chunk in utf16be.as_chunks_mut::<2>().0 {
3316            chunk.swap(0, 1);
3317        }
3318        let back = utf16be_to_utf8(&utf16be).unwrap();
3319        assert_eq!(original.to_vec(), back);
3320    }
3321
3322    #[test]
3323    fn test_latin1_roundtrip() {
3324        let original: Vec<u8> = (0x00..=0xFF).collect();
3325        let utf8 = latin1_to_utf8(&original);
3326        let back = utf8_to_latin1(&utf8).unwrap();
3327        assert_eq!(original, back);
3328    }
3329
3330    // ── Built-in handler callbacks ─────────────────────────────────────────
3331
3332    /// Verify the UTF-8 identity callback copies bytes up to the smaller
3333    /// length.
3334    ///
3335    /// # Safety
3336    ///
3337    /// - `output` is a valid mutable 64-byte buffer and `input` a valid byte
3338    ///   slice; the callback writes at most the minimum of the two lengths.
3339    #[test]
3340    fn test_utf8_handler_identity() {
3341        let input = b"Hello, UTF-8!";
3342        let mut output = [0u8; 64];
3343        let mut outlen = output.len() as c_int;
3344        let mut inlen = input.len() as c_int;
3345
3346        let ret = unsafe {
3347            utf8_input_func(output.as_mut_ptr(), &mut outlen, input.as_ptr(), &mut inlen)
3348        };
3349
3350        assert_eq!(ret, input.len() as c_int);
3351        assert_eq!(&output[..ret as usize], input);
3352        assert_eq!(inlen, input.len() as c_int);
3353    }
3354
3355    /// Verify a UTF-16LE output/input callback round-trip.
3356    ///
3357    /// # Safety
3358    ///
3359    /// - The `utf16_buf` and `decoded` arrays are valid buffers of the given
3360    ///   lengths, and the input slices are valid; the callbacks write only
3361    ///   up to the advertised output length.
3362    #[test]
3363    fn test_utf16le_handler_roundtrip() {
3364        init_encodings();
3365
3366        let original = b"Hello UTF-16LE!";
3367        let mut utf16_buf = [0u8; 128];
3368        let mut outlen = utf16_buf.len() as c_int;
3369        let mut inlen = original.len() as c_int;
3370
3371        let written = unsafe {
3372            utf16le_output_func(
3373                utf16_buf.as_mut_ptr(),
3374                &mut outlen,
3375                original.as_ptr(),
3376                &mut inlen,
3377            )
3378        };
3379        assert!(written > 0);
3380
3381        // Now decode back
3382        let mut decoded = [0u8; 128];
3383        let mut outlen2 = decoded.len() as c_int;
3384        let mut inlen2 = written;
3385
3386        let written2 = unsafe {
3387            utf16le_input_func(
3388                decoded.as_mut_ptr(),
3389                &mut outlen2,
3390                utf16_buf.as_ptr(),
3391                &mut inlen2,
3392            )
3393        };
3394        assert_eq!(written2 as usize, original.len());
3395        assert_eq!(&decoded[..written2 as usize], original);
3396    }
3397
3398    // ── xmlBuffer operations ───────────────────────────────────────────────
3399
3400    /// Verify `append_to_xml_buffer` grows the buffer and copies bytes.
3401    ///
3402    /// # Safety
3403    ///
3404    /// - `content` is a valid 64-byte allocation owned by the test and freed
3405    ///   exactly once with `xmlFreeImpl`; `buf` keeps consistent `use_` and
3406    ///   `size` fields while `append_to_xml_buffer` may reallocate `content`.
3407    #[test]
3408    fn test_append_to_xml_buffer() {
3409        unsafe {
3410            let content = xmlMallocImpl(64) as *mut xmlChar;
3411            assert!(!content.is_null());
3412
3413            let mut buf = _xmlBuffer {
3414                content,
3415                use_: 0,
3416                size: 64,
3417                alloc: 0,
3418                contentIO: ptr::null_mut(),
3419            };
3420
3421            append_to_xml_buffer(&mut buf, b"Hello");
3422            assert_eq!(buf.use_, 5);
3423            let slice = core::slice::from_raw_parts(buf.content, 5);
3424            assert_eq!(slice, b"Hello");
3425
3426            append_to_xml_buffer(&mut buf, b" World");
3427            assert_eq!(buf.use_, 11);
3428            let slice = core::slice::from_raw_parts(buf.content, 11);
3429            assert_eq!(slice, b"Hello World");
3430
3431            xmlFreeImpl(buf.content as *mut c_void);
3432        }
3433    }
3434
3435    // ── ABI export functions ───────────────────────────────────────────────
3436
3437    #[test]
3438    fn test_xml_parse_char_encoding() {
3439        let name = c"UTF-8".as_ptr() as *const c_char;
3440        assert_eq!(
3441            xmlParseCharEncoding(name),
3442            xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int
3443        );
3444
3445        let name = c"ISO-8859-1".as_ptr() as *const c_char;
3446        assert_eq!(
3447            xmlParseCharEncoding(name),
3448            xmlCharEncoding::XML_CHAR_ENCODING_8859_1 as c_int
3449        );
3450
3451        assert_eq!(
3452            xmlParseCharEncoding(ptr::null()),
3453            xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int
3454        );
3455    }
3456
3457    /// Verify `xmlNewCharEncodingHandler` and `xmlDelEncodingHandler`
3458    /// round-trip.
3459    ///
3460    /// # Safety
3461    ///
3462    /// - `name` is a valid NUL-terminated string; the returned handler is
3463    ///   non-NULL, its `name` field is a valid NUL-terminated string, and it
3464    ///   is freed exactly once by `xmlDelEncodingHandler`.
3465    #[test]
3466    fn test_xml_new_and_del_encoding_handler() {
3467        let name = c"TestEnc".as_ptr() as *const c_char;
3468        let handler = xmlNewCharEncodingHandler(
3469            name,
3470            utf8_input_func as xmlCharEncodingInputFunc,
3471            utf8_output_func as xmlCharEncodingOutputFunc,
3472        );
3473        assert!(!handler.is_null());
3474
3475        unsafe {
3476            assert!(!(*handler).name.is_null());
3477            let cstr = CStr::from_ptr((*handler).name);
3478            assert_eq!(cstr.to_bytes(), b"TestEnc");
3479        }
3480
3481        xmlDelEncodingHandler(handler);
3482    }
3483
3484    #[test]
3485    fn test_xml_init_and_cleanup() {
3486        xmlInitCharEncodingHandlers();
3487
3488        let name: *const xmlChar = c"UTF-8".as_ptr() as *const xmlChar;
3489        assert!(!find_encoding_handler(name).is_null());
3490
3491        xmlCleanupCharEncodingHandlers();
3492        // After cleanup, handlers should be empty
3493    }
3494
3495    // ── Shift_JIS / EUC-JP (encoding_rs-backed, R-000157 slice) ────────────
3496
3497    /// Drive a legacy func on whole buffers.
3498    fn call_func(
3499        func: unsafe extern "C" fn(*mut c_uchar, *mut c_int, *const c_uchar, *mut c_int) -> c_int,
3500        input: &[u8],
3501    ) -> (c_int, Vec<u8>, usize) {
3502        let mut out = vec![0u8; input.len() * 6 + 64];
3503        let mut outlen = out.len() as c_int;
3504        let mut inlen = input.len() as c_int;
3505        let rc = unsafe { func(out.as_mut_ptr(), &mut outlen, input.as_ptr(), &mut inlen) };
3506        out.truncate(outlen.max(0) as usize);
3507        (rc, out, inlen.max(0) as usize)
3508    }
3509
3510    #[test]
3511    fn test_shift_jis_output_roundtrip() {
3512        // ぁ (U+3041 → 0x82 0x9F), 漢 (U+6F22 → 0x8A 0xBF), half-width ア
3513        // (U+FF71 → 0xB1): byte-exact vs the oracle's iconv output.
3514        let (rc, out, consumed) = call_func(shift_jis_output_func, "ぁ漢ア".as_bytes());
3515        assert!(rc >= 0);
3516        assert_eq!(out, [0x82, 0x9F, 0x8A, 0xBF, 0xB1]);
3517        assert_eq!(consumed, "ぁ漢ア".len());
3518
3519        let (rc, back, _) = call_func(shift_jis_input_func, &out);
3520        assert!(rc >= 0);
3521        assert_eq!(back, "ぁ漢ア".as_bytes());
3522    }
3523
3524    #[test]
3525    fn test_shift_jis_output_unmappable_reports_input_error() {
3526        // U+1F600 is outside Shift_JIS: the func stops BEFORE it with the
3527        // -2 input-error convention (char_enc_out substitutes the decimal
3528        // character reference, exactly like the oracle iconv EILSEQ path).
3529        let (rc, out, consumed) = call_func(shift_jis_output_func, "A😀B".as_bytes());
3530        assert_eq!(rc, ENC_INPUT_ERROR);
3531        assert_eq!(out, b"A");
3532        assert_eq!(consumed, 1); // *inlen points AT the emoji
3533
3534        // Whole-buffer conversion through char_enc_out emits the charref and
3535        // continues: &#128512; (decimal), matching xmlSerializeDecCharRef.
3536        let handler = find_encoding_handler(c"SHIFT_JIS".as_ptr() as *const xmlChar);
3537        assert!(!handler.is_null());
3538        let in_buf = crate::xml::io::buf_create(64);
3539        let src = "A\u{1F600}B".as_bytes();
3540        assert!(
3541            crate::xml::io::buf_add(in_buf, src.as_ptr() as *const xmlChar, src.len() as c_int)
3542                >= 0
3543        );
3544        let out_buf = crate::xml::io::buf_create(64);
3545        let n = char_enc_out(handler, out_buf, in_buf);
3546        assert!(n >= 0);
3547        let bytes =
3548            unsafe { core::slice::from_raw_parts((*out_buf).content, (*out_buf).use_ as usize) };
3549        assert_eq!(bytes, b"A&#128512;B");
3550        crate::xml::io::buf_free(in_buf);
3551        crate::xml::io::buf_free(out_buf);
3552    }
3553
3554    #[test]
3555    fn test_euc_jp_output_roundtrip() {
3556        // ぁ (U+3041 → 0xA4 0xA1), 漢 (U+6F22 → 0xB4 0xC1), ア (U+FF71 →
3557        // 0x8E 0xB1) — oracle iconv byte-exact.
3558        let (rc, out, consumed) = call_func(euc_jp_output_func, "ぁ漢ア".as_bytes());
3559        assert!(rc >= 0);
3560        assert_eq!(out, [0xA4, 0xA1, 0xB4, 0xC1, 0x8E, 0xB1]);
3561        assert_eq!(consumed, "ぁ漢ア".len());
3562
3563        let (rc, back, _) = call_func(euc_jp_input_func, &out);
3564        assert!(rc >= 0);
3565        assert_eq!(back, "ぁ漢ア".as_bytes());
3566    }
3567
3568    #[test]
3569    fn test_east_asian_handlers_registered_and_findable() {
3570        for name in [
3571            c"SHIFT_JIS".as_ptr(),
3572            c"Shift_JIS".as_ptr(),
3573            c"SJIS".as_ptr(),
3574            c"CP932".as_ptr(),
3575            c"EUC-JP".as_ptr(),
3576            c"euc-jp".as_ptr(),
3577        ] {
3578            assert!(
3579                !find_encoding_handler(name as *const xmlChar).is_null(),
3580                "handler not found for {name:?}"
3581            );
3582        }
3583    }
3584
3585    #[test]
3586    fn test_shift_jis_output_invalid_utf8_errors() {
3587        let (rc, out, consumed) = call_func(shift_jis_output_func, b"A\xFFB");
3588        assert_eq!(rc, -1);
3589        assert_eq!(out, b"A");
3590        assert_eq!(consumed, 1);
3591    }
3592}