Skip to main content

libxml_rs/xml/encoding/
mod.rs

1//! Character encoding handling (§22, §85 Phase 4).
2//!
3//! Encoding detection, XML declaration encoding, BOM behavior, UTF-8/UTF-16
4//! validity, legacy encodings, conversion errors, output conversion,
5//! serializer fallback, custom encoding handlers.
6//!
7//! # Architecture
8//!
9//! ```text
10//! ABI exports (exports_xml2.rs)  ←  pub(crate) functions in this module
11//!                                           ↕
12//!                           Encoding handler registry (global RwLock)
13//!                                           ↕
14//!              Built-in handlers: UTF-8, UTF-16LE, UTF-16BE, Latin-1, ASCII
15//! ```
16//!
17//! The internal encoding is always UTF-8. All conversions go to/from UTF-8.
18//! The handler registry stores `_xmlCharEncodingHandler` structs that contain
19//! function pointers for input (→UTF-8) and output (UTF-8→) conversion.
20//!
21//! # Upstream contract
22//!
23//! Mirrors upstream `encoding.c` / `encoding.h`
24//! (`SRC-LIBXML2-2.15.0-ENCODING-C`, parity target libxml2 2.15.3 oracle).
25//! ABI surface: `xmlLookupCharEncodingHandler`, `xmlGetCharEncodingHandler`,
26//! `xmlOpenCharEncodingHandler`, `xmlCreateCharEncodingHandler`,
27//! `xmlCharEncInput`/`xmlCharEncOutput` and the `_xmlCharEncodingHandler`
28//! C-layout struct (R-000129 fixed the Rust mirror from 48 to the upstream
29//! 56 bytes).
30//!
31//! # Conceptual behavior
32//!
33//! Detection runs BOM first, then the XML declaration, then registry lookup.
34//! The registry mirrors upstream `defaultHandlers[32]` plus the extra-handler
35//! table (`globalHandlers`, encoding.c): named handlers are registered under
36//! their canonical lowercased alias and found by `xmlFindCharEncodingHandler`
37//! via `find_encoding_handler`.
38//!
39//! # Ownership & safety invariants
40//!
41//! Handlers are allocated with xmlMalloc and owned by the registry; `xmlFree` releases
42//! them at teardown. Registry access is serialized by an RwLock; that
43//! serialization is exactly what makes the raw `HandlerPtr` Send+Sync
44//! SAFETY sound (documented on the wrapper). Names from
45//! `xmlGetCharEncodingName`/alias tables are borrowed statics — the caller
46//! never frees them.
47//!
48//! # Historical quirks & epochs
49//!
50//! R-000157 (OPEN, UNRESOLVED): the crate ships no
51//! iconv/ICU backend, so the iconv/ICU-only encodings (UCS-4LE/BE, EBCDIC,
52//! UCS-2, ISO-8859-2..16, ISO-2022-JP, Shift_JIS, EUC-JP, windows-1252)
53//! report XML_ERR_UNSUPPORTED_ENCODING (32) where the 2.15.3 oracle (built
54//! with Iconv+ICU enabled) returns
55//! a converter, while the native set (UTF-8, UTF-16LE/BE, UTF-16,
56//! ISO-8859-1, US-ASCII) and all error paths are byte-identical. This is a
57//! REAL current executed-platform difference, so the residual is UNRESOLVED
58//! (11.1-Z.1) — closure requires implementing an iconv/ICU backend, a future
59//! implementation work item, not a waiver. Upstream
60//! itself removed the libiconv dependence where possible in the 2.10+ era
61//! (HISTORY.md §1.8), which is the epoch this module targets.
62//!
63//! # Deliberate oddities
64//!
65//! The bounded native set is a deliberate divergence, not a stub: the
66//! missing encodings are absent because no converter exists, and every
67//! error path matches the oracle. `xmlLookupCharEncodingHandler` returns
68//! XML_ERR_OK with a NULL handler for UTF-8/NONE exactly like upstream
69//! encoding.c (`/* Return NULL handler for UTF-8 */`). R-000157 is tracked
70//! UNRESOLVED: adding an iconv/ICU backend would close the gap for the
71//! encodings the executed oracle serves.
72//!
73//! # Proving courts
74//!
75//! ENCODING-001 (`courts/suites/data-abi/encoding-family-probe.c`) compiles
76//! one C probe against the oracle DSO and the candidate and requires
77//! byte-identical stdout across the native set and all error paths.
78//!
79//! # Tempting simplifications that would break parity
80//!
81//! Do not collapse the registry to a fixed match statement: custom handlers
82//! added through `xmlAddCharEncodingHandler` must stay discoverable by later
83//! lookups. Do not fabricate handlers for the iconv-only encodings — that
84//! would fake a converter that does not exist and break the R-000157
85//! UNRESOLVED record (the honest path is a real iconv/ICU backend). Do not
86//! touch the struct layout: R-000129
87//! proved a 48-byte mirror breaks the C ABI.
88
89#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
90
91use std::ffi::CStr;
92use std::os::raw::{c_char, c_int, c_uchar, c_uint, c_void};
93use std::ptr;
94use std::sync::atomic::{AtomicBool, Ordering};
95
96use once_cell::sync::Lazy;
97use parking_lot::RwLock;
98
99use crate::abi::allocator::{xmlFreeImpl, xmlMallocImpl, xmlReallocImpl};
100use crate::abi::callbacks::{
101    xmlCharEncConvCtxtDtor, xmlCharEncConvFunc, xmlCharEncConvImpl, xmlCharEncodingInputFunc,
102    xmlCharEncodingOutputFunc,
103};
104use crate::abi::structs::{
105    _xmlBuffer, _xmlCharEncodingHandler, EncodingInputUnion, EncodingOutputUnion,
106};
107use crate::abi::types::{xmlChar, xmlCharEncoding};
108
109// ── Constants ──────────────────────────────────────────────────────────────
110
111/// Maximum bytes needed per character for any supported encoding.
112#[allow(dead_code)]
113const MAX_CHAR_BYTES: usize = 6;
114
115/// UTF-8 BOM bytes.
116#[allow(dead_code)]
117const UTF8_BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];
118
119/// UTF-16LE BOM bytes.
120const UTF16LE_BOM: [u8; 2] = [0xFF, 0xFE];
121
122/// UTF-16BE BOM bytes.
123const UTF16BE_BOM: [u8; 2] = [0xFE, 0xFF];
124
125// ── Global handler registry ────────────────────────────────────────────────
126
127/// A raw pointer wrapper that implements `Send` and `Sync`.
128///
129/// This is safe because all access to the global handler registry is
130/// serialized through the `RwLock`, and handlers are only accessed from
131/// trusted internal code.
132#[derive(Clone, Copy)]
133struct HandlerPtr(*mut _xmlCharEncodingHandler);
134
135unsafe impl Send for HandlerPtr {}
136unsafe impl Sync for HandlerPtr {}
137
138/// Global list of registered encoding handlers, protected by a read-write lock.
139///
140/// UPSTREAM-PARITY: this is the Rust mirror of encoding.c `globalHandlers`
141/// (the table behind `xmlFindExtraHandler`); `xmlAddCharEncodingHandler`
142/// appends here and lookups scan it after the built-in `defaultHandlers[32]`
143/// set (R-000157: only the native subset is backed by real converters).
144static ENCODING_HANDLERS: Lazy<RwLock<Vec<HandlerPtr>>> = Lazy::new(|| RwLock::new(Vec::new()));
145
146/// Whether the built-in encoding handlers have been initialized.
147static ENCODING_INITIALIZED: AtomicBool = AtomicBool::new(false);
148
149/// Serializes first-time handler registration (see init_encodings).
150static ENCODING_INIT_MUTEX: parking_lot::Mutex<()> = parking_lot::Mutex::new(());
151
152// ═══════════════════════════════════════════════════════════════════════════════
153// 1. Encoding detection
154// ═══════════════════════════════════════════════════════════════════════════════
155
156/// Determine encoding from BOM bytes.
157///
158/// Returns `XML_CHAR_ENCODING_NONE` if no BOM is present, or if `data` is empty.
159/// Otherwise returns the matching encoding enum value.
160#[allow(dead_code)]
161pub(crate) fn detect_encoding_from_bom(data: &[u8]) -> xmlCharEncoding {
162    if data.len() >= 3 && data[0..3] == UTF8_BOM {
163        xmlCharEncoding::XML_CHAR_ENCODING_UTF8
164    } else if data.len() >= 2 && data[0..2] == UTF16LE_BOM {
165        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
166    } else if data.len() >= 2 && data[0..2] == UTF16BE_BOM {
167        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE
168    } else {
169        xmlCharEncoding::XML_CHAR_ENCODING_NONE
170    }
171}
172
173/// Determine encoding from an XML declaration's `encoding` attribute.
174///
175/// Scans for `<?xml ... encoding="..." ?>` and returns the encoding name
176/// as a byte vector (lowercased), or `None` if not found.
177#[allow(dead_code)]
178pub(crate) fn detect_encoding_from_declaration(data: &[u8]) -> Option<Vec<u8>> {
179    // Look for "<?xml" at the start (possibly after BOM)
180    let start = if data.len() >= 3 && data[0..3] == UTF8_BOM {
181        3
182    } else if data.len() >= 2 && (data[0..2] == UTF16LE_BOM || data[0..2] == UTF16BE_BOM) {
183        // For UTF-16, we can't easily scan the bytes; skip
184        return None;
185    } else {
186        0
187    };
188
189    let remaining = &data[start..];
190
191    // Must start with "<?xml"
192    if remaining.len() < 5 || !remaining[0..5].eq_ignore_ascii_case(b"<?xml") {
193        return None;
194    }
195
196    // Find the end of the PI: "?>"
197    let pi_end = remaining.windows(2).position(|w| w == b"?>")?;
198    let decl_content = &remaining[5..pi_end];
199
200    // Look for "encoding" attribute
201    let decl_str = core::str::from_utf8(decl_content).ok()?;
202    let lower = decl_str.to_ascii_lowercase();
203
204    // Find "encoding" keyword
205    let enc_pos = lower.find("encoding")?;
206
207    // After "encoding", expect optional whitespace and '='
208    let after_enc = &decl_content[enc_pos + 8..];
209    let after_enc_str = core::str::from_utf8(after_enc).ok()?;
210    let after_enc_trimmed = after_enc_str.trim_start();
211
212    if !after_enc_trimmed.starts_with('=') {
213        return None;
214    }
215
216    let after_eq = after_enc_trimmed[1..].trim_start();
217
218    // Expect quote character
219    let quote = after_eq.chars().next()?;
220    if quote != '"' && quote != '\'' {
221        return None;
222    }
223
224    // Find matching closing quote
225    let value_end = after_eq[1..].find(quote)?;
226    let encoding_value = &after_eq[1..=value_end];
227
228    Some(encoding_value.to_ascii_lowercase().as_bytes().to_vec())
229}
230
231/// Parse an encoding name string to an `xmlCharEncoding` enum value.
232///
233/// Matching is case-insensitive. Common aliases are recognized.
234/// Returns `XML_CHAR_ENCODING_ERROR` if the name is not recognized.
235pub(crate) fn encoding_from_name(name: &[u8]) -> xmlCharEncoding {
236    let s = core::str::from_utf8(name).unwrap_or("");
237    let s = s.trim().to_ascii_lowercase();
238
239    match s.as_str() {
240        // UTF-8
241        "utf-8" | "utf8" => xmlCharEncoding::XML_CHAR_ENCODING_UTF8,
242
243        // UTF-16
244        "utf-16" | "utf-16le" | "utf16le" => xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
245        "utf-16be" | "utf16be" => xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE,
246
247        // ISO-8859 variants
248        "iso-8859-1" | "iso_8859-1" | "latin1" | "latin-1" | "l1" | "cp819" | "ibm819"
249        | "iso-ir-100" | "iso_8859-1:1987" => xmlCharEncoding::XML_CHAR_ENCODING_8859_1,
250        "iso-8859-2" | "iso_8859-2" | "latin2" | "latin-2" | "l2" => {
251            xmlCharEncoding::XML_CHAR_ENCODING_8859_2
252        }
253        "iso-8859-3" | "iso_8859-3" | "latin3" | "latin-3" | "l3" => {
254            xmlCharEncoding::XML_CHAR_ENCODING_8859_3
255        }
256        "iso-8859-4" | "iso_8859-4" | "latin4" | "latin-4" | "l4" => {
257            xmlCharEncoding::XML_CHAR_ENCODING_8859_4
258        }
259        "iso-8859-5" | "iso_8859-5" | "cyrillic" => xmlCharEncoding::XML_CHAR_ENCODING_8859_5,
260        "iso-8859-6" | "iso_8859-6" | "arabic" => xmlCharEncoding::XML_CHAR_ENCODING_8859_6,
261        "iso-8859-7" | "iso_8859-7" | "greek" => xmlCharEncoding::XML_CHAR_ENCODING_8859_7,
262        "iso-8859-8" | "iso_8859-8" | "hebrew" => xmlCharEncoding::XML_CHAR_ENCODING_8859_8,
263        "iso-8859-9" | "iso_8859-9" | "latin5" | "latin-5" | "l5" | "turkish" => {
264            xmlCharEncoding::XML_CHAR_ENCODING_8859_9
265        }
266
267        // ASCII
268        "ascii" | "us-ascii" | "us" | "ansi_x3.4-1968" | "ansi_x3.4-1986" | "iso-ir-6"
269        | "iso_646.irv:1991" | "cp367" | "ibm367" => xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
270
271        // East Asian
272        "iso-2022-jp" | "iso2022-jp" => xmlCharEncoding::XML_CHAR_ENCODING_2022_JP,
273        "shift_jis" | "shift-jis" | "sjis" | "cp932" => {
274            xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS
275        }
276        "euc-jp" | "eucjp" => xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP,
277
278        // UCS/Unicode variants
279        "ucs-4" | "ucs4" => xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE,
280        "ucs-4le" | "ucs4le" => xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE,
281        "ucs-4be" | "ucs4be" => xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE,
282        "ucs-2" | "ucs2" => xmlCharEncoding::XML_CHAR_ENCODING_UCS2,
283
284        // EBCDIC
285        "ebcdic" | "cp037" | "ibm037" => xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC,
286
287        _ => xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
288    }
289}
290
291/// Get the canonical name for an encoding as a byte slice.
292///
293/// Returns `None` for `XML_CHAR_ENCODING_ERROR` and `XML_CHAR_ENCODING_NONE`.
294pub(crate) const fn encoding_name(enc: xmlCharEncoding) -> Option<&'static [u8]> {
295    match enc {
296        xmlCharEncoding::XML_CHAR_ENCODING_UTF8 => Some(b"UTF-8" as &[u8]),
297        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE => Some(b"UTF-16LE" as &[u8]),
298        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE => Some(b"UTF-16BE" as &[u8]),
299        xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE => Some(b"UCS-4LE" as &[u8]),
300        xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE => Some(b"UCS-4BE" as &[u8]),
301        xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC => Some(b"EBCDIC" as &[u8]),
302        xmlCharEncoding::XML_CHAR_ENCODING_UCS4_2143 => Some(b"UCS-4-2143" as &[u8]),
303        xmlCharEncoding::XML_CHAR_ENCODING_UCS4_3412 => Some(b"UCS-4-3412" as &[u8]),
304        xmlCharEncoding::XML_CHAR_ENCODING_UCS2 => Some(b"UCS-2" as &[u8]),
305        xmlCharEncoding::XML_CHAR_ENCODING_8859_1 => Some(b"ISO-8859-1" as &[u8]),
306        xmlCharEncoding::XML_CHAR_ENCODING_8859_2 => Some(b"ISO-8859-2" as &[u8]),
307        xmlCharEncoding::XML_CHAR_ENCODING_8859_3 => Some(b"ISO-8859-3" as &[u8]),
308        xmlCharEncoding::XML_CHAR_ENCODING_8859_4 => Some(b"ISO-8859-4" as &[u8]),
309        xmlCharEncoding::XML_CHAR_ENCODING_8859_5 => Some(b"ISO-8859-5" as &[u8]),
310        xmlCharEncoding::XML_CHAR_ENCODING_8859_6 => Some(b"ISO-8859-6" as &[u8]),
311        xmlCharEncoding::XML_CHAR_ENCODING_8859_7 => Some(b"ISO-8859-7" as &[u8]),
312        xmlCharEncoding::XML_CHAR_ENCODING_8859_8 => Some(b"ISO-8859-8" as &[u8]),
313        xmlCharEncoding::XML_CHAR_ENCODING_8859_9 => Some(b"ISO-8859-9" as &[u8]),
314        xmlCharEncoding::XML_CHAR_ENCODING_2022_JP => Some(b"ISO-2022-JP" as &[u8]),
315        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS => Some(b"SHIFT_JIS" as &[u8]),
316        xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP => Some(b"EUC-JP" as &[u8]),
317        xmlCharEncoding::XML_CHAR_ENCODING_ASCII => Some(b"US-ASCII" as &[u8]),
318        _ => None,
319    }
320}
321
322// ═══════════════════════════════════════════════════════════════════════════════
323// 2. UTF-8 validation
324// ═══════════════════════════════════════════════════════════════════════════════
325
326/// Check if a byte sequence is valid UTF-8.
327///
328/// Returns `true` if the entire slice is valid UTF-8, `false` otherwise.
329#[allow(dead_code)]
330pub(crate) const fn utf8_valid(data: &[u8]) -> bool {
331    core::str::from_utf8(data).is_ok()
332}
333
334/// Check if a Unicode codepoint is a valid XML character.
335///
336/// Per XML 1.0 (Fifth Edition) §2.2, the valid character ranges are:
337/// - `#x9` (tab)
338/// - `#xA` (LF)
339/// - `#xD` (CR)
340/// - `#x20` – `#xD7FF`
341/// - `#xE000` – `#xFFFD`
342/// - `#x10000` – `#x10FFFF`
343///
344/// Excludes surrogate halves (`#xD800` – `#xDFFF`) and `#xFFFE`/`#xFFFF`.
345#[allow(dead_code)]
346pub(crate) const fn is_valid_xml_char(cp: u32) -> bool {
347    matches!(
348        cp,
349        0x9 | 0xA | 0xD | 0x20..=0xD7FF | 0xE000..=0xFFFD | 0x10000..=0x10FFFF
350    )
351}
352
353// ═══════════════════════════════════════════════════════════════════════════════
354// 3. UTF-16 handling
355// ═══════════════════════════════════════════════════════════════════════════════
356
357/// Decode a single UTF-16LE code unit from two bytes.
358#[inline]
359const fn read_utf16le_unit(data: &[u8]) -> Option<u16> {
360    if data.len() < 2 {
361        return None;
362    }
363    Some(u16::from_le_bytes([data[0], data[1]]))
364}
365
366/// Decode a single UTF-16BE code unit from two bytes.
367#[inline]
368const fn read_utf16be_unit(data: &[u8]) -> Option<u16> {
369    if data.len() < 2 {
370        return None;
371    }
372    Some(u16::from_be_bytes([data[0], data[1]]))
373}
374
375/// Encode a Unicode codepoint as UTF-8 bytes.
376///
377/// Returns the number of bytes written (1–4), or 0 if the codepoint is invalid.
378const fn encode_codepoint_to_utf8(cp: u32, out: &mut [u8]) -> usize {
379    if cp < 0x80 {
380        if !out.is_empty() {
381            out[0] = cp as u8;
382        }
383        1
384    } else if cp < 0x800 {
385        if out.len() < 2 {
386            return 0;
387        }
388        out[0] = 0xC0 | ((cp >> 6) as u8);
389        out[1] = 0x80 | (cp as u8 & 0x3F);
390        2
391    } else if cp < 0x10000 {
392        if out.len() < 3 {
393            return 0;
394        }
395        out[0] = 0xE0 | ((cp >> 12) as u8);
396        out[1] = 0x80 | ((cp >> 6) as u8 & 0x3F);
397        out[2] = 0x80 | (cp as u8 & 0x3F);
398        3
399    } else if cp < 0x110000 {
400        if out.len() < 4 {
401            return 0;
402        }
403        out[0] = 0xF0 | ((cp >> 18) as u8);
404        out[1] = 0x80 | ((cp >> 12) as u8 & 0x3F);
405        out[2] = 0x80 | ((cp >> 6) as u8 & 0x3F);
406        out[3] = 0x80 | (cp as u8 & 0x3F);
407        4
408    } else {
409        0
410    }
411}
412
413/// Convert UTF-16LE bytes to UTF-8.
414///
415/// Returns `Ok(converted_bytes)` on success, or `Err(())` on invalid input
416/// (e.g., unpaired surrogates, truncated data).
417pub(crate) fn utf16le_to_utf8(data: &[u8]) -> Result<Vec<u8>, ()> {
418    if data.is_empty() {
419        return Ok(Vec::new());
420    }
421
422    // Skip BOM if present
423    let offset = if data.len() >= 2 && data[0..2] == UTF16LE_BOM {
424        2
425    } else {
426        0
427    };
428
429    let mut result = Vec::with_capacity(data.len() / 2 + data.len() / 4);
430    let mut i = offset;
431
432    while i < data.len() {
433        let unit = read_utf16le_unit(&data[i..]).ok_or(())?;
434        i += 2;
435
436        if (0xD800..=0xDBFF).contains(&unit) {
437            // High surrogate: expect a low surrogate
438            let low = read_utf16le_unit(&data[i..]).ok_or(())?;
439            i += 2;
440
441            if !(0xDC00..=0xDFFF).contains(&low) {
442                return Err(());
443            }
444
445            let cp = 0x10000 + ((unit as u32 - 0xD800) << 10) + (low as u32 - 0xDC00);
446            let mut buf = [0u8; 4];
447            let n = encode_codepoint_to_utf8(cp, &mut buf);
448            if n == 0 {
449                return Err(());
450            }
451            result.extend_from_slice(&buf[..n]);
452        } else if (0xDC00..=0xDFFF).contains(&unit) {
453            // Unexpected low surrogate
454            return Err(());
455        } else {
456            let cp = unit as u32;
457            let mut buf = [0u8; 4];
458            let n = encode_codepoint_to_utf8(cp, &mut buf);
459            result.extend_from_slice(&buf[..n]);
460        }
461    }
462
463    Ok(result)
464}
465
466/// Convert UTF-16BE bytes to UTF-8.
467///
468/// Returns `Ok(converted_bytes)` on success, or `Err(())` on invalid input.
469pub(crate) fn utf16be_to_utf8(data: &[u8]) -> Result<Vec<u8>, ()> {
470    if data.is_empty() {
471        return Ok(Vec::new());
472    }
473
474    // Skip BOM if present
475    let offset = if data.len() >= 2 && data[0..2] == UTF16BE_BOM {
476        2
477    } else {
478        0
479    };
480
481    let mut result = Vec::with_capacity(data.len() / 2 + data.len() / 4);
482    let mut i = offset;
483
484    while i < data.len() {
485        let unit = read_utf16be_unit(&data[i..]).ok_or(())?;
486        i += 2;
487
488        if (0xD800..=0xDBFF).contains(&unit) {
489            // High surrogate: expect a low surrogate
490            let low = read_utf16be_unit(&data[i..]).ok_or(())?;
491            i += 2;
492
493            if !(0xDC00..=0xDFFF).contains(&low) {
494                return Err(());
495            }
496
497            let cp = 0x10000 + ((unit as u32 - 0xD800) << 10) + (low as u32 - 0xDC00);
498            let mut buf = [0u8; 4];
499            let n = encode_codepoint_to_utf8(cp, &mut buf);
500            if n == 0 {
501                return Err(());
502            }
503            result.extend_from_slice(&buf[..n]);
504        } else if (0xDC00..=0xDFFF).contains(&unit) {
505            // Unexpected low surrogate
506            return Err(());
507        } else {
508            let cp = unit as u32;
509            let mut buf = [0u8; 4];
510            let n = encode_codepoint_to_utf8(cp, &mut buf);
511            result.extend_from_slice(&buf[..n]);
512        }
513    }
514
515    Ok(result)
516}
517
518/// Encode a Unicode codepoint as UTF-16LE bytes.
519///
520/// Returns the number of bytes written (2 or 4), or 0 if the codepoint is invalid.
521fn encode_codepoint_to_utf16le(cp: u32, out: &mut [u8]) -> usize {
522    if cp < 0x10000 {
523        if out.len() < 2 {
524            return 0;
525        }
526        let u = cp as u16;
527        out[..2].copy_from_slice(&u.to_le_bytes());
528        2
529    } else if cp < 0x110000 {
530        if out.len() < 4 {
531            return 0;
532        }
533        let cp = cp - 0x10000;
534        let high = 0xD800 | ((cp >> 10) as u16);
535        let low = 0xDC00 | (cp as u16 & 0x3FF);
536        out[..2].copy_from_slice(&high.to_le_bytes());
537        out[2..4].copy_from_slice(&low.to_le_bytes());
538        4
539    } else {
540        0
541    }
542}
543
544/// Convert UTF-8 bytes to UTF-16LE.
545///
546/// Returns `Ok(converted_bytes)` on success, or `Err(())` on invalid UTF-8 input.
547pub(crate) fn utf8_to_utf16le(data: &[u8]) -> Result<Vec<u8>, ()> {
548    let s = core::str::from_utf8(data).map_err(|_| ())?;
549    let mut result = Vec::with_capacity(data.len() * 2);
550
551    for ch in s.chars() {
552        let cp = ch as u32;
553        let mut buf = [0u8; 4];
554        let n = encode_codepoint_to_utf16le(cp, &mut buf);
555        if n == 0 {
556            return Err(());
557        }
558        result.extend_from_slice(&buf[..n]);
559    }
560
561    Ok(result)
562}
563
564// ═══════════════════════════════════════════════════════════════════════════════
565// 4. ISO-8859-1 (Latin-1) handling
566// ═══════════════════════════════════════════════════════════════════════════════
567
568/// Convert Latin-1 (ISO-8859-1) bytes to UTF-8.
569///
570/// Latin-1 maps codepoints 0x00–0xFF directly to Unicode codepoints U+0000–U+00FF.
571/// Each input byte produces either 1 or 2 UTF-8 bytes.
572#[allow(dead_code)]
573pub(crate) fn latin1_to_utf8(data: &[u8]) -> Vec<u8> {
574    let mut result = Vec::with_capacity(data.len() * 2);
575
576    for &byte in data {
577        let cp = byte as u32;
578        let mut buf = [0u8; 2];
579        let n = encode_codepoint_to_utf8(cp, &mut buf);
580        result.extend_from_slice(&buf[..n]);
581    }
582
583    result
584}
585
586/// Convert UTF-8 bytes to Latin-1 (ISO-8859-1).
587///
588/// Returns `Err(())` if the input is not valid UTF-8 or contains codepoints
589/// outside the Latin-1 range (U+0000–U+00FF).
590pub(crate) fn utf8_to_latin1(data: &[u8]) -> Result<Vec<u8>, ()> {
591    let s = core::str::from_utf8(data).map_err(|_| ())?;
592    let mut result = Vec::with_capacity(data.len());
593
594    for ch in s.chars() {
595        let cp = ch as u32;
596        if cp > 0xFF {
597            return Err(());
598        }
599        result.push(cp as u8);
600    }
601
602    Ok(result)
603}
604
605// ═══════════════════════════════════════════════════════════════════════════════
606// 5. Encoding handler registry
607// ═══════════════════════════════════════════════════════════════════════════════
608
609/// Initialize the built-in encoding handlers.
610///
611/// This function registers handlers for:
612/// - UTF-8 (identity/no conversion)
613/// - UTF-16LE
614/// - UTF-16BE
615/// - ISO-8859-1 (Latin-1)
616/// - ASCII
617///
618/// Safe to call multiple times — only the first call has an effect.
619pub(crate) fn init_encodings() {
620    if ENCODING_INITIALIZED.load(Ordering::SeqCst) {
621        return;
622    }
623    // Serialize first-time registration: without the mutex, a second thread
624    // can observe ENCODING_INITIALIZED == true and look up handlers while
625    // the first thread is still registering them (race found by the parallel
626    // test suite: xml::io test_output_buffer_with_encoding intermittently
627    // failed to find the Latin-1 handler).
628    let _guard = ENCODING_INIT_MUTEX.lock();
629    if ENCODING_INITIALIZED.load(Ordering::SeqCst) {
630        return;
631    }
632    register_builtin_handlers();
633    ENCODING_INITIALIZED.store(true, Ordering::SeqCst);
634}
635
636/// Register all built-in encoding handlers.
637fn register_builtin_handlers() {
638    // UTF-8 (identity handler — no conversion needed)
639    register_handler(
640        b"UTF-8\0",
641        xmlCharEncoding::XML_CHAR_ENCODING_UTF8,
642        xmlCharEncoding::XML_CHAR_ENCODING_UTF8,
643        Some(utf8_input_func as xmlCharEncodingInputFunc),
644        Some(utf8_output_func as xmlCharEncodingOutputFunc),
645    );
646
647    // UTF-16LE
648    register_handler(
649        b"UTF-16LE\0",
650        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
651        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
652        Some(utf16le_input_func as xmlCharEncodingInputFunc),
653        Some(utf16le_output_func as xmlCharEncodingOutputFunc),
654    );
655
656    // UTF-16BE
657    register_handler(
658        b"UTF-16BE\0",
659        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE,
660        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE,
661        Some(utf16be_input_func as xmlCharEncodingInputFunc),
662        Some(utf16be_output_func as xmlCharEncodingOutputFunc),
663    );
664
665    // ISO-8859-1 (Latin-1)
666    register_handler(
667        b"ISO-8859-1\0",
668        xmlCharEncoding::XML_CHAR_ENCODING_8859_1,
669        xmlCharEncoding::XML_CHAR_ENCODING_8859_1,
670        Some(latin1_input_func as xmlCharEncodingInputFunc),
671        Some(latin1_output_func as xmlCharEncodingOutputFunc),
672    );
673
674    // Windows-1252 (CP1252): served by iconv on the oracle; native converter
675    // here (R-000157 partial closure). Registered under both the canonical
676    // spelling and the cp1252 alias; lookup is case-insensitive so
677    // "Windows-1252" (the Dom\XMLDocument overrideEncoding the PHP court
678    // passes verbatim) resolves to the same entry.
679    register_handler(
680        b"windows-1252\0",
681        xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
682        xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
683        Some(cp1252_input_func as xmlCharEncodingInputFunc),
684        Some(cp1252_output_func as xmlCharEncodingOutputFunc),
685    );
686    register_handler(
687        b"cp1252\0",
688        xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
689        xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
690        Some(cp1252_input_func as xmlCharEncodingInputFunc),
691        Some(cp1252_output_func as xmlCharEncodingOutputFunc),
692    );
693
694    // ASCII — upstream's static default handler (defaultHandlers[22]) is named
695    // "US-ASCII"; the name "ASCII" is registered as a second entry so name-based
696    // lookups (xmlFindCharEncodingHandler, the saver path) accept both spellings
697    // exactly like upstream's xmlParseCharEncodingInternal mapping.
698    register_handler(
699        b"US-ASCII\0",
700        xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
701        xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
702        Some(ascii_input_func as xmlCharEncodingInputFunc),
703        Some(ascii_output_func as xmlCharEncodingOutputFunc),
704    );
705    register_handler(
706        b"ASCII\0",
707        xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
708        xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
709        Some(ascii_input_func as xmlCharEncodingInputFunc),
710        Some(ascii_output_func as xmlCharEncodingOutputFunc),
711    );
712
713    // UTF-16 (default handler for enc == XML_CHAR_ENCODING_UTF16 == 23): the
714    // upstream converter is UTF16LEToUTF8/UTF8ToUTF16 (the latter emits the LE
715    // BOM on its init call). Our converter pair is the UTF-16LE pair; the BOM
716    // init protocol is not emitted (documented divergence, conversion only).
717    register_handler(
718        b"UTF-16\0",
719        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
720        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
721        Some(utf16le_input_func as xmlCharEncodingInputFunc),
722        Some(utf16le_output_func as xmlCharEncodingOutputFunc),
723    );
724}
725
726/// Helper to create and register an encoding handler.
727///
728/// # Safety
729///
730/// - `name_bytes` must be a valid byte slice containing a NUL terminator;
731///   `xmlMemStrdupImpl` scans it as a C string.
732/// - The `xmlMallocImpl` result is NULL-checked before `ptr::write`
733///   initializes the handler; the written handler is inserted into the
734///   global registry, which keeps it alive for the process lifetime.
735fn register_handler(
736    name_bytes: &[u8],
737    _input_enc: xmlCharEncoding,
738    _output_enc: xmlCharEncoding,
739    input_func: Option<xmlCharEncodingInputFunc>,
740    output_func: Option<xmlCharEncodingOutputFunc>,
741) {
742    let name_raw =
743        unsafe { crate::abi::allocator::xmlMemStrdupImpl(name_bytes.as_ptr() as *const c_char) };
744    if name_raw.is_null() {
745        return;
746    }
747
748    let handler = unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) }
749        as *mut _xmlCharEncodingHandler;
750
751    if handler.is_null() {
752        unsafe { xmlFreeImpl(name_raw) };
753        return;
754    }
755
756    unsafe {
757        ptr::write(
758            handler,
759            _xmlCharEncodingHandler {
760                name: name_raw as *mut c_char,
761                input: EncodingInputUnion {
762                    legacyFunc: input_func,
763                },
764                output: EncodingOutputUnion {
765                    legacyFunc: output_func,
766                },
767                inputCtxt: ptr::null_mut(),
768                outputCtxt: ptr::null_mut(),
769                ctxtDtor: None,
770                flags: 0,
771            },
772        );
773    }
774
775    add_encoding_handler(handler);
776}
777
778/// Clean up encoding handlers.
779///
780/// Frees all registered handlers and resets the registry.
781///
782/// # Safety
783///
784/// - Every registered handler pointer must be NULL or a valid
785///   heap-allocated `_xmlCharEncodingHandler` whose `name` is NULL or a
786///   heap-allocated NUL-terminated string; each allocation is freed exactly
787///   once and must not be freed elsewhere.
788pub(crate) fn cleanup_encodings() {
789    let mut handlers = ENCODING_HANDLERS.write();
790    for &handler in handlers.iter() {
791        let ptr = handler.0;
792        if !ptr.is_null() {
793            unsafe {
794                if !(*ptr).name.is_null() {
795                    xmlFreeImpl((*ptr).name as *mut c_void);
796                }
797                xmlFreeImpl(ptr as *mut c_void);
798            }
799        }
800    }
801    handlers.clear();
802    // Re-allow registration on the next init_encodings()/cleanup round-trip so
803    // a caller that cleans up and then (re)initializes in another thread does
804    // not observe a stale "already initialized" registry that stays empty.
805    // (ENCODING_INITIALIZED/ENCODING_INIT_MUTEX are separate statics.)
806    drop(handlers);
807    ENCODING_INITIALIZED.store(false, Ordering::SeqCst);
808}
809
810/// Find an encoding handler by name.
811///
812/// Searches the global handler registry for a handler whose name matches
813/// (case-insensitive). Returns a pointer to the handler, or `ptr::null_mut()`
814/// if not found.
815///
816/// # Safety
817///
818/// - `name` must be NULL or a valid pointer to a NUL-terminated string.
819/// - Each registry entry must be NULL or a valid `_xmlCharEncodingHandler`
820///   whose `name` is NULL or a valid NUL-terminated string.
821pub(crate) fn find_encoding_handler(name: *const xmlChar) -> *mut _xmlCharEncodingHandler {
822    if name.is_null() {
823        return ptr::null_mut();
824    }
825
826    /* The upstream default-handler table is static and always present; the
827     * candidate's registry is populated lazily, so ensure it is initialized
828     * before any name-based lookup. Idempotent. */
829    init_encodings();
830
831    let name_str = unsafe {
832        match CStr::from_ptr(name as *const c_char).to_bytes() {
833            b"" => return ptr::null_mut(),
834            s => s,
835        }
836    };
837
838    let handlers = ENCODING_HANDLERS.read();
839    for &handler in handlers.iter() {
840        let ptr = handler.0;
841        if ptr.is_null() {
842            continue;
843        }
844        let h_name = unsafe {
845            if (*ptr).name.is_null() {
846                continue;
847            }
848            CStr::from_ptr((*ptr).name).to_bytes()
849        };
850
851        if name_str.eq_ignore_ascii_case(h_name) {
852            return ptr;
853        }
854    }
855
856    ptr::null_mut()
857}
858
859/// Build an owned, caller-freed copy of a registered encoding handler.
860///
861/// Upstream's `xmlFindCharEncodingHandler` hands the caller a handler it owns
862/// and is expected to release with `xmlCharEncCloseFunc` after use (Phase 14
863/// PHP court: `dom_document_encoding_write` finds a handler then closes it for
864/// every write). Returning the persistent registry pointer directly would let
865/// the exported close free the registry entry out from under later lookups
866/// (a use-after-free seen as `DOMDocument::$encoding = 'UTF-16'` corrupting the
867/// handler registry and crashing the next `find_encoding_handler`).
868///
869/// The copy duplicates the name with `xmlMemStrdupImpl` so the caller may free
870/// it; the conversion unions and context pointers are shared with the original
871/// registry entry. All built-in registry handlers the find path serves are
872/// stateless (`ctxtDtor` is None, contexts NULL), so `xmlCharEncCloseFunc` on
873/// the copy only releases the duplicated name and the struct.
874///
875/// Returns the new handler or `ptr::null_mut()` when `src` is NULL/alloc fails.
876pub(crate) fn clone_encoding_handler_for_find(
877    src: *mut _xmlCharEncodingHandler,
878) -> *mut _xmlCharEncodingHandler {
879    if src.is_null() {
880        return ptr::null_mut();
881    }
882    let name_raw = unsafe {
883        let nm = (*src).name;
884        if nm.is_null() {
885            ptr::null_mut()
886        } else {
887            crate::abi::allocator::xmlMemStrdupImpl(nm)
888        }
889    };
890    let handler = unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) }
891        as *mut _xmlCharEncodingHandler;
892    if handler.is_null() {
893        if !name_raw.is_null() {
894            unsafe { crate::abi::allocator::xmlFreeImpl(name_raw) };
895        }
896        return ptr::null_mut();
897    }
898    unsafe {
899        ptr::write(
900            handler,
901            _xmlCharEncodingHandler {
902                name: name_raw as *mut c_char,
903                input: ptr::read(&(*src).input),
904                output: ptr::read(&(*src).output),
905                inputCtxt: (*src).inputCtxt,
906                outputCtxt: (*src).outputCtxt,
907                ctxtDtor: (*src).ctxtDtor,
908                flags: (*src).flags,
909            },
910        );
911    }
912    handler
913}
914
915/// Upstream handler `flags` marker: the handler lives for the process lifetime
916/// (upstream `encoding.c` `{"UTF-8", ... , XML_HANDLER_STATIC}`) and
917/// `xmlCharEncCloseFunc` must therefore not release it.
918pub(crate) const XML_HANDLER_STATIC: c_int = 0x01;
919
920/// ABI `xmlFindCharEncodingHandler` mirror (upstream libxml2 2.15 encoding.c
921/// `xmlFindCharEncodingHandler`).
922///
923/// Upstream returns an OWNED handler the caller releases with
924/// `xmlCharEncCloseFunc`, except for UTF-8/UTF8 where it returns the static
925/// `defaultHandlers[XML_CHAR_ENCODING_UTF8]` (has `XML_HANDLER_STATIC`, so
926/// `xmlCharEncCloseFunc` is a no-op). Phase 14 PHP court:
927/// `dom_document_encoding_write` finds a handler for every `$dom->encoding=
928/// write and closes it — so returning the persistent registry pointer for a
929/// non-UTF-8 encoding let the caller's close free the registry entry (the
930/// use-after-free behind `DOMDocument::$encoding = 'UTF-16'` crashing the next
931/// `find_encoding_handler`).
932///
933/// Returns an owned heap copy for non-UTF-8 encodings, the flagged-static
934/// registry UTF-8 handler for UTF-8, or `ptr::null_mut()` when `name` is NULL
935/// or no handler is registered.
936pub(crate) fn xmlFindCharEncodingHandler_owned(
937    name: *const xmlChar,
938) -> *mut _xmlCharEncodingHandler {
939    if name.is_null() {
940        return ptr::null_mut();
941    }
942    let name_bytes = unsafe {
943        let len = libc::strlen(name as *const c_char);
944        core::slice::from_raw_parts(name as *const u8, len)
945    };
946
947    // UTF-8 / UTF8 special case (upstream returns the static handler).
948    if encoding_from_name(name_bytes) == xmlCharEncoding::XML_CHAR_ENCODING_UTF8 {
949        let utf8 = find_encoding_handler(c"UTF-8".as_ptr() as *const xmlChar);
950        if utf8.is_null() {
951            return ptr::null_mut();
952        }
953        // Flag it static so xmlCharEncCloseFunc does not free the registry entry.
954        unsafe {
955            (*utf8).flags |= XML_HANDLER_STATIC;
956        }
957        return utf8;
958    }
959
960    // Non-UTF-8: resolve the registry entry, preferring a canonical lookup when
961    // the raw spelling is not itself a registered key (mirrors the canonical
962    // re-lookup upstream performs in xmlCreateCharEncodingHandler).
963    let mut entry = find_encoding_handler(name as *const xmlChar);
964    if entry.is_null() {
965        if let Some(canon) = encoding_name(encoding_from_name(name_bytes)) {
966            entry = find_encoding_handler(canon.as_ptr() as *const xmlChar);
967        }
968    }
969    // Return an OWNED copy of the registry entry (never the entry itself), so
970    // the caller's xmlCharEncCloseFunc releases only the copy.
971    clone_encoding_handler_for_find(entry)
972}
973
974/// Add an encoding handler to the registry.
975///
976/// Returns 0 on success, -1 on failure (e.g., null pointer).
977pub(crate) fn add_encoding_handler(handler: *mut _xmlCharEncodingHandler) -> c_int {
978    if handler.is_null() {
979        return -1;
980    }
981
982    let mut handlers = ENCODING_HANDLERS.write();
983    handlers.push(HandlerPtr(handler));
984    0
985}
986
987// ═══════════════════════════════════════════════════════════════════════════════
988// 6. Encoding conversion functions
989// ═══════════════════════════════════════════════════════════════════════════════
990
991/// Input conversion: convert from handler's input encoding to UTF-8.
992///
993/// Calls the handler's `input.legacyFunc` callback. Returns bytes written or -1 on error.
994///
995/// # Safety
996///
997/// - `handler` must be NULL or a valid pointer to an initialized
998///   `_xmlCharEncodingHandler`; the stored `input.legacyFunc` callback, when
999///   present, must be a valid function pointer.
1000/// - `out` must be a valid mutable byte slice and `in_data` a valid byte
1001///   slice; both stay valid for the duration of the callback.
1002#[allow(dead_code)]
1003pub(crate) fn char_enc_in_func(
1004    handler: *mut _xmlCharEncodingHandler,
1005    out: &mut [u8],
1006    in_data: &[u8],
1007) -> c_int {
1008    if handler.is_null() {
1009        return -1;
1010    }
1011
1012    let h = unsafe { &*handler };
1013    let input_func = unsafe { h.input.legacyFunc };
1014    let input_func = match input_func {
1015        Some(f) => f,
1016        None => return -1,
1017    };
1018
1019    let mut outlen = out.len() as c_int;
1020    let mut inlen = in_data.len() as c_int;
1021
1022    unsafe { input_func(out.as_mut_ptr(), &mut outlen, in_data.as_ptr(), &mut inlen) }
1023}
1024
1025/// Output conversion: convert from UTF-8 to handler's output encoding.
1026///
1027/// Calls the handler's `output.legacyFunc` callback. Returns bytes written or -1 on error.
1028///
1029/// # Safety
1030///
1031/// - `handler` must be NULL or a valid pointer to an initialized
1032///   `_xmlCharEncodingHandler`; the stored `output.legacyFunc` callback,
1033///   when present, must be a valid function pointer.
1034/// - `out` must be a valid mutable byte slice and `in_data` a valid byte
1035///   slice; both stay valid for the duration of the callback.
1036#[allow(dead_code)]
1037pub(crate) fn char_enc_out_func(
1038    handler: *mut _xmlCharEncodingHandler,
1039    out: &mut [u8],
1040    in_data: &[u8],
1041) -> c_int {
1042    if handler.is_null() {
1043        return -1;
1044    }
1045
1046    let h = unsafe { &*handler };
1047    let output_func = unsafe { h.output.legacyFunc };
1048    let output_func = match output_func {
1049        Some(f) => f,
1050        None => return -1,
1051    };
1052
1053    let mut outlen = out.len() as c_int;
1054    let mut inlen = in_data.len() as c_int;
1055
1056    unsafe { output_func(out.as_mut_ptr(), &mut outlen, in_data.as_ptr(), &mut inlen) }
1057}
1058
1059/// Full input conversion (`xmlCharEncInFunc` equivalent).
1060///
1061/// Reads from the input `_xmlBuffer`, converts via the handler's `input.legacyFunc`,
1062/// and appends the result to the output `_xmlBuffer`.
1063///
1064/// Returns the number of bytes written to the output buffer, or -1 on error.
1065///
1066/// # Safety
1067///
1068/// - `handler` must be NULL or a valid `_xmlCharEncodingHandler` whose
1069///   `input.legacyFunc` callback is a valid function pointer.
1070/// - `in_` and `out` must be NULL or valid `_xmlBuffer` pointers; `in_`'s
1071///   `content` must be NULL or point to `use_` readable bytes, and `out`
1072///   must stay valid while `append_to_xml_buffer` may reallocate its
1073///   `content`.
1074pub(crate) fn char_enc_in(
1075    handler: *mut _xmlCharEncodingHandler,
1076    out: *mut _xmlBuffer,
1077    in_: *mut _xmlBuffer,
1078) -> c_int {
1079    if handler.is_null() || out.is_null() || in_.is_null() {
1080        return -1;
1081    }
1082
1083    let h = unsafe { &*handler };
1084    let input_func = unsafe { h.input.legacyFunc };
1085    let input_func = match input_func {
1086        Some(f) => f,
1087        None => return -1,
1088    };
1089
1090    let in_buf = unsafe { &*in_ };
1091    let out_buf = unsafe { &mut *out };
1092
1093    if in_buf.content.is_null() || in_buf.use_ == 0 {
1094        return 0;
1095    }
1096
1097    let in_data = unsafe { core::slice::from_raw_parts(in_buf.content, in_buf.use_ as usize) };
1098
1099    // Allocate an output buffer. A good heuristic is 2x input for UTF-16→UTF-8.
1100    let out_capacity = (in_buf.use_ as usize).saturating_mul(3).max(256);
1101    let mut out_vec = vec![0u8; out_capacity];
1102    let mut out_len = out_capacity as c_int;
1103    let mut in_len = in_buf.use_ as c_int;
1104
1105    let ret = unsafe {
1106        input_func(
1107            out_vec.as_mut_ptr(),
1108            &mut out_len,
1109            in_data.as_ptr(),
1110            &mut in_len,
1111        )
1112    };
1113
1114    if ret < 0 {
1115        return -1;
1116    }
1117
1118    let written = ret as usize;
1119
1120    // Append to output buffer
1121    append_to_xml_buffer(out_buf, &out_vec[..written]);
1122
1123    written as c_int
1124}
1125
1126/// Full output conversion (`xmlCharEncOutFunc` equivalent).
1127///
1128/// Reads from the input `_xmlBuffer` (UTF-8), converts via the handler's
1129/// `output.legacyFunc`, and appends the result to the output `_xmlBuffer`.
1130///
1131/// Returns the number of bytes written to the output buffer, or -1 on error.
1132///
1133/// # Safety
1134///
1135/// - `handler` must be NULL or a valid `_xmlCharEncodingHandler` whose
1136///   `output.legacyFunc` callback is a valid function pointer.
1137/// - `in_` and `out` must be NULL or valid `_xmlBuffer` pointers; `in_`'s
1138///   `content` must be NULL or point to `use_` readable bytes, and `out`
1139///   must stay valid while `append_to_xml_buffer` may reallocate its
1140///   `content`.
1141pub(crate) fn char_enc_out(
1142    handler: *mut _xmlCharEncodingHandler,
1143    out: *mut _xmlBuffer,
1144    in_: *mut _xmlBuffer,
1145) -> c_int {
1146    if handler.is_null() || out.is_null() || in_.is_null() {
1147        return -1;
1148    }
1149
1150    let h = unsafe { &*handler };
1151    let output_func = unsafe { h.output.legacyFunc };
1152    let output_func = match output_func {
1153        Some(f) => f,
1154        None => return -1,
1155    };
1156
1157    let in_buf = unsafe { &*in_ };
1158    let out_buf = unsafe { &mut *out };
1159
1160    if in_buf.content.is_null() || in_buf.use_ == 0 {
1161        return 0;
1162    }
1163
1164    let mut in_data = unsafe { core::slice::from_raw_parts(in_buf.content, in_buf.use_ as usize) };
1165
1166    // UPSTREAM-PARITY (encoding.c xmlCharEncOutput): the output conversion
1167    // runs in a loop; when the converter reports an INPUT error (a character
1168    // not representable in the output encoding — the ASCII handler stops at
1169    // the first byte >= 0x80), the offending UTF-8 character is decoded and
1170    // replaced by a DECIMAL character reference (&#NNN;), then conversion
1171    // continues. This is how libxml2 serializes non-ASCII text into an
1172    // ASCII output buffer (lxml's default `tostring` encoding, which
1173    // produces `&#195;&#169;` for the mojibake case).
1174    const ENC_INPUT_ERROR: c_int = -2;
1175    let mut total_written: usize = 0;
1176    loop {
1177        let out_capacity = (in_data.len().saturating_mul(3)).max(64) + 16;
1178        let mut out_vec = vec![0u8; out_capacity];
1179        let mut out_len = out_capacity as c_int;
1180        let mut in_len = in_data.len() as c_int;
1181        let ret = unsafe {
1182            output_func(
1183                out_vec.as_mut_ptr(),
1184                &mut out_len,
1185                in_data.as_ptr(),
1186                &mut in_len,
1187            )
1188        };
1189        let written = out_len.max(0) as usize;
1190        if written > 0 {
1191            append_to_xml_buffer(out_buf, &out_vec[..written]);
1192            total_written += written;
1193        }
1194        let consumed = in_len.max(0) as usize;
1195        if ret == ENC_INPUT_ERROR && consumed < in_data.len() {
1196            // Decode the UTF-8 character at the offending position and emit
1197            // a decimal character reference (upstream xmlSerializeDecCharRef).
1198            let mut clen: c_int = 4;
1199            let cp = unsafe {
1200                crate::abi::exports_misc::xmlGetUTF8Char(in_data[consumed..].as_ptr(), &mut clen)
1201            };
1202            if cp <= 0 || clen <= 0 || (consumed + clen as usize) > in_data.len() {
1203                return -1;
1204            }
1205            let ref_str = format!("&#{};", cp);
1206            append_to_xml_buffer(out_buf, ref_str.as_bytes());
1207            total_written += ref_str.len();
1208            in_data = &in_data[consumed + clen as usize..];
1209            if in_data.is_empty() {
1210                break;
1211            }
1212            continue;
1213        }
1214        if ret < 0 {
1215            return -1;
1216        }
1217        break;
1218    }
1219
1220    total_written as c_int
1221}
1222
1223/// Append bytes to an `_xmlBuffer`, reallocating if needed.
1224///
1225/// # Safety
1226///
1227/// - `buf` must be a valid `_xmlBuffer` whose `content` is NULL or points to
1228///   `size` allocated bytes; `buf.content` may be replaced by a fresh
1229///   `xmlReallocImpl` allocation when it must grow.
1230/// - `data` must be a valid byte slice; after the call, `buf.content` holds
1231///   `use_` initialized bytes.
1232fn append_to_xml_buffer(buf: &mut _xmlBuffer, data: &[u8]) {
1233    if data.is_empty() {
1234        return;
1235    }
1236
1237    let new_use = (buf.use_ as usize).saturating_add(data.len());
1238    if new_use > buf.size as usize {
1239        // Grow buffer: double or fit, whichever is larger
1240        let new_size = (buf.size as usize).saturating_mul(2).max(new_use).max(256);
1241        let new_content =
1242            unsafe { xmlReallocImpl(buf.content as *mut c_void, new_size) as *mut xmlChar };
1243        if new_content.is_null() {
1244            return; // Allocation failure — silently skip
1245        }
1246        buf.content = new_content;
1247        // UPSTREAM-PARITY (io/mod.rs buf_add realloc paths): when the buffer
1248        // grows, contentIO tracks the CURRENT allocation base — buf_free
1249        // frees contentIO, so a stale contentIO (the pre-realloc block) would
1250        // cause a double-free on buffers that grew through this conversion
1251        // path (nokogiri HTML4/HTML5 UTF-8 serialization).
1252        buf.contentIO = new_content;
1253        buf.size = new_size as c_uint;
1254    }
1255
1256    unsafe {
1257        ptr::copy_nonoverlapping(
1258            data.as_ptr(),
1259            buf.content.add(buf.use_ as usize),
1260            data.len(),
1261        );
1262    }
1263    buf.use_ = new_use as c_uint;
1264}
1265
1266// ═══════════════════════════════════════════════════════════════════════════════
1267// 7. Built-in encoding handler callbacks (extern "C")
1268// ═══════════════════════════════════════════════════════════════════════════════
1269
1270// ── UTF-8 (identity) ──────────────────────────────────────────────────────
1271
1272/// UTF-8 input function: identity (input is already UTF-8).
1273///
1274/// Simply copies bytes from input to output, up to the available space.
1275unsafe extern "C" fn utf8_input_func(
1276    out: *mut c_uchar,
1277    outlen: *mut c_int,
1278    in_: *const c_uchar,
1279    inlen: *mut c_int,
1280) -> c_int {
1281    let avail_out = *outlen as usize;
1282    let avail_in = *inlen as usize;
1283    let to_copy = avail_out.min(avail_in);
1284
1285    if to_copy > 0 {
1286        ptr::copy_nonoverlapping(in_, out, to_copy);
1287    }
1288
1289    *outlen = to_copy as c_int;
1290    *inlen = to_copy as c_int;
1291    to_copy as c_int
1292}
1293
1294/// UTF-8 output function: identity (output is already UTF-8).
1295unsafe extern "C" fn utf8_output_func(
1296    out: *mut c_uchar,
1297    outlen: *mut c_int,
1298    in_: *const c_uchar,
1299    inlen: *mut c_int,
1300) -> c_int {
1301    utf8_input_func(out, outlen, in_, inlen)
1302}
1303
1304// ── UTF-16LE ──────────────────────────────────────────────────────────────
1305
1306/// UTF-16LE input function: convert UTF-16LE to UTF-8.
1307unsafe extern "C" fn utf16le_input_func(
1308    out: *mut c_uchar,
1309    outlen: *mut c_int,
1310    in_: *const c_uchar,
1311    inlen: *mut c_int,
1312) -> c_int {
1313    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1314        return -1;
1315    }
1316
1317    let avail_in = *inlen as usize;
1318    let avail_out = *outlen as usize;
1319
1320    if avail_in == 0 || avail_out == 0 {
1321        *outlen = 0;
1322        *inlen = 0;
1323        return 0;
1324    }
1325
1326    let in_data = core::slice::from_raw_parts(in_, avail_in);
1327    let _out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1328
1329    // Use the safe wrapper
1330    let result = match utf16le_to_utf8(in_data) {
1331        Ok(v) => v,
1332        Err(()) => return -1,
1333    };
1334
1335    let written = result.len().min(avail_out);
1336    if written > 0 {
1337        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1338    }
1339
1340    *outlen = written as c_int;
1341    *inlen = avail_in as c_int; // All input consumed
1342    written as c_int
1343}
1344
1345/// UTF-16LE output function: convert UTF-8 to UTF-16LE.
1346unsafe extern "C" fn utf16le_output_func(
1347    out: *mut c_uchar,
1348    outlen: *mut c_int,
1349    in_: *const c_uchar,
1350    inlen: *mut c_int,
1351) -> c_int {
1352    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1353        return -1;
1354    }
1355
1356    let avail_in = *inlen as usize;
1357    let avail_out = *outlen as usize;
1358
1359    if avail_in == 0 || avail_out == 0 {
1360        *outlen = 0;
1361        *inlen = 0;
1362        return 0;
1363    }
1364
1365    let in_data = core::slice::from_raw_parts(in_, avail_in);
1366    let _out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1367
1368    let result = match utf8_to_utf16le(in_data) {
1369        Ok(v) => v,
1370        Err(()) => return -1,
1371    };
1372
1373    let written = result.len().min(avail_out);
1374    if written > 0 {
1375        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1376    }
1377
1378    *outlen = written as c_int;
1379    *inlen = avail_in as c_int;
1380    written as c_int
1381}
1382
1383// ── UTF-16BE ──────────────────────────────────────────────────────────────
1384
1385/// UTF-16BE input function: convert UTF-16BE to UTF-8.
1386unsafe extern "C" fn utf16be_input_func(
1387    out: *mut c_uchar,
1388    outlen: *mut c_int,
1389    in_: *const c_uchar,
1390    inlen: *mut c_int,
1391) -> c_int {
1392    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1393        return -1;
1394    }
1395
1396    let avail_in = *inlen as usize;
1397    let avail_out = *outlen as usize;
1398
1399    if avail_in == 0 || avail_out == 0 {
1400        *outlen = 0;
1401        *inlen = 0;
1402        return 0;
1403    }
1404
1405    let in_data = core::slice::from_raw_parts(in_, avail_in);
1406    let _out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1407
1408    let result = match utf16be_to_utf8(in_data) {
1409        Ok(v) => v,
1410        Err(()) => return -1,
1411    };
1412
1413    let written = result.len().min(avail_out);
1414    if written > 0 {
1415        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1416    }
1417
1418    *outlen = written as c_int;
1419    *inlen = avail_in as c_int;
1420    written as c_int
1421}
1422
1423/// UTF-16BE output function: convert UTF-8 to UTF-16BE.
1424unsafe extern "C" fn utf16be_output_func(
1425    out: *mut c_uchar,
1426    outlen: *mut c_int,
1427    in_: *const c_uchar,
1428    inlen: *mut c_int,
1429) -> c_int {
1430    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1431        return -1;
1432    }
1433
1434    let avail_in = *inlen as usize;
1435    let avail_out = *outlen as usize;
1436
1437    if avail_in == 0 || avail_out == 0 {
1438        *outlen = 0;
1439        *inlen = 0;
1440        return 0;
1441    }
1442
1443    let in_data = core::slice::from_raw_parts(in_, avail_in);
1444
1445    // First convert to UTF-16LE, then swap bytes
1446    let le_result = match utf8_to_utf16le(in_data) {
1447        Ok(v) => v,
1448        Err(()) => return -1,
1449    };
1450
1451    // Swap byte pairs to get UTF-16BE
1452    let mut result = le_result;
1453    for chunk in result.as_chunks_mut::<2>().0 {
1454        chunk.swap(0, 1);
1455    }
1456
1457    let written = result.len().min(avail_out);
1458    if written > 0 {
1459        ptr::copy_nonoverlapping(result.as_ptr(), out, written);
1460    }
1461
1462    *outlen = written as c_int;
1463    *inlen = avail_in as c_int;
1464    written as c_int
1465}
1466
1467// ── ISO-8859-1 (Latin-1) ─────────────────────────────────────────────────
1468
1469/// Latin-1 input function: convert ISO-8859-1 to UTF-8.
1470unsafe extern "C" fn latin1_input_func(
1471    out: *mut c_uchar,
1472    outlen: *mut c_int,
1473    in_: *const c_uchar,
1474    inlen: *mut c_int,
1475) -> c_int {
1476    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1477        return -1;
1478    }
1479
1480    let avail_in = *inlen as usize;
1481    let avail_out = *outlen as usize;
1482
1483    if avail_in == 0 || avail_out == 0 {
1484        *outlen = 0;
1485        *inlen = 0;
1486        return 0;
1487    }
1488
1489    let in_data = core::slice::from_raw_parts(in_, avail_in);
1490    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1491
1492    let mut in_pos = 0;
1493    let mut out_pos = 0;
1494
1495    while in_pos < avail_in && out_pos < avail_out {
1496        let byte = in_data[in_pos];
1497        in_pos += 1;
1498
1499        if byte < 0x80 {
1500            // Single byte UTF-8
1501            if out_pos < avail_out {
1502                out_slice[out_pos] = byte;
1503                out_pos += 1;
1504            } else {
1505                break;
1506            }
1507        } else {
1508            // Two byte UTF-8: 0xC0 | (byte >> 6), 0x80 | (byte & 0x3F)
1509            // For byte 0x80-0xFF, the encoding is 0xC2-0xC3 followed by continuation
1510            if out_pos + 1 < avail_out {
1511                out_slice[out_pos] = 0xC2 | (byte >> 6);
1512                out_slice[out_pos + 1] = 0x80 | (byte & 0x3F);
1513                out_pos += 2;
1514            } else {
1515                break;
1516            }
1517        }
1518    }
1519
1520    *outlen = out_pos as c_int;
1521    *inlen = in_pos as c_int;
1522    out_pos as c_int
1523}
1524
1525/// Latin-1 output function: convert UTF-8 to ISO-8859-1.
1526unsafe extern "C" fn latin1_output_func(
1527    out: *mut c_uchar,
1528    outlen: *mut c_int,
1529    in_: *const c_uchar,
1530    inlen: *mut c_int,
1531) -> c_int {
1532    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1533        return -1;
1534    }
1535
1536    let avail_in = *inlen as usize;
1537    let avail_out = *outlen as usize;
1538
1539    if avail_in == 0 || avail_out == 0 {
1540        *outlen = 0;
1541        *inlen = 0;
1542        return 0;
1543    }
1544
1545    let in_data = core::slice::from_raw_parts(in_, avail_in);
1546    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1547
1548    let mut in_pos = 0;
1549    let mut out_pos = 0;
1550
1551    while in_pos < avail_in && out_pos < avail_out {
1552        let byte = in_data[in_pos];
1553        in_pos += 1;
1554
1555        if byte < 0x80 {
1556            // ASCII — direct mapping
1557            out_slice[out_pos] = byte;
1558            out_pos += 1;
1559        } else if (0xC2..=0xC3).contains(&byte) {
1560            // Two-byte UTF-8 for codepoints U+0080–U+00FF
1561            if in_pos < avail_in {
1562                let second = in_data[in_pos];
1563                in_pos += 1;
1564                if second & 0xC0 != 0x80 {
1565                    return -1; // Invalid continuation byte
1566                }
1567                let cp = ((byte as u32 & 0x1F) << 6) | (second as u32 & 0x3F);
1568                if cp > 0xFF {
1569                    return -1; // Outside Latin-1 range
1570                }
1571                out_slice[out_pos] = cp as u8;
1572                out_pos += 1;
1573            } else {
1574                return -1; // Truncated
1575            }
1576        } else if (0x80..=0xBF).contains(&byte) {
1577            // Unexpected continuation byte
1578            return -1;
1579        } else {
1580            // Multi-byte sequence for codepoints > U+00FF
1581            // Skip the rest of the sequence and return error
1582            return -1;
1583        }
1584    }
1585
1586    *outlen = out_pos as c_int;
1587    *inlen = in_pos as c_int;
1588    out_pos as c_int
1589}
1590
1591// ── Windows-1252 (CP1252) ────────────────────────────────────────────────
1592
1593/// Windows-1252 mapping for bytes 0x80..=0xFF (WHATWG windows-1252 == glibc
1594/// iconv CP1252). Bytes 0x81, 0x8D, 0x8F, 0x90, 0x9D are UNDEFINED in the
1595/// encoding (iconv raises EILSEQ on them). 0x00..=0x7F are ASCII and 0xA0..=
1596/// 0xFF are the Latin-1 supplement, so only 0x80..=0x9F need the table below
1597/// (indexed by `byte - 0x80`, U+FFFF = undefined).
1598///
1599/// R-000157 closure (partial): the oracle serves windows-1252 through iconv;
1600/// the candidate now ships a native converter for this single-byte set.
1601const CP1252_C1: [u16; 32] = [
1602    0x20AC, 0xFFFF, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, // 80..87
1603    0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0xFFFF, 0x017D, 0xFFFF, // 88..8F
1604    0xFFFF, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, // 90..97
1605    0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0xFFFF, 0x017E, 0x0178, // 98..9F
1606];
1607
1608/// Map a Windows-1252 byte to its Unicode codepoint; `None` for the five
1609/// undefined C1 bytes.
1610#[allow(dead_code)]
1611pub(crate) const fn cp1252_byte_to_cp(byte: u8) -> Option<u32> {
1612    match byte {
1613        0x00..=0x7F => Some(byte as u32),
1614        0x80..=0x9F => {
1615            let cp = CP1252_C1[(byte - 0x80) as usize];
1616            if cp == 0xFFFF {
1617                None
1618            } else {
1619                Some(cp as u32)
1620            }
1621        }
1622        _ => Some(byte as u32), // 0xA0..=0xFF = Latin-1 supplement
1623    }
1624}
1625
1626/// Map a Unicode codepoint back to its Windows-1252 byte; `None` when the
1627/// codepoint is not representable in windows-1252.
1628#[allow(dead_code)]
1629pub(crate) const fn cp_to_cp1252_byte(cp: u32) -> Option<u8> {
1630    if cp < 0x80 || (cp >= 0xA0 && cp <= 0xFF) {
1631        Some(cp as u8)
1632    } else if cp >= 0x80 && cp <= 0x9F {
1633        // Reverse scan of the C1 table (32 entries; called per character on
1634        // output conversion only).
1635        let mut i = 0;
1636        while i < 32 {
1637            if CP1252_C1[i] == cp as u16 {
1638                return Some(0x80 + i as u8);
1639            }
1640            i += 1;
1641        }
1642        None
1643    } else {
1644        None
1645    }
1646}
1647
1648/// Convert a single UTF-8 character starting at `data[in_pos]` to its
1649/// codepoint. Returns `(cp, bytes_consumed)` or `None` on invalid UTF-8.
1650fn decode_utf8_char(data: &[u8], in_pos: usize) -> Option<(u32, usize)> {
1651    let b0 = *data.get(in_pos)?;
1652    if b0 < 0x80 {
1653        return Some((u32::from(b0), 1));
1654    }
1655    let (len, cp0) = match b0 {
1656        0xC2..=0xDF => (2, u32::from(b0 & 0x1F)),
1657        0xE0..=0xEF => (3, u32::from(b0 & 0x0F)),
1658        0xF0..=0xF4 => (4, u32::from(b0 & 0x07)),
1659        _ => return None,
1660    };
1661    if in_pos + len > data.len() {
1662        return None;
1663    }
1664    let mut cp = cp0;
1665    for k in 1..len {
1666        let b = data[in_pos + k];
1667        if b & 0xC0 != 0x80 {
1668            return None;
1669        }
1670        cp = (cp << 6) | u32::from(b & 0x3F);
1671    }
1672    Some((cp, len))
1673}
1674
1675/// Convert a whole CP1252 byte slice to UTF-8.
1676///
1677/// Returns `Err(())` when a byte has no windows-1252 mapping (the five
1678/// undefined C1 bytes 0x81/0x8D/0x8F/0x90/0x9D — iconv raises EILSEQ).
1679pub(crate) fn cp1252_to_utf8(data: &[u8]) -> Result<Vec<u8>, ()> {
1680    let mut result = Vec::with_capacity(data.len() * 2);
1681    for &byte in data {
1682        let cp = match cp1252_byte_to_cp(byte) {
1683            None => return Err(()),
1684            Some(cp) => cp,
1685        };
1686        let mut buf = [0u8; 4];
1687        let n = encode_codepoint_to_utf8(cp, &mut buf);
1688        result.extend_from_slice(&buf[..n]);
1689    }
1690    Ok(result)
1691}
1692
1693/// Convert UTF-8 bytes to CP1252 (used by whole-buffer output paths).
1694///
1695/// Returns `Err(())` on invalid UTF-8 or an unrepresentable codepoint.
1696#[allow(dead_code)]
1697pub(crate) fn utf8_to_cp1252(data: &[u8]) -> Result<Vec<u8>, ()> {
1698    let mut result = Vec::with_capacity(data.len());
1699    let mut pos = 0;
1700    while pos < data.len() {
1701        let (cp, consumed) = match decode_utf8_char(data, pos) {
1702            None => return Err(()),
1703            Some(v) => v,
1704        };
1705        let byte = match cp_to_cp1252_byte(cp) {
1706            None => return Err(()),
1707            Some(b) => b,
1708        };
1709        result.push(byte);
1710        pos += consumed;
1711    }
1712    Ok(result)
1713}
1714
1715/// Windows-1252 input function: convert CP1252 bytes to UTF-8.
1716unsafe extern "C" fn cp1252_input_func(
1717    out: *mut c_uchar,
1718    outlen: *mut c_int,
1719    in_: *const c_uchar,
1720    inlen: *mut c_int,
1721) -> c_int {
1722    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1723        return -1;
1724    }
1725
1726    let avail_in = *inlen as usize;
1727    let avail_out = *outlen as usize;
1728
1729    if avail_in == 0 || avail_out == 0 {
1730        *outlen = 0;
1731        *inlen = 0;
1732        return 0;
1733    }
1734
1735    let in_data = core::slice::from_raw_parts(in_, avail_in);
1736    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1737
1738    let mut in_pos = 0;
1739    let mut out_pos = 0;
1740
1741    while in_pos < avail_in && out_pos < avail_out {
1742        let byte = in_data[in_pos];
1743        let cp = match cp1252_byte_to_cp(byte) {
1744            // Undefined byte (0x81/0x8D/0x8F/0x90/0x9D): EILSEQ like iconv.
1745            None => {
1746                *outlen = out_pos as c_int;
1747                *inlen = in_pos as c_int;
1748                return -1;
1749            }
1750            Some(cp) => cp,
1751        };
1752        let mut buf = [0u8; 4];
1753        let n = encode_codepoint_to_utf8(cp, &mut buf);
1754        if out_pos + n > avail_out {
1755            break;
1756        }
1757        out_slice[out_pos..out_pos + n].copy_from_slice(&buf[..n]);
1758        out_pos += n;
1759        in_pos += 1;
1760    }
1761
1762    *outlen = out_pos as c_int;
1763    *inlen = in_pos as c_int;
1764    out_pos as c_int
1765}
1766
1767/// Windows-1252 output function: convert UTF-8 to CP1252 bytes.
1768unsafe extern "C" fn cp1252_output_func(
1769    out: *mut c_uchar,
1770    outlen: *mut c_int,
1771    in_: *const c_uchar,
1772    inlen: *mut c_int,
1773) -> c_int {
1774    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1775        return -1;
1776    }
1777
1778    let avail_in = *inlen as usize;
1779    let avail_out = *outlen as usize;
1780
1781    if avail_in == 0 || avail_out == 0 {
1782        *outlen = 0;
1783        *inlen = 0;
1784        return 0;
1785    }
1786
1787    let in_data = core::slice::from_raw_parts(in_, avail_in);
1788    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1789
1790    let mut in_pos = 0;
1791    let mut out_pos = 0;
1792
1793    while in_pos < avail_in && out_pos < avail_out {
1794        let (cp, consumed) = match decode_utf8_char(in_data, in_pos) {
1795            None => {
1796                *outlen = out_pos as c_int;
1797                *inlen = in_pos as c_int;
1798                return -1;
1799            }
1800            Some(v) => v,
1801        };
1802        let byte = match cp_to_cp1252_byte(cp) {
1803            None => {
1804                // Not representable in windows-1252: EILSEQ like iconv.
1805                *outlen = out_pos as c_int;
1806                *inlen = in_pos as c_int;
1807                return -1;
1808            }
1809            Some(b) => b,
1810        };
1811        out_slice[out_pos] = byte;
1812        out_pos += 1;
1813        in_pos += consumed;
1814    }
1815
1816    *outlen = out_pos as c_int;
1817    *inlen = in_pos as c_int;
1818    out_pos as c_int
1819}
1820
1821// ── ASCII ─────────────────────────────────────────────────────────────────
1822
1823/// ASCII input function: verify and pass through ASCII data to UTF-8.
1824///
1825/// Returns the number of bytes written, `-1` on invalid arguments, or
1826/// `-2` (the candidate's input-error code) when a byte >= 0x80 is reached
1827/// — in that case `*inlen`/`*outlen` hold the bytes consumed/written before
1828/// the offending character, so the output converter (`char_enc_out`) can
1829/// decode the UTF-8 character and replace it with a decimal character
1830/// reference (upstream `asciiToAscii` returns XML_ENC_ERR_INPUT).
1831unsafe extern "C" fn ascii_input_func(
1832    out: *mut c_uchar,
1833    outlen: *mut c_int,
1834    in_: *const c_uchar,
1835    inlen: *mut c_int,
1836) -> c_int {
1837    if out.is_null() || outlen.is_null() || in_.is_null() || inlen.is_null() {
1838        return -1;
1839    }
1840
1841    let avail_in = *inlen as usize;
1842    let avail_out = *outlen as usize;
1843
1844    if avail_in == 0 || avail_out == 0 {
1845        *outlen = 0;
1846        *inlen = 0;
1847        return 0;
1848    }
1849
1850    let in_data = core::slice::from_raw_parts(in_, avail_in);
1851    let out_slice = core::slice::from_raw_parts_mut(out, avail_out);
1852
1853    let mut pos = 0;
1854    while pos < avail_in && pos < avail_out {
1855        let byte = in_data[pos];
1856        if byte > 0x7F {
1857            // Not valid ASCII: report how much was consumed so the caller
1858            // can substitute a character reference and retry.
1859            *outlen = pos as c_int;
1860            *inlen = pos as c_int;
1861            return -2;
1862        }
1863        out_slice[pos] = byte;
1864        pos += 1;
1865    }
1866
1867    *outlen = pos as c_int;
1868    *inlen = pos as c_int;
1869    pos as c_int
1870}
1871
1872/// ASCII output function: verify and pass through UTF-8 data that is ASCII.
1873unsafe extern "C" fn ascii_output_func(
1874    out: *mut c_uchar,
1875    outlen: *mut c_int,
1876    in_: *const c_uchar,
1877    inlen: *mut c_int,
1878) -> c_int {
1879    // For output, ASCII handler requires that input is already ASCII
1880    ascii_input_func(out, outlen, in_, inlen)
1881}
1882
1883// ═══════════════════════════════════════════════════════════════════════════════
1884// 8. ABI export functions (called from exports_xml2.rs)
1885// ═══════════════════════════════════════════════════════════════════════════════
1886
1887/// `xmlFindCharEncodingHandler` implementation.
1888///
1889/// Finds an encoding handler by name. Returns a pointer to the handler,
1890/// or `ptr::null_mut()` if not found.
1891pub(crate) fn xmlFindCharEncodingHandler(name: *const c_char) -> *mut _xmlCharEncodingHandler {
1892    if name.is_null() {
1893        return ptr::null_mut();
1894    }
1895    find_encoding_handler(name as *const xmlChar)
1896}
1897
1898/// `xmlGetCharEncodingName` implementation.
1899///
1900/// Returns the canonical name for an encoding, or `ptr::null()` if unknown.
1901pub(crate) const fn xmlGetCharEncodingName(enc: xmlCharEncoding) -> *const c_char {
1902    // Return null-terminated C strings using static CStr literals.
1903    // Mirrors upstream 2.15 xmlGetCharEncodingName: the UTF-16/UCS-4 pairs
1904    // return the W3C canonical names before the defaultHandlers table.
1905    match enc {
1906        xmlCharEncoding::XML_CHAR_ENCODING_UTF8 => c"UTF-8".as_ptr(),
1907        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE | xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE => {
1908            c"UTF-16".as_ptr()
1909        }
1910        xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE | xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE => {
1911            c"UCS-4".as_ptr()
1912        }
1913        xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC => c"IBM037".as_ptr(),
1914        xmlCharEncoding::XML_CHAR_ENCODING_UCS2 => c"UCS-2".as_ptr(),
1915        xmlCharEncoding::XML_CHAR_ENCODING_8859_1 => c"ISO-8859-1".as_ptr(),
1916        xmlCharEncoding::XML_CHAR_ENCODING_8859_2 => c"ISO-8859-2".as_ptr(),
1917        xmlCharEncoding::XML_CHAR_ENCODING_8859_3 => c"ISO-8859-3".as_ptr(),
1918        xmlCharEncoding::XML_CHAR_ENCODING_8859_4 => c"ISO-8859-4".as_ptr(),
1919        xmlCharEncoding::XML_CHAR_ENCODING_8859_5 => c"ISO-8859-5".as_ptr(),
1920        xmlCharEncoding::XML_CHAR_ENCODING_8859_6 => c"ISO-8859-6".as_ptr(),
1921        xmlCharEncoding::XML_CHAR_ENCODING_8859_7 => c"ISO-8859-7".as_ptr(),
1922        xmlCharEncoding::XML_CHAR_ENCODING_8859_8 => c"ISO-8859-8".as_ptr(),
1923        xmlCharEncoding::XML_CHAR_ENCODING_8859_9 => c"ISO-8859-9".as_ptr(),
1924        xmlCharEncoding::XML_CHAR_ENCODING_2022_JP => c"ISO-2022-JP".as_ptr(),
1925        xmlCharEncoding::XML_CHAR_ENCODING_SHIFT_JIS => c"Shift_JIS".as_ptr(),
1926        xmlCharEncoding::XML_CHAR_ENCODING_EUC_JP => c"EUC-JP".as_ptr(),
1927        // upstream defaultHandlers[22].name
1928        xmlCharEncoding::XML_CHAR_ENCODING_ASCII => c"US-ASCII".as_ptr(),
1929        _ => ptr::null(),
1930    }
1931}
1932
1933/// `xmlParseCharEncoding` implementation.
1934///
1935/// Parses an encoding name string to an `xmlCharEncoding` enum value,
1936/// returned as `c_int`.
1937///
1938/// # Safety
1939///
1940/// - `name` must be NULL or a valid pointer to a NUL-terminated string.
1941pub(crate) fn xmlParseCharEncoding(name: *const c_char) -> c_int {
1942    if name.is_null() {
1943        return xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int;
1944    }
1945    let bytes = unsafe { CStr::from_ptr(name).to_bytes() };
1946    encoding_from_name(bytes) as c_int
1947}
1948
1949// ── Encoding aliases (upstream encoding.c xmlAddEncodingAlias etc.) ──────────
1950//
1951// A global alias table maps alias names to canonical encoding names.
1952// Upstream keeps a static hash of aliases; the candidate uses a
1953// process-lifetime RwLock<HashMap>. Thread-safe; matches upstream's
1954// observable contract (add/del/get by name).
1955
1956static ENCODING_ALIASES: std::sync::OnceLock<
1957    parking_lot::RwLock<std::collections::HashMap<Vec<u8>, Vec<u8>>>,
1958> = std::sync::OnceLock::new();
1959
1960fn encoding_aliases() -> &'static parking_lot::RwLock<std::collections::HashMap<Vec<u8>, Vec<u8>>> {
1961    ENCODING_ALIASES.get_or_init(|| parking_lot::RwLock::new(std::collections::HashMap::new()))
1962}
1963
1964/// `xmlAddEncodingAlias` implementation: register `alias` for `name`.
1965/// Returns 0 on success, -1 on error (NULL arguments).
1966///
1967/// # Safety
1968///
1969/// - `name` and `alias` must be NULL or valid pointers to NUL-terminated
1970///   strings; both are copied before insertion into the alias table.
1971pub(crate) fn add_encoding_alias(name: *const c_char, alias: *const c_char) -> c_int {
1972    if name.is_null() || alias.is_null() {
1973        return -1;
1974    }
1975    let n = unsafe { CStr::from_ptr(name).to_bytes().to_vec() };
1976    let a = unsafe { CStr::from_ptr(alias).to_bytes().to_vec() };
1977    encoding_aliases().write().insert(a, n);
1978    0
1979}
1980
1981/// `xmlDelEncodingAlias` implementation: remove `alias`.
1982/// Returns 0 on success, -1 if the alias does not exist.
1983///
1984/// # Safety
1985///
1986/// - `alias` must be NULL or a valid pointer to a NUL-terminated string.
1987pub(crate) fn del_encoding_alias(alias: *const c_char) -> c_int {
1988    if alias.is_null() {
1989        return -1;
1990    }
1991    let a = unsafe { CStr::from_ptr(alias).to_bytes().to_vec() };
1992    if encoding_aliases().write().remove(&a).is_some() {
1993        0
1994    } else {
1995        -1
1996    }
1997}
1998
1999/// `xmlGetEncodingAlias` implementation: return the canonical name for
2000/// `alias`, or NULL when not registered.
2001///
2002/// # Safety
2003///
2004/// - `alias` must be NULL or a valid pointer to a NUL-terminated string.
2005/// - The returned pointer is a leaked, process-lifetime NUL-terminated
2006///   string, or NULL; the caller must not free it.
2007pub(crate) fn get_encoding_alias(alias: *const c_char) -> *const c_char {
2008    if alias.is_null() {
2009        return ptr::null();
2010    }
2011    let a = unsafe { CStr::from_ptr(alias).to_bytes().to_vec() };
2012    let guard = encoding_aliases().read();
2013    match guard.get(&a) {
2014        Some(v) => {
2015            // leak the canonical name: upstream returns a pointer valid for
2016            // the process lifetime (the alias hash owns the strings)
2017            let leaked: &'static [u8] = Box::leak(v.clone().into_boxed_slice());
2018            leaked.as_ptr() as *const c_char
2019        }
2020        None => ptr::null(),
2021    }
2022}
2023
2024/// `xmlCleanupEncodingAliases` implementation: drop all aliases.
2025pub(crate) fn cleanup_encoding_aliases() {
2026    encoding_aliases().write().clear();
2027}
2028
2029/// `xmlCharEncInFunc` implementation.
2030///
2031/// Converts the input buffer's encoding to UTF-8 using the given handler.
2032pub(crate) fn xmlCharEncInFunc(
2033    handler: *mut _xmlCharEncodingHandler,
2034    out: *mut _xmlBuffer,
2035    in_: *mut _xmlBuffer,
2036) -> c_int {
2037    char_enc_in(handler, out, in_)
2038}
2039
2040/// `xmlCharEncOutFunc` implementation.
2041///
2042/// Converts the input buffer from UTF-8 to the handler's output encoding.
2043pub(crate) fn xmlCharEncOutFunc(
2044    handler: *mut _xmlCharEncodingHandler,
2045    out: *mut _xmlBuffer,
2046    in_: *mut _xmlBuffer,
2047) -> c_int {
2048    char_enc_out(handler, out, in_)
2049}
2050
2051/// `xmlNewCharEncodingHandler` implementation.
2052///
2053/// Creates a new encoding handler with the given name and conversion functions.
2054/// The name string is duplicated. Returns a pointer to the new handler,
2055/// or `ptr::null_mut()` on allocation failure.
2056///
2057/// # Safety
2058///
2059/// - `name` must be NULL or a valid pointer to a NUL-terminated string that
2060///   stays valid until it is duplicated.
2061/// - `input` and `output` must be valid function pointers matching the
2062///   callback ABI; on success the returned handler owns a duplicated name
2063///   and must be released with `xmlDelEncodingHandler`.
2064pub(crate) fn xmlNewCharEncodingHandler(
2065    name: *const c_char,
2066    input: xmlCharEncodingInputFunc,
2067    output: xmlCharEncodingOutputFunc,
2068) -> *mut _xmlCharEncodingHandler {
2069    if name.is_null() {
2070        return ptr::null_mut();
2071    }
2072
2073    let name_raw = unsafe { crate::abi::allocator::xmlMemStrdupImpl(name) };
2074    if name_raw.is_null() {
2075        return ptr::null_mut();
2076    }
2077
2078    let handler = unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) }
2079        as *mut _xmlCharEncodingHandler;
2080
2081    if handler.is_null() {
2082        unsafe { xmlFreeImpl(name_raw) };
2083        return ptr::null_mut();
2084    }
2085
2086    unsafe {
2087        ptr::write(
2088            handler,
2089            _xmlCharEncodingHandler {
2090                name: name_raw as *mut c_char,
2091                input: EncodingInputUnion {
2092                    legacyFunc: Some(input),
2093                },
2094                output: EncodingOutputUnion {
2095                    legacyFunc: Some(output),
2096                },
2097                inputCtxt: ptr::null_mut(),
2098                outputCtxt: ptr::null_mut(),
2099                ctxtDtor: None,
2100                flags: 0,
2101            },
2102        );
2103    }
2104
2105    handler
2106}
2107
2108/// `xmlDelEncodingHandler` implementation.
2109///
2110/// Frees an encoding handler previously created with `xmlNewCharEncodingHandler`.
2111///
2112/// # Safety
2113///
2114/// - `handler` must be NULL or a valid heap-allocated
2115///   `_xmlCharEncodingHandler` whose `name` is NULL or a heap-allocated
2116///   NUL-terminated string; both allocations are freed exactly once, and the
2117///   handler must have been removed from the registry.
2118#[allow(dead_code)]
2119pub(crate) fn xmlDelEncodingHandler(handler: *mut _xmlCharEncodingHandler) {
2120    if handler.is_null() {
2121        return;
2122    }
2123
2124    // Remove from registry if present
2125    {
2126        let mut handlers = ENCODING_HANDLERS.write();
2127        handlers.retain(|&h| h.0 != handler);
2128    }
2129
2130    unsafe {
2131        if !(*handler).name.is_null() {
2132            xmlFreeImpl((*handler).name as *mut c_void);
2133        }
2134        xmlFreeImpl(handler as *mut c_void);
2135    }
2136}
2137
2138/// `xmlInitCharEncodingHandlers` implementation.
2139pub(crate) fn xmlInitCharEncodingHandlers() {
2140    init_encodings();
2141}
2142
2143/// `xmlCleanupCharEncodingHandlers` implementation.
2144pub(crate) fn xmlCleanupCharEncodingHandlers() {
2145    cleanup_encodings();
2146}
2147
2148// ═══════════════════════════════════════════════════════════════════════════════
2149// 7. Handler lookup / creation (upstream 2.13.0+ encoding.c)
2150// ═══════════════════════════════════════════════════════════════════════════════
2151//
2152// Upstream keeps a static `defaultHandlers[32]` table indexed by xmlCharEncoding
2153// plus iconv/ICU fallbacks. The candidate ships no iconv/ICU, so encodings whose
2154// upstream default handler carries a real converter (UTF-8, UTF-16LE, UTF-16BE,
2155// UTF-16, ISO-8859-1, US-ASCII) resolve to the registered built-in handlers;
2156// every other encoding reports XML_ERR_UNSUPPORTED_ENCODING exactly where
2157// upstream would fall through to iconv/ICU.
2158
2159/// `xmlLookupCharEncodingHandler` implementation (upstream encoding.c).
2160///
2161/// Mirrors the upstream control flow:
2162///  - `out == NULL`                     → XML_ERR_ARGUMENT (115)
2163///  - `enc <= 0 || enc >= 32`           → XML_ERR_UNSUPPORTED_ENCODING (32)
2164///  - UTF-8                             → XML_ERR_OK, `*out` stays NULL
2165///  - native built-in encoding          → XML_ERR_OK, `*out` = static handler
2166///  - iconv/ICU-only encoding           → XML_ERR_UNSUPPORTED_ENCODING
2167///
2168/// The returned handler is a static registry entry and must NOT be freed.
2169///
2170/// # Safety
2171///
2172/// - `out` must be a valid pointer to a `*mut c_void` out-parameter; it is
2173///   written with NULL or a pointer to a static registry handler that the
2174///   caller must not free.
2175pub(crate) fn xmlLookupCharEncodingHandler(enc: c_int, out: *mut *mut c_void) -> c_int {
2176    if out.is_null() {
2177        return crate::abi::types::XML_ERR_ARGUMENT;
2178    }
2179    unsafe {
2180        *out = ptr::null_mut();
2181    }
2182    if enc <= 0 || enc >= 32 {
2183        return crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING;
2184    }
2185    /* Return NULL handler for UTF-8 */
2186    if enc == xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int {
2187        return crate::abi::types::XML_ERR_OK;
2188    }
2189    let canonical: &[u8] = match enc {
2190        /* XML_CHAR_ENCODING_UTF16LE */
2191        2 => b"UTF-16LE\0",
2192        /* XML_CHAR_ENCODING_UTF16BE */
2193        3 => b"UTF-16BE\0",
2194        /* XML_CHAR_ENCODING_8859_1 */
2195        10 => b"ISO-8859-1\0",
2196        /* XML_CHAR_ENCODING_ASCII */
2197        22 => b"US-ASCII\0",
2198        /* XML_CHAR_ENCODING_UTF16 (not in the local enum) */
2199        23 => b"UTF-16\0",
2200        _ => return crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING,
2201    };
2202    let h = find_encoding_handler(canonical.as_ptr() as *const xmlChar);
2203    if h.is_null() {
2204        return crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING;
2205    }
2206    unsafe {
2207        *out = h as *mut c_void;
2208    }
2209    crate::abi::types::XML_ERR_OK
2210}
2211
2212/// `xmlGetCharEncodingHandler` implementation (deprecated upstream wrapper).
2213pub(crate) fn xmlGetCharEncodingHandler(enc: c_int) -> *mut c_void {
2214    let mut ret: *mut c_void = ptr::null_mut();
2215    let _rc = xmlLookupCharEncodingHandler(enc, &mut ret);
2216    ret
2217}
2218
2219/// `xmlCreateCharEncodingHandler` implementation (upstream 2.14.0+ encoding.c).
2220///
2221/// Flags: XML_ENC_INPUT = 1, XML_ENC_OUTPUT = 2, XML_ENC_HTML = 4.
2222/// Unlike upstream, no iconv/ICU backend exists, so encodings without a native
2223/// converter fall through to `find_extra_handler` (custom impl / deprecated
2224/// global registry) and otherwise report XML_ERR_UNSUPPORTED_ENCODING.
2225///
2226/// # Safety
2227///
2228/// - `out` must be a valid pointer to a `*mut c_void` out-parameter; it is
2229///   written with NULL or a heap-allocated handler copy the caller owns.
2230/// - `name` must be NULL or a valid pointer to a NUL-terminated string.
2231/// - `implCtxt` is an opaque context forwarded to `find_extra_handler` and
2232///   must be valid for the callback that consumes it.
2233pub(crate) fn xmlCreateCharEncodingHandler(
2234    name: *const c_char,
2235    flags: c_int,
2236    impl_: Option<xmlCharEncConvImpl>,
2237    implCtxt: *mut c_void,
2238    out: *mut *mut c_void,
2239) -> c_int {
2240    if out.is_null() {
2241        return crate::abi::types::XML_ERR_ARGUMENT;
2242    }
2243    unsafe {
2244        *out = ptr::null_mut();
2245    }
2246    if name.is_null() || flags == 0 {
2247        return crate::abi::types::XML_ERR_ARGUMENT;
2248    }
2249    let norig = unsafe { CStr::from_ptr(name).to_bytes() };
2250
2251    /* Alias resolution (upstream xmlGetEncodingAlias). */
2252    let mut eff: &[u8] = norig;
2253    let alias = get_encoding_alias(name);
2254    if !alias.is_null() {
2255        eff = unsafe { CStr::from_ptr(alias).to_bytes() };
2256    }
2257
2258    let enc = encoding_from_name(eff);
2259
2260    /* Return NULL handler for UTF-8 */
2261    if enc == xmlCharEncoding::XML_CHAR_ENCODING_UTF8 {
2262        return crate::abi::types::XML_ERR_OK;
2263    }
2264
2265    let canonical: &[u8] = match enc {
2266        xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE => b"UTF-16LE\0",
2267        xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE => b"UTF-16BE\0",
2268        xmlCharEncoding::XML_CHAR_ENCODING_8859_1 => b"ISO-8859-1\0",
2269        xmlCharEncoding::XML_CHAR_ENCODING_ASCII => b"US-ASCII\0",
2270        _ => {
2271            return find_extra_handler(norig, eff, flags, impl_, implCtxt, out);
2272        }
2273    };
2274    let h = find_encoding_handler(canonical.as_ptr() as *const xmlChar);
2275    if h.is_null() {
2276        return find_extra_handler(norig, eff, flags, impl_, implCtxt, out);
2277    }
2278    unsafe {
2279        let src = &*h;
2280        let has_in = (flags & 1) == 0 || !src.input.legacyFunc.is_none();
2281        let has_out = (flags & 2) == 0 || !src.output.legacyFunc.is_none();
2282        if !has_in || !has_out {
2283            return find_extra_handler(norig, eff, flags, impl_, implCtxt, out);
2284        }
2285        /*
2286         * Return a copy of the handler with the original name (upstream
2287         * "Return a copy of the handler with the original name").
2288         */
2289        let copy =
2290            xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) as *mut _xmlCharEncodingHandler;
2291        if copy.is_null() {
2292            return crate::abi::types::XML_ERR_NO_MEMORY;
2293        }
2294        let name_copy = crate::abi::allocator::xmlMemStrdupImpl(name) as *mut c_char;
2295        if name_copy.is_null() {
2296            xmlFreeImpl(copy as *mut c_void);
2297            return crate::abi::types::XML_ERR_NO_MEMORY;
2298        }
2299        ptr::write(
2300            copy,
2301            _xmlCharEncodingHandler {
2302                name: name_copy,
2303                input: EncodingInputUnion {
2304                    legacyFunc: src.input.legacyFunc,
2305                },
2306                output: EncodingOutputUnion {
2307                    legacyFunc: src.output.legacyFunc,
2308                },
2309                inputCtxt: src.inputCtxt,
2310                outputCtxt: src.outputCtxt,
2311                ctxtDtor: src.ctxtDtor,
2312                flags: src.flags,
2313            },
2314        );
2315        *out = copy as *mut c_void;
2316    }
2317    crate::abi::types::XML_ERR_OK
2318}
2319
2320/// Fallback path of `xmlCreateCharEncodingHandler` (upstream `xmlFindExtraHandler`).
2321///
2322/// Tries the caller-supplied custom implementation first, then the deprecated
2323/// global handler registry. iconv/ICU do not exist in the candidate, so the
2324/// final result is XML_ERR_UNSUPPORTED_ENCODING.
2325///
2326/// # Safety
2327///
2328/// - `norig` and `name` must be valid byte slices; NUL-terminated copies are
2329///   built from them for lookups and callbacks.
2330/// - `out` must be a valid out-parameter; it is written with NULL or a
2331///   registry handler pointer that must not be freed.
2332/// - `implCtxt` must be a valid context for the custom `impl_` callback when
2333///   one is supplied.
2334fn find_extra_handler(
2335    norig: &[u8],
2336    name: &[u8],
2337    flags: c_int,
2338    impl_: Option<xmlCharEncConvImpl>,
2339    implCtxt: *mut c_void,
2340    out: *mut *mut c_void,
2341) -> c_int {
2342    /* Custom implementation before deprecated global handlers. */
2343    if let Some(f) = impl_ {
2344        let mut n = norig.to_vec();
2345        n.push(0);
2346        let rc = unsafe {
2347            f(
2348                implCtxt,
2349                n.as_ptr() as *const c_char,
2350                flags,
2351                out as *mut *mut crate::abi::structs::_xmlCharEncodingHandler,
2352            )
2353        };
2354        return rc;
2355    }
2356    /* Deprecated global handlers registry (xmlRegisterCharEncodingHandler). */
2357    let mut n = name.to_vec();
2358    n.push(0);
2359    let h = find_encoding_handler(n.as_ptr() as *const xmlChar);
2360    if !h.is_null() {
2361        unsafe {
2362            let src = &*h;
2363            let has_in = (flags & 1) == 0 || !src.input.legacyFunc.is_none();
2364            let has_out = (flags & 2) == 0 || !src.output.legacyFunc.is_none();
2365            if has_in && has_out {
2366                *out = h as *mut c_void;
2367                return crate::abi::types::XML_ERR_OK;
2368            }
2369        }
2370    }
2371    crate::abi::types::XML_ERR_UNSUPPORTED_ENCODING
2372}
2373
2374/// `xmlOpenCharEncodingHandler` implementation (upstream encoding.c).
2375pub(crate) fn xmlOpenCharEncodingHandler(
2376    name: *const c_char,
2377    output: c_int,
2378    out: *mut *mut c_void,
2379) -> c_int {
2380    /* XML_ENC_OUTPUT if output else XML_ENC_INPUT */
2381    let flags: c_int = if output != 0 { 2 } else { 1 };
2382    xmlCreateCharEncodingHandler(name, flags, None, ptr::null_mut(), out)
2383}
2384
2385/// `xmlCharEncNewCustomHandler` implementation (upstream 2.15.0+ encoding.c).
2386///
2387/// Creates a handler backed by modern `xmlCharEncConvFunc` callbacks (with
2388/// per-direction contexts and a context destructor). The handler must be
2389/// released with `xmlCharEncCloseFunc`.
2390///
2391/// # Safety
2392///
2393/// - `out` must be a valid pointer to a `*mut c_void` out-parameter.
2394/// - `name` must be NULL or a valid pointer to a NUL-terminated string.
2395/// - `input` and `output` must be valid `xmlCharEncConvFunc` callbacks;
2396///   `inputCtxt` and `outputCtxt` are opaque contexts consumed by them and
2397///   by `ctxtDtor`, which is invoked on each non-NULL context when
2398///   allocation fails (and later by `xmlCharEncCloseFunc`).
2399pub(crate) fn xmlCharEncNewCustomHandler(
2400    name: *const c_char,
2401    input: xmlCharEncConvFunc,
2402    output: xmlCharEncConvFunc,
2403    ctxtDtor: Option<xmlCharEncConvCtxtDtor>,
2404    inputCtxt: *mut c_void,
2405    outputCtxt: *mut c_void,
2406    out: *mut *mut c_void,
2407) -> c_int {
2408    if out.is_null() {
2409        return crate::abi::types::XML_ERR_ARGUMENT;
2410    }
2411    let handler = unsafe { xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) }
2412        as *mut _xmlCharEncodingHandler;
2413    if handler.is_null() {
2414        unsafe {
2415            if let Some(d) = ctxtDtor {
2416                if !inputCtxt.is_null() {
2417                    d(inputCtxt);
2418                }
2419                if !outputCtxt.is_null() {
2420                    d(outputCtxt);
2421                }
2422            }
2423        }
2424        return crate::abi::types::XML_ERR_NO_MEMORY;
2425    }
2426    let name_copy = if name.is_null() {
2427        ptr::null_mut()
2428    } else {
2429        let nc = unsafe { crate::abi::allocator::xmlMemStrdupImpl(name) } as *mut c_char;
2430        if nc.is_null() {
2431            unsafe { xmlFreeImpl(handler as *mut c_void) };
2432            unsafe {
2433                if let Some(d) = ctxtDtor {
2434                    if !inputCtxt.is_null() {
2435                        d(inputCtxt);
2436                    }
2437                    if !outputCtxt.is_null() {
2438                        d(outputCtxt);
2439                    }
2440                }
2441            }
2442            return crate::abi::types::XML_ERR_NO_MEMORY;
2443        }
2444        nc
2445    };
2446    unsafe {
2447        ptr::write(
2448            handler,
2449            _xmlCharEncodingHandler {
2450                name: name_copy,
2451                input: EncodingInputUnion { func: Some(input) },
2452                output: EncodingOutputUnion { func: Some(output) },
2453                inputCtxt,
2454                outputCtxt,
2455                ctxtDtor,
2456                flags: 0,
2457            },
2458        );
2459        *out = handler as *mut c_void;
2460    }
2461    crate::abi::types::XML_ERR_OK
2462}
2463
2464// ═══════════════════════════════════════════════════════════════════════════════
2465// Tests
2466// ═══════════════════════════════════════════════════════════════════════════════
2467
2468#[cfg(test)]
2469mod tests {
2470    use super::*;
2471
2472    // ── BOM detection ──────────────────────────────────────────────────────
2473
2474    #[test]
2475    fn test_detect_bom_utf8() {
2476        let data = [0xEF, 0xBB, 0xBF, b'<', b'?', b'x', b'm', b'l'];
2477        assert_eq!(
2478            detect_encoding_from_bom(&data),
2479            xmlCharEncoding::XML_CHAR_ENCODING_UTF8
2480        );
2481    }
2482
2483    #[test]
2484    fn test_detect_bom_utf16le() {
2485        let data = [0xFF, 0xFE, 0x00, 0x01];
2486        assert_eq!(
2487            detect_encoding_from_bom(&data),
2488            xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
2489        );
2490    }
2491
2492    #[test]
2493    fn test_detect_bom_utf16be() {
2494        let data = [0xFE, 0xFF, 0x00, 0x01];
2495        assert_eq!(
2496            detect_encoding_from_bom(&data),
2497            xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE
2498        );
2499    }
2500
2501    #[test]
2502    fn test_detect_bom_none() {
2503        let data = b"<xml>";
2504        assert_eq!(
2505            detect_encoding_from_bom(data),
2506            xmlCharEncoding::XML_CHAR_ENCODING_NONE
2507        );
2508    }
2509
2510    #[test]
2511    fn test_detect_bom_empty() {
2512        assert_eq!(
2513            detect_encoding_from_bom(b""),
2514            xmlCharEncoding::XML_CHAR_ENCODING_NONE
2515        );
2516    }
2517
2518    // ── Encoding from declaration ──────────────────────────────────────────
2519
2520    #[test]
2521    fn test_detect_encoding_declaration_utf8() {
2522        let data = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
2523        let result = detect_encoding_from_declaration(data);
2524        assert_eq!(result, Some(b"utf-8".to_vec()));
2525    }
2526
2527    #[test]
2528    fn test_detect_encoding_declaration_iso() {
2529        let data = b"<?xml version='1.0' encoding='ISO-8859-1'?>";
2530        let result = detect_encoding_from_declaration(data);
2531        assert_eq!(result, Some(b"iso-8859-1".to_vec()));
2532    }
2533
2534    #[test]
2535    fn test_detect_encoding_declaration_none() {
2536        let data = b"<?xml version=\"1.0\"?>";
2537        let result = detect_encoding_from_declaration(data);
2538        assert!(result.is_none());
2539    }
2540
2541    #[test]
2542    fn test_detect_encoding_declaration_no_xml() {
2543        let data = b"<root>";
2544        let result = detect_encoding_from_declaration(data);
2545        assert!(result.is_none());
2546    }
2547
2548    #[test]
2549    fn test_detect_encoding_declaration_with_bom() {
2550        let mut data = vec![0xEF, 0xBB, 0xBF];
2551        data.extend_from_slice(b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
2552        let result = detect_encoding_from_declaration(&data);
2553        assert_eq!(result, Some(b"utf-8".to_vec()));
2554    }
2555
2556    // ── Encoding from name ─────────────────────────────────────────────────
2557
2558    #[test]
2559    fn test_encoding_from_name_utf8() {
2560        assert_eq!(
2561            encoding_from_name(b"UTF-8"),
2562            xmlCharEncoding::XML_CHAR_ENCODING_UTF8
2563        );
2564        assert_eq!(
2565            encoding_from_name(b"utf8"),
2566            xmlCharEncoding::XML_CHAR_ENCODING_UTF8
2567        );
2568    }
2569
2570    #[test]
2571    fn test_encoding_from_name_utf16() {
2572        assert_eq!(
2573            encoding_from_name(b"UTF-16LE"),
2574            xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
2575        );
2576        assert_eq!(
2577            encoding_from_name(b"UTF-16BE"),
2578            xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE
2579        );
2580        assert_eq!(
2581            encoding_from_name(b"utf-16"),
2582            xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE
2583        );
2584    }
2585
2586    #[test]
2587    fn test_encoding_from_name_latin1() {
2588        assert_eq!(
2589            encoding_from_name(b"ISO-8859-1"),
2590            xmlCharEncoding::XML_CHAR_ENCODING_8859_1
2591        );
2592        assert_eq!(
2593            encoding_from_name(b"Latin1"),
2594            xmlCharEncoding::XML_CHAR_ENCODING_8859_1
2595        );
2596    }
2597
2598    #[test]
2599    fn test_encoding_from_name_ascii() {
2600        assert_eq!(
2601            encoding_from_name(b"ASCII"),
2602            xmlCharEncoding::XML_CHAR_ENCODING_ASCII
2603        );
2604        assert_eq!(
2605            encoding_from_name(b"US-ASCII"),
2606            xmlCharEncoding::XML_CHAR_ENCODING_ASCII
2607        );
2608    }
2609
2610    #[test]
2611    fn test_encoding_from_name_error() {
2612        assert_eq!(
2613            encoding_from_name(b"invalid-encoding"),
2614            xmlCharEncoding::XML_CHAR_ENCODING_ERROR
2615        );
2616    }
2617
2618    #[test]
2619    fn test_encoding_from_name_empty() {
2620        assert_eq!(
2621            encoding_from_name(b""),
2622            xmlCharEncoding::XML_CHAR_ENCODING_ERROR
2623        );
2624    }
2625
2626    // ── Encoding name ──────────────────────────────────────────────────────
2627
2628    #[test]
2629    fn test_encoding_name_utf8() {
2630        assert_eq!(
2631            encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_UTF8),
2632            Some(b"UTF-8" as &[u8])
2633        );
2634    }
2635
2636    #[test]
2637    fn test_encoding_name_utf16le() {
2638        assert_eq!(
2639            encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE),
2640            Some(b"UTF-16LE" as &[u8])
2641        );
2642    }
2643
2644    #[test]
2645    fn test_encoding_name_none() {
2646        assert!(encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_NONE).is_none());
2647    }
2648
2649    #[test]
2650    fn test_encoding_name_error() {
2651        assert!(encoding_name(xmlCharEncoding::XML_CHAR_ENCODING_ERROR).is_none());
2652    }
2653
2654    // ── UTF-8 validation ───────────────────────────────────────────────────
2655
2656    #[test]
2657    fn test_utf8_valid_ascii() {
2658        assert!(utf8_valid(b"hello world"));
2659    }
2660
2661    #[test]
2662    fn test_utf8_valid_multi_byte() {
2663        assert!(utf8_valid("héllo wörld 🌍".as_bytes()));
2664    }
2665
2666    #[test]
2667    fn test_utf8_valid_empty() {
2668        assert!(utf8_valid(b""));
2669    }
2670
2671    #[test]
2672    fn test_utf8_invalid() {
2673        assert!(!utf8_valid(&[0xFF, 0xFE, 0x00]));
2674    }
2675
2676    // ── XML char validation ────────────────────────────────────────────────
2677
2678    #[test]
2679    fn test_valid_xml_chars() {
2680        assert!(is_valid_xml_char(0x9)); // Tab
2681        assert!(is_valid_xml_char(0xA)); // LF
2682        assert!(is_valid_xml_char(0xD)); // CR
2683        assert!(is_valid_xml_char(0x20)); // Space
2684        assert!(is_valid_xml_char(0x41)); // 'A'
2685        assert!(is_valid_xml_char(0xD7FF));
2686        assert!(is_valid_xml_char(0xE000));
2687        assert!(is_valid_xml_char(0xFFFD));
2688        assert!(is_valid_xml_char(0x10000));
2689        assert!(is_valid_xml_char(0x10FFFF));
2690    }
2691
2692    #[test]
2693    fn test_invalid_xml_chars() {
2694        assert!(!is_valid_xml_char(0x00));
2695        assert!(!is_valid_xml_char(0x08));
2696        assert!(!is_valid_xml_char(0x0B));
2697        assert!(!is_valid_xml_char(0x0C));
2698        assert!(!is_valid_xml_char(0x0E));
2699        assert!(!is_valid_xml_char(0x1F));
2700        assert!(!is_valid_xml_char(0xD800)); // Surrogate
2701        assert!(!is_valid_xml_char(0xDFFF)); // Surrogate
2702        assert!(!is_valid_xml_char(0xFFFE));
2703        assert!(!is_valid_xml_char(0xFFFF));
2704        assert!(!is_valid_xml_char(0x110000));
2705    }
2706
2707    // ── UTF-16LE to UTF-8 ──────────────────────────────────────────────────
2708
2709    #[test]
2710    fn test_utf16le_to_utf8_ascii() {
2711        // "AB" in UTF-16LE
2712        let data = [b'A', 0x00, b'B', 0x00];
2713        let result = utf16le_to_utf8(&data).unwrap();
2714        assert_eq!(result, b"AB");
2715    }
2716
2717    #[test]
2718    fn test_utf16le_to_utf8_bom() {
2719        let mut data = vec![0xFF, 0xFE]; // BOM
2720        data.extend_from_slice(&[b'A', 0x00, b'B', 0x00]);
2721        let result = utf16le_to_utf8(&data).unwrap();
2722        assert_eq!(result, b"AB");
2723    }
2724
2725    #[test]
2726    fn test_utf16le_to_utf8_bmp() {
2727        // U+00E9 (é) in UTF-16LE = 0xE9 0x00
2728        let data = [0xE9, 0x00];
2729        let result = utf16le_to_utf8(&data).unwrap();
2730        assert_eq!(result, "é".as_bytes());
2731    }
2732
2733    #[test]
2734    fn test_utf16le_to_utf8_supplementary() {
2735        // U+1F600 (😀) in UTF-16LE = 0x3D 0xD8 0x00 0xDE
2736        let data = [0x3D, 0xD8, 0x00, 0xDE];
2737        let result = utf16le_to_utf8(&data).unwrap();
2738        assert_eq!(result, "😀".as_bytes());
2739    }
2740
2741    #[test]
2742    fn test_utf16le_to_utf8_unpaired_surrogate() {
2743        let data = [0x00, 0xD8]; // High surrogate without low
2744        assert!(utf16le_to_utf8(&data).is_err());
2745    }
2746
2747    #[test]
2748    fn test_utf16le_to_utf8_truncated() {
2749        let data = [0x00]; // Odd length
2750        assert!(utf16le_to_utf8(&data).is_err());
2751    }
2752
2753    #[test]
2754    fn test_utf16le_to_utf8_empty() {
2755        let result = utf16le_to_utf8(b"").unwrap();
2756        assert!(result.is_empty());
2757    }
2758
2759    // ── UTF-16BE to UTF-8 ──────────────────────────────────────────────────
2760
2761    #[test]
2762    fn test_utf16be_to_utf8_ascii() {
2763        let data = [0x00, b'A', 0x00, b'B'];
2764        let result = utf16be_to_utf8(&data).unwrap();
2765        assert_eq!(result, b"AB");
2766    }
2767
2768    #[test]
2769    fn test_utf16be_to_utf8_bom() {
2770        let mut data = vec![0xFE, 0xFF]; // BOM
2771        data.extend_from_slice(&[0x00, b'A', 0x00, b'B']);
2772        let result = utf16be_to_utf8(&data).unwrap();
2773        assert_eq!(result, b"AB");
2774    }
2775
2776    #[test]
2777    fn test_utf16be_to_utf8_supplementary() {
2778        // U+1F600 (😀) in UTF-16BE = 0xD8 0x3D 0xDE 0x00
2779        let data = [0xD8, 0x3D, 0xDE, 0x00];
2780        let result = utf16be_to_utf8(&data).unwrap();
2781        assert_eq!(result, "😀".as_bytes());
2782    }
2783
2784    #[test]
2785    fn test_utf16be_to_utf8_empty() {
2786        let result = utf16be_to_utf8(b"").unwrap();
2787        assert!(result.is_empty());
2788    }
2789
2790    // ── UTF-8 to UTF-16LE ──────────────────────────────────────────────────
2791
2792    #[test]
2793    fn test_utf8_to_utf16le_ascii() {
2794        let result = utf8_to_utf16le(b"AB").unwrap();
2795        assert_eq!(result, [b'A', 0x00, b'B', 0x00]);
2796    }
2797
2798    #[test]
2799    fn test_utf8_to_utf16le_bmp() {
2800        let result = utf8_to_utf16le("é".as_bytes()).unwrap();
2801        assert_eq!(result, [0xE9, 0x00]);
2802    }
2803
2804    #[test]
2805    fn test_utf8_to_utf16le_supplementary() {
2806        let result = utf8_to_utf16le("😀".as_bytes()).unwrap();
2807        assert_eq!(result, [0x3D, 0xD8, 0x00, 0xDE]);
2808    }
2809
2810    #[test]
2811    fn test_utf8_to_utf16le_invalid_utf8() {
2812        assert!(utf8_to_utf16le(&[0xFF]).is_err());
2813    }
2814
2815    #[test]
2816    fn test_utf8_to_utf16le_empty() {
2817        let result = utf8_to_utf16le(b"").unwrap();
2818        assert!(result.is_empty());
2819    }
2820
2821    // ── Latin-1 to UTF-8 ───────────────────────────────────────────────────
2822
2823    #[test]
2824    fn test_latin1_to_utf8_ascii() {
2825        let result = latin1_to_utf8(b"ABC");
2826        assert_eq!(result, b"ABC");
2827    }
2828
2829    #[test]
2830    fn test_latin1_to_utf8_accented() {
2831        // 0xE9 = é in Latin-1
2832        let result = latin1_to_utf8(&[0xE9]);
2833        assert_eq!(result, "é".as_bytes());
2834    }
2835
2836    #[test]
2837    fn test_latin1_to_utf8_all_255() {
2838        let result = latin1_to_utf8(&[0xFF]);
2839        // U+00FF = ÿ, UTF-8: 0xC3 0xBF
2840        assert_eq!(result, [0xC3, 0xBF]);
2841    }
2842
2843    #[test]
2844    fn test_latin1_to_utf8_empty() {
2845        let result = latin1_to_utf8(b"");
2846        assert!(result.is_empty());
2847    }
2848
2849    #[test]
2850    fn test_latin1_to_utf8_mixed() {
2851        let result = latin1_to_utf8(b"caf\xE9");
2852        assert_eq!(result, "café".as_bytes());
2853    }
2854
2855    // ── UTF-8 to Latin-1 ───────────────────────────────────────────────────
2856
2857    #[test]
2858    fn test_utf8_to_latin1_ascii() {
2859        let result = utf8_to_latin1(b"ABC").unwrap();
2860        assert_eq!(result, b"ABC");
2861    }
2862
2863    #[test]
2864    fn test_utf8_to_latin1_accented() {
2865        let result = utf8_to_latin1("é".as_bytes()).unwrap();
2866        assert_eq!(result, [0xE9]);
2867    }
2868
2869    #[test]
2870    fn test_utf8_to_latin1_out_of_range() {
2871        assert!(utf8_to_latin1("€".as_bytes()).is_err()); // U+20AC not in Latin-1
2872    }
2873
2874    #[test]
2875    fn test_utf8_to_latin1_invalid_utf8() {
2876        assert!(utf8_to_latin1(&[0xFF]).is_err());
2877    }
2878
2879    #[test]
2880    fn test_utf8_to_latin1_empty() {
2881        let result = utf8_to_latin1(b"").unwrap();
2882        assert!(result.is_empty());
2883    }
2884
2885    // ── Encoding handler registry ──────────────────────────────────────────
2886
2887    #[test]
2888    fn test_init_and_find_encodings() {
2889        init_encodings();
2890
2891        let utf8_name: *const xmlChar = c"UTF-8".as_ptr() as *const xmlChar;
2892        assert!(!find_encoding_handler(utf8_name).is_null());
2893
2894        let utf16le_name: *const xmlChar = c"UTF-16LE".as_ptr() as *const xmlChar;
2895        assert!(!find_encoding_handler(utf16le_name).is_null());
2896
2897        let utf16be_name: *const xmlChar = c"UTF-16BE".as_ptr() as *const xmlChar;
2898        assert!(!find_encoding_handler(utf16be_name).is_null());
2899
2900        let latin1_name: *const xmlChar = c"ISO-8859-1".as_ptr() as *const xmlChar;
2901        assert!(!find_encoding_handler(latin1_name).is_null());
2902
2903        let ascii_name: *const xmlChar = c"ASCII".as_ptr() as *const xmlChar;
2904        assert!(!find_encoding_handler(ascii_name).is_null());
2905
2906        // Case insensitive
2907        let lower_name: *const xmlChar = c"utf-8".as_ptr() as *const xmlChar;
2908        assert!(!find_encoding_handler(lower_name).is_null());
2909    }
2910
2911    /// Phase 14 PHP court regression: the ABI `xmlFindCharEncodingHandler`
2912    /// hands the caller an OWNED handler that it may release with
2913    /// `xmlCharEncCloseFunc` — except UTF-8, where upstream returns a static
2914    /// handler that close must not release (so the registry is never freed
2915    /// out from under subsequent lookups). Closing a returned non-UTF-8
2916    /// handler must not free the persistent registry entry.
2917    ///
2918    /// # Safety
2919    ///
2920    /// - The handler returned by `xmlFindCharEncodingHandler_owned` is owned by
2921    ///   the caller and released here with the allocator, mirroring the export
2922    ///   `xmlCharEncCloseFunc` (which, for these stateless built-in handlers,
2923    ///   frees `name` and the struct without invoking any context destructor).
2924    #[test]
2925    fn test_find_owned_close_keeps_registry_intact() {
2926        init_encodings();
2927        let name: *const xmlChar = c"ISO-8859-1".as_ptr() as *const xmlChar;
2928
2929        // The registry entry is a long-lived borrow.
2930        let registry = find_encoding_handler(name);
2931        assert!(!registry.is_null());
2932        // First retrieval returns an OWNED copy, distinct from the registry entry.
2933        let h1 = xmlFindCharEncodingHandler_owned(name);
2934        assert!(!h1.is_null());
2935        assert_ne!(h1 as *const c_void, registry as *const c_void);
2936
2937        // Closing h1 (simulate xmlCharEncCloseFunc on a non-static handler):
2938        // frees its name + struct but NOT the registry entry.
2939        unsafe {
2940            if !(*h1).name.is_null() {
2941                crate::abi::allocator::xmlFreeImpl((*h1).name as *mut c_void);
2942            }
2943            xmlFreeImpl(h1 as *mut c_void);
2944        }
2945
2946        // The registry entry must survive the close of a previous result with
2947        // its name intact (the PHP `$dom->encoding='UTF-16'` crash was the
2948        // registry entry itself being freed by this very close, so the next
2949        // lookup returned freed memory).
2950        let registry2 = find_encoding_handler(name);
2951        assert_eq!(registry2 as *const c_void, registry as *const c_void);
2952        assert!(!unsafe { (*registry2).name }.is_null());
2953        let reg_name = unsafe { CStr::from_ptr((*registry2).name as *const c_char) };
2954        assert_eq!(reg_name.to_bytes(), b"ISO-8859-1");
2955
2956        // A second owned retrieval still works and is usable.
2957        let h2 = xmlFindCharEncodingHandler_owned(name);
2958        assert!(!h2.is_null());
2959        assert_ne!(h2 as *const c_void, registry as *const c_void);
2960        unsafe {
2961            if !(*h2).name.is_null() {
2962                crate::abi::allocator::xmlFreeImpl((*h2).name as *mut c_void);
2963            }
2964            xmlFreeImpl(h2 as *mut c_void);
2965        }
2966    }
2967
2968    /// Phase 14 PHP court regression (UTF-8 subset): retrieval for UTF-8/UTF8
2969    /// returns the persistent static handler, and referencing it from a second
2970    /// caller must yield the same live pointer (the registry entry is never
2971    /// freed by a close — `xmlCharEncCloseFunc` on XML_HANDLER_STATIC is a
2972    /// no-op).
2973    #[test]
2974    fn test_find_owned_utf8_static_and_persistent() {
2975        init_encodings();
2976        let name: *const xmlChar = c"UTF-8".as_ptr() as *const xmlChar;
2977        let u1 = xmlFindCharEncodingHandler_owned(name);
2978        assert!(!u1.is_null());
2979        // Static: close must not release it, so a second find returns the same
2980        // live registry handler.
2981        let u2 = xmlFindCharEncodingHandler_owned(c"utf8".as_ptr() as *const xmlChar);
2982        assert_eq!(u1, u2);
2983        assert_eq!(
2984            unsafe { (*u1).flags } & XML_HANDLER_STATIC,
2985            XML_HANDLER_STATIC
2986        );
2987    }
2988
2989    #[test]
2990    fn test_find_encoding_handler_not_found() {
2991        let name: *const xmlChar = c"NONEXISTENT".as_ptr() as *const xmlChar;
2992        assert!(find_encoding_handler(name).is_null());
2993    }
2994
2995    #[test]
2996    fn test_find_encoding_handler_null() {
2997        assert!(find_encoding_handler(ptr::null()).is_null());
2998    }
2999
3000    /// Verify registering a handler in the global registry and looking it
3001    /// up.
3002    ///
3003    /// # Safety
3004    ///
3005    /// - The `xmlMallocImpl` and `xmlMemStrdupImpl` results are NULL-checked
3006    ///   before `ptr::write` initializes the handler; the handler is removed
3007    ///   from the registry before its allocations are freed exactly once.
3008    #[test]
3009    fn test_add_encoding_handler() {
3010        let handler = unsafe {
3011            xmlMallocImpl(size_of::<_xmlCharEncodingHandler>()) as *mut _xmlCharEncodingHandler
3012        };
3013        assert!(!handler.is_null());
3014
3015        let name = unsafe {
3016            crate::abi::allocator::xmlMemStrdupImpl(c"TEST-ENC".as_ptr() as *const c_char)
3017        };
3018        unsafe {
3019            ptr::write(
3020                handler,
3021                _xmlCharEncodingHandler {
3022                    name: name as *mut c_char,
3023                    input: EncodingInputUnion { legacyFunc: None },
3024                    output: EncodingOutputUnion { legacyFunc: None },
3025                    inputCtxt: ptr::null_mut(),
3026                    outputCtxt: ptr::null_mut(),
3027                    ctxtDtor: None,
3028                    flags: 0,
3029                },
3030            );
3031        }
3032
3033        assert_eq!(add_encoding_handler(handler), 0);
3034
3035        let found = find_encoding_handler(c"TEST-ENC".as_ptr() as *const xmlChar);
3036        assert_eq!(found, handler);
3037
3038        // Remove from registry before freeing to avoid dangling pointers
3039        {
3040            let mut handlers = ENCODING_HANDLERS.write();
3041            handlers.retain(|&h| h.0 != handler);
3042        }
3043
3044        unsafe {
3045            xmlFreeImpl(name as *mut c_void);
3046            xmlFreeImpl(handler as *mut c_void);
3047        }
3048    }
3049
3050    // ── Conversion round-trips ─────────────────────────────────────────────
3051
3052    #[test]
3053    fn test_utf16le_roundtrip() {
3054        let original = b"Hello, World! UTF-16LE test: \xC3\xA9\xF0\x9F\x98\x80";
3055        let utf16 = utf8_to_utf16le(original).unwrap();
3056        let back = utf16le_to_utf8(&utf16).unwrap();
3057        assert_eq!(original.to_vec(), back);
3058    }
3059
3060    #[test]
3061    fn test_utf16be_roundtrip() {
3062        let original = b"Hello, World! UTF-16BE test: \xC3\xA9\xF0\x9F\x98\x80";
3063        let utf16le = utf8_to_utf16le(original).unwrap();
3064        // Convert LE to BE by swapping bytes
3065        let mut utf16be = utf16le.clone();
3066        for chunk in utf16be.as_chunks_mut::<2>().0 {
3067            chunk.swap(0, 1);
3068        }
3069        let back = utf16be_to_utf8(&utf16be).unwrap();
3070        assert_eq!(original.to_vec(), back);
3071    }
3072
3073    #[test]
3074    fn test_latin1_roundtrip() {
3075        let original: Vec<u8> = (0x00..=0xFF).collect();
3076        let utf8 = latin1_to_utf8(&original);
3077        let back = utf8_to_latin1(&utf8).unwrap();
3078        assert_eq!(original, back);
3079    }
3080
3081    // ── Built-in handler callbacks ─────────────────────────────────────────
3082
3083    /// Verify the UTF-8 identity callback copies bytes up to the smaller
3084    /// length.
3085    ///
3086    /// # Safety
3087    ///
3088    /// - `output` is a valid mutable 64-byte buffer and `input` a valid byte
3089    ///   slice; the callback writes at most the minimum of the two lengths.
3090    #[test]
3091    fn test_utf8_handler_identity() {
3092        let input = b"Hello, UTF-8!";
3093        let mut output = [0u8; 64];
3094        let mut outlen = output.len() as c_int;
3095        let mut inlen = input.len() as c_int;
3096
3097        let ret = unsafe {
3098            utf8_input_func(output.as_mut_ptr(), &mut outlen, input.as_ptr(), &mut inlen)
3099        };
3100
3101        assert_eq!(ret, input.len() as c_int);
3102        assert_eq!(&output[..ret as usize], input);
3103        assert_eq!(inlen, input.len() as c_int);
3104    }
3105
3106    /// Verify a UTF-16LE output/input callback round-trip.
3107    ///
3108    /// # Safety
3109    ///
3110    /// - The `utf16_buf` and `decoded` arrays are valid buffers of the given
3111    ///   lengths, and the input slices are valid; the callbacks write only
3112    ///   up to the advertised output length.
3113    #[test]
3114    fn test_utf16le_handler_roundtrip() {
3115        init_encodings();
3116
3117        let original = b"Hello UTF-16LE!";
3118        let mut utf16_buf = [0u8; 128];
3119        let mut outlen = utf16_buf.len() as c_int;
3120        let mut inlen = original.len() as c_int;
3121
3122        let written = unsafe {
3123            utf16le_output_func(
3124                utf16_buf.as_mut_ptr(),
3125                &mut outlen,
3126                original.as_ptr(),
3127                &mut inlen,
3128            )
3129        };
3130        assert!(written > 0);
3131
3132        // Now decode back
3133        let mut decoded = [0u8; 128];
3134        let mut outlen2 = decoded.len() as c_int;
3135        let mut inlen2 = written;
3136
3137        let written2 = unsafe {
3138            utf16le_input_func(
3139                decoded.as_mut_ptr(),
3140                &mut outlen2,
3141                utf16_buf.as_ptr(),
3142                &mut inlen2,
3143            )
3144        };
3145        assert_eq!(written2 as usize, original.len());
3146        assert_eq!(&decoded[..written2 as usize], original);
3147    }
3148
3149    // ── xmlBuffer operations ───────────────────────────────────────────────
3150
3151    /// Verify `append_to_xml_buffer` grows the buffer and copies bytes.
3152    ///
3153    /// # Safety
3154    ///
3155    /// - `content` is a valid 64-byte allocation owned by the test and freed
3156    ///   exactly once with `xmlFreeImpl`; `buf` keeps consistent `use_` and
3157    ///   `size` fields while `append_to_xml_buffer` may reallocate `content`.
3158    #[test]
3159    fn test_append_to_xml_buffer() {
3160        unsafe {
3161            let content = xmlMallocImpl(64) as *mut xmlChar;
3162            assert!(!content.is_null());
3163
3164            let mut buf = _xmlBuffer {
3165                content,
3166                use_: 0,
3167                size: 64,
3168                alloc: 0,
3169                contentIO: ptr::null_mut(),
3170            };
3171
3172            append_to_xml_buffer(&mut buf, b"Hello");
3173            assert_eq!(buf.use_, 5);
3174            let slice = core::slice::from_raw_parts(buf.content, 5);
3175            assert_eq!(slice, b"Hello");
3176
3177            append_to_xml_buffer(&mut buf, b" World");
3178            assert_eq!(buf.use_, 11);
3179            let slice = core::slice::from_raw_parts(buf.content, 11);
3180            assert_eq!(slice, b"Hello World");
3181
3182            xmlFreeImpl(buf.content as *mut c_void);
3183        }
3184    }
3185
3186    // ── ABI export functions ───────────────────────────────────────────────
3187
3188    #[test]
3189    fn test_xml_parse_char_encoding() {
3190        let name = c"UTF-8".as_ptr() as *const c_char;
3191        assert_eq!(
3192            xmlParseCharEncoding(name),
3193            xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int
3194        );
3195
3196        let name = c"ISO-8859-1".as_ptr() as *const c_char;
3197        assert_eq!(
3198            xmlParseCharEncoding(name),
3199            xmlCharEncoding::XML_CHAR_ENCODING_8859_1 as c_int
3200        );
3201
3202        assert_eq!(
3203            xmlParseCharEncoding(ptr::null()),
3204            xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int
3205        );
3206    }
3207
3208    /// Verify `xmlNewCharEncodingHandler` and `xmlDelEncodingHandler`
3209    /// round-trip.
3210    ///
3211    /// # Safety
3212    ///
3213    /// - `name` is a valid NUL-terminated string; the returned handler is
3214    ///   non-NULL, its `name` field is a valid NUL-terminated string, and it
3215    ///   is freed exactly once by `xmlDelEncodingHandler`.
3216    #[test]
3217    fn test_xml_new_and_del_encoding_handler() {
3218        let name = c"TestEnc".as_ptr() as *const c_char;
3219        let handler = xmlNewCharEncodingHandler(
3220            name,
3221            utf8_input_func as xmlCharEncodingInputFunc,
3222            utf8_output_func as xmlCharEncodingOutputFunc,
3223        );
3224        assert!(!handler.is_null());
3225
3226        unsafe {
3227            assert!(!(*handler).name.is_null());
3228            let cstr = CStr::from_ptr((*handler).name);
3229            assert_eq!(cstr.to_bytes(), b"TestEnc");
3230        }
3231
3232        xmlDelEncodingHandler(handler);
3233    }
3234
3235    #[test]
3236    fn test_xml_init_and_cleanup() {
3237        xmlInitCharEncodingHandlers();
3238
3239        let name: *const xmlChar = c"UTF-8".as_ptr() as *const xmlChar;
3240        assert!(!find_encoding_handler(name).is_null());
3241
3242        xmlCleanupCharEncodingHandlers();
3243        // After cleanup, handlers should be empty
3244    }
3245}