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}