Skip to main content

libxml_rs/abi/
exports_hash.rs

1//! exports_hash — family closure (11.1-I). Filled by workstream.
2//!
3//! C ABI exports for the libxml2 hash-table and dictionary families:
4//! `xmlHashAdd`, `xmlHashAdd2`, `xmlHashAdd3`, `xmlHashCopySafe`,
5//! `xmlHashDefaultDeallocator`, `xmlHashQLookup`, `xmlHashQLookup2`,
6//! `xmlHashQLookup3`, `xmlHashScan3`, `xmlHashScanFull3` and
7//! `xmlDictCleanup`, `xmlDictOwns`, `xmlDictQLookup`, `xmlDictReference`.
8//!
9//! The public `xmlHashTable` / `xmlDict` types are opaque (`*mut c_void` at
10//! the ABI boundary); they are cast to the internal
11//! `crate::xml::hash::HashTable` and `crate::xml::dictionary::Dict`.
12//!
13//! # UPSTREAM-PARITY
14//!
15//! All semantics follow upstream libxml2 2.15
16//! (`oracle/historical/src/libxml2-2.15.0/hash.c` and `dict.c`).
17
18#![allow(
19    missing_docs,
20    non_snake_case,
21    non_camel_case_types,
22    non_upper_case_globals
23)]
24#![allow(clippy::missing_safety_doc)]
25#![allow(clippy::not_unsafe_ptr_arg_deref)]
26
27use core::ffi::c_void;
28use core::ptr;
29use once_cell::sync::Lazy;
30use parking_lot::Mutex;
31use std::collections::{HashMap, HashSet};
32use std::os::raw::c_int;
33
34use crate::abi::allocator;
35use crate::abi::callbacks::{
36    xmlHashCopier, xmlHashDeallocator, xmlHashScanner, xmlHashScannerFull,
37};
38use crate::abi::types::xmlChar;
39use crate::xml::dictionary::{dict_lookup, Dict};
40use crate::xml::hash::{
41    hash_add_entry3, hash_create, hash_free, hash_lookup3, hash_scan_full, HashTable,
42};
43
44// ═══════════════════════════════════════════════════════════════════════════════
45// Shared Helpers
46// ═══════════════════════════════════════════════════════════════════════════════
47
48/// Byte-wise comparison of two null-terminated xmlChar strings (null-safe).
49unsafe fn c_str_eq(a: *const xmlChar, b: *const xmlChar) -> bool {
50    if a.is_null() && b.is_null() {
51        return true;
52    }
53    if a.is_null() || b.is_null() {
54        return false;
55    }
56    let mut i = 0usize;
57    loop {
58        // SAFETY: Both pointers are valid null-terminated strings.
59        let ca = unsafe { *a.add(i) };
60        let cb = unsafe { *b.add(i) };
61        if ca != cb {
62            return false;
63        }
64        if ca == 0 {
65            return true;
66        }
67        i += 1;
68    }
69}
70
71/// Append the bytes of a null-terminated xmlChar string (excluding the
72/// terminator) to a byte vector.
73unsafe fn push_c_str(v: &mut Vec<u8>, s: *const xmlChar) {
74    if s.is_null() {
75        return;
76    }
77    let mut i = 0usize;
78    loop {
79        // SAFETY: `s` is a valid null-terminated string.
80        let c = unsafe { *s.add(i) };
81        if c == 0 {
82            break;
83        }
84        v.push(c);
85        i += 1;
86    }
87}
88
89/// Snapshot of a single hash-table entry collected during a scan.
90///
91/// The internal hash table's buckets are private, so scans that need to
92/// filter entries (or invoke user callbacks afterwards) go through the
93/// module's full-scanner API and buffer the entries here.
94#[derive(Clone, Copy)]
95struct HashEntrySnapshot {
96    payload: *mut c_void,
97    key1: *const xmlChar,
98    key2: *const xmlChar,
99    key3: *const xmlChar,
100}
101
102/// Full-scanner callback that appends entries to a `Vec<HashEntrySnapshot>`
103/// whose pointer is passed via the scanner `data` argument.
104unsafe extern "C" fn collect_entry(
105    payload: *mut c_void,
106    data: *mut c_void,
107    name: *const xmlChar,
108    name2: *const xmlChar,
109    name3: *const xmlChar,
110) {
111    let collected = unsafe { &mut *(data as *mut Vec<HashEntrySnapshot>) };
112    collected.push(HashEntrySnapshot {
113        payload,
114        key1: name,
115        key2: name2,
116        key3: name3,
117    });
118}
119
120// ═══════════════════════════════════════════════════════════════════════════════
121// xmlHashAdd / xmlHashAdd2 / xmlHashAdd3
122// ═══════════════════════════════════════════════════════════════════════════════
123
124/// Shared core for the modern (2.13+) `xmlHashAdd*` family.
125///
126/// Mirrors upstream `xmlHashUpdateInternal(..., update = 0)`:
127/// - `-1` if `hash`/`name` is NULL (or on allocation failure),
128/// - `0` if an entry with the key already exists (payload untouched),
129/// - `1` on success.
130unsafe fn hash_add_impl(
131    hash: *mut c_void,
132    name: *const xmlChar,
133    name2: *const xmlChar,
134    name3: *const xmlChar,
135    userdata: *mut c_void,
136) -> c_int {
137    if hash.is_null() || name.is_null() {
138        return -1;
139    }
140    let table = hash as *mut HashTable;
141    // The internal add_entry3 returns -1 exactly when the key is already
142    // present (NULL hash/name were rejected above) and 0 on success.
143    if unsafe { hash_add_entry3(table, name, name2, name3, userdata) } == 0 {
144        1
145    } else {
146        0
147    }
148}
149
150/// Add a hash table entry.
151///
152/// # UPSTREAM-PARITY
153///
154/// ```c
155/// int xmlHashAdd(xmlHashTable *hash, const xmlChar *name, void *userdata);
156/// ```
157///
158/// Returns 1 on success, 0 if an entry exists and -1 in case of error.
159#[no_mangle]
160pub unsafe extern "C" fn xmlHashAdd(
161    hash: *mut c_void,
162    name: *const xmlChar,
163    userdata: *mut c_void,
164) -> c_int {
165    unsafe { hash_add_impl(hash, name, ptr::null(), ptr::null(), userdata) }
166}
167
168/// Add a hash table entry with two strings as key.
169///
170/// # UPSTREAM-PARITY
171///
172/// ```c
173/// int xmlHashAdd2(xmlHashTable *hash, const xmlChar *name,
174///                 const xmlChar *name2, void *userdata);
175/// ```
176///
177/// Returns 1 on success, 0 if an entry exists and -1 in case of error.
178#[no_mangle]
179pub unsafe extern "C" fn xmlHashAdd2(
180    hash: *mut c_void,
181    name: *const xmlChar,
182    name2: *const xmlChar,
183    userdata: *mut c_void,
184) -> c_int {
185    unsafe { hash_add_impl(hash, name, name2, ptr::null(), userdata) }
186}
187
188/// Add a hash table entry with three strings as key.
189///
190/// # UPSTREAM-PARITY
191///
192/// ```c
193/// int xmlHashAdd3(xmlHashTable *hash, const xmlChar *name,
194///                 const xmlChar *name2, const xmlChar *name3,
195///                 void *userdata);
196/// ```
197///
198/// Returns 1 on success, 0 if an entry exists and -1 in case of error.
199#[no_mangle]
200pub unsafe extern "C" fn xmlHashAdd3(
201    hash: *mut c_void,
202    name: *const xmlChar,
203    name2: *const xmlChar,
204    name3: *const xmlChar,
205    userdata: *mut c_void,
206) -> c_int {
207    unsafe { hash_add_impl(hash, name, name2, name3, userdata) }
208}
209
210// ═══════════════════════════════════════════════════════════════════════════════
211// xmlHashCopySafe
212// ═══════════════════════════════════════════════════════════════════════════════
213
214/// Copy a hash table using `copy` to copy payloads; on error the partial
215/// table is freed with `dealloc` and NULL is returned.
216///
217/// # UPSTREAM-PARITY
218///
219/// ```c
220/// xmlHashTable *xmlHashCopySafe(xmlHashTable *hash, xmlHashCopier copy,
221///                               xmlHashDeallocator dealloc);
222/// ```
223///
224/// Returns the new table or NULL if a memory allocation failed.
225#[no_mangle]
226pub unsafe extern "C" fn xmlHashCopySafe(
227    hash: *mut c_void,
228    copy: Option<xmlHashCopier>,
229    dealloc: Option<xmlHashDeallocator>,
230) -> *mut c_void {
231    if hash.is_null() || copy.is_none() {
232        return ptr::null_mut();
233    }
234    let copy_fn = copy.unwrap();
235
236    // Snapshot every entry first so the copier runs without holding a borrow
237    // on the table (the internal buckets are not publicly reachable).
238    let mut collected: Vec<HashEntrySnapshot> = Vec::new();
239    unsafe {
240        hash_scan_full(
241            hash as *mut HashTable,
242            Some(collect_entry),
243            &mut collected as *mut Vec<HashEntrySnapshot> as *mut c_void,
244        );
245    }
246
247    let new_table = unsafe { hash_create(0) };
248    if new_table.is_null() {
249        return ptr::null_mut();
250    }
251
252    for e in &collected {
253        // SAFETY: `copy_fn` is a valid C callback supplied by the caller.
254        let copied = unsafe { copy_fn(e.payload, e.key1) };
255        if copied.is_null()
256            || unsafe { hash_add_entry3(new_table, e.key1, e.key2, e.key3, copied) } != 0
257        {
258            // Upstream: deallocate the failed copy, then free the partial
259            // table (which deallocates every successfully copied payload).
260            if let Some(f) = dealloc {
261                // SAFETY: `f` is a valid C deallocator supplied by the caller.
262                unsafe { f(copied, e.key1 as *mut xmlChar) };
263            }
264            unsafe { hash_free(new_table, dealloc) };
265            return ptr::null_mut();
266        }
267    }
268
269    new_table as *mut c_void
270}
271
272// ═══════════════════════════════════════════════════════════════════════════════
273// xmlHashDefaultDeallocator
274// ═══════════════════════════════════════════════════════════════════════════════
275
276/// Free a hash table entry with `xmlFree`.
277///
278/// # UPSTREAM-PARITY
279///
280/// ```c
281/// void xmlHashDefaultDeallocator(void *entry, const xmlChar *name);
282/// ```
283#[no_mangle]
284pub unsafe extern "C" fn xmlHashDefaultDeallocator(entry: *mut c_void, _name: *const xmlChar) {
285    if !entry.is_null() {
286        unsafe { allocator::xmlFreeImpl(entry) };
287    }
288}
289
290// ═══════════════════════════════════════════════════════════════════════════════
291// xmlHashQLookup / xmlHashQLookup2 / xmlHashQLookup3
292// ═══════════════════════════════════════════════════════════════════════════════
293
294/// Find the payload specified by the QNames tuple.
295///
296/// # UPSTREAM-PARITY
297///
298/// ```c
299/// void *xmlHashQLookup(xmlHashTable *hash, const xmlChar *prefix,
300///                      const xmlChar *name);
301/// ```
302///
303/// The entry key is matched against the concatenation `prefix:name` (the
304/// colon is only included when `prefix` is non-NULL); a NULL `prefix` makes
305/// this a plain single-key lookup.
306///
307/// Returns a pointer to the payload or NULL if no entry was found.
308#[no_mangle]
309pub unsafe extern "C" fn xmlHashQLookup(
310    hash: *mut c_void,
311    prefix: *const xmlChar,
312    name: *const xmlChar,
313) -> *mut c_void {
314    unsafe {
315        qlookup3(
316            hash,
317            prefix,
318            name,
319            ptr::null(),
320            ptr::null(),
321            ptr::null(),
322            ptr::null(),
323        )
324    }
325}
326
327/// Find the payload specified by two QNames.
328///
329/// # UPSTREAM-PARITY
330///
331/// ```c
332/// void *xmlHashQLookup2(xmlHashTable *hash, const xmlChar *prefix,
333///                       const xmlChar *name, const xmlChar *prefix2,
334///                       const xmlChar *name2);
335/// ```
336///
337/// Returns a pointer to the payload or NULL if no entry was found.
338#[no_mangle]
339pub unsafe extern "C" fn xmlHashQLookup2(
340    hash: *mut c_void,
341    prefix: *const xmlChar,
342    name: *const xmlChar,
343    prefix2: *const xmlChar,
344    name2: *const xmlChar,
345) -> *mut c_void {
346    unsafe { qlookup3(hash, prefix, name, prefix2, name2, ptr::null(), ptr::null()) }
347}
348
349/// Find the payload specified by three QNames.
350///
351/// # UPSTREAM-PARITY
352///
353/// ```c
354/// void *xmlHashQLookup3(xmlHashTable *hash, const xmlChar *prefix,
355///                       const xmlChar *name, const xmlChar *prefix2,
356///                       const xmlChar *name2, const xmlChar *prefix3,
357///                       const xmlChar *name3);
358/// ```
359///
360/// Returns a pointer to the payload or NULL if no entry was found.
361#[no_mangle]
362pub unsafe extern "C" fn xmlHashQLookup3(
363    hash: *mut c_void,
364    prefix: *const xmlChar,
365    name: *const xmlChar,
366    prefix2: *const xmlChar,
367    name2: *const xmlChar,
368    prefix3: *const xmlChar,
369    name3: *const xmlChar,
370) -> *mut c_void {
371    unsafe { qlookup3(hash, prefix, name, prefix2, name2, prefix3, name3) }
372}
373
374/// Core QName lookup: each entry key is compared against the `prefix:name`
375/// concatenation (prefix omitted when NULL), matching upstream
376/// `xmlStrQEqual`/`xmlHashQNameValue` semantics.
377unsafe fn qlookup3(
378    hash: *mut c_void,
379    prefix: *const xmlChar,
380    name: *const xmlChar,
381    prefix2: *const xmlChar,
382    name2: *const xmlChar,
383    prefix3: *const xmlChar,
384    name3: *const xmlChar,
385) -> *mut c_void {
386    if hash.is_null() || name.is_null() {
387        return ptr::null_mut();
388    }
389    // Upstream xmlStrQEqual: a NULL local name with a non-NULL prefix can
390    // never match any stored key, so the whole lookup fails.
391    if (!prefix2.is_null() && name2.is_null()) || (!prefix3.is_null() && name3.is_null()) {
392        return ptr::null_mut();
393    }
394
395    let table = hash as *mut HashTable;
396
397    // Build the three qualified keys. The buffers are only needed for the
398    // duration of the lookup (the internal lookup copies nothing).
399    let mut buf1: Vec<u8> = Vec::new();
400    if !prefix.is_null() {
401        unsafe { push_c_str(&mut buf1, prefix) };
402        buf1.push(b':');
403    }
404    unsafe { push_c_str(&mut buf1, name) };
405    buf1.push(0);
406
407    let mut buf2: Vec<u8> = Vec::new();
408    let key2: *const xmlChar = if prefix2.is_null() {
409        name2
410    } else {
411        unsafe { push_c_str(&mut buf2, prefix2) };
412        buf2.push(b':');
413        unsafe { push_c_str(&mut buf2, name2) };
414        buf2.push(0);
415        buf2.as_ptr() as *const xmlChar
416    };
417
418    let mut buf3: Vec<u8> = Vec::new();
419    let key3: *const xmlChar = if prefix3.is_null() {
420        name3
421    } else {
422        unsafe { push_c_str(&mut buf3, prefix3) };
423        buf3.push(b':');
424        unsafe { push_c_str(&mut buf3, name3) };
425        buf3.push(0);
426        buf3.as_ptr() as *const xmlChar
427    };
428
429    unsafe { hash_lookup3(table, buf1.as_ptr() as *const xmlChar, key2, key3) }
430}
431
432// ═══════════════════════════════════════════════════════════════════════════════
433// xmlHashScan3 / xmlHashScanFull3
434// ═══════════════════════════════════════════════════════════════════════════════
435
436/// Collect the entries matching a (`name`, `name2`, `name3`) triple; a NULL
437/// key acts as a wildcard. Entries with a NULL payload are skipped, matching
438/// upstream `xmlHashScanFull3`.
439///
440/// The table is fully scanned into a snapshot first, so user callbacks never
441/// run while the internal buckets are being iterated.
442unsafe fn scan3_collect(
443    hash: *mut c_void,
444    name: *const xmlChar,
445    name2: *const xmlChar,
446    name3: *const xmlChar,
447) -> Vec<HashEntrySnapshot> {
448    let mut collected: Vec<HashEntrySnapshot> = Vec::new();
449    if hash.is_null() {
450        return collected;
451    }
452    unsafe {
453        hash_scan_full(
454            hash as *mut HashTable,
455            Some(collect_entry),
456            &mut collected as *mut Vec<HashEntrySnapshot> as *mut c_void,
457        );
458    }
459    collected.retain(|e| {
460        if e.payload.is_null() {
461            return false;
462        }
463        if !name.is_null() && !unsafe { c_str_eq(name, e.key1) } {
464            return false;
465        }
466        if !name2.is_null() && !unsafe { c_str_eq(name2, e.key2) } {
467            return false;
468        }
469        if !name3.is_null() && !unsafe { c_str_eq(name3, e.key3) } {
470            return false;
471        }
472        true
473    });
474    collected
475}
476
477/// Scan the hash `table` and apply `scan` to each value matching the
478/// (`name`, `name2`, `name3`) triple. A NULL key matches any value.
479///
480/// # UPSTREAM-PARITY
481///
482/// ```c
483/// void xmlHashScan3(xmlHashTable *hash, const xmlChar *name,
484///                   const xmlChar *name2, const xmlChar *name3,
485///                   xmlHashScanner scan, void *data);
486/// ```
487#[no_mangle]
488pub unsafe extern "C" fn xmlHashScan3(
489    hash: *mut c_void,
490    name: *const xmlChar,
491    name2: *const xmlChar,
492    name3: *const xmlChar,
493    scan: Option<xmlHashScanner>,
494    data: *mut c_void,
495) {
496    if scan.is_none() {
497        return;
498    }
499    let scan = scan.unwrap();
500    let matches = unsafe { scan3_collect(hash, name, name2, name3) };
501    for e in &matches {
502        // Upstream xmlHashScan3 passes a stub that forwards only key1.
503        unsafe { scan(e.payload, data, e.key1) };
504    }
505}
506
507/// Scan the hash `table` and apply `scan` to each value matching the
508/// (`name`, `name2`, `name3`) triple. A NULL key matches any value.
509///
510/// # UPSTREAM-PARITY
511///
512/// ```c
513/// void xmlHashScanFull3(xmlHashTable *hash, const xmlChar *name,
514///                       const xmlChar *name2, const xmlChar *name3,
515///                       xmlHashScannerFull scan, void *data);
516/// ```
517#[no_mangle]
518pub unsafe extern "C" fn xmlHashScanFull3(
519    hash: *mut c_void,
520    name: *const xmlChar,
521    name2: *const xmlChar,
522    name3: *const xmlChar,
523    scan: Option<xmlHashScannerFull>,
524    data: *mut c_void,
525) {
526    if scan.is_none() {
527        return;
528    }
529    let scan = scan.unwrap();
530    let matches = unsafe { scan3_collect(hash, name, name2, name3) };
531    for e in &matches {
532        unsafe { scan(e.payload, data, e.key1, e.key2, e.key3) };
533    }
534}
535
536// ═══════════════════════════════════════════════════════════════════════════════
537// Dictionary Exports (xmlDict*)
538// ═══════════════════════════════════════════════════════════════════════════════
539
540/// Reference counts tracked for `xmlDictReference`.
541///
542/// Upstream keeps `ref_counter` inside the `xmlDict` struct; the internal
543/// `Dict` has no self-refcount field (its refcounts are per-entry), so the
544/// Reference counts for dictionaries (upstream xmlDictReference). The
545/// reference count is tracked here in a side table keyed by the opaque dict
546/// pointer.
547pub static DICT_REFS: Lazy<Mutex<HashMap<usize, u32>>> = Lazy::new(|| Mutex::new(HashMap::new()));
548
549/// Interned string pointers owned by each dictionary, keyed by dict pointer.
550///
551/// Used by `xmlDictOwns`; upstream checks whether `str` falls inside the
552/// dict's string pool, which the internal `Dict` does not track, so pointers
553/// returned by `xmlDictQLookup` are recorded here.
554static DICT_OWNED: Lazy<Mutex<HashMap<usize, HashSet<usize>>>> =
555    Lazy::new(|| Mutex::new(HashMap::new()));
556
557/// Record an interned string pointer as owned by `dict` (for `xmlDictOwns`).
558fn register_owned(dict: *mut c_void, s: *const xmlChar) {
559    if dict.is_null() || s.is_null() {
560        return;
561    }
562    DICT_OWNED
563        .lock()
564        .entry(dict as usize)
565        .or_insert_with(HashSet::new)
566        .insert(s as usize);
567}
568
569/// Increment the reference counter of a dictionary.
570///
571/// # UPSTREAM-PARITY
572///
573/// ```c
574/// int xmlDictReference(xmlDict *dict);
575/// ```
576///
577/// Returns 0 in case of success and -1 in case of error.
578#[no_mangle]
579pub unsafe extern "C" fn xmlDictReference(dict: *mut c_void) -> c_int {
580    if dict.is_null() {
581        return -1;
582    }
583    *DICT_REFS.lock().entry(dict as usize).or_insert(0) += 1;
584    0
585}
586
587/// Look up a QName (`prefix:name`) in the dictionary, adding it if not found.
588///
589/// # UPSTREAM-PARITY
590///
591/// ```c
592/// const xmlChar *xmlDictQLookup(xmlDict *dict, const xmlChar *prefix,
593///                               const xmlChar *name);
594/// ```
595///
596/// If `prefix` is NULL this is a plain lookup of `name`. Returns the interned
597/// copy of the string or NULL in case of error.
598#[no_mangle]
599pub unsafe extern "C" fn xmlDictQLookup(
600    dict: *mut c_void,
601    prefix: *const xmlChar,
602    name: *const xmlChar,
603) -> *const xmlChar {
604    if dict.is_null() || name.is_null() {
605        return ptr::null();
606    }
607    if prefix.is_null() {
608        let ret = unsafe { dict_lookup(dict as *mut Dict, name, -1) };
609        register_owned(dict, ret);
610        return ret;
611    }
612
613    // Build "prefix:name", intern it, then drop the temporary buffer.
614    let mut qname: Vec<u8> = Vec::new();
615    unsafe { push_c_str(&mut qname, prefix) };
616    qname.push(b':');
617    unsafe { push_c_str(&mut qname, name) };
618    qname.push(0);
619
620    let ret = unsafe { dict_lookup(dict as *mut Dict, qname.as_ptr() as *const xmlChar, -1) };
621    register_owned(dict, ret);
622    ret
623}
624
625/// Check if a string is owned by the dictionary.
626///
627/// # UPSTREAM-PARITY
628///
629/// ```c
630/// int xmlDictOwns(xmlDict *dict, const xmlChar *str);
631/// ```
632///
633/// Returns 1 if `str` points into the dictionary's memory, 0 if not, and
634/// -1 in case of error (NULL `dict` or `str`).
635#[no_mangle]
636pub unsafe extern "C" fn xmlDictOwns(dict: *mut c_void, str: *const xmlChar) -> c_int {
637    if dict.is_null() || str.is_null() {
638        return -1;
639    }
640    let owned = DICT_OWNED
641        .lock()
642        .get(&(dict as usize))
643        .map_or(false, |set| set.contains(&(str as usize)));
644    if owned {
645        1
646    } else {
647        0
648    }
649}
650
651/// Free the dictionary data.
652///
653/// # UPSTREAM-PARITY
654///
655/// ```c
656/// void xmlDictCleanup(void);
657/// ```
658///
659/// Upstream deprecated this function in 2.13 when the global dictionary was
660/// removed; it is a no-op (takes no arguments) in the oracle 2.15 headers.
661#[no_mangle]
662pub unsafe extern "C" fn xmlDictCleanup() {
663    // No-op: there is no global dictionary to clean up (upstream 2.13+).
664}