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];
124const UTF32LE_BOM: [u8; 4] = [0xFF, 0xFE, 0x00, 0x00];
125const UTF32BE_BOM: [u8; 4] = [0x00, 0x00, 0xFE, 0xFF];
126
127// ── Global handler registry ────────────────────────────────────────────────
128
129/// A raw pointer wrapper that implements `Send` and `Sync`.
130///
131/// This is safe because all access to the global handler registry is
132/// serialized through the `RwLock`, and handlers are only accessed from
133/// trusted internal code.
134#[derive(Clone, Copy)]
135struct HandlerPtr(*mut _xmlCharEncodingHandler);
136
137unsafe impl Send for HandlerPtr {}
138unsafe impl Sync for HandlerPtr {}
139
140/// Global list of registered encoding handlers, protected by a read-write lock.
141///
142/// UPSTREAM-PARITY: this is the Rust mirror of encoding.c `globalHandlers`
143/// (the table behind `xmlFindExtraHandler`); `xmlAddCharEncodingHandler`
144/// appends here and lookups scan it after the built-in `defaultHandlers[32]`
145/// set (R-000157: only the native subset is backed by real converters).
146static ENCODING_HANDLERS: Lazy<RwLock<Vec<HandlerPtr>>> = Lazy::new(|| RwLock::new(Vec::new()));
147
148/// Whether the built-in encoding handlers have been initialized.
149static ENCODING_INITIALIZED: AtomicBool = AtomicBool::new(false);
150
151/// Serializes first-time handler registration (see init_encodings).
152static ENCODING_INIT_MUTEX: parking_lot::Mutex<()> = parking_lot::Mutex::new(());
153
154// ═══════════════════════════════════════════════════════════════════════════════
155// 1. Encoding detection
156// ═══════════════════════════════════════════════════════════════════════════════
157
158/// Determine encoding from BOM bytes.
159///
160/// Returns `XML_CHAR_ENCODING_NONE` if no BOM is present, or if `data` is empty.
161/// Otherwise returns the matching encoding enum value.
162#[allow(dead_code)]
163pub(crate) fn detect_encoding_from_bom(data: &[u8]) -> xmlCharEncoding {
164    if data.len() >= 3 && data[0..3] == UTF8_BOM {
165        xmlCharEncoding::XML_CHAR_ENCODING_UTF8
166    } else if data.len() >= 2 && data[0..2] == UTF16LE_BOM {
167        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
168    } else if data.len() >= 2 && data[0..2] == UTF16BE_BOM {
169        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE
170    } else {
171        xmlCharEncoding::XML_CHAR_ENCODING_NONE
172    }
173}
174
175/// Determine encoding from an XML declaration's `encoding` attribute.
176///
177/// Scans for `<?xml ... encoding="..." ?>` and returns the encoding name
178/// as a byte vector (lowercased), or `None` if not found.
179#[allow(dead_code)]
180pub(crate) fn detect_encoding_from_declaration(data: &[u8]) -> Option<Vec<u8>> {
181    // Look for "<?xml" at the start (possibly after BOM)
182    let start = if data.len() >= 3 && data[0..3] == UTF8_BOM {
183        3
184    } else if data.len() >= 2 && (data[0..2] == UTF16LE_BOM || data[0..2] == UTF16BE_BOM) {
185        // For UTF-16, we can't easily scan the bytes; skip
186        return None;
187    } else {
188        0
189    };
190
191    let remaining = &data[start..];
192
193    // Must start with "<?xml"
194    if remaining.len() < 5 || !remaining[0..5].eq_ignore_ascii_case(b"<?xml") {
195        return None;
196    }
197
198    // Find the end of the PI: "?>"
199    let pi_end = remaining.windows(2).position(|w| w == b"?>")?;
200    let decl_content = &remaining[5..pi_end];
201
202    // Look for "encoding" attribute
203    let decl_str = core::str::from_utf8(decl_content).ok()?;
204    let lower = decl_str.to_ascii_lowercase();
205
206    // Find "encoding" keyword
207    let enc_pos = lower.find("encoding")?;
208
209    // After "encoding", expect optional whitespace and '='
210    let after_enc = &decl_content[enc_pos + 8..];
211    let after_enc_str = core::str::from_utf8(after_enc).ok()?;
212    let after_enc_trimmed = after_enc_str.trim_start();
213
214    if !after_enc_trimmed.starts_with('=') {
215        return None;
216    }
217
218    let after_eq = after_enc_trimmed[1..].trim_start();
219
220    // Expect quote character
221    let quote = after_eq.chars().next()?;
222    if quote != '"' && quote != '\'' {
223        return None;
224    }
225
226    // Find matching closing quote
227    let value_end = after_eq[1..].find(quote)?;
228    let encoding_value = &after_eq[1..=value_end];
229
230    Some(encoding_value.to_ascii_lowercase().as_bytes().to_vec())
231}
232
233/// Parse an encoding name string to an `xmlCharEncoding` enum value.
234///
235/// Matching is case-insensitive. Common aliases are recognized.
236/// Returns `XML_CHAR_ENCODING_ERROR` if the name is not recognized.
237pub(crate) fn encoding_from_name(name: &[u8]) -> xmlCharEncoding {
238    let s = core::str::from_utf8(name).unwrap_or("");
239    let s = s.trim().to_ascii_lowercase();
240
241    match s.as_str() {
242        // UTF-8
243        "utf-8" | "utf8" => xmlCharEncoding::XML_CHAR_ENCODING_UTF8,
244
245        // UTF-16
246        "utf-16" | "utf-16le" | "utf16le" => xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
247        "utf-16be" | "utf16be" => xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE,
248
249        // UTF-32/UCS-4 (upstream resolves these through its iconv/ICU fallback;
250        // the candidate maps them to the native UCS-4 handlers, R-000157)
251        "utf-32" | "utf-32le" | "utf32le" => xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE,
252        "utf-32be" | "utf32be" => xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE,
253
254        // ISO-8859 variants
255        "iso-8859-1" | "iso_8859-1" | "latin1" | "latin-1" | "l1" | "cp819" | "ibm819"
256        | "iso-ir-100" | "iso_8859-1:1987" => xmlCharEncoding::XML_CHAR_ENCODING_8859_1,
257        "iso-8859-2" | "iso_8859-2" | "latin2" | "latin-2" | "l2" => {
258            xmlCharEncoding::XML_CHAR_ENCODING_8859_2
259        }
260        "iso-8859-3" | "iso_8859-3" | "latin3" | "latin-3" | "l3" => {
261            xmlCharEncoding::XML_CHAR_ENCODING_8859_3
262        }
263        "iso-8859-4" | "iso_8859-4" | "latin4" | "latin-4" | "l4" => {
264            xmlCharEncoding::XML_CHAR_ENCODING_8859_4
265        }
266        "iso-8859-5" | "iso_8859-5" | "cyrillic" => xmlCharEncoding::XML_CHAR_ENCODING_8859_5,
267        "iso-8859-6" | "iso_8859-6" | "arabic" => xmlCharEncoding::XML_CHAR_ENCODING_8859_6,
268        "iso-8859-7" | "iso_8859-7" | "greek" => xmlCharEncoding::XML_CHAR_ENCODING_8859_7,
269        "iso-8859-8" | "iso_8859-8" | "hebrew" => xmlCharEncoding::XML_CHAR_ENCODING_8859_8,
270        "iso-8859-9" | "iso_8859-9" | "latin5" | "latin-5" | "l5" | "turkish" => {
271            xmlCharEncoding::XML_CHAR_ENCODING_8859_9
272        }
273
274        // ASCII
275        "ascii" | "us-ascii" | "us" | "ansi_x3.4-1968" | "ansi_x3.4-1986" | "iso-ir-6"
276        | "iso_646.irv:1991" | "cp367" | "ibm367" => xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
277
278        // East Asian
279        "iso-2022-jp" | "iso2022-jp" => xmlCharEncoding::XML_CHAR_ENCODING_2022_JP,
280        "shift_jis" | "shift-jis" | "sjis" | "cp932" => {
281            xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS
282        }
283        "euc-jp" | "eucjp" => xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP,
284
285        // UCS/Unicode variants
286        "ucs-4" | "ucs4" => xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE,
287        "ucs-4le" | "ucs4le" => xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE,
288        "ucs-4be" | "ucs4be" => xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE,
289        "ucs-2" | "ucs2" => xmlCharEncoding::XML_CHAR_ENCODING_UCS2,
290
291        // EBCDIC
292        "ebcdic" | "cp037" | "ibm037" => xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC,
293
294        _ => xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
295    }
296}
297
298/// Get the canonical name for an encoding as a byte slice.
299///
300/// Returns `None` for `XML_CHAR_ENCODING_ERROR` and `XML_CHAR_ENCODING_NONE`.
301pub(crate) const fn encoding_name(enc: xmlCharEncoding) -> Option<&'static [u8]> {
302    match enc {
303        xmlCharEncoding::XML_CHAR_ENCODING_UTF8 => Some(b"UTF-8" as &[u8]),
304        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE => Some(b"UTF-16LE" as &[u8]),
305        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE => Some(b"UTF-16BE" as &[u8]),
306        xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE => Some(b"UCS-4LE" as &[u8]),
307        xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE => Some(b"UCS-4BE" as &[u8]),
308        xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC => Some(b"EBCDIC" as &[u8]),
309        xmlCharEncoding::XML_CHAR_ENCODING_UCS4_2143 => Some(b"UCS-4-2143" as &[u8]),
310        xmlCharEncoding::XML_CHAR_ENCODING_UCS4_3412 => Some(b"UCS-4-3412" as &[u8]),
311        xmlCharEncoding::XML_CHAR_ENCODING_UCS2 => Some(b"UCS-2" as &[u8]),
312        xmlCharEncoding::XML_CHAR_ENCODING_8859_1 => Some(b"ISO-8859-1" as &[u8]),
313        xmlCharEncoding::XML_CHAR_ENCODING_8859_2 => Some(b"ISO-8859-2" as &[u8]),
314        xmlCharEncoding::XML_CHAR_ENCODING_8859_3 => Some(b"ISO-8859-3" as &[u8]),
315        xmlCharEncoding::XML_CHAR_ENCODING_8859_4 => Some(b"ISO-8859-4" as &[u8]),
316        xmlCharEncoding::XML_CHAR_ENCODING_8859_5 => Some(b"ISO-8859-5" as &[u8]),
317        xmlCharEncoding::XML_CHAR_ENCODING_8859_6 => Some(b"ISO-8859-6" as &[u8]),
318        xmlCharEncoding::XML_CHAR_ENCODING_8859_7 => Some(b"ISO-8859-7" as &[u8]),
319        xmlCharEncoding::XML_CHAR_ENCODING_8859_8 => Some(b"ISO-8859-8" as &[u8]),
320        xmlCharEncoding::XML_CHAR_ENCODING_8859_9 => Some(b"ISO-8859-9" as &[u8]),
321        xmlCharEncoding::XML_CHAR_ENCODING_2022_JP => Some(b"ISO-2022-JP" as &[u8]),
322        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS => Some(b"SHIFT_JIS" as &[u8]),
323        xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP => Some(b"EUC-JP" as &[u8]),
324        xmlCharEncoding::XML_CHAR_ENCODING_ASCII => Some(b"US-ASCII" as &[u8]),
325        _ => None,
326    }
327}
328
329// ═══════════════════════════════════════════════════════════════════════════════
330// 2. UTF-8 validation
331// ═══════════════════════════════════════════════════════════════════════════════
332
333/// Check if a byte sequence is valid UTF-8.
334///
335/// Returns `true` if the entire slice is valid UTF-8, `false` otherwise.
336#[allow(dead_code)]
337pub(crate) const fn utf8_valid(data: &[u8]) -> bool {
338    core::str::from_utf8(data).is_ok()
339}
340
341/// Check if a Unicode codepoint is a valid XML character.
342///
343/// Per XML 1.0 (Fifth Edition) §2.2, the valid character ranges are:
344/// - `#x9` (tab)
345/// - `#xA` (LF)
346/// - `#xD` (CR)
347/// - `#x20` – `#xD7FF`
348/// - `#xE000` – `#xFFFD`
349/// - `#x10000` – `#x10FFFF`
350///
351/// Excludes surrogate halves (`#xD800` – `#xDFFF`) and `#xFFFE`/`#xFFFF`.
352#[allow(dead_code)]
353pub(crate) const fn is_valid_xml_char(cp: u32) -> bool {
354    matches!(
355        cp,
356        0x9 | 0xA | 0xD | 0x20..=0xD7FF | 0xE000..=0xFFFD | 0x10000..=0x10FFFF
357    )
358}
359
360// ═══════════════════════════════════════════════════════════════════════════════
361// 3. UTF-16 handling
362// ═══════════════════════════════════════════════════════════════════════════════
363
364/// Decode a single UTF-16LE code unit from two bytes.
365#[inline]
366const fn read_utf16le_unit(data: &[u8]) -> Option<u16> {
367    if data.len() < 2 {
368        return None;
369    }
370    Some(u16::from_le_bytes([data[0], data[1]]))
371}
372
373/// Decode a single UTF-16BE code unit from two bytes.
374#[inline]
375const fn read_utf16be_unit(data: &[u8]) -> Option<u16> {
376    if data.len() < 2 {
377        return None;
378    }
379    Some(u16::from_be_bytes([data[0], data[1]]))
380}
381
382/// Encode a Unicode codepoint as UTF-8 bytes.
383///
384/// Returns the number of bytes written (1–4), or 0 if the codepoint is invalid.
385const fn encode_codepoint_to_utf8(cp: u32, out: &mut [u8]) -> usize {
386    if cp < 0x80 {
387        if !out.is_empty() {
388            out[0] = cp as u8;
389        }
390        1
391    } else if cp < 0x800 {
392        if out.len() < 2 {
393            return 0;
394        }
395        out[0] = 0xC0 | ((cp >> 6) as u8);
396        out[1] = 0x80 | (cp as u8 & 0x3F);
397        2
398    } else if cp < 0x10000 {
399        if out.len() < 3 {
400            return 0;
401        }
402        out[0] = 0xE0 | ((cp >> 12) as u8);
403        out[1] = 0x80 | ((cp >> 6) as u8 & 0x3F);
404        out[2] = 0x80 | (cp as u8 & 0x3F);
405        3
406    } else if cp < 0x110000 {
407        if out.len() < 4 {
408            return 0;
409        }
410        out[0] = 0xF0 | ((cp >> 18) as u8);
411        out[1] = 0x80 | ((cp >> 12) as u8 & 0x3F);
412        out[2] = 0x80 | ((cp >> 6) as u8 & 0x3F);
413        out[3] = 0x80 | (cp as u8 & 0x3F);
414        4
415    } else {
416        0
417    }
418}
419
420/// Convert UTF-16LE bytes to UTF-8.
421///
422/// Returns `Ok(converted_bytes)` on success, or `Err(())` on invalid input
423/// (e.g., unpaired surrogates, truncated data).
424pub(crate) fn utf16le_to_utf8(data: &[u8]) -> Result<Vec<u8>, ()> {
425    if data.is_empty() {
426        return Ok(Vec::new());
427    }
428
429    // Skip BOM if present
430    let offset = if data.len() >= 2 && data[0..2] == UTF16LE_BOM {
431        2
432    } else {
433        0
434    };
435
436    let mut result = Vec::with_capacity(data.len() / 2 + data.len() / 4);
437    let mut i = offset;
438
439    while i < data.len() {
440        let unit = read_utf16le_unit(&data[i..]).ok_or(())?;
441        i += 2;
442
443        if (0xD800..=0xDBFF).contains(&unit) {
444            // High surrogate: expect a low surrogate
445            let low = read_utf16le_unit(&data[i..]).ok_or(())?;
446            i += 2;
447
448            if !(0xDC00..=0xDFFF).contains(&low) {
449                return Err(());
450            }
451
452            let cp = 0x10000 + ((unit as u32 - 0xD800) << 10) + (low as u32 - 0xDC00);
453            let mut buf = [0u8; 4];
454            let n = encode_codepoint_to_utf8(cp, &mut buf);
455            if n == 0 {
456                return Err(());
457            }
458            result.extend_from_slice(&buf[..n]);
459        } else if (0xDC00..=0xDFFF).contains(&unit) {
460            // Unexpected low surrogate
461            return Err(());
462        } else {
463            let cp = unit as u32;
464            let mut buf = [0u8; 4];
465            let n = encode_codepoint_to_utf8(cp, &mut buf);
466            result.extend_from_slice(&buf[..n]);
467        }
468    }
469
470    Ok(result)
471}
472
473/// Convert UTF-16BE bytes to UTF-8.
474///
475/// Returns `Ok(converted_bytes)` on success, or `Err(())` on invalid input.
476pub(crate) fn utf16be_to_utf8(data: &[u8]) -> Result<Vec<u8>, ()> {
477    if data.is_empty() {
478        return Ok(Vec::new());
479    }
480
481    // Skip BOM if present
482    let offset = if data.len() >= 2 && data[0..2] == UTF16BE_BOM {
483        2
484    } else {
485        0
486    };
487
488    let mut result = Vec::with_capacity(data.len() / 2 + data.len() / 4);
489    let mut i = offset;
490
491    while i < data.len() {
492        let unit = read_utf16be_unit(&data[i..]).ok_or(())?;
493        i += 2;
494
495        if (0xD800..=0xDBFF).contains(&unit) {
496            // High surrogate: expect a low surrogate
497            let low = read_utf16be_unit(&data[i..]).ok_or(())?;
498            i += 2;
499
500            if !(0xDC00..=0xDFFF).contains(&low) {
501                return Err(());
502            }
503
504            let cp = 0x10000 + ((unit as u32 - 0xD800) << 10) + (low as u32 - 0xDC00);
505            let mut buf = [0u8; 4];
506            let n = encode_codepoint_to_utf8(cp, &mut buf);
507            if n == 0 {
508                return Err(());
509            }
510            result.extend_from_slice(&buf[..n]);
511        } else if (0xDC00..=0xDFFF).contains(&unit) {
512            // Unexpected low surrogate
513            return Err(());
514        } else {
515            let cp = unit as u32;
516            let mut buf = [0u8; 4];
517            let n = encode_codepoint_to_utf8(cp, &mut buf);
518            result.extend_from_slice(&buf[..n]);
519        }
520    }
521
522    Ok(result)
523}
524
525/// Convert UCS-4/UTF-32LE bytes (4-byte little-endian code points) to UTF-8.
526///
527/// Returns `Ok(converted_bytes)` on success, or `Err(())` on invalid input
528/// (truncated unit, surrogate, or out-of-range code point). A UTF-32LE BOM is
529/// skipped when present.
530pub(crate) fn ucs4le_to_utf8(data: &[u8]) -> Result<Vec<u8>, ()> {
531    fixed_width_to_utf8(data, true, UTF32LE_BOM)
532}
533
534/// Convert UCS-4/UTF-32BE bytes (4-byte big-endian code points) to UTF-8.
535///
536/// Returns `Ok(converted_bytes)` on success, or `Err(())` on invalid input
537/// (truncated unit, surrogate, or out-of-range code point). A UTF-32BE BOM is
538/// skipped when present.
539pub(crate) fn ucs4be_to_utf8(data: &[u8]) -> Result<Vec<u8>, ()> {
540    fixed_width_to_utf8(data, false, UTF32BE_BOM)
541}
542
543/// Shared 4-byte-unit-to-UTF-8 conversion (UCS-4/UTF-32 LE and BE).
544fn fixed_width_to_utf8(data: &[u8], little_endian: bool, bom: [u8; 4]) -> Result<Vec<u8>, ()> {
545    if data.is_empty() {
546        return Ok(Vec::new());
547    }
548    let offset = if data.len() >= 4 && data[0..4] == bom {
549        4
550    } else {
551        0
552    };
553    let mut result = Vec::with_capacity(data.len() / 2);
554    let mut i = offset;
555    while i + 4 <= data.len() {
556        let raw = [data[i], data[i + 1], data[i + 2], data[i + 3]];
557        i += 4;
558        let cp = if little_endian {
559            u32::from_le_bytes(raw)
560        } else {
561            u32::from_be_bytes(raw)
562        };
563        // Surrogates and out-of-range code points are invalid in UCS-4 text
564        // (upstream fixed_width_input rejects them the same way).
565        if cp > 0x10FFFF || (0xD800..=0xDFFF).contains(&cp) {
566            return Err(());
567        }
568        let mut buf = [0u8; 4];
569        let n = encode_codepoint_to_utf8(cp, &mut buf);
570        if n == 0 {
571            return Err(());
572        }
573        result.extend_from_slice(&buf[..n]);
574    }
575    // Trailing partial unit: invalid.
576    if i != data.len() {
577        return Err(());
578    }
579    Ok(result)
580}
581
582/// Encode a Unicode codepoint as UTF-16LE bytes.
583///
584/// Returns the number of bytes written (2 or 4), or 0 if the codepoint is invalid.
585fn encode_codepoint_to_utf16le(cp: u32, out: &mut [u8]) -> usize {
586    if cp < 0x10000 {
587        if out.len() < 2 {
588            return 0;
589        }
590        let u = cp as u16;
591        out[..2].copy_from_slice(&u.to_le_bytes());
592        2
593    } else if cp < 0x110000 {
594        if out.len() < 4 {
595            return 0;
596        }
597        let cp = cp - 0x10000;
598        let high = 0xD800 | ((cp >> 10) as u16);
599        let low = 0xDC00 | (cp as u16 & 0x3FF);
600        out[..2].copy_from_slice(&high.to_le_bytes());
601        out[2..4].copy_from_slice(&low.to_le_bytes());
602        4
603    } else {
604        0
605    }
606}
607
608/// Convert UTF-8 bytes to UTF-16LE.
609///
610/// Returns `Ok(converted_bytes)` on success, or `Err(())` on invalid UTF-8 input.
611pub(crate) fn utf8_to_utf16le(data: &[u8]) -> Result<Vec<u8>, ()> {
612    let s = core::str::from_utf8(data).map_err(|_| ())?;
613    let mut result = Vec::with_capacity(data.len() * 2);
614
615    for ch in s.chars() {
616        let cp = ch as u32;
617        let mut buf = [0u8; 4];
618        let n = encode_codepoint_to_utf16le(cp, &mut buf);
619        if n == 0 {
620            return Err(());
621        }
622        result.extend_from_slice(&buf[..n]);
623    }
624
625    Ok(result)
626}
627
628// ═══════════════════════════════════════════════════════════════════════════════
629// 4. ISO-8859-1 (Latin-1) handling
630// ═══════════════════════════════════════════════════════════════════════════════
631
632/// Convert Latin-1 (ISO-8859-1) bytes to UTF-8.
633///
634/// Latin-1 maps codepoints 0x00–0xFF directly to Unicode codepoints U+0000–U+00FF.
635/// Each input byte produces either 1 or 2 UTF-8 bytes.
636#[allow(dead_code)]
637pub(crate) fn latin1_to_utf8(data: &[u8]) -> Vec<u8> {
638    let mut result = Vec::with_capacity(data.len() * 2);
639
640    for &byte in data {
641        let cp = byte as u32;
642        let mut buf = [0u8; 2];
643        let n = encode_codepoint_to_utf8(cp, &mut buf);
644        result.extend_from_slice(&buf[..n]);
645    }
646
647    result
648}
649
650/// Convert UTF-8 bytes to Latin-1 (ISO-8859-1).
651///
652/// Returns `Err(())` if the input is not valid UTF-8 or contains codepoints
653/// outside the Latin-1 range (U+0000–U+00FF).
654pub(crate) fn utf8_to_latin1(data: &[u8]) -> Result<Vec<u8>, ()> {
655    let s = core::str::from_utf8(data).map_err(|_| ())?;
656    let mut result = Vec::with_capacity(data.len());
657
658    for ch in s.chars() {
659        let cp = ch as u32;
660        if cp > 0xFF {
661            return Err(());
662        }
663        result.push(cp as u8);
664    }
665
666    Ok(result)
667}
668
669// ═══════════════════════════════════════════════════════════════════════════════
670// 5. Encoding handler registry
671// ═══════════════════════════════════════════════════════════════════════════════
672
673/// Initialize the built-in encoding handlers.
674///
675/// This function registers handlers for:
676/// - UTF-8 (identity/no conversion)
677/// - UTF-16LE
678/// - UTF-16BE
679/// - ISO-8859-1 (Latin-1)
680/// - ASCII
681///
682/// Safe to call multiple times — only the first call has an effect.
683pub(crate) fn init_encodings() {
684    if ENCODING_INITIALIZED.load(Ordering::SeqCst) {
685        return;
686    }
687    // Serialize first-time registration: without the mutex, a second thread
688    // can observe ENCODING_INITIALIZED == true and look up handlers while
689    // the first thread is still registering them (race found by the parallel
690    // test suite: xml::io test_output_buffer_with_encoding intermittently
691    // failed to find the Latin-1 handler).
692    let _guard = ENCODING_INIT_MUTEX.lock();
693    if ENCODING_INITIALIZED.load(Ordering::SeqCst) {
694        return;
695    }
696    register_builtin_handlers();
697    ENCODING_INITIALIZED.store(true, Ordering::SeqCst);
698}
699
700/// Register all built-in encoding handlers.
701fn register_builtin_handlers() {
702    // UTF-8 (identity handler — no conversion needed)
703    register_handler(
704        b"UTF-8\0",
705        xmlCharEncoding::XML_CHAR_ENCODING_UTF8,
706        xmlCharEncoding::XML_CHAR_ENCODING_UTF8,
707        Some(utf8_input_func as xmlCharEncodingInputFunc),
708        Some(utf8_output_func as xmlCharEncodingOutputFunc),
709    );
710
711    // UTF-16LE
712    register_handler(
713        b"UTF-16LE\0",
714        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
715        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
716        Some(utf16le_input_func as xmlCharEncodingInputFunc),
717        Some(utf16le_output_func as xmlCharEncodingOutputFunc),
718    );
719
720    // UTF-16BE
721    register_handler(
722        b"UTF-16BE\0",
723        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE,
724        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE,
725        Some(utf16be_input_func as xmlCharEncodingInputFunc),
726        Some(utf16be_output_func as xmlCharEncodingOutputFunc),
727    );
728
729    // UCS-4LE (UTF-32LE) — R-000157: the oracle serves these through its
730    // iconv/ICU fallback; the candidate's native 4-byte converters make the
731    // handlers registrable (xmlFindCharEncodingHandler / xmlSwitchEncoding /
732    // the XML input layer's whole-buffer decode all resolve through this
733    // registry).
734    register_handler(
735        b"UCS-4LE\0",
736        xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE,
737        xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE,
738        Some(ucs4le_input_func as xmlCharEncodingInputFunc),
739        Some(ucs4le_output_func as xmlCharEncodingOutputFunc),
740    );
741
742    // UCS-4BE (UTF-32BE)
743    register_handler(
744        b"UCS-4BE\0",
745        xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE,
746        xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE,
747        Some(ucs4be_input_func as xmlCharEncodingInputFunc),
748        Some(ucs4be_output_func as xmlCharEncodingOutputFunc),
749    );
750
751    // ISO-8859-1 (Latin-1)
752    register_handler(
753        b"ISO-8859-1\0",
754        xmlCharEncoding::XML_CHAR_ENCODING_8859_1,
755        xmlCharEncoding::XML_CHAR_ENCODING_8859_1,
756        Some(latin1_input_func as xmlCharEncodingInputFunc),
757        Some(latin1_output_func as xmlCharEncodingOutputFunc),
758    );
759
760    // Windows-1252 (CP1252): served by iconv on the oracle; native converter
761    // here (R-000157 partial closure). Registered under both the canonical
762    // spelling and the cp1252 alias; lookup is case-insensitive so
763    // "Windows-1252" (the Dom\XMLDocument overrideEncoding the PHP court
764    // passes verbatim) resolves to the same entry.
765    register_handler(
766        b"windows-1252\0",
767        xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
768        xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
769        Some(cp1252_input_func as xmlCharEncodingInputFunc),
770        Some(cp1252_output_func as xmlCharEncodingOutputFunc),
771    );
772    register_handler(
773        b"cp1252\0",
774        xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
775        xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
776        Some(cp1252_input_func as xmlCharEncodingInputFunc),
777        Some(cp1252_output_func as xmlCharEncodingOutputFunc),
778    );
779
780    // ASCII — upstream's static default handler (defaultHandlers[22]) is named
781    // "US-ASCII"; the name "ASCII" is registered as a second entry so name-based
782    // lookups (xmlFindCharEncodingHandler, the saver path) accept both spellings
783    // exactly like upstream's xmlParseCharEncodingInternal mapping.
784    register_handler(
785        b"US-ASCII\0",
786        xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
787        xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
788        Some(ascii_input_func as xmlCharEncodingInputFunc),
789        Some(ascii_output_func as xmlCharEncodingOutputFunc),
790    );
791    register_handler(
792        b"ASCII\0",
793        xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
794        xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
795        Some(ascii_input_func as xmlCharEncodingInputFunc),
796        Some(ascii_output_func as xmlCharEncodingOutputFunc),
797    );
798
799    // UTF-16 (default handler for enc == XML_CHAR_ENCODING_UTF16 == 23): the
800    // upstream converter is UTF16LEToUTF8/UTF8ToUTF16 (the latter emits the LE
801    // BOM on its init call). Our converter pair is the UTF-16LE pair; the BOM
802    // init protocol is not emitted (documented divergence, conversion only).
803    register_handler(
804        b"UTF-16\0",
805        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
806        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
807        Some(utf16le_input_func as xmlCharEncodingInputFunc),
808        Some(utf16le_output_func as xmlCharEncodingOutputFunc),
809    );
810
811    // Shift_JIS + EUC-JP — encoding_rs-backed converters (R-000157 closure
812    // slice, Phase 14.27). Upstream serves these through iconv on the
813    // executed oracle (2.15.3, Iconv+ICU); the crate ships no iconv/ICU
814    // backend, so the converters are implemented natively over WHATWG
815    // Shift_JIS (a CP932-compatible superset) / EUC-JP, which byte-match
816    // glibc iconv on the shared JIS X 0208 repertoire the php suite and
817    // byte-parity probes exercise (the WHATWG/CP932 extension differences
818    // are residualized in R-000157). Registered under the canonical
819    // spellings + the aliases upstream's name path accepts (registry lookup
820    // is case-insensitive).
821    register_handler(
822        b"SHIFT_JIS\0",
823        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS,
824        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS,
825        Some(shift_jis_input_func as xmlCharEncodingInputFunc),
826        Some(shift_jis_output_func as xmlCharEncodingOutputFunc),
827    );
828    register_handler(
829        b"SJIS\0",
830        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS,
831        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS,
832        Some(shift_jis_input_func as xmlCharEncodingInputFunc),
833        Some(shift_jis_output_func as xmlCharEncodingOutputFunc),
834    );
835    register_handler(
836        b"CP932\0",
837        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS,
838        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS,
839        Some(shift_jis_input_func as xmlCharEncodingInputFunc),
840        Some(shift_jis_output_func as xmlCharEncodingOutputFunc),
841    );
842    register_handler(
843        b"EUC-JP\0",
844        xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP,
845        xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP,
846        Some(euc_jp_input_func as xmlCharEncodingInputFunc),
847        Some(euc_jp_output_func as xmlCharEncodingOutputFunc),
848    );
849    register_handler(
850        b"EUCJP\0",
851        xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP,
852        xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP,
853        Some(euc_jp_input_func as xmlCharEncodingInputFunc),
854        Some(euc_jp_output_func as xmlCharEncodingOutputFunc),
855    );
856
857    // ISO-8859-2..16 (R-000157 remainder, Phase 14.29): encoding_rs-backed
858    // single-byte converters (upstream serves these via iconv on the
859    // executed oracle). ISO-8859-11 == TIS-620 == the WHATWG windows-874
860    // single-byte set on the shared repertoire; both spellings are
861    // registered. The canonical names are the registered keys; lookups are
862    // case-insensitive.
863    macro_rules! register_iso8859 {
864        ($name:literal, $enc:expr, $input:ident, $output:ident) => {
865            register_handler(
866                concat!($name, "\0").as_bytes(),
867                xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
868                xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
869                Some($input as xmlCharEncodingInputFunc),
870                Some($output as xmlCharEncodingOutputFunc),
871            );
872            let _ = $enc; // (encoding identity carried by the func pair)
873        };
874    }
875    register_iso8859!(
876        "ISO-8859-2",
877        encoding_rs::ISO_8859_2,
878        iso_8859_2_input_func,
879        iso_8859_2_output_func
880    );
881    register_iso8859!(
882        "ISO-8859-3",
883        encoding_rs::ISO_8859_3,
884        iso_8859_3_input_func,
885        iso_8859_3_output_func
886    );
887    register_iso8859!(
888        "ISO-8859-4",
889        encoding_rs::ISO_8859_4,
890        iso_8859_4_input_func,
891        iso_8859_4_output_func
892    );
893    register_iso8859!(
894        "ISO-8859-5",
895        encoding_rs::ISO_8859_5,
896        iso_8859_5_input_func,
897        iso_8859_5_output_func
898    );
899    register_iso8859!(
900        "ISO-8859-6",
901        encoding_rs::ISO_8859_6,
902        iso_8859_6_input_func,
903        iso_8859_6_output_func
904    );
905    register_iso8859!(
906        "ISO-8859-7",
907        encoding_rs::ISO_8859_7,
908        iso_8859_7_input_func,
909        iso_8859_7_output_func
910    );
911    register_iso8859!(
912        "ISO-8859-8",
913        encoding_rs::ISO_8859_8,
914        iso_8859_8_input_func,
915        iso_8859_8_output_func
916    );
917    register_iso8859!(
918        "ISO-8859-9",
919        encoding_rs::WINDOWS_1254,
920        iso_8859_9_input_func,
921        iso_8859_9_output_func
922    );
923    register_iso8859!(
924        "ISO-8859-10",
925        encoding_rs::ISO_8859_10,
926        iso_8859_10_input_func,
927        iso_8859_10_output_func
928    );
929    register_iso8859!(
930        "ISO-8859-11",
931        encoding_rs::WINDOWS_874,
932        iso_8859_11_input_func,
933        iso_8859_11_output_func
934    );
935    register_iso8859!(
936        "windows-874",
937        encoding_rs::WINDOWS_874,
938        iso_8859_11_input_func,
939        iso_8859_11_output_func
940    );
941    register_iso8859!(
942        "ISO-8859-13",
943        encoding_rs::ISO_8859_13,
944        iso_8859_13_input_func,
945        iso_8859_13_output_func
946    );
947    register_iso8859!(
948        "ISO-8859-14",
949        encoding_rs::ISO_8859_14,
950        iso_8859_14_input_func,
951        iso_8859_14_output_func
952    );
953    register_iso8859!(
954        "ISO-8859-15",
955        encoding_rs::ISO_8859_15,
956        iso_8859_15_input_func,
957        iso_8859_15_output_func
958    );
959    register_iso8859!(
960        "ISO-8859-16",
961        encoding_rs::ISO_8859_16,
962        iso_8859_16_input_func,
963        iso_8859_16_output_func
964    );
965
966    // ISO-2022-JP (stateful escape-sequence encoding; encoding_rs keeps the
967    // JIS X 0208 / ASCII escape state inside each conversion call).
968    register_handler(
969        b"ISO-2022-JP\0",
970        xmlCharEncoding::XML_CHAR_ENCODING_2022_JP,
971        xmlCharEncoding::XML_CHAR_ENCODING_2022_JP,
972        Some(iso_2022_jp_input_func as xmlCharEncodingInputFunc),
973        Some(iso_2022_jp_output_func as xmlCharEncodingOutputFunc),
974    );
975
976    // UCS-2 (2-byte big-endian units; the glibc iconv "UCS-2" the oracle
977    // serves) and UCS-4LE/BE (4-byte units). Native fixed-width codecs.
978    register_handler(
979        b"UCS-2\0",
980        xmlCharEncoding::XML_CHAR_ENCODING_UCS2,
981        xmlCharEncoding::XML_CHAR_ENCODING_UCS2,
982        Some(ucs2_input_func as xmlCharEncodingInputFunc),
983        Some(ucs2_output_func as xmlCharEncodingOutputFunc),
984    );
985    register_handler(
986        b"UCS-4LE\0",
987        xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE,
988        xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE,
989        Some(ucs4le_input_func as xmlCharEncodingInputFunc),
990        Some(ucs4le_output_func as xmlCharEncodingOutputFunc),
991    );
992    register_handler(
993        b"UCS-4BE\0",
994        xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE,
995        xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE,
996        Some(ucs4be_input_func as xmlCharEncodingInputFunc),
997        Some(ucs4be_output_func as xmlCharEncodingOutputFunc),
998    );
999    register_handler(
1000        b"UCS-4\0",
1001        xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE,
1002        xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE,
1003        Some(ucs4le_input_func as xmlCharEncodingInputFunc),
1004        Some(ucs4le_output_func as xmlCharEncodingOutputFunc),
1005    );
1006
1007    // EBCDIC code page 037 (the glibc iconv "IBM037"/"EBCDIC-US" the
1008    // oracle serves). Native 037 table.
1009    register_handler(
1010        b"IBM037\0",
1011        xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC,
1012        xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC,
1013        Some(ebcdic_input_func as xmlCharEncodingInputFunc),
1014        Some(ebcdic_output_func as xmlCharEncodingOutputFunc),
1015    );
1016    register_handler(
1017        b"EBCDIC-US\0",
1018        xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC,
1019        xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC,
1020        Some(ebcdic_input_func as xmlCharEncodingInputFunc),
1021        Some(ebcdic_output_func as xmlCharEncodingOutputFunc),
1022    );
1023    register_handler(
1024        b"EBCDIC\0",
1025        xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC,
1026        xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC,
1027        Some(ebcdic_input_func as xmlCharEncodingInputFunc),
1028        Some(ebcdic_output_func as xmlCharEncodingOutputFunc),
1029    );
1030}
1031
1032/// Helper to create and register an encoding handler.
1033///
1034/// # Safety
1035///
1036/// - `name_bytes` must be a valid byte slice containing a NUL terminator;
1037///   `xmlMemStrdupImpl` scans it as a C string.
1038/// - The `xmlMallocImpl` result is NULL-checked before `ptr::write`
1039///   initializes the handler; the written handler is inserted into the
1040///   global registry, which keeps it alive for the process lifetime.
1041fn register_handler(
1042    name_bytes: &[u8],
1043    _input_enc: xmlCharEncoding,
1044    _output_enc: xmlCharEncoding,
1045    input_func: Option<xmlCharEncodingInputFunc>,
1046    output_func: Option<xmlCharEncodingOutputFunc>,
1047) {
1048    let name_raw =
1049        unsafe { crate::abi::allocator::xmlMemStrdupImpl(name_bytes.as_ptr() as *const c_char) };
1050    if name_raw.is_null() {
1051        return;
1052    }
1053
1054    let handler = unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) }
1055        as *mut _xmlCharEncodingHandler;
1056
1057    if handler.is_null() {
1058        unsafe { xmlFreeImpl(name_raw) };
1059        return;
1060    }
1061
1062    unsafe {
1063        ptr::write(
1064            handler,
1065            _xmlCharEncodingHandler {
1066                name: name_raw as *mut c_char,
1067                input: EncodingInputUnion {
1068                    legacyFunc: input_func,
1069                },
1070                output: EncodingOutputUnion {
1071                    legacyFunc: output_func,
1072                },
1073                inputCtxt: ptr::null_mut(),
1074                outputCtxt: ptr::null_mut(),
1075                ctxtDtor: None,
1076                flags: 0,
1077            },
1078        );
1079    }
1080
1081    add_encoding_handler(handler);
1082}
1083
1084/// Clean up encoding handlers.
1085///
1086/// Frees all registered handlers and resets the registry.
1087///
1088/// # Safety
1089///
1090/// - Every registered handler pointer must be NULL or a valid
1091///   heap-allocated `_xmlCharEncodingHandler` whose `name` is NULL or a
1092///   heap-allocated NUL-terminated string; each allocation is freed exactly
1093///   once and must not be freed elsewhere.
1094pub(crate) fn cleanup_encodings() {
1095    let mut handlers = ENCODING_HANDLERS.write();
1096    for &handler in handlers.iter() {
1097        let ptr = handler.0;
1098        if !ptr.is_null() {
1099            unsafe {
1100                if !(*ptr).name.is_null() {
1101                    xmlFreeImpl((*ptr).name as *mut c_void);
1102                }
1103                xmlFreeImpl(ptr as *mut c_void);
1104            }
1105        }
1106    }
1107    handlers.clear();
1108    // Re-allow registration on the next init_encodings()/cleanup round-trip so
1109    // a caller that cleans up and then (re)initializes in another thread does
1110    // not observe a stale "already initialized" registry that stays empty.
1111    // (ENCODING_INITIALIZED/ENCODING_INIT_MUTEX are separate statics.)
1112    drop(handlers);
1113    ENCODING_INITIALIZED.store(false, Ordering::SeqCst);
1114}
1115
1116/// Find an encoding handler by name.
1117///
1118/// Searches the global handler registry for a handler whose name matches
1119/// (case-insensitive). Returns a pointer to the handler, or `ptr::null_mut()`
1120/// if not found.
1121///
1122/// # Safety
1123///
1124/// - `name` must be NULL or a valid pointer to a NUL-terminated string.
1125/// - Each registry entry must be NULL or a valid `_xmlCharEncodingHandler`
1126///   whose `name` is NULL or a valid NUL-terminated string.
1127/// The canonical names of every handler in the process-wide encoding registry
1128/// (sorted, deduplicated).
1129///
1130/// Exists so the parser's [`RegistryKind`](crate::xml::parser::input) fail-closed
1131/// invariant can be a TEST rather than a comment: a newly registered codec that
1132/// nobody classified shows up as unclassified there instead of silently
1133/// acquiring the chunk-independent tail path.
1134pub(crate) fn registered_handler_names() -> Vec<String> {
1135    init_encodings();
1136    let handlers = ENCODING_HANDLERS.read();
1137    let mut out = Vec::new();
1138    for &handler in handlers.iter() {
1139        let ptr = handler.0;
1140        if ptr.is_null() {
1141            continue;
1142        }
1143        // SAFETY: registry entries are created by register_handler with a
1144        // valid NUL-terminated name and live for the process lifetime.
1145        let name = unsafe {
1146            if (*ptr).name.is_null() {
1147                continue;
1148            }
1149            CStr::from_ptr((*ptr).name).to_string_lossy().into_owned()
1150        };
1151        out.push(name);
1152    }
1153    out.sort();
1154    out.dedup();
1155    out
1156}
1157
1158pub(crate) fn find_encoding_handler(name: *const xmlChar) -> *mut _xmlCharEncodingHandler {
1159    if name.is_null() {
1160        return ptr::null_mut();
1161    }
1162
1163    /* The upstream default-handler table is static and always present; the
1164     * candidate's registry is populated lazily, so ensure it is initialized
1165     * before any name-based lookup. Idempotent. */
1166    init_encodings();
1167
1168    let name_str = unsafe {
1169        match CStr::from_ptr(name as *const c_char).to_bytes() {
1170            b"" => return ptr::null_mut(),
1171            s => s,
1172        }
1173    };
1174
1175    let handlers = ENCODING_HANDLERS.read();
1176    for &handler in handlers.iter() {
1177        let ptr = handler.0;
1178        if ptr.is_null() {
1179            continue;
1180        }
1181        let h_name = unsafe {
1182            if (*ptr).name.is_null() {
1183                continue;
1184            }
1185            CStr::from_ptr((*ptr).name).to_bytes()
1186        };
1187
1188        if name_str.eq_ignore_ascii_case(h_name) {
1189            return ptr;
1190        }
1191    }
1192    drop(handlers);
1193
1194    // Canonical re-lookup for alias spellings (upstream resolves unknown
1195    // names through xmlAddEncodingAlias and its iconv/ICU fallback; the
1196    // candidate maps aliases through encoding_from_name — e.g. "utf-32be" /
1197    // "shift-jis" resolve to the registered "UCS-4BE" / "Shift_JIS"
1198    // handlers).
1199    if let Some(canon) = encoding_name(encoding_from_name(name_str)) {
1200        if let Ok(canon_c) = std::ffi::CString::new(canon) {
1201            let handlers = ENCODING_HANDLERS.read();
1202            for &handler in handlers.iter() {
1203                let ptr = handler.0;
1204                if ptr.is_null() {
1205                    continue;
1206                }
1207                let h_name = unsafe {
1208                    if (*ptr).name.is_null() {
1209                        continue;
1210                    }
1211                    CStr::from_ptr((*ptr).name).to_bytes()
1212                };
1213                if canon.eq_ignore_ascii_case(h_name) {
1214                    return ptr;
1215                }
1216            }
1217        }
1218    }
1219
1220    ptr::null_mut()
1221}
1222
1223/// Build an owned, caller-freed copy of a registered encoding handler.
1224///
1225/// Upstream's `xmlFindCharEncodingHandler` hands the caller a handler it owns
1226/// and is expected to release with `xmlCharEncCloseFunc` after use (Phase 14
1227/// PHP court: `dom_document_encoding_write` finds a handler then closes it for
1228/// every write). Returning the persistent registry pointer directly would let
1229/// the exported close free the registry entry out from under later lookups
1230/// (a use-after-free seen as `DOMDocument::$encoding = 'UTF-16'` corrupting the
1231/// handler registry and crashing the next `find_encoding_handler`).
1232///
1233/// The copy duplicates the name with `xmlMemStrdupImpl` so the caller may free
1234/// it; the conversion unions and context pointers are shared with the original
1235/// registry entry. All built-in registry handlers the find path serves are
1236/// stateless (`ctxtDtor` is None, contexts NULL), so `xmlCharEncCloseFunc` on
1237/// the copy only releases the duplicated name and the struct.
1238///
1239/// Returns the new handler or `ptr::null_mut()` when `src` is NULL/alloc fails.
1240pub(crate) fn clone_encoding_handler_for_find(
1241    src: *mut _xmlCharEncodingHandler,
1242) -> *mut _xmlCharEncodingHandler {
1243    if src.is_null() {
1244        return ptr::null_mut();
1245    }
1246    let name_raw = unsafe {
1247        let nm = (*src).name;
1248        if nm.is_null() {
1249            ptr::null_mut()
1250        } else {
1251            crate::abi::allocator::xmlMemStrdupImpl(nm)
1252        }
1253    };
1254    let handler = unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) }
1255        as *mut _xmlCharEncodingHandler;
1256    if handler.is_null() {
1257        if !name_raw.is_null() {
1258            unsafe { crate::abi::allocator::xmlFreeImpl(name_raw) };
1259        }
1260        return ptr::null_mut();
1261    }
1262    unsafe {
1263        ptr::write(
1264            handler,
1265            _xmlCharEncodingHandler {
1266                name: name_raw as *mut c_char,
1267                input: ptr::read(&(*src).input),
1268                output: ptr::read(&(*src).output),
1269                inputCtxt: (*src).inputCtxt,
1270                outputCtxt: (*src).outputCtxt,
1271                ctxtDtor: (*src).ctxtDtor,
1272                flags: (*src).flags,
1273            },
1274        );
1275    }
1276    handler
1277}
1278
1279/// Upstream handler `flags` marker: the handler lives for the process lifetime
1280/// (upstream `encoding.c` `{"UTF-8", ... , XML_HANDLER_STATIC}`) and
1281/// `xmlCharEncCloseFunc` must therefore not release it.
1282pub(crate) const XML_HANDLER_STATIC: c_int = 0x01;
1283
1284/// ABI `xmlFindCharEncodingHandler` mirror (upstream libxml2 2.15 encoding.c
1285/// `xmlFindCharEncodingHandler`).
1286///
1287/// Upstream returns an OWNED handler the caller releases with
1288/// `xmlCharEncCloseFunc`, except for UTF-8/UTF8 where it returns the static
1289/// `defaultHandlers[XML_CHAR_ENCODING_UTF8]` (has `XML_HANDLER_STATIC`, so
1290/// `xmlCharEncCloseFunc` is a no-op). Phase 14 PHP court:
1291/// `dom_document_encoding_write` finds a handler for every `$dom->encoding=
1292/// write and closes it — so returning the persistent registry pointer for a
1293/// non-UTF-8 encoding let the caller's close free the registry entry (the
1294/// use-after-free behind `DOMDocument::$encoding = 'UTF-16'` crashing the next
1295/// `find_encoding_handler`).
1296///
1297/// Returns an owned heap copy for non-UTF-8 encodings, the flagged-static
1298/// registry UTF-8 handler for UTF-8, or `ptr::null_mut()` when `name` is NULL
1299/// or no handler is registered.
1300pub(crate) fn xmlFindCharEncodingHandler_owned(
1301    name: *const xmlChar,
1302) -> *mut _xmlCharEncodingHandler {
1303    if name.is_null() {
1304        return ptr::null_mut();
1305    }
1306    let name_bytes = unsafe {
1307        let len = libc::strlen(name as *const c_char);
1308        core::slice::from_raw_parts(name as *const u8, len)
1309    };
1310
1311    // UTF-8 / UTF8 special case (upstream returns the static handler).
1312    if encoding_from_name(name_bytes) == xmlCharEncoding::XML_CHAR_ENCODING_UTF8 {
1313        let utf8 = find_encoding_handler(c"UTF-8".as_ptr() as *const xmlChar);
1314        if utf8.is_null() {
1315            return ptr::null_mut();
1316        }
1317        // Flag it static so xmlCharEncCloseFunc does not free the registry entry.
1318        unsafe {
1319            (*utf8).flags |= XML_HANDLER_STATIC;
1320        }
1321        return utf8;
1322    }
1323
1324    // Non-UTF-8: resolve the registry entry, preferring a canonical lookup when
1325    // the raw spelling is not itself a registered key (mirrors the canonical
1326    // re-lookup upstream performs in xmlCreateCharEncodingHandler).
1327    let mut entry = find_encoding_handler(name as *const xmlChar);
1328    if entry.is_null() {
1329        if let Some(canon) = encoding_name(encoding_from_name(name_bytes)) {
1330            entry = find_encoding_handler(canon.as_ptr() as *const xmlChar);
1331        }
1332    }
1333    // Return an OWNED copy of the registry entry (never the entry itself), so
1334    // the caller's xmlCharEncCloseFunc releases only the copy.
1335    clone_encoding_handler_for_find(entry)
1336}
1337
1338/// Add an encoding handler to the registry.
1339///
1340/// Returns 0 on success, -1 on failure (e.g., null pointer).
1341pub(crate) fn add_encoding_handler(handler: *mut _xmlCharEncodingHandler) -> c_int {
1342    if handler.is_null() {
1343        return -1;
1344    }
1345
1346    let mut handlers = ENCODING_HANDLERS.write();
1347    handlers.push(HandlerPtr(handler));
1348    0
1349}
1350
1351// ═══════════════════════════════════════════════════════════════════════════════
1352// 6. Encoding conversion functions
1353// ═══════════════════════════════════════════════════════════════════════════════
1354
1355/// Input conversion: convert from handler's input encoding to UTF-8.
1356///
1357/// Calls the handler's `input.legacyFunc` callback. Returns bytes written or -1 on error.
1358///
1359/// # Safety
1360///
1361/// - `handler` must be NULL or a valid pointer to an initialized
1362///   `_xmlCharEncodingHandler`; the stored `input.legacyFunc` callback, when
1363///   present, must be a valid function pointer.
1364/// - `out` must be a valid mutable byte slice and `in_data` a valid byte
1365///   slice; both stay valid for the duration of the callback.
1366#[allow(dead_code)]
1367pub(crate) fn char_enc_in_func(
1368    handler: *mut _xmlCharEncodingHandler,
1369    out: &mut [u8],
1370    in_data: &[u8],
1371) -> c_int {
1372    if handler.is_null() {
1373        return -1;
1374    }
1375
1376    let h = unsafe { &*handler };
1377    let input_func = unsafe { h.input.legacyFunc };
1378    let input_func = match input_func {
1379        Some(f) => f,
1380        None => return -1,
1381    };
1382
1383    let mut outlen = out.len() as c_int;
1384    let mut inlen = in_data.len() as c_int;
1385
1386    unsafe { input_func(out.as_mut_ptr(), &mut outlen, in_data.as_ptr(), &mut inlen) }
1387}
1388
1389/// Output conversion: convert from UTF-8 to handler's output encoding.
1390///
1391/// Calls the handler's `output.legacyFunc` callback. Returns bytes written or -1 on error.
1392///
1393/// # Safety
1394///
1395/// - `handler` must be NULL or a valid pointer to an initialized
1396///   `_xmlCharEncodingHandler`; the stored `output.legacyFunc` callback,
1397///   when present, must be a valid function pointer.
1398/// - `out` must be a valid mutable byte slice and `in_data` a valid byte
1399///   slice; both stay valid for the duration of the callback.
1400#[allow(dead_code)]
1401pub(crate) fn char_enc_out_func(
1402    handler: *mut _xmlCharEncodingHandler,
1403    out: &mut [u8],
1404    in_data: &[u8],
1405) -> c_int {
1406    if handler.is_null() {
1407        return -1;
1408    }
1409
1410    let h = unsafe { &*handler };
1411    let output_func = unsafe { h.output.legacyFunc };
1412    let output_func = match output_func {
1413        Some(f) => f,
1414        None => return -1,
1415    };
1416
1417    let mut outlen = out.len() as c_int;
1418    let mut inlen = in_data.len() as c_int;
1419
1420    unsafe { output_func(out.as_mut_ptr(), &mut outlen, in_data.as_ptr(), &mut inlen) }
1421}
1422
1423/// Full input conversion (`xmlCharEncInFunc` equivalent).
1424///
1425/// Reads from the input `_xmlBuffer`, converts via the handler's `input.legacyFunc`,
1426/// and appends the result to the output `_xmlBuffer`.
1427///
1428/// Returns the number of bytes written to the output buffer, or -1 on error.
1429///
1430/// # Safety
1431///
1432/// - `handler` must be NULL or a valid `_xmlCharEncodingHandler` whose
1433///   `input.legacyFunc` callback is a valid function pointer.
1434/// - `in_` and `out` must be NULL or valid `_xmlBuffer` pointers; `in_`'s
1435///   `content` must be NULL or point to `use_` readable bytes, and `out`
1436///   must stay valid while `append_to_xml_buffer` may reallocate its
1437///   `content`.
1438pub(crate) fn char_enc_in(
1439    handler: *mut _xmlCharEncodingHandler,
1440    out: *mut _xmlBuffer,
1441    in_: *mut _xmlBuffer,
1442) -> c_int {
1443    if handler.is_null() || out.is_null() || in_.is_null() {
1444        return -1;
1445    }
1446
1447    let h = unsafe { &*handler };
1448    let input_func = unsafe { h.input.legacyFunc };
1449    let input_func = match input_func {
1450        Some(f) => f,
1451        None => return -1,
1452    };
1453
1454    let in_buf = unsafe { &*in_ };
1455    let out_buf = unsafe { &mut *out };
1456
1457    if in_buf.content.is_null() || in_buf.use_ == 0 {
1458        return 0;
1459    }
1460
1461    let in_data = unsafe { core::slice::from_raw_parts(in_buf.content, in_buf.use_ as usize) };
1462
1463    // Allocate an output buffer. A good heuristic is 2x input for UTF-16→UTF-8.
1464    let out_capacity = (in_buf.use_ as usize).saturating_mul(3).max(256);
1465    let mut out_vec = vec![0u8; out_capacity];
1466    let mut out_len = out_capacity as c_int;
1467    let mut in_len = in_buf.use_ as c_int;
1468
1469    let ret = unsafe {
1470        input_func(
1471            out_vec.as_mut_ptr(),
1472            &mut out_len,
1473            in_data.as_ptr(),
1474            &mut in_len,
1475        )
1476    };
1477
1478    if ret < 0 {
1479        return -1;
1480    }
1481
1482    let written = ret as usize;
1483
1484    // Append to output buffer
1485    append_to_xml_buffer(out_buf, &out_vec[..written]);
1486
1487    written as c_int
1488}
1489
1490/// Full output conversion (`xmlCharEncOutFunc` equivalent).
1491///
1492/// Reads from the input `_xmlBuffer` (UTF-8), converts via the handler's
1493/// `output.legacyFunc`, and appends the result to the output `_xmlBuffer`.
1494///
1495/// Returns the number of bytes written to the output buffer, or -1 on error.
1496///
1497/// # Safety
1498///
1499/// - `handler` must be NULL or a valid `_xmlCharEncodingHandler` whose
1500///   `output.legacyFunc` callback is a valid function pointer.
1501/// - `in_` and `out` must be NULL or valid `_xmlBuffer` pointers; `in_`'s
1502///   `content` must be NULL or point to `use_` readable bytes, and `out`
1503///   must stay valid while `append_to_xml_buffer` may reallocate its
1504///   `content`.
1505pub(crate) fn char_enc_out(
1506    handler: *mut _xmlCharEncodingHandler,
1507    out: *mut _xmlBuffer,
1508    in_: *mut _xmlBuffer,
1509) -> c_int {
1510    if handler.is_null() || out.is_null() || in_.is_null() {
1511        return -1;
1512    }
1513
1514    let h = unsafe { &*handler };
1515    let output_func = unsafe { h.output.legacyFunc };
1516    let output_func = match output_func {
1517        Some(f) => f,
1518        None => return -1,
1519    };
1520
1521    let in_buf = unsafe { &*in_ };
1522    let out_buf = unsafe { &mut *out };
1523
1524    if in_buf.content.is_null() || in_buf.use_ == 0 {
1525        return 0;
1526    }
1527
1528    let mut in_data = unsafe { core::slice::from_raw_parts(in_buf.content, in_buf.use_ as usize) };
1529
1530    // UPSTREAM-PARITY (encoding.c xmlCharEncOutput): the output conversion
1531    // runs in a loop; when the converter reports an INPUT error (a character
1532    // not representable in the output encoding — the ASCII handler stops at
1533    // the first byte >= 0x80), the offending UTF-8 character is decoded and
1534    // replaced by a DECIMAL character reference (&#NNN;), then conversion
1535    // continues. This is how libxml2 serializes non-ASCII text into an
1536    // ASCII output buffer (lxml's default `tostring` encoding, which
1537    // produces `&#195;&#169;` for the mojibake case).
1538    const ENC_INPUT_ERROR: c_int = -2;
1539    let mut total_written: usize = 0;
1540    loop {
1541        // Scratch is >= 5x the input so even the widest native codec
1542        // (UCS-4: 4 bytes per ASCII input byte) can never exhaust it
1543        // mid-buffer; unmappable characters stop with ENC_INPUT_ERROR and
1544        // are substituted here without expansion through the func.
1545        let out_capacity = (in_data.len().saturating_mul(5)).max(64) + 16;
1546        let mut out_vec = vec![0u8; out_capacity];
1547        let mut out_len = out_capacity as c_int;
1548        let mut in_len = in_data.len() as c_int;
1549        let ret = unsafe {
1550            output_func(
1551                out_vec.as_mut_ptr(),
1552                &mut out_len,
1553                in_data.as_ptr(),
1554                &mut in_len,
1555            )
1556        };
1557        let written = out_len.max(0) as usize;
1558        if written > 0 {
1559            append_to_xml_buffer(out_buf, &out_vec[..written]);
1560            total_written += written;
1561        }
1562        let consumed = in_len.max(0) as usize;
1563        if ret == ENC_INPUT_ERROR && consumed < in_data.len() {
1564            // Decode the UTF-8 character at the offending position and emit
1565            // a decimal character reference (upstream xmlSerializeDecCharRef).
1566            let mut clen: c_int = 4;
1567            let cp = unsafe {
1568                crate::abi::exports_misc::xmlGetUTF8Char(in_data[consumed..].as_ptr(), &mut clen)
1569            };
1570            if cp <= 0 || clen <= 0 || (consumed + clen as usize) > in_data.len() {
1571                return -1;
1572            }
1573            let ref_str = format!("&#{};", cp);
1574            append_to_xml_buffer(out_buf, ref_str.as_bytes());
1575            total_written += ref_str.len();
1576            in_data = &in_data[consumed + clen as usize..];
1577            if in_data.is_empty() {
1578                break;
1579            }
1580            continue;
1581        }
1582        if ret < 0 {
1583            return -1;
1584        }
1585        break;
1586    }
1587
1588    total_written as c_int
1589}
1590
1591// ═══════════════════════════════════════════════════════════════════════════════
1592// 6b. Whole-buffer declared-encoding decode (parser input layer; R-000157)
1593// ═══════════════════════════════════════════════════════════════════════════════
1594
1595/// Decode a whole raw byte buffer to UTF-8 through the registry handler for
1596/// a declared encoding NAME, canonicalizing alias spellings exactly like
1597/// `xmlFindCharEncodingHandler_owned`. Used by the parser input layer for
1598/// BOM-less inputs whose XML declaration names a legacy encoding (and for
1599/// the pattern-detected UCS-4/EBCDIC family, whose canonical names are
1600/// passed directly). Returns `Err(())` when the name has no handler or the
1601/// bytes are not decodable (iconv EILSEQ semantics — the caller falls back
1602/// to the tokenizer's invalid-character diagnostics).
1603pub(crate) fn decode_whole_buffer_declared(name: &[u8], data: &[u8]) -> Result<Vec<u8>, ()> {
1604    if data.is_empty() {
1605        return Ok(Vec::new());
1606    }
1607    let Ok(cname) = std::ffi::CString::new(name) else {
1608        return Err(());
1609    };
1610    let mut handler = find_encoding_handler(cname.as_ptr() as *const xmlChar);
1611    if handler.is_null() {
1612        // Canonical re-lookup for alias spellings (upstream
1613        // xmlFindCharEncodingHandler: latin2 -> ISO-8859-2, sjis ->
1614        // SHIFT_JIS, ...).
1615        if let Some(canon) = encoding_name(encoding_from_name(name)) {
1616            if let Ok(canon_c) = std::ffi::CString::new(canon) {
1617                handler = find_encoding_handler(canon_c.as_ptr() as *const xmlChar);
1618            }
1619        }
1620    }
1621    if handler.is_null() {
1622        return Err(());
1623    }
1624    decode_bytes_with_handler(handler, data)
1625}
1626
1627/// Drive a registry handler's `input.legacyFunc` over a whole byte buffer,
1628/// growing the output as needed. Each input func converts complete source
1629/// characters; source encodings expand at most ~3x into UTF-8, so the
1630/// initial 3x+16 scratch completes valid input in one call and the growth
1631/// branch only guards pathological (near-invalid) content.
1632pub(crate) fn decode_bytes_with_handler(
1633    handler: *mut _xmlCharEncodingHandler,
1634    data: &[u8],
1635) -> Result<Vec<u8>, ()> {
1636    if handler.is_null() || data.is_empty() {
1637        return Ok(Vec::new());
1638    }
1639    let input_func = unsafe { (*handler).input.legacyFunc };
1640    let Some(input_func) = input_func else {
1641        return Err(());
1642    };
1643    let mut out: Vec<u8> = vec![0u8; data.len().saturating_mul(3) + 16];
1644    let mut in_pos: usize = 0;
1645    let mut written_total: usize = 0;
1646    loop {
1647        let mut out_len = (out.len() - written_total) as c_int;
1648        let mut in_len = (data.len() - in_pos) as c_int;
1649        // SAFETY: `out[written_total..]` and `data[in_pos..]` are valid
1650        // writable/readable slices for the call; the func respects the
1651        // length pointers (house func contract).
1652        let ret = unsafe {
1653            input_func(
1654                out[written_total..].as_mut_ptr(),
1655                &mut out_len,
1656                data[in_pos..].as_ptr(),
1657                &mut in_len,
1658            )
1659        };
1660        let written = out_len.max(0) as usize;
1661        let consumed = in_len.max(0) as usize;
1662        written_total += written;
1663        in_pos += consumed;
1664        if ret < 0 {
1665            return Err(());
1666        }
1667        if in_pos >= data.len() {
1668            break;
1669        }
1670        if written == 0 {
1671            // No progress with input left: undecodable tail.
1672            return Err(());
1673        }
1674        out.resize(out.len().saturating_mul(2).max(written_total + 64), 0);
1675    }
1676    out.truncate(written_total);
1677    Ok(out)
1678}
1679
1680/// Append bytes to an `_xmlBuffer`, reallocating if needed.
1681///
1682/// - `buf` must be a valid `_xmlBuffer` whose `content` is NULL or points to
1683///   `size` allocated bytes; `buf.content` may be replaced by a fresh
1684///   `xmlReallocImpl` allocation when it must grow.
1685/// - `data` must be a valid byte slice; after the call, `buf.content` holds
1686///   `use_` initialized bytes.
1687fn append_to_xml_buffer(buf: &mut _xmlBuffer, data: &[u8]) {
1688    if data.is_empty() {
1689        return;
1690    }
1691
1692    let new_use = (buf.use_ as usize).saturating_add(data.len());
1693    if new_use > buf.size as usize {
1694        // Grow buffer: double or fit, whichever is larger
1695        let new_size = (buf.size as usize).saturating_mul(2).max(new_use).max(256);
1696        let new_content =
1697            unsafe { xmlReallocImpl(buf.content as *mut c_void, new_size) as *mut xmlChar };
1698        if new_content.is_null() {
1699            return; // Allocation failure — silently skip
1700        }
1701        buf.content = new_content;
1702        // UPSTREAM-PARITY (io/mod.rs buf_add realloc paths): when the buffer
1703        // grows, contentIO tracks the CURRENT allocation base — buf_free
1704        // frees contentIO, so a stale contentIO (the pre-realloc block) would
1705        // cause a double-free on buffers that grew through this conversion
1706        // path (nokogiri HTML4/HTML5 UTF-8 serialization).
1707        buf.contentIO = new_content;
1708        buf.size = new_size as c_uint;
1709    }
1710
1711    unsafe {
1712        ptr::copy_nonoverlapping(
1713            data.as_ptr(),
1714            buf.content.add(buf.use_ as usize),
1715            data.len(),
1716        );
1717    }
1718    buf.use_ = new_use as c_uint;
1719}
1720
1721// ═══════════════════════════════════════════════════════════════════════════════
1722// 7. Built-in encoding handler callbacks (extern "C")
1723// ═══════════════════════════════════════════════════════════════════════════════
1724
1725// ── UTF-8 (identity) ──────────────────────────────────────────────────────
1726
1727/// UTF-8 input function: identity (input is already UTF-8).
1728///
1729/// Simply copies bytes from input to output, up to the available space.
1730unsafe extern "C" fn utf8_input_func(
1731    out: *mut c_uchar,
1732    outlen: *mut c_int,
1733    in_: *const c_uchar,
1734    inlen: *mut c_int,
1735) -> c_int {
1736    let avail_out = *outlen as usize;
1737    let avail_in = *inlen as usize;
1738    let to_copy = avail_out.min(avail_in);
1739
1740    if to_copy > 0 {
1741        ptr::copy_nonoverlapping(in_, out, to_copy);
1742    }
1743
1744    *outlen = to_copy as c_int;
1745    *inlen = to_copy as c_int;
1746    to_copy as c_int
1747}
1748
1749/// UTF-8 output function: identity (output is already UTF-8).
1750unsafe extern "C" fn utf8_output_func(
1751    out: *mut c_uchar,
1752    outlen: *mut c_int,
1753    in_: *const c_uchar,
1754    inlen: *mut c_int,
1755) -> c_int {
1756    utf8_input_func(out, outlen, in_, inlen)
1757}
1758
1759// ── UTF-16LE ──────────────────────────────────────────────────────────────
1760
1761/// UTF-16LE input function: convert UTF-16LE to UTF-8.
1762unsafe extern "C" fn utf16le_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    // Use the safe wrapper
1785    let result = match utf16le_to_utf8(in_data) {
1786        Ok(v) => v,
1787        Err(()) => return -1,
1788    };
1789
1790    let written = result.len().min(avail_out);
1791    if written > 0 {
1792        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1793    }
1794
1795    *outlen = written as c_int;
1796    *inlen = avail_in as c_int; // All input consumed
1797    written as c_int
1798}
1799
1800/// UTF-16LE output function: convert UTF-8 to UTF-16LE.
1801unsafe extern "C" fn utf16le_output_func(
1802    out: *mut c_uchar,
1803    outlen: *mut c_int,
1804    in_: *const c_uchar,
1805    inlen: *mut c_int,
1806) -> c_int {
1807    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1808        return -1;
1809    }
1810
1811    let avail_in = *inlen as usize;
1812    let avail_out = *outlen as usize;
1813
1814    if avail_in == 0 || avail_out == 0 {
1815        *outlen = 0;
1816        *inlen = 0;
1817        return 0;
1818    }
1819
1820    let in_data = core::slice::from_raw_parts(in_, avail_in);
1821    let _out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1822
1823    let result = match utf8_to_utf16le(in_data) {
1824        Ok(v) => v,
1825        Err(()) => return -1,
1826    };
1827
1828    let written = result.len().min(avail_out);
1829    if written > 0 {
1830        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1831    }
1832
1833    *outlen = written as c_int;
1834    *inlen = avail_in as c_int;
1835    written as c_int
1836}
1837
1838// ── UTF-16BE ──────────────────────────────────────────────────────────────
1839
1840/// UTF-16BE input function: convert UTF-16BE to UTF-8.
1841unsafe extern "C" fn utf16be_input_func(
1842    out: *mut c_uchar,
1843    outlen: *mut c_int,
1844    in_: *const c_uchar,
1845    inlen: *mut c_int,
1846) -> c_int {
1847    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1848        return -1;
1849    }
1850
1851    let avail_in = *inlen as usize;
1852    let avail_out = *outlen as usize;
1853
1854    if avail_in == 0 || avail_out == 0 {
1855        *outlen = 0;
1856        *inlen = 0;
1857        return 0;
1858    }
1859
1860    let in_data = core::slice::from_raw_parts(in_, avail_in);
1861    let _out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1862
1863    let result = match utf16be_to_utf8(in_data) {
1864        Ok(v) => v,
1865        Err(()) => return -1,
1866    };
1867
1868    let written = result.len().min(avail_out);
1869    if written > 0 {
1870        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1871    }
1872
1873    *outlen = written as c_int;
1874    *inlen = avail_in as c_int;
1875    written as c_int
1876}
1877
1878/// UTF-16BE output function: convert UTF-8 to UTF-16BE.
1879unsafe extern "C" fn utf16be_output_func(
1880    out: *mut c_uchar,
1881    outlen: *mut c_int,
1882    in_: *const c_uchar,
1883    inlen: *mut c_int,
1884) -> c_int {
1885    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1886        return -1;
1887    }
1888
1889    let avail_in = *inlen as usize;
1890    let avail_out = *outlen as usize;
1891
1892    if avail_in == 0 || avail_out == 0 {
1893        *outlen = 0;
1894        *inlen = 0;
1895        return 0;
1896    }
1897
1898    let in_data = core::slice::from_raw_parts(in_, avail_in);
1899
1900    // First convert to UTF-16LE, then swap bytes
1901    let le_result = match utf8_to_utf16le(in_data) {
1902        Ok(v) => v,
1903        Err(()) => return -1,
1904    };
1905
1906    // Swap byte pairs to get UTF-16BE
1907    let mut result = le_result;
1908    for chunk in result.as_chunks_mut::<2>().0 {
1909        chunk.swap(0, 1);
1910    }
1911
1912    let written = result.len().min(avail_out);
1913    if written > 0 {
1914        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1915    }
1916
1917    *outlen = written as c_int;
1918    *inlen = avail_in as c_int;
1919    written as c_int
1920}
1921
1922// ── ISO-8859-1 (Latin-1) ─────────────────────────────────────────────────
1923
1924/// Latin-1 input function: convert ISO-8859-1 to UTF-8.
1925unsafe extern "C" fn latin1_input_func(
1926    out: *mut c_uchar,
1927    outlen: *mut c_int,
1928    in_: *const c_uchar,
1929    inlen: *mut c_int,
1930) -> c_int {
1931    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1932        return -1;
1933    }
1934
1935    let avail_in = *inlen as usize;
1936    let avail_out = *outlen as usize;
1937
1938    if avail_in == 0 || avail_out == 0 {
1939        *outlen = 0;
1940        *inlen = 0;
1941        return 0;
1942    }
1943
1944    let in_data = core::slice::from_raw_parts(in_, avail_in);
1945    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1946
1947    let mut in_pos = 0;
1948    let mut out_pos = 0;
1949
1950    while in_pos < avail_in && out_pos < avail_out {
1951        let byte = in_data[in_pos];
1952        in_pos += 1;
1953
1954        if byte < 0x80 {
1955            // Single byte UTF-8
1956            if out_pos < avail_out {
1957                out_slice[out_pos] = byte;
1958                out_pos += 1;
1959            } else {
1960                break;
1961            }
1962        } else {
1963            // Two byte UTF-8: 0xC0 | (byte >> 6), 0x80 | (byte & 0x3F)
1964            // For byte 0x80-0xFF, the encoding is 0xC2-0xC3 followed by continuation
1965            if out_pos + 1 < avail_out {
1966                out_slice[out_pos] = 0xC2 | (byte >> 6);
1967                out_slice[out_pos + 1] = 0x80 | (byte & 0x3F);
1968                out_pos += 2;
1969            } else {
1970                break;
1971            }
1972        }
1973    }
1974
1975    *outlen = out_pos as c_int;
1976    *inlen = in_pos as c_int;
1977    out_pos as c_int
1978}
1979
1980/// Latin-1 output function: convert UTF-8 to ISO-8859-1.
1981unsafe extern "C" fn latin1_output_func(
1982    out: *mut c_uchar,
1983    outlen: *mut c_int,
1984    in_: *const c_uchar,
1985    inlen: *mut c_int,
1986) -> c_int {
1987    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1988        return -1;
1989    }
1990
1991    let avail_in = *inlen as usize;
1992    let avail_out = *outlen as usize;
1993
1994    if avail_in == 0 || avail_out == 0 {
1995        *outlen = 0;
1996        *inlen = 0;
1997        return 0;
1998    }
1999
2000    let in_data = core::slice::from_raw_parts(in_, avail_in);
2001    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
2002
2003    let mut in_pos = 0;
2004    let mut out_pos = 0;
2005
2006    while in_pos < avail_in && out_pos < avail_out {
2007        let byte = in_data[in_pos];
2008        in_pos += 1;
2009
2010        if byte < 0x80 {
2011            // ASCII — direct mapping
2012            out_slice[out_pos] = byte;
2013            out_pos += 1;
2014        } else if (0xC2..=0xC3).contains(&byte) {
2015            // Two-byte UTF-8 for codepoints U+0080–U+00FF
2016            if in_pos < avail_in {
2017                let second = in_data[in_pos];
2018                in_pos += 1;
2019                if second & 0xC0 != 0x80 {
2020                    return -1; // Invalid continuation byte
2021                }
2022                let cp = ((byte as u32 & 0x1F) << 6) | (second as u32 & 0x3F);
2023                if cp > 0xFF {
2024                    return -1; // Outside Latin-1 range
2025                }
2026                out_slice[out_pos] = cp as u8;
2027                out_pos += 1;
2028            } else {
2029                return -1; // Truncated
2030            }
2031        } else if (0x80..=0xBF).contains(&byte) {
2032            // Unexpected continuation byte
2033            return -1;
2034        } else {
2035            // Multi-byte sequence for codepoints > U+00FF
2036            // Skip the rest of the sequence and return error
2037            return -1;
2038        }
2039    }
2040
2041    *outlen = out_pos as c_int;
2042    *inlen = in_pos as c_int;
2043    out_pos as c_int
2044}
2045
2046// ── Windows-1252 (CP1252) ────────────────────────────────────────────────
2047
2048/// Windows-1252 mapping for bytes 0x80..=0xFF (WHATWG windows-1252 == glibc
2049/// iconv CP1252). Bytes 0x81, 0x8D, 0x8F, 0x90, 0x9D are UNDEFINED in the
2050/// encoding (iconv raises EILSEQ on them). 0x00..=0x7F are ASCII and 0xA0..=
2051/// 0xFF are the Latin-1 supplement, so only 0x80..=0x9F need the table below
2052/// (indexed by `byte - 0x80`, U+FFFF = undefined).
2053///
2054/// R-000157 closure (partial): the oracle serves windows-1252 through iconv;
2055/// the candidate now ships a native converter for this single-byte set.
2056const CP1252_C1: [u16; 32] = [
2057    0x20AC, 0xFFFF, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, // 80..87
2058    0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0xFFFF, 0x017D, 0xFFFF, // 88..8F
2059    0xFFFF, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, // 90..97
2060    0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0xFFFF, 0x017E, 0x0178, // 98..9F
2061];
2062
2063/// Map a Windows-1252 byte to its Unicode codepoint; `None` for the five
2064/// undefined C1 bytes.
2065#[allow(dead_code)]
2066pub(crate) const fn cp1252_byte_to_cp(byte: u8) -> Option<u32> {
2067    match byte {
2068        0x00..=0x7F => Some(byte as u32),
2069        0x80..=0x9F => {
2070            let cp = CP1252_C1[(byte - 0x80) as usize];
2071            if cp == 0xFFFF {
2072                None
2073            } else {
2074                Some(cp as u32)
2075            }
2076        }
2077        _ => Some(byte as u32), // 0xA0..=0xFF = Latin-1 supplement
2078    }
2079}
2080
2081/// Map a Unicode codepoint back to its Windows-1252 byte; `None` when the
2082/// codepoint is not representable in windows-1252.
2083#[allow(dead_code)]
2084pub(crate) const fn cp_to_cp1252_byte(cp: u32) -> Option<u8> {
2085    if cp < 0x80 || (cp >= 0xA0 && cp <= 0xFF) {
2086        Some(cp as u8)
2087    } else if cp >= 0x80 && cp <= 0x9F {
2088        // Reverse scan of the C1 table (32 entries; called per character on
2089        // output conversion only).
2090        let mut i = 0;
2091        while i < 32 {
2092            if CP1252_C1[i] == cp as u16 {
2093                return Some(0x80 + i as u8);
2094            }
2095            i += 1;
2096        }
2097        None
2098    } else {
2099        None
2100    }
2101}
2102
2103/// Convert a single UTF-8 character starting at `data[in_pos]` to its
2104/// codepoint. Returns `(cp, bytes_consumed)` or `None` on invalid UTF-8.
2105fn decode_utf8_char(data: &[u8], in_pos: usize) -> Option<(u32, usize)> {
2106    let b0 = *data.get(in_pos)?;
2107    if b0 < 0x80 {
2108        return Some((u32::from(b0), 1));
2109    }
2110    let (len, cp0) = match b0 {
2111        0xC2..=0xDF => (2, u32::from(b0 & 0x1F)),
2112        0xE0..=0xEF => (3, u32::from(b0 & 0x0F)),
2113        0xF0..=0xF4 => (4, u32::from(b0 & 0x07)),
2114        _ => return None,
2115    };
2116    if in_pos + len > data.len() {
2117        return None;
2118    }
2119    let mut cp = cp0;
2120    for k in 1..len {
2121        let b = data[in_pos + k];
2122        if b & 0xC0 != 0x80 {
2123            return None;
2124        }
2125        cp = (cp << 6) | u32::from(b & 0x3F);
2126    }
2127    Some((cp, len))
2128}
2129
2130/// Convert a whole CP1252 byte slice to UTF-8.
2131///
2132/// Returns `Err(())` when a byte has no windows-1252 mapping (the five
2133/// undefined C1 bytes 0x81/0x8D/0x8F/0x90/0x9D — iconv raises EILSEQ).
2134pub(crate) fn cp1252_to_utf8(data: &[u8]) -> Result<Vec<u8>, ()> {
2135    let mut result = Vec::with_capacity(data.len() * 2);
2136    for &byte in data {
2137        let cp = match cp1252_byte_to_cp(byte) {
2138            None => return Err(()),
2139            Some(cp) => cp,
2140        };
2141        let mut buf = [0u8; 4];
2142        let n = encode_codepoint_to_utf8(cp, &mut buf);
2143        result.extend_from_slice(&buf[..n]);
2144    }
2145    Ok(result)
2146}
2147
2148/// Convert UTF-8 bytes to CP1252 (used by whole-buffer output paths).
2149///
2150/// Returns `Err(())` on invalid UTF-8 or an unrepresentable codepoint.
2151#[allow(dead_code)]
2152pub(crate) fn utf8_to_cp1252(data: &[u8]) -> Result<Vec<u8>, ()> {
2153    let mut result = Vec::with_capacity(data.len());
2154    let mut pos = 0;
2155    while pos < data.len() {
2156        let (cp, consumed) = match decode_utf8_char(data, pos) {
2157            None => return Err(()),
2158            Some(v) => v,
2159        };
2160        let byte = match cp_to_cp1252_byte(cp) {
2161            None => return Err(()),
2162            Some(b) => b,
2163        };
2164        result.push(byte);
2165        pos += consumed;
2166    }
2167    Ok(result)
2168}
2169
2170/// Windows-1252 input function: convert CP1252 bytes to UTF-8.
2171unsafe extern "C" fn cp1252_input_func(
2172    out: *mut c_uchar,
2173    outlen: *mut c_int,
2174    in_: *const c_uchar,
2175    inlen: *mut c_int,
2176) -> c_int {
2177    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
2178        return -1;
2179    }
2180
2181    let avail_in = *inlen as usize;
2182    let avail_out = *outlen as usize;
2183
2184    if avail_in == 0 || avail_out == 0 {
2185        *outlen = 0;
2186        *inlen = 0;
2187        return 0;
2188    }
2189
2190    let in_data = core::slice::from_raw_parts(in_, avail_in);
2191    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
2192
2193    let mut in_pos = 0;
2194    let mut out_pos = 0;
2195
2196    while in_pos < avail_in && out_pos < avail_out {
2197        let byte = in_data[in_pos];
2198        let cp = match cp1252_byte_to_cp(byte) {
2199            // Undefined byte (0x81/0x8D/0x8F/0x90/0x9D): EILSEQ like iconv.
2200            None => {
2201                *outlen = out_pos as c_int;
2202                *inlen = in_pos as c_int;
2203                return -1;
2204            }
2205            Some(cp) => cp,
2206        };
2207        let mut buf = [0u8; 4];
2208        let n = encode_codepoint_to_utf8(cp, &mut buf);
2209        if out_pos + n > avail_out {
2210            break;
2211        }
2212        out_slice[out_pos..out_pos + n].copy_from_slice(&buf[..n]);
2213        out_pos += n;
2214        in_pos += 1;
2215    }
2216
2217    *outlen = out_pos as c_int;
2218    *inlen = in_pos as c_int;
2219    out_pos as c_int
2220}
2221
2222/// Windows-1252 output function: convert UTF-8 to CP1252 bytes.
2223unsafe extern "C" fn cp1252_output_func(
2224    out: *mut c_uchar,
2225    outlen: *mut c_int,
2226    in_: *const c_uchar,
2227    inlen: *mut c_int,
2228) -> c_int {
2229    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
2230        return -1;
2231    }
2232
2233    let avail_in = *inlen as usize;
2234    let avail_out = *outlen as usize;
2235
2236    if avail_in == 0 || avail_out == 0 {
2237        *outlen = 0;
2238        *inlen = 0;
2239        return 0;
2240    }
2241
2242    let in_data = core::slice::from_raw_parts(in_, avail_in);
2243    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
2244
2245    let mut in_pos = 0;
2246    let mut out_pos = 0;
2247
2248    while in_pos < avail_in && out_pos < avail_out {
2249        let (cp, consumed) = match decode_utf8_char(in_data, in_pos) {
2250            None => {
2251                *outlen = out_pos as c_int;
2252                *inlen = in_pos as c_int;
2253                return -1;
2254            }
2255            Some(v) => v,
2256        };
2257        let byte = match cp_to_cp1252_byte(cp) {
2258            None => {
2259                // Not representable in windows-1252: EILSEQ like iconv.
2260                *outlen = out_pos as c_int;
2261                *inlen = in_pos as c_int;
2262                return -1;
2263            }
2264            Some(b) => b,
2265        };
2266        out_slice[out_pos] = byte;
2267        out_pos += 1;
2268        in_pos += consumed;
2269    }
2270
2271    *outlen = out_pos as c_int;
2272    *inlen = in_pos as c_int;
2273    out_pos as c_int
2274}
2275
2276// ── ASCII ─────────────────────────────────────────────────────────────────
2277
2278/// ASCII input function: verify and pass through ASCII data to UTF-8.
2279///
2280/// Returns the number of bytes written, `-1` on invalid arguments, or
2281/// `-2` (the candidate's input-error code) when a byte >= 0x80 is reached
2282/// — in that case `*inlen`/`*outlen` hold the bytes consumed/written before
2283/// the offending character, so the output converter (`char_enc_out`) can
2284/// decode the UTF-8 character and replace it with a decimal character
2285/// reference (upstream `asciiToAscii` returns XML_ENC_ERR_INPUT).
2286unsafe extern "C" fn ascii_input_func(
2287    out: *mut c_uchar,
2288    outlen: *mut c_int,
2289    in_: *const c_uchar,
2290    inlen: *mut c_int,
2291) -> c_int {
2292    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
2293        return -1;
2294    }
2295
2296    let avail_in = *inlen as usize;
2297    let avail_out = *outlen as usize;
2298
2299    if avail_in == 0 || avail_out == 0 {
2300        *outlen = 0;
2301        *inlen = 0;
2302        return 0;
2303    }
2304
2305    let in_data = core::slice::from_raw_parts(in_, avail_in);
2306    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
2307
2308    let mut pos = 0;
2309    while pos < avail_in && pos < avail_out {
2310        let byte = in_data[pos];
2311        if byte > 0x7F {
2312            // Not valid ASCII: report how much was consumed so the caller
2313            // can substitute a character reference and retry.
2314            *outlen = pos as c_int;
2315            *inlen = pos as c_int;
2316            return -2;
2317        }
2318        out_slice[pos] = byte;
2319        pos += 1;
2320    }
2321
2322    *outlen = pos as c_int;
2323    *inlen = pos as c_int;
2324    pos as c_int
2325}
2326
2327/// ASCII output function: verify and pass through UTF-8 data that is ASCII.
2328unsafe extern "C" fn ascii_output_func(
2329    out: *mut c_uchar,
2330    outlen: *mut c_int,
2331    in_: *const c_uchar,
2332    inlen: *mut c_int,
2333) -> c_int {
2334    // For output, ASCII handler requires that input is already ASCII
2335    ascii_input_func(out, outlen, in_, inlen)
2336}
2337
2338// ── Shift_JIS / EUC-JP (encoding_rs-backed; R-000157 closure slice) ────────
2339
2340/// Module-level input-error code: a converter reports the character at
2341/// `*inlen` as unrepresentable and `char_enc_out` substitutes the upstream
2342/// decimal character reference (&#NNN;) before retrying (encoding.c
2343/// xmlCharEncOutput XML_ENC_ERR_INPUT path).
2344const ENC_INPUT_ERROR: c_int = -2;
2345
2346/// Shared output conversion for the encoding_rs-backed East-Asian handlers
2347/// (UTF-8 → `target`). House func contract (see cp1252): complete UTF-8
2348/// characters are converted while output space lasts; the first character
2349/// `target` cannot represent stops the conversion and is reported with the
2350/// -2 input-error convention (so `char_enc_out` emits the decimal character
2351/// reference and retries); invalid UTF-8 (or an incomplete trailing
2352/// sequence) reports -1 with the bytes before the error in `*inlen`. No
2353/// charref expansion happens inside the func, and Shift_JIS/EUC-JP output is
2354/// at most 1:1 with the UTF-8 input on the representable repertoire, so the
2355/// caller's >= 3x scratch can never overflow.
2356unsafe fn enc_rs_output(
2357    target: &'static encoding_rs::Encoding,
2358    out: *mut c_uchar,
2359    outlen: *mut c_int,
2360    in_: *const c_uchar,
2361    inlen: *mut c_int,
2362) -> c_int {
2363    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
2364        return -1;
2365    }
2366    let avail_in = *inlen as usize;
2367    let avail_out = *outlen as usize;
2368
2369    if avail_in == 0 || avail_out == 0 {
2370        *outlen = 0;
2371        *inlen = 0;
2372        return 0;
2373    }
2374
2375    let in_data = core::slice::from_raw_parts(in_, avail_in);
2376    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
2377
2378    // Convert only complete UTF-8 characters. On invalid bytes (or an
2379    // incomplete trailing sequence) the valid prefix is converted and the
2380    // error is reported at the first offending byte (upstream iconv EILSEQ;
2381    // the xmlCharEncOutput error path decodes the UTF-8 character there).
2382    let (s, error_at) = match core::str::from_utf8(in_data) {
2383        Ok(s) => (s, None),
2384        Err(e) => {
2385            let valid = e.valid_up_to();
2386            if valid == 0 {
2387                *outlen = 0;
2388                *inlen = 0;
2389                return -1;
2390            }
2391            // SAFETY: `valid` is a UTF-8 boundary (from_utf8 guarantees the
2392            // valid prefix ends on a character boundary).
2393            (
2394                unsafe { core::str::from_utf8_unchecked(&in_data[..valid]) },
2395                Some(valid),
2396            )
2397        }
2398    };
2399
2400    let mut encoder = target.new_encoder();
2401    let mut in_pos: usize = 0;
2402    let mut out_pos: usize = 0;
2403    while in_pos < s.len() && out_pos < avail_out {
2404        let dst = &mut out_slice[out_pos..];
2405        let (res, read, written) =
2406            encoder.encode_from_utf8_without_replacement(&s[in_pos..], dst, true);
2407        out_pos += written;
2408        in_pos += read;
2409        match res {
2410            encoding_rs::EncoderResult::InputEmpty => break,
2411            encoding_rs::EncoderResult::OutputFull => {
2412                // Output exhausted: report the partial conversion (with the
2413                // caller's >= 3x scratch this is unreachable for these
2414                // encodings on complete input).
2415                break;
2416            }
2417            encoding_rs::EncoderResult::Unmappable(c) => {
2418                // The encoder consumed the unrepresentable character `c`
2419                // (its UTF-8 bytes are the last len_utf8() bytes of the
2420                // consumed prefix), so rewind *inlen to point AT it:
2421                // char_enc_out substitutes the decimal character reference
2422                // for the character there and retries the remainder.
2423                *outlen = out_pos as c_int;
2424                *inlen = (in_pos - c.len_utf8()) as c_int;
2425                return ENC_INPUT_ERROR;
2426            }
2427        }
2428    }
2429
2430    if let Some(err) = error_at {
2431        if in_pos == s.len() {
2432            // The whole convertible prefix was converted; report the UTF-8
2433            // error at the offending byte (the trailing partial is not
2434            // converted).
2435            *outlen = out_pos as c_int;
2436            *inlen = err as c_int;
2437            return -1;
2438        }
2439    }
2440    *outlen = out_pos as c_int;
2441    *inlen = in_pos as c_int;
2442    out_pos as c_int
2443}
2444
2445/// Shared input conversion for the encoding_rs-backed East-Asian handlers
2446/// (`source` → UTF-8). Converts complete characters while output space
2447/// lasts; an undefined byte or an incomplete trailing sequence reports -1
2448/// with the bytes before the error in `*inlen` (iconv EILSEQ semantics —
2449/// deterministic and loop-free for the caller).
2450unsafe fn enc_rs_input(
2451    source: &'static encoding_rs::Encoding,
2452    out: *mut c_uchar,
2453    outlen: *mut c_int,
2454    in_: *const c_uchar,
2455    inlen: *mut c_int,
2456) -> c_int {
2457    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
2458        return -1;
2459    }
2460    let avail_in = *inlen as usize;
2461    let avail_out = *outlen as usize;
2462
2463    if avail_in == 0 || avail_out == 0 {
2464        *outlen = 0;
2465        *inlen = 0;
2466        return 0;
2467    }
2468
2469    let in_data = core::slice::from_raw_parts(in_, avail_in);
2470    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
2471
2472    let mut decoder = source.new_decoder_without_bom_handling();
2473    let mut in_pos: usize = 0;
2474    let mut out_pos: usize = 0;
2475    while in_pos < avail_in && out_pos < avail_out {
2476        let (res, read, written) = decoder.decode_to_utf8_without_replacement(
2477            &in_data[in_pos..],
2478            &mut out_slice[out_pos..],
2479            true,
2480        );
2481        out_pos += written;
2482        in_pos += read;
2483        match res {
2484            encoding_rs::DecoderResult::InputEmpty => break,
2485            encoding_rs::DecoderResult::OutputFull => break,
2486            encoding_rs::DecoderResult::Malformed(..) => {
2487                // Undefined byte or incomplete tail: hard error after the
2488                // complete prefix (iconv EILSEQ).
2489                *outlen = out_pos as c_int;
2490                *inlen = in_pos as c_int;
2491                return -1;
2492            }
2493        }
2494    }
2495
2496    *outlen = out_pos as c_int;
2497    *inlen = in_pos as c_int;
2498    out_pos as c_int
2499}
2500
2501/// Shift_JIS input function (CP932-compatible WHATWG Shift_JIS → UTF-8).
2502unsafe extern "C" fn shift_jis_input_func(
2503    out: *mut c_uchar,
2504    outlen: *mut c_int,
2505    in_: *const c_uchar,
2506    inlen: *mut c_int,
2507) -> c_int {
2508    enc_rs_input(encoding_rs::SHIFT_JIS, out, outlen, in_, inlen)
2509}
2510
2511/// Shift_JIS output function (UTF-8 → CP932-compatible WHATWG Shift_JIS).
2512unsafe extern "C" fn shift_jis_output_func(
2513    out: *mut c_uchar,
2514    outlen: *mut c_int,
2515    in_: *const c_uchar,
2516    inlen: *mut c_int,
2517) -> c_int {
2518    enc_rs_output(encoding_rs::SHIFT_JIS, out, outlen, in_, inlen)
2519}
2520
2521/// EUC-JP input function (EUC-JP → UTF-8).
2522unsafe extern "C" fn euc_jp_input_func(
2523    out: *mut c_uchar,
2524    outlen: *mut c_int,
2525    in_: *const c_uchar,
2526    inlen: *mut c_int,
2527) -> c_int {
2528    enc_rs_input(encoding_rs::EUC_JP, out, outlen, in_, inlen)
2529}
2530
2531/// EUC-JP output function (UTF-8 → EUC-JP).
2532unsafe extern "C" fn euc_jp_output_func(
2533    out: *mut c_uchar,
2534    outlen: *mut c_int,
2535    in_: *const c_uchar,
2536    inlen: *mut c_int,
2537) -> c_int {
2538    enc_rs_output(encoding_rs::EUC_JP, out, outlen, in_, inlen)
2539}
2540
2541// ── ISO-8859-2..11 / 13..16 + ISO-2022-JP (encoding_rs-backed, R-000157) ──
2542
2543/// Generate the input/output func pair for an encoding_rs single-byte or
2544/// stateful legacy encoding served by name (upstream: iconv).
2545macro_rules! define_enc_rs_codec {
2546    ($input_fn:ident, $output_fn:ident, $enc:expr) => {
2547        #[allow(dead_code)]
2548        unsafe extern "C" fn $input_fn(
2549            out: *mut c_uchar,
2550            outlen: *mut c_int,
2551            in_: *const c_uchar,
2552            inlen: *mut c_int,
2553        ) -> c_int {
2554            enc_rs_input($enc, out, outlen, in_, inlen)
2555        }
2556        #[allow(dead_code)]
2557        unsafe extern "C" fn $output_fn(
2558            out: *mut c_uchar,
2559            outlen: *mut c_int,
2560            in_: *const c_uchar,
2561            inlen: *mut c_int,
2562        ) -> c_int {
2563            enc_rs_output($enc, out, outlen, in_, inlen)
2564        }
2565    };
2566}
2567
2568define_enc_rs_codec!(
2569    iso_8859_2_input_func,
2570    iso_8859_2_output_func,
2571    encoding_rs::ISO_8859_2
2572);
2573define_enc_rs_codec!(
2574    iso_8859_3_input_func,
2575    iso_8859_3_output_func,
2576    encoding_rs::ISO_8859_3
2577);
2578define_enc_rs_codec!(
2579    iso_8859_4_input_func,
2580    iso_8859_4_output_func,
2581    encoding_rs::ISO_8859_4
2582);
2583define_enc_rs_codec!(
2584    iso_8859_5_input_func,
2585    iso_8859_5_output_func,
2586    encoding_rs::ISO_8859_5
2587);
2588define_enc_rs_codec!(
2589    iso_8859_6_input_func,
2590    iso_8859_6_output_func,
2591    encoding_rs::ISO_8859_6
2592);
2593define_enc_rs_codec!(
2594    iso_8859_7_input_func,
2595    iso_8859_7_output_func,
2596    encoding_rs::ISO_8859_7
2597);
2598define_enc_rs_codec!(
2599    iso_8859_8_input_func,
2600    iso_8859_8_output_func,
2601    encoding_rs::ISO_8859_8
2602);
2603define_enc_rs_codec!(
2604    iso_8859_9_input_func,
2605    iso_8859_9_output_func,
2606    encoding_rs::WINDOWS_1254
2607);
2608define_enc_rs_codec!(
2609    iso_8859_10_input_func,
2610    iso_8859_10_output_func,
2611    encoding_rs::ISO_8859_10
2612);
2613define_enc_rs_codec!(
2614    iso_8859_11_input_func,
2615    iso_8859_11_output_func,
2616    encoding_rs::WINDOWS_874
2617);
2618define_enc_rs_codec!(
2619    iso_8859_13_input_func,
2620    iso_8859_13_output_func,
2621    encoding_rs::ISO_8859_13
2622);
2623define_enc_rs_codec!(
2624    iso_8859_14_input_func,
2625    iso_8859_14_output_func,
2626    encoding_rs::ISO_8859_14
2627);
2628define_enc_rs_codec!(
2629    iso_8859_15_input_func,
2630    iso_8859_15_output_func,
2631    encoding_rs::ISO_8859_15
2632);
2633define_enc_rs_codec!(
2634    iso_8859_16_input_func,
2635    iso_8859_16_output_func,
2636    encoding_rs::ISO_8859_16
2637);
2638define_enc_rs_codec!(
2639    iso_2022_jp_input_func,
2640    iso_2022_jp_output_func,
2641    encoding_rs::ISO_2022_JP
2642);
2643
2644// ── UCS-2 / UCS-4 / EBCDIC (native codecs; R-000157 remainder) ────────────
2645
2646/// Shared fixed-width-input converter: 2/4-byte big/little-endian code
2647/// units → UTF-8. `width` is 2 (UCS-2) or 4 (UCS-4). Undefined code units
2648/// (surrogates for UCS-2, > U+10FFFF for UCS-4) report -1 after the complete
2649/// prefix (iconv EILSEQ); an incomplete trailing unit stops cleanly with the
2650/// complete prefix consumed (iconv EINVAL semantics — the caller owns the
2651/// tail).
2652unsafe fn fixed_width_input(
2653    le: bool,
2654    width: usize,
2655    out: *mut c_uchar,
2656    outlen: *mut c_int,
2657    in_: *const c_uchar,
2658    inlen: *mut c_int,
2659) -> c_int {
2660    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
2661        return -1;
2662    }
2663    let avail_in = *inlen as usize;
2664    let avail_out = *outlen as usize;
2665    if avail_in == 0 || avail_out == 0 {
2666        *outlen = 0;
2667        *inlen = 0;
2668        return 0;
2669    }
2670    let in_data = core::slice::from_raw_parts(in_, avail_in);
2671    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
2672    let mut in_pos = 0usize;
2673    let mut out_pos = 0usize;
2674    // A leading U+FEFF code unit (the UTF-32 LE/BE BOM, which as a unit is
2675    // 0x0000FEFF in both byte orders) is a byte-order marker, not content:
2676    // upstream consumes it in the encoding switch, so the parser must never
2677    // see it (mirrors the UTF-16 converters' BOM skip).
2678    if avail_in >= width {
2679        let mut first: u32 = 0;
2680        for k in 0..width {
2681            let b = in_data[k] as u32;
2682            first = if le {
2683                first | (b << (8 * k))
2684            } else {
2685                (first << 8) | b
2686            };
2687        }
2688        if first == 0x0000_FEFF {
2689            in_pos = width;
2690        }
2691    }
2692    while in_pos + width <= avail_in {
2693        let mut unit: u32 = 0;
2694        for k in 0..width {
2695            let b = in_data[in_pos + k] as u32;
2696            unit = if le {
2697                unit | (b << (8 * k))
2698            } else {
2699                (unit << 8) | b
2700            };
2701        }
2702        if unit > 0x10FFFF || (0xD800..=0xDFFF).contains(&unit) {
2703            // Undefined code unit: hard error after the complete prefix.
2704            *outlen = out_pos as c_int;
2705            *inlen = in_pos as c_int;
2706            return -1;
2707        }
2708        let mut buf = [0u8; 4];
2709        // SAFETY: unit <= 0x10FFFF and not a surrogate, so char::from_u32
2710        // succeeds.
2711        let ch = unsafe { char::from_u32_unchecked(unit) };
2712        let n = ch.encode_utf8(&mut buf).len();
2713        if out_pos + n > avail_out {
2714            break;
2715        }
2716        out_slice[out_pos..out_pos + n].copy_from_slice(&buf[..n]);
2717        out_pos += n;
2718        in_pos += width;
2719    }
2720    *outlen = out_pos as c_int;
2721    *inlen = in_pos as c_int;
2722    out_pos as c_int
2723}
2724
2725/// Shared fixed-width OUTPUT converter: UTF-8 → 2/4-byte big/little-endian
2726/// code units. A code point that does not fit the width (astral under UCS-2)
2727/// stops with the -2 input-error convention (charref substitution).
2728unsafe fn fixed_width_output(
2729    le: bool,
2730    width: usize,
2731    out: *mut c_uchar,
2732    outlen: *mut c_int,
2733    in_: *const c_uchar,
2734    inlen: *mut c_int,
2735) -> c_int {
2736    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
2737        return -1;
2738    }
2739    let avail_in = *inlen as usize;
2740    let avail_out = *outlen as usize;
2741    if avail_in == 0 || avail_out == 0 {
2742        *outlen = 0;
2743        *inlen = 0;
2744        return 0;
2745    }
2746    let in_data = core::slice::from_raw_parts(in_, avail_in);
2747    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
2748    let mut in_pos = 0usize;
2749    let mut out_pos = 0usize;
2750    while in_pos < avail_in {
2751        let (cp, consumed) = match decode_utf8_char(in_data, in_pos) {
2752            None => {
2753                // Invalid UTF-8 (or an incomplete trailing sequence): hard
2754                // error after the complete prefix (iconv EILSEQ/EINVAL).
2755                *outlen = out_pos as c_int;
2756                *inlen = in_pos as c_int;
2757                return -1;
2758            }
2759            Some(v) => v,
2760        };
2761        let max_cp = if width == 2 { 0xFFFF } else { 0x10FFFF };
2762        if cp > max_cp {
2763            // Not representable at this width (astral under UCS-2): stop
2764            // BEFORE it — char_enc_out substitutes the decimal charref.
2765            *outlen = out_pos as c_int;
2766            *inlen = in_pos as c_int;
2767            return ENC_INPUT_ERROR;
2768        }
2769        if out_pos + width > avail_out {
2770            break;
2771        }
2772        for k in 0..width {
2773            let shift = 8 * if le { k } else { width - 1 - k };
2774            out_slice[out_pos + k] = ((cp >> shift) & 0xFF) as u8;
2775        }
2776        out_pos += width;
2777        in_pos += consumed;
2778    }
2779    *outlen = out_pos as c_int;
2780    *inlen = in_pos as c_int;
2781    out_pos as c_int
2782}
2783
2784/// UCS-2 input (2-byte units → UTF-8). glibc iconv "UCS-2" uses the host
2785/// byte order (little-endian on the executed x86-64 oracle), so the codec
2786/// is little-endian to match.
2787unsafe extern "C" fn ucs2_input_func(
2788    out: *mut c_uchar,
2789    outlen: *mut c_int,
2790    in_: *const c_uchar,
2791    inlen: *mut c_int,
2792) -> c_int {
2793    fixed_width_input(true, 2, out, outlen, in_, inlen)
2794}
2795
2796/// UCS-2 output (UTF-8 → 2-byte little-endian units).
2797unsafe extern "C" fn ucs2_output_func(
2798    out: *mut c_uchar,
2799    outlen: *mut c_int,
2800    in_: *const c_uchar,
2801    inlen: *mut c_int,
2802) -> c_int {
2803    fixed_width_output(true, 2, out, outlen, in_, inlen)
2804}
2805
2806/// UCS-4LE input (4-byte little-endian units → UTF-8).
2807unsafe extern "C" fn ucs4le_input_func(
2808    out: *mut c_uchar,
2809    outlen: *mut c_int,
2810    in_: *const c_uchar,
2811    inlen: *mut c_int,
2812) -> c_int {
2813    fixed_width_input(true, 4, out, outlen, in_, inlen)
2814}
2815
2816/// UCS-4LE output (UTF-8 → 4-byte little-endian units).
2817unsafe extern "C" fn ucs4le_output_func(
2818    out: *mut c_uchar,
2819    outlen: *mut c_int,
2820    in_: *const c_uchar,
2821    inlen: *mut c_int,
2822) -> c_int {
2823    fixed_width_output(true, 4, out, outlen, in_, inlen)
2824}
2825
2826/// UCS-4BE input (4-byte big-endian units → UTF-8).
2827unsafe extern "C" fn ucs4be_input_func(
2828    out: *mut c_uchar,
2829    outlen: *mut c_int,
2830    in_: *const c_uchar,
2831    inlen: *mut c_int,
2832) -> c_int {
2833    fixed_width_input(false, 4, out, outlen, in_, inlen)
2834}
2835
2836/// UCS-4BE output (UTF-8 → 4-byte big-endian units).
2837unsafe extern "C" fn ucs4be_output_func(
2838    out: *mut c_uchar,
2839    outlen: *mut c_int,
2840    in_: *const c_uchar,
2841    inlen: *mut c_int,
2842) -> c_int {
2843    fixed_width_output(false, 4, out, outlen, in_, inlen)
2844}
2845
2846/// EBCDIC code page 037 → Unicode (derived from the oracle container's glibc
2847/// iconv IBM037 table: byte i maps to EBCDIC037_TO_UNICODE[i]; the mapping is
2848/// a bijection onto U+0000..U+00FF).
2849const EBCDIC037_TO_UNICODE: [u16; 256] = [
2850    0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F, 0x0097, 0x008D, 0x008E, 0x000B,
2851    0x000C, 0x000D, 0x000E, 0x000F, 0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
2852    0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F, 0x0080, 0x0081, 0x0082, 0x0083,
2853    0x0084, 0x000A, 0x0017, 0x001B, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
2854    0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004, 0x0098, 0x0099, 0x009A, 0x009B,
2855    0x0014, 0x0015, 0x009E, 0x001A, 0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
2856    0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C, 0x0026, 0x00E9, 0x00EA, 0x00EB,
2857    0x00E8, 0x00ED, 0x00EE, 0x00EF, 0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x00AC,
2858    0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5, 0x00C7, 0x00D1, 0x00A6, 0x002C,
2859    0x0025, 0x005F, 0x003E, 0x003F, 0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
2860    0x00CC, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022, 0x00D8, 0x0061, 0x0062, 0x0063,
2861    0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
2862    0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070, 0x0071, 0x0072, 0x00AA, 0x00BA,
2863    0x00E6, 0x00B8, 0x00C6, 0x00A4, 0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
2864    0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE, 0x005E, 0x00A3, 0x00A5, 0x00B7,
2865    0x00A9, 0x00A7, 0x00B6, 0x00BC, 0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
2866    0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x00AD, 0x00F4,
2867    0x00F6, 0x00F2, 0x00F3, 0x00F5, 0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
2868    0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF, 0x005C, 0x00F7, 0x0053, 0x0054,
2869    0x0055, 0x0056, 0x0057, 0x0058, 0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
2870    0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, 0x00B3, 0x00DB,
2871    0x00DC, 0x00D9, 0x00DA, 0x009F,
2872];
2873
2874/// Reverse lookup: Unicode code point → EBCDIC 037 byte. The forward table
2875/// is a bijection onto U+0000..U+00FF, so any cp <= 0xFF resolves.
2876const fn ebcdic037_cp_to_byte(cp: u32) -> Option<u8> {
2877    if cp > 0xFF {
2878        return None;
2879    }
2880    let mut i = 0;
2881    while i < 256 {
2882        if EBCDIC037_TO_UNICODE[i] as u32 == cp {
2883            return Some(i as u8);
2884        }
2885        i += 1;
2886    }
2887    None
2888}
2889
2890/// EBCDIC (IBM037) input: bytes → UTF-8 via the 037 table.
2891unsafe extern "C" fn ebcdic_input_func(
2892    out: *mut c_uchar,
2893    outlen: *mut c_int,
2894    in_: *const c_uchar,
2895    inlen: *mut c_int,
2896) -> c_int {
2897    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
2898        return -1;
2899    }
2900    let avail_in = *inlen as usize;
2901    let avail_out = *outlen as usize;
2902    if avail_in == 0 || avail_out == 0 {
2903        *outlen = 0;
2904        *inlen = 0;
2905        return 0;
2906    }
2907    let in_data = core::slice::from_raw_parts(in_, avail_in);
2908    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
2909    let mut in_pos = 0usize;
2910    let mut out_pos = 0usize;
2911    while in_pos < avail_in {
2912        let cp = u32::from(EBCDIC037_TO_UNICODE[in_data[in_pos] as usize]);
2913        let mut buf = [0u8; 2];
2914        let ch = unsafe { char::from_u32_unchecked(cp) };
2915        let n = ch.encode_utf8(&mut buf).len();
2916        if out_pos + n > avail_out {
2917            break;
2918        }
2919        out_slice[out_pos..out_pos + n].copy_from_slice(&buf[..n]);
2920        out_pos += n;
2921        in_pos += 1;
2922    }
2923    *outlen = out_pos as c_int;
2924    *inlen = in_pos as c_int;
2925    out_pos as c_int
2926}
2927
2928/// EBCDIC (IBM037) output: UTF-8 → 037 bytes. Unmappable code points
2929/// (> U+00FF) stop with the -2 input-error convention (charref).
2930unsafe extern "C" fn ebcdic_output_func(
2931    out: *mut c_uchar,
2932    outlen: *mut c_int,
2933    in_: *const c_uchar,
2934    inlen: *mut c_int,
2935) -> c_int {
2936    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
2937        return -1;
2938    }
2939    let avail_in = *inlen as usize;
2940    let avail_out = *outlen as usize;
2941    if avail_in == 0 || avail_out == 0 {
2942        *outlen = 0;
2943        *inlen = 0;
2944        return 0;
2945    }
2946    let in_data = core::slice::from_raw_parts(in_, avail_in);
2947    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
2948    let mut in_pos = 0usize;
2949    let mut out_pos = 0usize;
2950    while in_pos < avail_in {
2951        let (cp, consumed) = match decode_utf8_char(in_data, in_pos) {
2952            None => {
2953                *outlen = out_pos as c_int;
2954                *inlen = in_pos as c_int;
2955                return -1;
2956            }
2957            Some(v) => v,
2958        };
2959        match ebcdic037_cp_to_byte(cp) {
2960            None => {
2961                *outlen = out_pos as c_int;
2962                *inlen = in_pos as c_int;
2963                return ENC_INPUT_ERROR;
2964            }
2965            Some(byte) => {
2966                if out_pos + 1 > avail_out {
2967                    break;
2968                }
2969                out_slice[out_pos] = byte;
2970                out_pos += 1;
2971            }
2972        }
2973        in_pos += consumed;
2974    }
2975    *outlen = out_pos as c_int;
2976    *inlen = in_pos as c_int;
2977    out_pos as c_int
2978}
2979
2980// ═══════════════════════════════════════════════════════════════════════════════
2981// 8. ABI export functions (called from exports_xml2.rs)
2982// ═══════════════════════════════════════════════════════════════════════════════
2983
2984/// `xmlFindCharEncodingHandler` implementation.
2985///
2986/// Finds an encoding handler by name. Returns a pointer to the handler,
2987/// or `ptr::null_mut()` if not found.
2988pub(crate) fn xmlFindCharEncodingHandler(name: *const c_char) -> *mut _xmlCharEncodingHandler {
2989    if name.is_null() {
2990        return ptr::null_mut();
2991    }
2992    find_encoding_handler(name as *const xmlChar)
2993}
2994
2995/// `xmlGetCharEncodingName` implementation.
2996///
2997/// Returns the canonical name for an encoding, or `ptr::null()` if unknown.
2998pub(crate) const fn xmlGetCharEncodingName(enc: xmlCharEncoding) -> *const c_char {
2999    // Return null-terminated C strings using static CStr literals.
3000    // Mirrors upstream 2.15 xmlGetCharEncodingName: the UTF-16/UCS-4 pairs
3001    // return the W3C canonical names before the defaultHandlers table.
3002    match enc {
3003        xmlCharEncoding::XML_CHAR_ENCODING_UTF8 => c"UTF-8".as_ptr(),
3004        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE | xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE => {
3005            c"UTF-16".as_ptr()
3006        }
3007        xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE | xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE => {
3008            c"UCS-4".as_ptr()
3009        }
3010        xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC => c"IBM037".as_ptr(),
3011        xmlCharEncoding::XML_CHAR_ENCODING_UCS2 => c"UCS-2".as_ptr(),
3012        xmlCharEncoding::XML_CHAR_ENCODING_8859_1 => c"ISO-8859-1".as_ptr(),
3013        xmlCharEncoding::XML_CHAR_ENCODING_8859_2 => c"ISO-8859-2".as_ptr(),
3014        xmlCharEncoding::XML_CHAR_ENCODING_8859_3 => c"ISO-8859-3".as_ptr(),
3015        xmlCharEncoding::XML_CHAR_ENCODING_8859_4 => c"ISO-8859-4".as_ptr(),
3016        xmlCharEncoding::XML_CHAR_ENCODING_8859_5 => c"ISO-8859-5".as_ptr(),
3017        xmlCharEncoding::XML_CHAR_ENCODING_8859_6 => c"ISO-8859-6".as_ptr(),
3018        xmlCharEncoding::XML_CHAR_ENCODING_8859_7 => c"ISO-8859-7".as_ptr(),
3019        xmlCharEncoding::XML_CHAR_ENCODING_8859_8 => c"ISO-8859-8".as_ptr(),
3020        xmlCharEncoding::XML_CHAR_ENCODING_8859_9 => c"ISO-8859-9".as_ptr(),
3021        xmlCharEncoding::XML_CHAR_ENCODING_2022_JP => c"ISO-2022-JP".as_ptr(),
3022        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS => c"Shift_JIS".as_ptr(),
3023        xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP => c"EUC-JP".as_ptr(),
3024        // upstream defaultHandlers[22].name
3025        xmlCharEncoding::XML_CHAR_ENCODING_ASCII => c"US-ASCII".as_ptr(),
3026        _ => ptr::null(),
3027    }
3028}
3029
3030/// `xmlParseCharEncoding` implementation.
3031///
3032/// Parses an encoding name string to an `xmlCharEncoding` enum value,
3033/// returned as `c_int`.
3034///
3035/// # Safety
3036///
3037/// - `name` must be NULL or a valid pointer to a NUL-terminated string.
3038pub(crate) fn xmlParseCharEncoding(name: *const c_char) -> c_int {
3039    if name.is_null() {
3040        return xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int;
3041    }
3042    let bytes = unsafe { CStr::from_ptr(name).to_bytes() };
3043    encoding_from_name(bytes) as c_int
3044}
3045
3046// ── Encoding aliases (upstream encoding.c xmlAddEncodingAlias etc.) ──────────
3047//
3048// A global alias table maps alias names to canonical encoding names.
3049// Upstream keeps a static hash of aliases; the candidate uses a
3050// process-lifetime RwLock<HashMap>. Thread-safe; matches upstream's
3051// observable contract (add/del/get by name).
3052
3053static ENCODING_ALIASES: std::sync::OnceLock<
3054    parking_lot::RwLock<std::collections::HashMap<Vec<u8>, Vec<u8>>>,
3055> = std::sync::OnceLock::new();
3056
3057fn encoding_aliases() -> &'static parking_lot::RwLock<std::collections::HashMap<Vec<u8>, Vec<u8>>> {
3058    ENCODING_ALIASES.get_or_init(|| parking_lot::RwLock::new(std::collections::HashMap::new()))
3059}
3060
3061/// `xmlAddEncodingAlias` implementation: register `alias` for `name`.
3062/// Returns 0 on success, -1 on error (NULL arguments).
3063///
3064/// # Safety
3065///
3066/// - `name` and `alias` must be NULL or valid pointers to NUL-terminated
3067///   strings; both are copied before insertion into the alias table.
3068pub(crate) fn add_encoding_alias(name: *const c_char, alias: *const c_char) -> c_int {
3069    if name.is_null() || alias.is_null() {
3070        return -1;
3071    }
3072    let n = unsafe { CStr::from_ptr(name).to_bytes().to_vec() };
3073    let a = unsafe { CStr::from_ptr(alias).to_bytes().to_vec() };
3074    encoding_aliases().write().insert(a, n);
3075    0
3076}
3077
3078/// `xmlDelEncodingAlias` implementation: remove `alias`.
3079/// Returns 0 on success, -1 if the alias does not exist.
3080///
3081/// # Safety
3082///
3083/// - `alias` must be NULL or a valid pointer to a NUL-terminated string.
3084pub(crate) fn del_encoding_alias(alias: *const c_char) -> c_int {
3085    if alias.is_null() {
3086        return -1;
3087    }
3088    let a = unsafe { CStr::from_ptr(alias).to_bytes().to_vec() };
3089    if encoding_aliases().write().remove(&a).is_some() {
3090        0
3091    } else {
3092        -1
3093    }
3094}
3095
3096/// `xmlGetEncodingAlias` implementation: return the canonical name for
3097/// `alias`, or NULL when not registered.
3098///
3099/// # Safety
3100///
3101/// - `alias` must be NULL or a valid pointer to a NUL-terminated string.
3102/// - The returned pointer is a leaked, process-lifetime NUL-terminated
3103///   string, or NULL; the caller must not free it.
3104pub(crate) fn get_encoding_alias(alias: *const c_char) -> *const c_char {
3105    if alias.is_null() {
3106        return ptr::null();
3107    }
3108    let a = unsafe { CStr::from_ptr(alias).to_bytes().to_vec() };
3109    let guard = encoding_aliases().read();
3110    match guard.get(&a) {
3111        Some(v) => {
3112            // leak the canonical name: upstream returns a pointer valid for
3113            // the process lifetime (the alias hash owns the strings)
3114            let leaked: &'static [u8] = Box::leak(v.clone().into_boxed_slice());
3115            leaked.as_ptr() as *const c_char
3116        }
3117        None => ptr::null(),
3118    }
3119}
3120
3121/// `xmlCleanupEncodingAliases` implementation: drop all aliases.
3122pub(crate) fn cleanup_encoding_aliases() {
3123    encoding_aliases().write().clear();
3124}
3125
3126/// `xmlCharEncInFunc` implementation.
3127///
3128/// Converts the input buffer's encoding to UTF-8 using the given handler.
3129pub(crate) fn xmlCharEncInFunc(
3130    handler: *mut _xmlCharEncodingHandler,
3131    out: *mut _xmlBuffer,
3132    in_: *mut _xmlBuffer,
3133) -> c_int {
3134    char_enc_in(handler, out, in_)
3135}
3136
3137/// `xmlCharEncOutFunc` implementation.
3138///
3139/// Converts the input buffer from UTF-8 to the handler's output encoding.
3140pub(crate) fn xmlCharEncOutFunc(
3141    handler: *mut _xmlCharEncodingHandler,
3142    out: *mut _xmlBuffer,
3143    in_: *mut _xmlBuffer,
3144) -> c_int {
3145    char_enc_out(handler, out, in_)
3146}
3147
3148/// `xmlNewCharEncodingHandler` implementation.
3149///
3150/// Creates a new encoding handler with the given name and conversion functions.
3151/// The name string is duplicated. Returns a pointer to the new handler,
3152/// or `ptr::null_mut()` on allocation failure.
3153///
3154/// # Safety
3155///
3156/// - `name` must be NULL or a valid pointer to a NUL-terminated string that
3157///   stays valid until it is duplicated.
3158/// - `input` and `output` must be valid function pointers matching the
3159///   callback ABI; on success the returned handler owns a duplicated name
3160///   and must be released with `xmlDelEncodingHandler`.
3161pub(crate) fn xmlNewCharEncodingHandler(
3162    name: *const c_char,
3163    input: xmlCharEncodingInputFunc,
3164    output: xmlCharEncodingOutputFunc,
3165) -> *mut _xmlCharEncodingHandler {
3166    if name.is_null() {
3167        return ptr::null_mut();
3168    }
3169
3170    let name_raw = unsafe { crate::abi::allocator::xmlMemStrdupImpl(name) };
3171    if name_raw.is_null() {
3172        return ptr::null_mut();
3173    }
3174
3175    let handler = unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) }
3176        as *mut _xmlCharEncodingHandler;
3177
3178    if handler.is_null() {
3179        unsafe { xmlFreeImpl(name_raw) };
3180        return ptr::null_mut();
3181    }
3182
3183    unsafe {
3184        ptr::write(
3185            handler,
3186            _xmlCharEncodingHandler {
3187                name: name_raw as *mut c_char,
3188                input: EncodingInputUnion {
3189                    legacyFunc: Some(input),
3190                },
3191                output: EncodingOutputUnion {
3192                    legacyFunc: Some(output),
3193                },
3194                inputCtxt: ptr::null_mut(),
3195                outputCtxt: ptr::null_mut(),
3196                ctxtDtor: None,
3197                flags: 0,
3198            },
3199        );
3200    }
3201
3202    handler
3203}
3204
3205/// `xmlDelEncodingHandler` implementation.
3206///
3207/// Frees an encoding handler previously created with `xmlNewCharEncodingHandler`.
3208///
3209/// # Safety
3210///
3211/// - `handler` must be NULL or a valid heap-allocated
3212///   `_xmlCharEncodingHandler` whose `name` is NULL or a heap-allocated
3213///   NUL-terminated string; both allocations are freed exactly once, and the
3214///   handler must have been removed from the registry.
3215#[allow(dead_code)]
3216pub(crate) fn xmlDelEncodingHandler(handler: *mut _xmlCharEncodingHandler) {
3217    if handler.is_null() {
3218        return;
3219    }
3220
3221    // Remove from registry if present
3222    {
3223        let mut handlers = ENCODING_HANDLERS.write();
3224        handlers.retain(|&h| h.0 != handler);
3225    }
3226
3227    unsafe {
3228        if !(*handler).name.is_null() {
3229            xmlFreeImpl((*handler).name as *mut c_void);
3230        }
3231        xmlFreeImpl(handler as *mut c_void);
3232    }
3233}
3234
3235/// `xmlInitCharEncodingHandlers` implementation.
3236pub(crate) fn xmlInitCharEncodingHandlers() {
3237    init_encodings();
3238}
3239
3240/// `xmlCleanupCharEncodingHandlers` implementation.
3241pub(crate) fn xmlCleanupCharEncodingHandlers() {
3242    cleanup_encodings();
3243}
3244
3245// ═══════════════════════════════════════════════════════════════════════════════
3246// 7. Handler lookup / creation (upstream 2.13.0+ encoding.c)
3247// ═══════════════════════════════════════════════════════════════════════════════
3248//
3249// Upstream keeps a static `defaultHandlers[32]` table indexed by xmlCharEncoding
3250// plus iconv/ICU fallbacks. The candidate ships no iconv/ICU, so encodings whose
3251// upstream default handler carries a real converter (UTF-8, UTF-16LE, UTF-16BE,
3252// UTF-16, ISO-8859-1, US-ASCII) resolve to the registered built-in handlers;
3253// every other encoding reports XML_ERR_UNSUPPORTED_ENCODING exactly where
3254// upstream would fall through to iconv/ICU.
3255
3256/// `xmlLookupCharEncodingHandler` implementation (upstream encoding.c).
3257///
3258/// Mirrors the upstream control flow:
3259///  - `out == NULL`                     → XML_ERR_ARGUMENT (115)
3260///  - `enc <= 0 || enc >= 32`           → XML_ERR_UNSUPPORTED_ENCODING (32)
3261///  - UTF-8                             → XML_ERR_OK, `*out` stays NULL
3262///  - native built-in encoding          → XML_ERR_OK, `*out` = static handler
3263///  - iconv/ICU-only encoding           → XML_ERR_UNSUPPORTED_ENCODING
3264///
3265/// The returned handler is a static registry entry and must NOT be freed.
3266///
3267/// # Safety
3268///
3269/// - `out` must be a valid pointer to a `*mut c_void` out-parameter; it is
3270///   written with NULL or a pointer to a static registry handler that the
3271///   caller must not free.
3272pub(crate) fn xmlLookupCharEncodingHandler(enc: c_int, out: *mut *mut c_void) -> c_int {
3273    if out.is_null() {
3274        return crate::abi::types::XML_ERR_ARGUMENT;
3275    }
3276    unsafe {
3277        *out = ptr::null_mut();
3278    }
3279    if enc <= 0 || enc >= 32 {
3280        return crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING;
3281    }
3282    /* Return NULL handler for UTF-8 */
3283    if enc == xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int {
3284        return crate::abi::types::XML_ERR_OK;
3285    }
3286    let canonical: &[u8] = match enc {
3287        /* XML_CHAR_ENCODING_UTF16LE */
3288        2 => b"UTF-16LE\0",
3289        /* XML_CHAR_ENCODING_UTF16BE */
3290        3 => b"UTF-16BE\0",
3291        /* XML_CHAR_ENCODING_8859_1 */
3292        10 => b"ISO-8859-1\0",
3293        /* XML_CHAR_ENCODING_ASCII */
3294        22 => b"US-ASCII\0",
3295        /* XML_CHAR_ENCODING_UTF16 (not in the local enum) */
3296        23 => b"UTF-16\0",
3297        _ => return crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING,
3298    };
3299    let h = find_encoding_handler(canonical.as_ptr() as *const xmlChar);
3300    if h.is_null() {
3301        return crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING;
3302    }
3303    unsafe {
3304        *out = h as *mut c_void;
3305    }
3306    crate::abi::types::XML_ERR_OK
3307}
3308
3309/// `xmlGetCharEncodingHandler` implementation (deprecated upstream wrapper).
3310pub(crate) fn xmlGetCharEncodingHandler(enc: c_int) -> *mut c_void {
3311    let mut ret: *mut c_void = ptr::null_mut();
3312    let _rc = xmlLookupCharEncodingHandler(enc, &mut ret);
3313    ret
3314}
3315
3316/// `xmlCreateCharEncodingHandler` implementation (upstream 2.14.0+ encoding.c).
3317///
3318/// Flags: XML_ENC_INPUT = 1, XML_ENC_OUTPUT = 2, XML_ENC_HTML = 4.
3319/// Unlike upstream, no iconv/ICU backend exists, so encodings without a native
3320/// converter fall through to `find_extra_handler` (custom impl / deprecated
3321/// global registry) and otherwise report XML_ERR_UNSUPPORTED_ENCODING.
3322///
3323/// # Safety
3324///
3325/// - `out` must be a valid pointer to a `*mut c_void` out-parameter; it is
3326///   written with NULL or a heap-allocated handler copy the caller owns.
3327/// - `name` must be NULL or a valid pointer to a NUL-terminated string.
3328/// - `implCtxt` is an opaque context forwarded to `find_extra_handler` and
3329///   must be valid for the callback that consumes it.
3330pub(crate) fn xmlCreateCharEncodingHandler(
3331    name: *const c_char,
3332    flags: c_int,
3333    impl_: Option<xmlCharEncConvImpl>,
3334    implCtxt: *mut c_void,
3335    out: *mut *mut c_void,
3336) -> c_int {
3337    if out.is_null() {
3338        return crate::abi::types::XML_ERR_ARGUMENT;
3339    }
3340    unsafe {
3341        *out = ptr::null_mut();
3342    }
3343    if name.is_null() || flags == 0 {
3344        return crate::abi::types::XML_ERR_ARGUMENT;
3345    }
3346    let norig = unsafe { CStr::from_ptr(name).to_bytes() };
3347
3348    /* Alias resolution (upstream xmlGetEncodingAlias). */
3349    let mut eff: &[u8] = norig;
3350    let alias = get_encoding_alias(name);
3351    if !alias.is_null() {
3352        eff = unsafe { CStr::from_ptr(alias).to_bytes() };
3353    }
3354
3355    let enc = encoding_from_name(eff);
3356
3357    /* Return NULL handler for UTF-8 */
3358    if enc == xmlCharEncoding::XML_CHAR_ENCODING_UTF8 {
3359        return crate::abi::types::XML_ERR_OK;
3360    }
3361
3362    let canonical: &[u8] = match enc {
3363        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE => b"UTF-16LE\0",
3364        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE => b"UTF-16BE\0",
3365        xmlCharEncoding::XML_CHAR_ENCODING_8859_1 => b"ISO-8859-1\0",
3366        xmlCharEncoding::XML_CHAR_ENCODING_ASCII => b"US-ASCII\0",
3367        _ => {
3368            return find_extra_handler(norig, eff, flags, impl_, implCtxt, out);
3369        }
3370    };
3371    let h = find_encoding_handler(canonical.as_ptr() as *const xmlChar);
3372    if h.is_null() {
3373        return find_extra_handler(norig, eff, flags, impl_, implCtxt, out);
3374    }
3375    unsafe {
3376        let src = &*h;
3377        let has_in = (flags & 1) == 0 || !src.input.legacyFunc.is_none();
3378        let has_out = (flags & 2) == 0 || !src.output.legacyFunc.is_none();
3379        if !has_in || !has_out {
3380            return find_extra_handler(norig, eff, flags, impl_, implCtxt, out);
3381        }
3382        /*
3383         * Return a copy of the handler with the original name (upstream
3384         * "Return a copy of the handler with the original name").
3385         */
3386        let copy =
3387            xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) as *mut _xmlCharEncodingHandler;
3388        if copy.is_null() {
3389            return crate::abi::types::XML_ERR_NO_MEMORY;
3390        }
3391        let name_copy = crate::abi::allocator::xmlMemStrdupImpl(name) as *mut c_char;
3392        if name_copy.is_null() {
3393            xmlFreeImpl(copy as *mut c_void);
3394            return crate::abi::types::XML_ERR_NO_MEMORY;
3395        }
3396        ptr::write(
3397            copy,
3398            _xmlCharEncodingHandler {
3399                name: name_copy,
3400                input: EncodingInputUnion {
3401                    legacyFunc: src.input.legacyFunc,
3402                },
3403                output: EncodingOutputUnion {
3404                    legacyFunc: src.output.legacyFunc,
3405                },
3406                inputCtxt: src.inputCtxt,
3407                outputCtxt: src.outputCtxt,
3408                ctxtDtor: src.ctxtDtor,
3409                flags: src.flags,
3410            },
3411        );
3412        *out = copy as *mut c_void;
3413    }
3414    crate::abi::types::XML_ERR_OK
3415}
3416
3417/// Fallback path of `xmlCreateCharEncodingHandler` (upstream `xmlFindExtraHandler`).
3418///
3419/// Tries the caller-supplied custom implementation first, then the deprecated
3420/// global handler registry. iconv/ICU do not exist in the candidate, so the
3421/// final result is XML_ERR_UNSUPPORTED_ENCODING.
3422///
3423/// # Safety
3424///
3425/// - `norig` and `name` must be valid byte slices; NUL-terminated copies are
3426///   built from them for lookups and callbacks.
3427/// - `out` must be a valid out-parameter; it is written with NULL or a
3428///   registry handler pointer that must not be freed.
3429/// - `implCtxt` must be a valid context for the custom `impl_` callback when
3430///   one is supplied.
3431fn find_extra_handler(
3432    norig: &[u8],
3433    name: &[u8],
3434    flags: c_int,
3435    impl_: Option<xmlCharEncConvImpl>,
3436    implCtxt: *mut c_void,
3437    out: *mut *mut c_void,
3438) -> c_int {
3439    /* Custom implementation before deprecated global handlers. */
3440    if let Some(f) = impl_ {
3441        let mut n = norig.to_vec();
3442        n.push(0);
3443        let rc = unsafe {
3444            f(
3445                implCtxt,
3446                n.as_ptr() as *const c_char,
3447                flags,
3448                out as *mut *mut crate::abi::structs::_xmlCharEncodingHandler,
3449            )
3450        };
3451        return rc;
3452    }
3453    /* Deprecated global handlers registry (xmlRegisterCharEncodingHandler). */
3454    let mut n = name.to_vec();
3455    n.push(0);
3456    let h = find_encoding_handler(n.as_ptr() as *const xmlChar);
3457    if !h.is_null() {
3458        unsafe {
3459            let src = &*h;
3460            let has_in = (flags & 1) == 0 || !src.input.legacyFunc.is_none();
3461            let has_out = (flags & 2) == 0 || !src.output.legacyFunc.is_none();
3462            if has_in && has_out {
3463                *out = h as *mut c_void;
3464                return crate::abi::types::XML_ERR_OK;
3465            }
3466        }
3467    }
3468    crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING
3469}
3470
3471/// `xmlOpenCharEncodingHandler` implementation (upstream encoding.c).
3472pub(crate) fn xmlOpenCharEncodingHandler(
3473    name: *const c_char,
3474    output: c_int,
3475    out: *mut *mut c_void,
3476) -> c_int {
3477    /* XML_ENC_OUTPUT if output else XML_ENC_INPUT */
3478    let flags: c_int = if output != 0 { 2 } else { 1 };
3479    xmlCreateCharEncodingHandler(name, flags, None, ptr::null_mut(), out)
3480}
3481
3482/// `xmlCharEncNewCustomHandler` implementation (upstream 2.15.0+ encoding.c).
3483///
3484/// Creates a handler backed by modern `xmlCharEncConvFunc` callbacks (with
3485/// per-direction contexts and a context destructor). The handler must be
3486/// released with `xmlCharEncCloseFunc`.
3487///
3488/// # Safety
3489///
3490/// - `out` must be a valid pointer to a `*mut c_void` out-parameter.
3491/// - `name` must be NULL or a valid pointer to a NUL-terminated string.
3492/// - `input` and `output` must be valid `xmlCharEncConvFunc` callbacks;
3493///   `inputCtxt` and `outputCtxt` are opaque contexts consumed by them and
3494///   by `ctxtDtor`, which is invoked on each non-NULL context when
3495///   allocation fails (and later by `xmlCharEncCloseFunc`).
3496pub(crate) fn xmlCharEncNewCustomHandler(
3497    name: *const c_char,
3498    input: xmlCharEncConvFunc,
3499    output: xmlCharEncConvFunc,
3500    ctxtDtor: Option<xmlCharEncConvCtxtDtor>,
3501    inputCtxt: *mut c_void,
3502    outputCtxt: *mut c_void,
3503    out: *mut *mut c_void,
3504) -> c_int {
3505    if out.is_null() {
3506        return crate::abi::types::XML_ERR_ARGUMENT;
3507    }
3508    let handler = unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) }
3509        as *mut _xmlCharEncodingHandler;
3510    if handler.is_null() {
3511        unsafe {
3512            if let Some(d) = ctxtDtor {
3513                if !inputCtxt.is_null() {
3514                    d(inputCtxt);
3515                }
3516                if !outputCtxt.is_null() {
3517                    d(outputCtxt);
3518                }
3519            }
3520        }
3521        return crate::abi::types::XML_ERR_NO_MEMORY;
3522    }
3523    let name_copy = if name.is_null() {
3524        ptr::null_mut()
3525    } else {
3526        let nc = unsafe { crate::abi::allocator::xmlMemStrdupImpl(name) } as *mut c_char;
3527        if nc.is_null() {
3528            unsafe { xmlFreeImpl(handler as *mut c_void) };
3529            unsafe {
3530                if let Some(d) = ctxtDtor {
3531                    if !inputCtxt.is_null() {
3532                        d(inputCtxt);
3533                    }
3534                    if !outputCtxt.is_null() {
3535                        d(outputCtxt);
3536                    }
3537                }
3538            }
3539            return crate::abi::types::XML_ERR_NO_MEMORY;
3540        }
3541        nc
3542    };
3543    unsafe {
3544        ptr::write(
3545            handler,
3546            _xmlCharEncodingHandler {
3547                name: name_copy,
3548                input: EncodingInputUnion { func: Some(input) },
3549                output: EncodingOutputUnion { func: Some(output) },
3550                inputCtxt,
3551                outputCtxt,
3552                ctxtDtor,
3553                flags: 0,
3554            },
3555        );
3556        *out = handler as *mut c_void;
3557    }
3558    crate::abi::types::XML_ERR_OK
3559}
3560
3561// ═══════════════════════════════════════════════════════════════════════════════
3562// Tests
3563// ═══════════════════════════════════════════════════════════════════════════════
3564
3565#[cfg(test)]
3566mod tests {
3567    use super::*;
3568
3569    // ── BOM detection ──────────────────────────────────────────────────────
3570
3571    #[test]
3572    fn test_detect_bom_utf8() {
3573        let data = [0xEF, 0xBB, 0xBF, b'<', b'?', b'x', b'm', b'l'];
3574        assert_eq!(
3575            detect_encoding_from_bom(&data),
3576            xmlCharEncoding::XML_CHAR_ENCODING_UTF8
3577        );
3578    }
3579
3580    #[test]
3581    fn test_detect_bom_utf16le() {
3582        let data = [0xFF, 0xFE, 0x00, 0x01];
3583        assert_eq!(
3584            detect_encoding_from_bom(&data),
3585            xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
3586        );
3587    }
3588
3589    #[test]
3590    fn test_detect_bom_utf16be() {
3591        let data = [0xFE, 0xFF, 0x00, 0x01];
3592        assert_eq!(
3593            detect_encoding_from_bom(&data),
3594            xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE
3595        );
3596    }
3597
3598    #[test]
3599    fn test_detect_bom_none() {
3600        let data = b"<xml>";
3601        assert_eq!(
3602            detect_encoding_from_bom(data),
3603            xmlCharEncoding::XML_CHAR_ENCODING_NONE
3604        );
3605    }
3606
3607    #[test]
3608    fn test_detect_bom_empty() {
3609        assert_eq!(
3610            detect_encoding_from_bom(b""),
3611            xmlCharEncoding::XML_CHAR_ENCODING_NONE
3612        );
3613    }
3614
3615    // ── Encoding from declaration ──────────────────────────────────────────
3616
3617    #[test]
3618    fn test_detect_encoding_declaration_utf8() {
3619        let data = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
3620        let result = detect_encoding_from_declaration(data);
3621        assert_eq!(result, Some(b"utf-8".to_vec()));
3622    }
3623
3624    #[test]
3625    fn test_detect_encoding_declaration_iso() {
3626        let data = b"<?xml version='1.0' encoding='ISO-8859-1'?>";
3627        let result = detect_encoding_from_declaration(data);
3628        assert_eq!(result, Some(b"iso-8859-1".to_vec()));
3629    }
3630
3631    #[test]
3632    fn test_detect_encoding_declaration_none() {
3633        let data = b"<?xml version=\"1.0\"?>";
3634        let result = detect_encoding_from_declaration(data);
3635        assert!(result.is_none());
3636    }
3637
3638    #[test]
3639    fn test_detect_encoding_declaration_no_xml() {
3640        let data = b"<root>";
3641        let result = detect_encoding_from_declaration(data);
3642        assert!(result.is_none());
3643    }
3644
3645    #[test]
3646    fn test_detect_encoding_declaration_with_bom() {
3647        let mut data = vec![0xEF, 0xBB, 0xBF];
3648        data.extend_from_slice(b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
3649        let result = detect_encoding_from_declaration(&data);
3650        assert_eq!(result, Some(b"utf-8".to_vec()));
3651    }
3652
3653    // ── Encoding from name ─────────────────────────────────────────────────
3654
3655    #[test]
3656    fn test_encoding_from_name_utf8() {
3657        assert_eq!(
3658            encoding_from_name(b"UTF-8"),
3659            xmlCharEncoding::XML_CHAR_ENCODING_UTF8
3660        );
3661        assert_eq!(
3662            encoding_from_name(b"utf8"),
3663            xmlCharEncoding::XML_CHAR_ENCODING_UTF8
3664        );
3665    }
3666
3667    #[test]
3668    fn test_encoding_from_name_utf16() {
3669        assert_eq!(
3670            encoding_from_name(b"UTF-16LE"),
3671            xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
3672        );
3673        assert_eq!(
3674            encoding_from_name(b"UTF-16BE"),
3675            xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE
3676        );
3677        assert_eq!(
3678            encoding_from_name(b"utf-16"),
3679            xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
3680        );
3681    }
3682
3683    #[test]
3684    fn test_encoding_from_name_latin1() {
3685        assert_eq!(
3686            encoding_from_name(b"ISO-8859-1"),
3687            xmlCharEncoding::XML_CHAR_ENCODING_8859_1
3688        );
3689        assert_eq!(
3690            encoding_from_name(b"Latin1"),
3691            xmlCharEncoding::XML_CHAR_ENCODING_8859_1
3692        );
3693    }
3694
3695    #[test]
3696    fn test_encoding_from_name_ascii() {
3697        assert_eq!(
3698            encoding_from_name(b"ASCII"),
3699            xmlCharEncoding::XML_CHAR_ENCODING_ASCII
3700        );
3701        assert_eq!(
3702            encoding_from_name(b"US-ASCII"),
3703            xmlCharEncoding::XML_CHAR_ENCODING_ASCII
3704        );
3705    }
3706
3707    #[test]
3708    fn test_encoding_from_name_error() {
3709        assert_eq!(
3710            encoding_from_name(b"invalid-encoding"),
3711            xmlCharEncoding::XML_CHAR_ENCODING_ERROR
3712        );
3713    }
3714
3715    #[test]
3716    fn test_encoding_from_name_empty() {
3717        assert_eq!(
3718            encoding_from_name(b""),
3719            xmlCharEncoding::XML_CHAR_ENCODING_ERROR
3720        );
3721    }
3722
3723    // ── Encoding name ──────────────────────────────────────────────────────
3724
3725    #[test]
3726    fn test_encoding_name_utf8() {
3727        assert_eq!(
3728            encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_UTF8),
3729            Some(b"UTF-8" as &[u8])
3730        );
3731    }
3732
3733    #[test]
3734    fn test_encoding_name_utf16le() {
3735        assert_eq!(
3736            encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE),
3737            Some(b"UTF-16LE" as &[u8])
3738        );
3739    }
3740
3741    #[test]
3742    fn test_encoding_name_none() {
3743        assert!(encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_NONE).is_none());
3744    }
3745
3746    #[test]
3747    fn test_encoding_name_error() {
3748        assert!(encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_ERROR).is_none());
3749    }
3750
3751    // ── UTF-8 validation ───────────────────────────────────────────────────
3752
3753    #[test]
3754    fn test_utf8_valid_ascii() {
3755        assert!(utf8_valid(b"hello world"));
3756    }
3757
3758    #[test]
3759    fn test_utf8_valid_multi_byte() {
3760        assert!(utf8_valid("héllo wörld 🌍".as_bytes()));
3761    }
3762
3763    #[test]
3764    fn test_utf8_valid_empty() {
3765        assert!(utf8_valid(b""));
3766    }
3767
3768    #[test]
3769    fn test_utf8_invalid() {
3770        assert!(!utf8_valid(&[0xFF, 0xFE, 0x00]));
3771    }
3772
3773    // ── XML char validation ────────────────────────────────────────────────
3774
3775    #[test]
3776    fn test_valid_xml_chars() {
3777        assert!(is_valid_xml_char(0x9)); // Tab
3778        assert!(is_valid_xml_char(0xA)); // LF
3779        assert!(is_valid_xml_char(0xD)); // CR
3780        assert!(is_valid_xml_char(0x20)); // Space
3781        assert!(is_valid_xml_char(0x41)); // 'A'
3782        assert!(is_valid_xml_char(0xD7FF));
3783        assert!(is_valid_xml_char(0xE000));
3784        assert!(is_valid_xml_char(0xFFFD));
3785        assert!(is_valid_xml_char(0x10000));
3786        assert!(is_valid_xml_char(0x10FFFF));
3787    }
3788
3789    #[test]
3790    fn test_invalid_xml_chars() {
3791        assert!(!is_valid_xml_char(0x00));
3792        assert!(!is_valid_xml_char(0x08));
3793        assert!(!is_valid_xml_char(0x0B));
3794        assert!(!is_valid_xml_char(0x0C));
3795        assert!(!is_valid_xml_char(0x0E));
3796        assert!(!is_valid_xml_char(0x1F));
3797        assert!(!is_valid_xml_char(0xD800)); // Surrogate
3798        assert!(!is_valid_xml_char(0xDFFF)); // Surrogate
3799        assert!(!is_valid_xml_char(0xFFFE));
3800        assert!(!is_valid_xml_char(0xFFFF));
3801        assert!(!is_valid_xml_char(0x110000));
3802    }
3803
3804    // ── UTF-16LE to UTF-8 ──────────────────────────────────────────────────
3805
3806    #[test]
3807    fn test_utf16le_to_utf8_ascii() {
3808        // "AB" in UTF-16LE
3809        let data = [b'A', 0x00, b'B', 0x00];
3810        let result = utf16le_to_utf8(&data).unwrap();
3811        assert_eq!(result, b"AB");
3812    }
3813
3814    #[test]
3815    fn test_utf16le_to_utf8_bom() {
3816        let mut data = vec![0xFF, 0xFE]; // BOM
3817        data.extend_from_slice(&[b'A', 0x00, b'B', 0x00]);
3818        let result = utf16le_to_utf8(&data).unwrap();
3819        assert_eq!(result, b"AB");
3820    }
3821
3822    #[test]
3823    fn test_utf16le_to_utf8_bmp() {
3824        // U+00E9 (é) in UTF-16LE = 0xE9 0x00
3825        let data = [0xE9, 0x00];
3826        let result = utf16le_to_utf8(&data).unwrap();
3827        assert_eq!(result, "é".as_bytes());
3828    }
3829
3830    #[test]
3831    fn test_utf16le_to_utf8_supplementary() {
3832        // U+1F600 (😀) in UTF-16LE = 0x3D 0xD8 0x00 0xDE
3833        let data = [0x3D, 0xD8, 0x00, 0xDE];
3834        let result = utf16le_to_utf8(&data).unwrap();
3835        assert_eq!(result, "😀".as_bytes());
3836    }
3837
3838    #[test]
3839    fn test_utf16le_to_utf8_unpaired_surrogate() {
3840        let data = [0x00, 0xD8]; // High surrogate without low
3841        assert!(utf16le_to_utf8(&data).is_err());
3842    }
3843
3844    #[test]
3845    fn test_utf16le_to_utf8_truncated() {
3846        let data = [0x00]; // Odd length
3847        assert!(utf16le_to_utf8(&data).is_err());
3848    }
3849
3850    #[test]
3851    fn test_utf16le_to_utf8_empty() {
3852        let result = utf16le_to_utf8(b"").unwrap();
3853        assert!(result.is_empty());
3854    }
3855
3856    // ── UTF-16BE to UTF-8 ──────────────────────────────────────────────────
3857
3858    #[test]
3859    fn test_utf16be_to_utf8_ascii() {
3860        let data = [0x00, b'A', 0x00, b'B'];
3861        let result = utf16be_to_utf8(&data).unwrap();
3862        assert_eq!(result, b"AB");
3863    }
3864
3865    #[test]
3866    fn test_utf16be_to_utf8_bom() {
3867        let mut data = vec![0xFE, 0xFF]; // BOM
3868        data.extend_from_slice(&[0x00, b'A', 0x00, b'B']);
3869        let result = utf16be_to_utf8(&data).unwrap();
3870        assert_eq!(result, b"AB");
3871    }
3872
3873    #[test]
3874    fn test_utf16be_to_utf8_supplementary() {
3875        // U+1F600 (😀) in UTF-16BE = 0xD8 0x3D 0xDE 0x00
3876        let data = [0xD8, 0x3D, 0xDE, 0x00];
3877        let result = utf16be_to_utf8(&data).unwrap();
3878        assert_eq!(result, "😀".as_bytes());
3879    }
3880
3881    #[test]
3882    fn test_utf16be_to_utf8_empty() {
3883        let result = utf16be_to_utf8(b"").unwrap();
3884        assert!(result.is_empty());
3885    }
3886
3887    // ── UTF-8 to UTF-16LE ──────────────────────────────────────────────────
3888
3889    #[test]
3890    fn test_utf8_to_utf16le_ascii() {
3891        let result = utf8_to_utf16le(b"AB").unwrap();
3892        assert_eq!(result, [b'A', 0x00, b'B', 0x00]);
3893    }
3894
3895    #[test]
3896    fn test_utf8_to_utf16le_bmp() {
3897        let result = utf8_to_utf16le("é".as_bytes()).unwrap();
3898        assert_eq!(result, [0xE9, 0x00]);
3899    }
3900
3901    #[test]
3902    fn test_utf8_to_utf16le_supplementary() {
3903        let result = utf8_to_utf16le("😀".as_bytes()).unwrap();
3904        assert_eq!(result, [0x3D, 0xD8, 0x00, 0xDE]);
3905    }
3906
3907    #[test]
3908    fn test_utf8_to_utf16le_invalid_utf8() {
3909        assert!(utf8_to_utf16le(&[0xFF]).is_err());
3910    }
3911
3912    #[test]
3913    fn test_utf8_to_utf16le_empty() {
3914        let result = utf8_to_utf16le(b"").unwrap();
3915        assert!(result.is_empty());
3916    }
3917
3918    // ── Latin-1 to UTF-8 ───────────────────────────────────────────────────
3919
3920    #[test]
3921    fn test_latin1_to_utf8_ascii() {
3922        let result = latin1_to_utf8(b"ABC");
3923        assert_eq!(result, b"ABC");
3924    }
3925
3926    #[test]
3927    fn test_latin1_to_utf8_accented() {
3928        // 0xE9 = é in Latin-1
3929        let result = latin1_to_utf8(&[0xE9]);
3930        assert_eq!(result, "é".as_bytes());
3931    }
3932
3933    #[test]
3934    fn test_latin1_to_utf8_all_255() {
3935        let result = latin1_to_utf8(&[0xFF]);
3936        // U+00FF = ÿ, UTF-8: 0xC3 0xBF
3937        assert_eq!(result, [0xC3, 0xBF]);
3938    }
3939
3940    #[test]
3941    fn test_latin1_to_utf8_empty() {
3942        let result = latin1_to_utf8(b"");
3943        assert!(result.is_empty());
3944    }
3945
3946    #[test]
3947    fn test_latin1_to_utf8_mixed() {
3948        let result = latin1_to_utf8(b"caf\xE9");
3949        assert_eq!(result, "café".as_bytes());
3950    }
3951
3952    // ── UTF-8 to Latin-1 ───────────────────────────────────────────────────
3953
3954    #[test]
3955    fn test_utf8_to_latin1_ascii() {
3956        let result = utf8_to_latin1(b"ABC").unwrap();
3957        assert_eq!(result, b"ABC");
3958    }
3959
3960    #[test]
3961    fn test_utf8_to_latin1_accented() {
3962        let result = utf8_to_latin1("é".as_bytes()).unwrap();
3963        assert_eq!(result, [0xE9]);
3964    }
3965
3966    #[test]
3967    fn test_utf8_to_latin1_out_of_range() {
3968        assert!(utf8_to_latin1("€".as_bytes()).is_err()); // U+20AC not in Latin-1
3969    }
3970
3971    #[test]
3972    fn test_utf8_to_latin1_invalid_utf8() {
3973        assert!(utf8_to_latin1(&[0xFF]).is_err());
3974    }
3975
3976    #[test]
3977    fn test_utf8_to_latin1_empty() {
3978        let result = utf8_to_latin1(b"").unwrap();
3979        assert!(result.is_empty());
3980    }
3981
3982    // ── Encoding handler registry ──────────────────────────────────────────
3983
3984    #[test]
3985    fn test_init_and_find_encodings() {
3986        init_encodings();
3987
3988        let utf8_name: *const xmlChar = c"UTF-8".as_ptr() as *const xmlChar;
3989        assert!(!find_encoding_handler(utf8_name).is_null());
3990
3991        let utf16le_name: *const xmlChar = c"UTF-16LE".as_ptr() as *const xmlChar;
3992        assert!(!find_encoding_handler(utf16le_name).is_null());
3993
3994        let utf16be_name: *const xmlChar = c"UTF-16BE".as_ptr() as *const xmlChar;
3995        assert!(!find_encoding_handler(utf16be_name).is_null());
3996
3997        let latin1_name: *const xmlChar = c"ISO-8859-1".as_ptr() as *const xmlChar;
3998        assert!(!find_encoding_handler(latin1_name).is_null());
3999
4000        let ascii_name: *const xmlChar = c"ASCII".as_ptr() as *const xmlChar;
4001        assert!(!find_encoding_handler(ascii_name).is_null());
4002
4003        // Case insensitive
4004        let lower_name: *const xmlChar = c"utf-8".as_ptr() as *const xmlChar;
4005        assert!(!find_encoding_handler(lower_name).is_null());
4006    }
4007
4008    /// Phase 14 PHP court regression: the ABI `xmlFindCharEncodingHandler`
4009    /// hands the caller an OWNED handler that it may release with
4010    /// `xmlCharEncCloseFunc` — except UTF-8, where upstream returns a static
4011    /// handler that close must not release (so the registry is never freed
4012    /// out from under subsequent lookups). Closing a returned non-UTF-8
4013    /// handler must not free the persistent registry entry.
4014    ///
4015    /// # Safety
4016    ///
4017    /// - The handler returned by `xmlFindCharEncodingHandler_owned` is owned by
4018    ///   the caller and released here with the allocator, mirroring the export
4019    ///   `xmlCharEncCloseFunc` (which, for these stateless built-in handlers,
4020    ///   frees `name` and the struct without invoking any context destructor).
4021    #[test]
4022    fn test_find_owned_close_keeps_registry_intact() {
4023        init_encodings();
4024        let name: *const xmlChar = c"ISO-8859-1".as_ptr() as *const xmlChar;
4025
4026        // The registry entry is a long-lived borrow.
4027        let registry = find_encoding_handler(name);
4028        assert!(!registry.is_null());
4029        // First retrieval returns an OWNED copy, distinct from the registry entry.
4030        let h1 = xmlFindCharEncodingHandler_owned(name);
4031        assert!(!h1.is_null());
4032        assert_ne!(h1 as *const c_void, registry as *const c_void);
4033
4034        // Closing h1 (simulate xmlCharEncCloseFunc on a non-static handler):
4035        // frees its name + struct but NOT the registry entry.
4036        unsafe {
4037            if !(*h1).name.is_null() {
4038                crate::abi::allocator::xmlFreeImpl((*h1).name as *mut c_void);
4039            }
4040            xmlFreeImpl(h1 as *mut c_void);
4041        }
4042
4043        // The registry entry must survive the close of a previous result with
4044        // its name intact (the PHP `$dom->encoding='UTF-16'` crash was the
4045        // registry entry itself being freed by this very close, so the next
4046        // lookup returned freed memory).
4047        let registry2 = find_encoding_handler(name);
4048        assert_eq!(registry2 as *const c_void, registry as *const c_void);
4049        assert!(!unsafe { (*registry2).name }.is_null());
4050        let reg_name = unsafe { CStr::from_ptr((*registry2).name as *const c_char) };
4051        assert_eq!(reg_name.to_bytes(), b"ISO-8859-1");
4052
4053        // A second owned retrieval still works and is usable.
4054        let h2 = xmlFindCharEncodingHandler_owned(name);
4055        assert!(!h2.is_null());
4056        assert_ne!(h2 as *const c_void, registry as *const c_void);
4057        unsafe {
4058            if !(*h2).name.is_null() {
4059                crate::abi::allocator::xmlFreeImpl((*h2).name as *mut c_void);
4060            }
4061            xmlFreeImpl(h2 as *mut c_void);
4062        }
4063    }
4064
4065    /// Phase 14 PHP court regression (UTF-8 subset): retrieval for UTF-8/UTF8
4066    /// returns the persistent static handler, and referencing it from a second
4067    /// caller must yield the same live pointer (the registry entry is never
4068    /// freed by a close — `xmlCharEncCloseFunc` on XML_HANDLER_STATIC is a
4069    /// no-op).
4070    #[test]
4071    fn test_find_owned_utf8_static_and_persistent() {
4072        init_encodings();
4073        let name: *const xmlChar = c"UTF-8".as_ptr() as *const xmlChar;
4074        let u1 = xmlFindCharEncodingHandler_owned(name);
4075        assert!(!u1.is_null());
4076        // Static: close must not release it, so a second find returns the same
4077        // live registry handler.
4078        let u2 = xmlFindCharEncodingHandler_owned(c"utf8".as_ptr() as *const xmlChar);
4079        assert_eq!(u1, u2);
4080        assert_eq!(
4081            unsafe { (*u1).flags } & XML_HANDLER_STATIC,
4082            XML_HANDLER_STATIC
4083        );
4084    }
4085
4086    #[test]
4087    fn test_find_encoding_handler_not_found() {
4088        let name: *const xmlChar = c"NONEXISTENT".as_ptr() as *const xmlChar;
4089        assert!(find_encoding_handler(name).is_null());
4090    }
4091
4092    #[test]
4093    fn test_find_encoding_handler_null() {
4094        assert!(find_encoding_handler(ptr::null()).is_null());
4095    }
4096
4097    /// Verify registering a handler in the global registry and looking it
4098    /// up.
4099    ///
4100    /// # Safety
4101    ///
4102    /// - The `xmlMallocImpl` and `xmlMemStrdupImpl` results are NULL-checked
4103    ///   before `ptr::write` initializes the handler; the handler is removed
4104    ///   from the registry before its allocations are freed exactly once.
4105    #[test]
4106    fn test_add_encoding_handler() {
4107        let handler = unsafe {
4108            xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) as *mut _xmlCharEncodingHandler
4109        };
4110        assert!(!handler.is_null());
4111
4112        let name = unsafe {
4113            crate::abi::allocator::xmlMemStrdupImpl(c"TEST-ENC".as_ptr() as *const c_char)
4114        };
4115        unsafe {
4116            ptr::write(
4117                handler,
4118                _xmlCharEncodingHandler {
4119                    name: name as *mut c_char,
4120                    input: EncodingInputUnion { legacyFunc: None },
4121                    output: EncodingOutputUnion { legacyFunc: None },
4122                    inputCtxt: ptr::null_mut(),
4123                    outputCtxt: ptr::null_mut(),
4124                    ctxtDtor: None,
4125                    flags: 0,
4126                },
4127            );
4128        }
4129
4130        assert_eq!(add_encoding_handler(handler), 0);
4131
4132        let found = find_encoding_handler(c"TEST-ENC".as_ptr() as *const xmlChar);
4133        assert_eq!(found, handler);
4134
4135        // Remove from registry before freeing to avoid dangling pointers
4136        {
4137            let mut handlers = ENCODING_HANDLERS.write();
4138            handlers.retain(|&h| h.0 != handler);
4139        }
4140
4141        unsafe {
4142            xmlFreeImpl(name as *mut c_void);
4143            xmlFreeImpl(handler as *mut c_void);
4144        }
4145    }
4146
4147    // ── Conversion round-trips ─────────────────────────────────────────────
4148
4149    #[test]
4150    fn test_utf16le_roundtrip() {
4151        let original = b"Hello, World! UTF-16LE test: \xC3\xA9\xF0\x9F\x98\x80";
4152        let utf16 = utf8_to_utf16le(original).unwrap();
4153        let back = utf16le_to_utf8(&utf16).unwrap();
4154        assert_eq!(original.to_vec(), back);
4155    }
4156
4157    #[test]
4158    fn test_utf16be_roundtrip() {
4159        let original = b"Hello, World! UTF-16BE test: \xC3\xA9\xF0\x9F\x98\x80";
4160        let utf16le = utf8_to_utf16le(original).unwrap();
4161        // Convert LE to BE by swapping bytes
4162        let mut utf16be = utf16le.clone();
4163        for chunk in utf16be.as_chunks_mut::<2>().0 {
4164            chunk.swap(0, 1);
4165        }
4166        let back = utf16be_to_utf8(&utf16be).unwrap();
4167        assert_eq!(original.to_vec(), back);
4168    }
4169
4170    #[test]
4171    fn test_latin1_roundtrip() {
4172        let original: Vec<u8> = (0x00..=0xFF).collect();
4173        let utf8 = latin1_to_utf8(&original);
4174        let back = utf8_to_latin1(&utf8).unwrap();
4175        assert_eq!(original, back);
4176    }
4177
4178    // ── Built-in handler callbacks ─────────────────────────────────────────
4179
4180    /// Verify the UTF-8 identity callback copies bytes up to the smaller
4181    /// length.
4182    ///
4183    /// # Safety
4184    ///
4185    /// - `output` is a valid mutable 64-byte buffer and `input` a valid byte
4186    ///   slice; the callback writes at most the minimum of the two lengths.
4187    #[test]
4188    fn test_utf8_handler_identity() {
4189        let input = b"Hello, UTF-8!";
4190        let mut output = [0u8; 64];
4191        let mut outlen = output.len() as c_int;
4192        let mut inlen = input.len() as c_int;
4193
4194        let ret = unsafe {
4195            utf8_input_func(output.as_mut_ptr(), &mut outlen, input.as_ptr(), &mut inlen)
4196        };
4197
4198        assert_eq!(ret, input.len() as c_int);
4199        assert_eq!(&output[..ret as usize], input);
4200        assert_eq!(inlen, input.len() as c_int);
4201    }
4202
4203    /// Verify a UTF-16LE output/input callback round-trip.
4204    ///
4205    /// # Safety
4206    ///
4207    /// - The `utf16_buf` and `decoded` arrays are valid buffers of the given
4208    ///   lengths, and the input slices are valid; the callbacks write only
4209    ///   up to the advertised output length.
4210    #[test]
4211    fn test_utf16le_handler_roundtrip() {
4212        init_encodings();
4213
4214        let original = b"Hello UTF-16LE!";
4215        let mut utf16_buf = [0u8; 128];
4216        let mut outlen = utf16_buf.len() as c_int;
4217        let mut inlen = original.len() as c_int;
4218
4219        let written = unsafe {
4220            utf16le_output_func(
4221                utf16_buf.as_mut_ptr(),
4222                &mut outlen,
4223                original.as_ptr(),
4224                &mut inlen,
4225            )
4226        };
4227        assert!(written > 0);
4228
4229        // Now decode back
4230        let mut decoded = [0u8; 128];
4231        let mut outlen2 = decoded.len() as c_int;
4232        let mut inlen2 = written;
4233
4234        let written2 = unsafe {
4235            utf16le_input_func(
4236                decoded.as_mut_ptr(),
4237                &mut outlen2,
4238                utf16_buf.as_ptr(),
4239                &mut inlen2,
4240            )
4241        };
4242        assert_eq!(written2 as usize, original.len());
4243        assert_eq!(&decoded[..written2 as usize], original);
4244    }
4245
4246    // ── xmlBuffer operations ───────────────────────────────────────────────
4247
4248    /// Verify `append_to_xml_buffer` grows the buffer and copies bytes.
4249    ///
4250    /// # Safety
4251    ///
4252    /// - `content` is a valid 64-byte allocation owned by the test and freed
4253    ///   exactly once with `xmlFreeImpl`; `buf` keeps consistent `use_` and
4254    ///   `size` fields while `append_to_xml_buffer` may reallocate `content`.
4255    #[test]
4256    fn test_append_to_xml_buffer() {
4257        unsafe {
4258            let content = xmlMallocImpl(64) as *mut xmlChar;
4259            assert!(!content.is_null());
4260
4261            let mut buf = _xmlBuffer {
4262                content,
4263                use_: 0,
4264                size: 64,
4265                alloc: 0,
4266                contentIO: ptr::null_mut(),
4267            };
4268
4269            append_to_xml_buffer(&mut buf, b"Hello");
4270            assert_eq!(buf.use_, 5);
4271            let slice = core::slice::from_raw_parts(buf.content, 5);
4272            assert_eq!(slice, b"Hello");
4273
4274            append_to_xml_buffer(&mut buf, b" World");
4275            assert_eq!(buf.use_, 11);
4276            let slice = core::slice::from_raw_parts(buf.content, 11);
4277            assert_eq!(slice, b"Hello World");
4278
4279            xmlFreeImpl(buf.content as *mut c_void);
4280        }
4281    }
4282
4283    // ── ABI export functions ───────────────────────────────────────────────
4284
4285    #[test]
4286    fn test_xml_parse_char_encoding() {
4287        let name = c"UTF-8".as_ptr() as *const c_char;
4288        assert_eq!(
4289            xmlParseCharEncoding(name),
4290            xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int
4291        );
4292
4293        let name = c"ISO-8859-1".as_ptr() as *const c_char;
4294        assert_eq!(
4295            xmlParseCharEncoding(name),
4296            xmlCharEncoding::XML_CHAR_ENCODING_8859_1 as c_int
4297        );
4298
4299        assert_eq!(
4300            xmlParseCharEncoding(ptr::null()),
4301            xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int
4302        );
4303    }
4304
4305    /// Verify `xmlNewCharEncodingHandler` and `xmlDelEncodingHandler`
4306    /// round-trip.
4307    ///
4308    /// # Safety
4309    ///
4310    /// - `name` is a valid NUL-terminated string; the returned handler is
4311    ///   non-NULL, its `name` field is a valid NUL-terminated string, and it
4312    ///   is freed exactly once by `xmlDelEncodingHandler`.
4313    #[test]
4314    fn test_xml_new_and_del_encoding_handler() {
4315        let name = c"TestEnc".as_ptr() as *const c_char;
4316        let handler = xmlNewCharEncodingHandler(
4317            name,
4318            utf8_input_func as xmlCharEncodingInputFunc,
4319            utf8_output_func as xmlCharEncodingOutputFunc,
4320        );
4321        assert!(!handler.is_null());
4322
4323        unsafe {
4324            assert!(!(*handler).name.is_null());
4325            let cstr = CStr::from_ptr((*handler).name);
4326            assert_eq!(cstr.to_bytes(), b"TestEnc");
4327        }
4328
4329        xmlDelEncodingHandler(handler);
4330    }
4331
4332    #[test]
4333    fn test_xml_init_and_cleanup() {
4334        xmlInitCharEncodingHandlers();
4335
4336        let name: *const xmlChar = c"UTF-8".as_ptr() as *const xmlChar;
4337        assert!(!find_encoding_handler(name).is_null());
4338
4339        xmlCleanupCharEncodingHandlers();
4340        // After cleanup, handlers should be empty
4341    }
4342
4343    // ── Shift_JIS / EUC-JP (encoding_rs-backed, R-000157 slice) ────────────
4344
4345    /// Drive a legacy func on whole buffers.
4346    fn call_func(
4347        func: unsafe extern "C" fn(*mut c_uchar, *mut c_int, *const c_uchar, *mut c_int) -> c_int,
4348        input: &[u8],
4349    ) -> (c_int, Vec<u8>, usize) {
4350        let mut out = vec![0u8; input.len() * 6 + 64];
4351        let mut outlen = out.len() as c_int;
4352        let mut inlen = input.len() as c_int;
4353        let rc = unsafe { func(out.as_mut_ptr(), &mut outlen, input.as_ptr(), &mut inlen) };
4354        out.truncate(outlen.max(0) as usize);
4355        (rc, out, inlen.max(0) as usize)
4356    }
4357
4358    #[test]
4359    fn test_shift_jis_output_roundtrip() {
4360        // ぁ (U+3041 → 0x82 0x9F), 漢 (U+6F22 → 0x8A 0xBF), half-width ア
4361        // (U+FF71 → 0xB1): byte-exact vs the oracle's iconv output.
4362        let (rc, out, consumed) = call_func(shift_jis_output_func, "ぁ漢ア".as_bytes());
4363        assert!(rc >= 0);
4364        assert_eq!(out, [0x82, 0x9F, 0x8A, 0xBF, 0xB1]);
4365        assert_eq!(consumed, "ぁ漢ア".len());
4366
4367        let (rc, back, _) = call_func(shift_jis_input_func, &out);
4368        assert!(rc >= 0);
4369        assert_eq!(back, "ぁ漢ア".as_bytes());
4370    }
4371
4372    #[test]
4373    fn test_shift_jis_output_unmappable_reports_input_error() {
4374        // U+1F600 is outside Shift_JIS: the func stops BEFORE it with the
4375        // -2 input-error convention (char_enc_out substitutes the decimal
4376        // character reference, exactly like the oracle iconv EILSEQ path).
4377        let (rc, out, consumed) = call_func(shift_jis_output_func, "A😀B".as_bytes());
4378        assert_eq!(rc, ENC_INPUT_ERROR);
4379        assert_eq!(out, b"A");
4380        assert_eq!(consumed, 1); // *inlen points AT the emoji
4381
4382        // Whole-buffer conversion through char_enc_out emits the charref and
4383        // continues: &#128512; (decimal), matching xmlSerializeDecCharRef.
4384        let handler = find_encoding_handler(c"SHIFT_JIS".as_ptr() as *const xmlChar);
4385        assert!(!handler.is_null());
4386        let in_buf = crate::xml::io::buf_create(64);
4387        let src = "A\u{1F600}B".as_bytes();
4388        assert!(
4389            crate::xml::io::buf_add(in_buf, src.as_ptr() as *const xmlChar, src.len() as c_int)
4390                >= 0
4391        );
4392        let out_buf = crate::xml::io::buf_create(64);
4393        let n = char_enc_out(handler, out_buf, in_buf);
4394        assert!(n >= 0);
4395        let bytes =
4396            unsafe { core::slice::from_raw_parts((*out_buf).content, (*out_buf).use_ as usize) };
4397        assert_eq!(bytes, b"A&#128512;B");
4398        crate::xml::io::buf_free(in_buf);
4399        crate::xml::io::buf_free(out_buf);
4400    }
4401
4402    #[test]
4403    fn test_euc_jp_output_roundtrip() {
4404        // ぁ (U+3041 → 0xA4 0xA1), 漢 (U+6F22 → 0xB4 0xC1), ア (U+FF71 →
4405        // 0x8E 0xB1) — oracle iconv byte-exact.
4406        let (rc, out, consumed) = call_func(euc_jp_output_func, "ぁ漢ア".as_bytes());
4407        assert!(rc >= 0);
4408        assert_eq!(out, [0xA4, 0xA1, 0xB4, 0xC1, 0x8E, 0xB1]);
4409        assert_eq!(consumed, "ぁ漢ア".len());
4410
4411        let (rc, back, _) = call_func(euc_jp_input_func, &out);
4412        assert!(rc >= 0);
4413        assert_eq!(back, "ぁ漢ア".as_bytes());
4414    }
4415
4416    #[test]
4417    fn test_east_asian_handlers_registered_and_findable() {
4418        for name in [
4419            c"SHIFT_JIS".as_ptr(),
4420            c"Shift_JIS".as_ptr(),
4421            c"SJIS".as_ptr(),
4422            c"CP932".as_ptr(),
4423            c"EUC-JP".as_ptr(),
4424            c"euc-jp".as_ptr(),
4425        ] {
4426            assert!(
4427                !find_encoding_handler(name as *const xmlChar).is_null(),
4428                "handler not found for {name:?}"
4429            );
4430        }
4431    }
4432
4433    #[test]
4434    fn test_shift_jis_output_invalid_utf8_errors() {
4435        let (rc, out, consumed) = call_func(shift_jis_output_func, b"A\xFFB");
4436        assert_eq!(rc, -1);
4437        assert_eq!(out, b"A");
4438        assert_eq!(consumed, 1);
4439    }
4440
4441    // ── R-000157 remainder codecs (UCS-4/UCS-2/EBCDIC/ISO-8859-x) ─────────
4442
4443    #[test]
4444    fn test_ucs4le_output_matches_utf32le() {
4445        // あ U+3042 → 42 30 00 00 little-endian; 中 U+4E2D → 2D 4E 00 00.
4446        let (rc, out, consumed) = call_func(ucs4le_output_func, "Aあ中".as_bytes());
4447        assert!(rc >= 0);
4448        assert_eq!(out, [0x41, 0, 0, 0, 0x42, 0x30, 0, 0, 0x2D, 0x4E, 0, 0]);
4449        assert_eq!(consumed, "Aあ中".len());
4450        let (rc, back, _) = call_func(ucs4le_input_func, &out);
4451        assert!(rc >= 0);
4452        assert_eq!(back, "Aあ中".as_bytes());
4453    }
4454
4455    #[test]
4456    fn test_ucs4be_output_matches_utf32be() {
4457        let (rc, out, _) = call_func(ucs4be_output_func, "Aあ".as_bytes());
4458        assert!(rc >= 0);
4459        assert_eq!(out, [0, 0, 0, 0x41, 0, 0, 0x30, 0x42]);
4460        let (rc, back, _) = call_func(ucs4be_input_func, &out);
4461        assert!(rc >= 0);
4462        assert_eq!(back, "Aあ".as_bytes());
4463    }
4464
4465    #[test]
4466    fn test_ucs2_output_astral_is_unmappable() {
4467        // glibc "UCS-2" on x86 is little-endian; astral chars are unmappable
4468        // (the -2 input-error convention -> decimal charref).
4469        let (rc, out, consumed) = call_func(ucs2_output_func, "A😀".as_bytes());
4470        assert_eq!(rc, ENC_INPUT_ERROR);
4471        assert_eq!(out, [0x41, 0]);
4472        assert_eq!(consumed, 1);
4473    }
4474
4475    #[test]
4476    fn test_ebcdic037_bijection() {
4477        // cp037: space 0x40, 'A' 0xC1, '0' 0xF0.
4478        let (rc, out, _) = call_func(ebcdic_output_func, b"A 0");
4479        assert!(rc >= 0);
4480        assert_eq!(out, [0xC1, 0x40, 0xF0]);
4481        let (rc, back, _) = call_func(ebcdic_input_func, &out);
4482        assert!(rc >= 0);
4483        assert_eq!(back, b"A 0");
4484        // Every byte is defined and the mapping is a bijection onto
4485        // U+0000..U+00FF (derived from the oracle glibc iconv IBM037).
4486        for (b, cp) in EBCDIC037_TO_UNICODE.iter().enumerate() {
4487            assert_eq!(ebcdic037_cp_to_byte(u32::from(*cp)), Some(b as u8));
4488        }
4489        assert!(ebcdic037_cp_to_byte(0x100).is_none());
4490    }
4491
4492    #[test]
4493    fn test_iso_8859_2_output_roundtrip() {
4494        // ą U+0105 → 0xB1, ć U+0107 → 0xE6, ę U+0119 → 0xEA (ISO-8859-2).
4495        let (rc, out, _) = call_func(iso_8859_2_output_func, "Aąćę".as_bytes());
4496        assert!(rc >= 0);
4497        assert_eq!(out, [0x41, 0xB1, 0xE6, 0xEA]);
4498        let (rc, back, _) = call_func(iso_8859_2_input_func, &out);
4499        assert!(rc >= 0);
4500        assert_eq!(back, "Aąćę".as_bytes());
4501    }
4502
4503    #[test]
4504    fn test_decode_whole_buffer_declared_dispatch() {
4505        // Whole-buffer decode via the registry (parser input layer path).
4506        let iso2 = [0x41u8, 0xB1, 0xE6, 0xEA];
4507        assert_eq!(
4508            decode_whole_buffer_declared(b"ISO-8859-2", &iso2).unwrap(),
4509            "Aąćę".as_bytes()
4510        );
4511        // Alias spelling resolves through the canonical re-lookup.
4512        assert_eq!(
4513            decode_whole_buffer_declared(b"latin2", &iso2).unwrap(),
4514            "Aąćę".as_bytes()
4515        );
4516        // Unknown names error (no handler).
4517        assert!(decode_whole_buffer_declared(b"no-such-encoding", b"abc").is_err());
4518    }
4519
4520    #[test]
4521    fn test_iso_2022_jp_output_uses_escape_sequences() {
4522        // A → ASCII; あ U+3042 → ESC $ B + 0x24 0x22; back to ASCII ESC ( B.
4523        let (rc, out, consumed) = call_func(iso_2022_jp_output_func, "AあB".as_bytes());
4524        assert!(rc >= 0);
4525        assert_eq!(out, b"A\x1B$B$\"\x1B(BB");
4526        assert_eq!(consumed, "AあB".len());
4527        let (rc, back, _) = call_func(iso_2022_jp_input_func, &out);
4528        assert!(rc >= 0);
4529        assert_eq!(back, "AあB".as_bytes());
4530    }
4531}