Skip to main content

libxml_rs/xml/encoding/
mod.rs

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