Skip to main content

libxml_rs/xml/encoding/
mod.rs

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