libxml_rs/xml/hash/mod.rs
1//! Hash table — public xmlHash API (§85 Phase 1).
2//!
3//! Implements the libxml2 hash table, used for DTD element/attribute tables,
4//! XPath function lookup, catalog entries, and more.
5//!
6//! # UPSTREAM-PARITY
7//!
8//! The hash table supports:
9//! - Single-key, 2-key, and 3-key lookups
10//! - Custom deallocator and copier functions
11//! - Dictionary-backed keys (for memory efficiency)
12//! - Scanning with callback functions
13//! - Copying with custom copier
14//!
15//! # Phase 1 status
16//!
17//! Complete — all hash table operations are implemented.
18//!
19//! # Upstream contract
20//!
21//! Mirrors upstream `hash.c` / `hash.h` (`SRC-LIBXML2-2.15.0-HASH-C`, parity
22//! target libxml2 2.15.3 oracle): `xmlHashCreate` / `xmlHashCreateDict` /
23//! `xmlHashLookup` / `xmlHashAddEntry` / `xmlHashScan` / `xmlHashCopy` /
24//! `xmlHashFree`, including the 1/2/3-key variants and the dict-backed key
25//! mode.
26//!
27//! # Conceptual behavior
28//!
29//! Implements a chained hash table whose keys are NUL-terminated xmlChar
30//! strings (one, two or three of them) with payloads owned per the table
31//! deallocator/copier. Two-key tables are used for DTD element/attribute
32//! declarations (key order (name,prefix,elem), R-000164) and XPath
33//! function lookup; the dictionary-backed variant keeps keys in the dict so
34//! the table never frees them.
35//!
36//! # Ownership & safety invariants
37//!
38//! The table owns its payloads and frees them through the registered
39//! deallocator on `xmlHashFree`/removal; with a copier, `xmlHashCopy`
40//! duplicates payloads. In dict mode the keys are borrowed from the dict
41//! (never freed by the table). Keys are always copied in; the caller keeps
42//! its own strings.
43//!
44//! # Historical quirks & epochs
45//!
46//! The multi-key and dict-backed extensions were added in the 2.6
47//! validation-era expansion and have been stable across the 2.7.8 → 2.15.3
48//! span; the HIST-SURFACE-EPOCH court family (HISTORICAL_SURFACE_EPOCHS.md)
49//! fingerprints the API surface per epoch so a future signature change is
50//! caught.
51//!
52//! # Deliberate oddities
53//!
54//! The 3-key variant (used by the attribute tables) is an upstream
55//! historical extension, not part of any spec — it is kept byte-faithful
56//! because the DTD lookup order depends on it.
57//!
58//! # Proving courts
59//!
60//! HIST-SURFACE-EPOCH fingerprints this module per release epoch; the
61//! TREE-001 structural probe (R-000164) exercises the attribute hash key
62//! order against the oracle; cargo test runs the unit suites.
63//!
64//! # Tempting simplifications that would break parity
65//!
66//! Do not re-key the DTD tables by (elem,name): R-000164 proved the
67//! upstream (name,prefix,elem) order is observable. Do not drop the
68//! deallocator/copier contract: consumers register custom free/copy
69//! functions and observe them firing.
70
71use core::ffi::c_void;
72use core::ptr;
73use std::os::raw::{c_char, c_int};
74
75use crate::abi::allocator;
76use crate::abi::callbacks::xmlHashCopier;
77use crate::abi::callbacks::xmlHashDeallocator;
78use crate::abi::callbacks::xmlHashScanner;
79use crate::abi::callbacks::xmlHashScannerFull;
80
81use crate::abi::types::xmlChar;
82
83// ═══════════════════════════════════════════════════════════════════════════════
84// Constants
85// ═══════════════════════════════════════════════════════════════════════════════
86
87/// Default initial hash table size.
88const HASH_INIT_SIZE: usize = 16;
89
90/// Maximum load factor (entries/buckets) before resize.
91#[allow(dead_code)]
92const MAX_LOAD_FACTOR: f64 = 0.75;
93
94// ═══════════════════════════════════════════════════════════════════════════════
95// Internal Types
96// ═══════════════════════════════════════════════════════════════════════════════
97
98/// A hash table entry.
99#[derive(Debug)]
100struct HashEntry {
101 /// The key string(s), stored as raw C string pointers.
102 key1: *const xmlChar,
103 key2: *const xmlChar,
104 key3: *const xmlChar,
105 /// The value.
106 payload: *mut c_void,
107}
108
109/// The hash table struct.
110#[derive(Debug)]
111pub struct HashTable {
112 /// Buckets (each is a vector of entries).
113 buckets: Vec<Vec<HashEntry>>,
114 /// Number of entries.
115 count: usize,
116 /// Optional deallocator for payloads.
117 deallocator: Option<xmlHashDeallocator>,
118 /// Optional copier for payloads.
119 copier: Option<xmlHashCopier>,
120 /// Whether the keys are dictionary-owned (don't free them).
121 dict_owned: bool,
122}
123
124// ═══════════════════════════════════════════════════════════════════════════════
125// String Hashing
126// ═══════════════════════════════════════════════════════════════════════════════
127
128/// Simple FNV-1a hash for xmlChar strings.
129const fn hash_xml_str(s: *const xmlChar) -> u64 {
130 if s.is_null() {
131 return 0;
132 }
133 let mut hash: u64 = 0xcbf29ce484222325;
134 let mut i = 0;
135 loop {
136 // SAFETY: Caller guarantees null-terminated string.
137 let c = unsafe { *s.add(i) };
138 if c == 0 {
139 break;
140 }
141 hash ^= c as u64;
142 hash = hash.wrapping_mul(0x100000001b3);
143 i += 1;
144 }
145 hash
146}
147
148/// Compute combined hash for 1-3 keys.
149const fn combined_hash(key1: *const xmlChar, key2: *const xmlChar, key3: *const xmlChar) -> u64 {
150 let mut h = hash_xml_str(key1);
151 h = h.wrapping_mul(31).wrapping_add(hash_xml_str(key2));
152 h = h.wrapping_mul(31).wrapping_add(hash_xml_str(key3));
153 h
154}
155
156// ═══════════════════════════════════════════════════════════════════════════════
157// Public API
158// ═══════════════════════════════════════════════════════════════════════════════
159
160/// Create a new hash table.
161///
162/// # UPSTREAM-PARITY
163///
164/// ```c
165/// xmlHashTablePtr xmlHashCreate(int size);
166/// ```
167///
168/// Creates a hash table with the given initial size.
169/// If size <= 0, uses the default size.
170pub fn hash_create(size: c_int) -> *mut HashTable {
171 let table = Box::new(HashTable {
172 buckets: (0..if size <= 0 {
173 HASH_INIT_SIZE
174 } else {
175 size as usize
176 })
177 .map(|_| Vec::new())
178 .collect(),
179 count: 0,
180 deallocator: None,
181 copier: None,
182 dict_owned: false,
183 });
184
185 Box::into_raw(table)
186}
187
188/// Create a hash table with dictionary-backed keys.
189///
190/// # UPSTREAM-PARITY
191///
192/// ```c
193/// xmlHashTablePtr xmlHashCreateDict(int size, xmlDictPtr dict);
194/// ```
195///
196/// Creates a hash table where keys are interned in the given dictionary.
197/// The dictionary reference is stored but in this implementation, keys
198/// are still stored as raw pointers for simplicity.
199pub fn hash_create_dict(size: c_int, _dict: *mut c_void) -> *mut HashTable {
200 let table = hash_create(size);
201 if !table.is_null() {
202 unsafe { (*table).dict_owned = true };
203 }
204 table
205}
206
207/// Free a hash table.
208///
209/// # UPSTREAM-PARITY
210///
211/// ```c
212/// void xmlHashFree(xmlHashTablePtr table, xmlHashDeallocator f);
213/// ```
214///
215/// Frees all entries and calls `f` on each payload to free it.
216///
217/// # SAFETY
218///
219/// - `table` must be a valid pointer to a HashTable, or NULL.
220/// - `f` may be NULL (no deallocation of payloads).
221pub unsafe fn hash_free(table: *mut HashTable, f: Option<xmlHashDeallocator>) {
222 if table.is_null() {
223 return;
224 }
225
226 let t = unsafe { &mut *table };
227
228 for bucket in t.buckets.iter_mut() {
229 for entry in bucket.drain(..) {
230 if let Some(dealloc) = f {
231 dealloc(entry.payload, entry.key1 as *mut u8);
232 }
233 if !t.dict_owned {
234 if !entry.key1.is_null() {
235 allocator::xmlFreeImpl(entry.key1 as *mut c_void);
236 }
237 if !entry.key2.is_null() {
238 allocator::xmlFreeImpl(entry.key2 as *mut c_void);
239 }
240 if !entry.key3.is_null() {
241 allocator::xmlFreeImpl(entry.key3 as *mut c_void);
242 }
243 }
244 }
245 }
246
247 drop(Box::from_raw(table));
248}
249
250/// Add an entry with a single key.
251///
252/// # UPSTREAM-PARITY
253///
254/// ```c
255/// int xmlHashAddEntry(xmlHashTablePtr table, const xmlChar *name, void *userdata);
256/// ```
257///
258/// Returns 0 on success, -1 if the key already exists or on failure.
259///
260/// # SAFETY
261///
262/// - `table` must be a valid pointer to a HashTable, or NULL.
263/// - `name` must be a valid null-terminated string.
264pub unsafe fn hash_add_entry(
265 table: *mut HashTable,
266 name: *const xmlChar,
267 userdata: *mut c_void,
268) -> c_int {
269 hash_add_entry3(table, name, ptr::null(), ptr::null(), userdata)
270}
271
272/// Add an entry with two keys.
273///
274/// # UPSTREAM-PARITY
275///
276/// ```c
277/// int xmlHashAddEntry2(xmlHashTablePtr table, const xmlChar *name,
278/// const xmlChar *name2, void *userdata);
279/// ```
280///
281/// # SAFETY
282///
283/// Same as `hash_add_entry` with an additional key.
284pub unsafe fn hash_add_entry2(
285 table: *mut HashTable,
286 name: *const xmlChar,
287 name2: *const xmlChar,
288 userdata: *mut c_void,
289) -> c_int {
290 hash_add_entry3(table, name, name2, ptr::null(), userdata)
291}
292
293/// Add an entry with three keys.
294///
295/// # UPSTREAM-PARITY
296///
297/// ```c
298/// int xmlHashAddEntry3(xmlHashTablePtr table, const xmlChar *name,
299/// const xmlChar *name2, const xmlChar *name3,
300/// void *userdata);
301/// ```
302///
303/// Returns 0 on success, -1 if the key already exists or on failure.
304///
305/// # SAFETY
306///
307/// - `table` must be a valid pointer to a HashTable.
308/// - `name`, `name2`, `name3` must be valid null-terminated strings or NULL.
309pub unsafe fn hash_add_entry3(
310 table: *mut HashTable,
311 name: *const xmlChar,
312 name2: *const xmlChar,
313 name3: *const xmlChar,
314 userdata: *mut c_void,
315) -> c_int {
316 if table.is_null() {
317 return -1;
318 }
319
320 let t = unsafe { &mut *table };
321
322 // Check if the key already exists
323 if !hash_find_entry(table, name, name2, name3).is_null() {
324 return -1;
325 }
326
327 let hash = combined_hash(name, name2, name3);
328 let bucket_idx = (hash as usize) % t.buckets.len();
329
330 // Copy keys
331 let k1 = if name.is_null() {
332 ptr::null()
333 } else {
334 unsafe { allocator::xmlMemStrdupImpl(name as *const c_char) as *const xmlChar }
335 };
336 let k2 = if name2.is_null() {
337 ptr::null()
338 } else {
339 unsafe { allocator::xmlMemStrdupImpl(name2 as *const c_char) as *const xmlChar }
340 };
341 let k3 = if name3.is_null() {
342 ptr::null()
343 } else {
344 unsafe { allocator::xmlMemStrdupImpl(name3 as *const c_char) as *const xmlChar }
345 };
346
347 t.buckets[bucket_idx].push(HashEntry {
348 key1: k1,
349 key2: k2,
350 key3: k3,
351 payload: userdata,
352 });
353 t.count += 1;
354
355 0
356}
357
358/// Update an entry (add or replace).
359///
360/// # UPSTREAM-PARITY
361///
362/// ```c
363/// int xmlHashUpdateEntry(xmlHashTablePtr table, const xmlChar *name,
364/// void *userdata, xmlHashDeallocator f);
365/// ```
366///
367/// If the key exists, the old payload is deallocated with `f` and replaced.
368/// If the key doesn't exist, a new entry is added.
369///
370/// Returns 0 on success, -1 on failure.
371///
372/// # SAFETY
373///
374/// Same as `hash_add_entry` with an additional deallocator.
375pub unsafe fn hash_update_entry(
376 table: *mut HashTable,
377 name: *const xmlChar,
378 userdata: *mut c_void,
379 f: Option<xmlHashDeallocator>,
380) -> c_int {
381 hash_update_entry3(table, name, ptr::null(), ptr::null(), userdata, f)
382}
383
384/// Update an entry with two keys.
385///
386/// # SAFETY
387///
388/// - `table`, `userdata` must be valid pointers (or NULL
389/// where the upstream C contract allows), obtained from the
390/// matching constructor/owner and not yet freed; the callee may
391/// take or keep ownership exactly as the C API specifies.
392///
393/// - `name`, `name2` must point to valid NUL-terminated
394/// strings (or NULL where the C contract allows) for the lifetime
395/// of the call.
396///
397/// - `f` must be a valid callback (or None);
398/// the callback is invoked with the documented context pointer and
399/// must itself uphold the same pointer invariants.
400///
401/// The caller must not race this call with concurrent mutation of the
402/// same objects from other threads (per-object state is not internally
403/// synchronized). Violating any of the above is undefined behavior.
404///
405/// Exercised by the C-API differential courts
406/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
407/// courts; those pass byte-for-byte against the upstream oracle.
408pub unsafe fn hash_update_entry2(
409 table: *mut HashTable,
410 name: *const xmlChar,
411 name2: *const xmlChar,
412 userdata: *mut c_void,
413 f: Option<xmlHashDeallocator>,
414) -> c_int {
415 hash_update_entry3(table, name, name2, ptr::null(), userdata, f)
416}
417
418/// Update an entry with three keys.
419///
420/// # UPSTREAM-PARITY
421///
422/// ```c
423/// int xmlHashUpdateEntry3(xmlHashTablePtr table, const xmlChar *name,
424/// const xmlChar *name2, const xmlChar *name3,
425/// void *userdata, xmlHashDeallocator f);
426/// ```
427///
428/// # SAFETY
429///
430/// Same as `hash_add_entry3` with an additional deallocator.
431pub unsafe fn hash_update_entry3(
432 table: *mut HashTable,
433 name: *const xmlChar,
434 name2: *const xmlChar,
435 name3: *const xmlChar,
436 userdata: *mut c_void,
437 f: Option<xmlHashDeallocator>,
438) -> c_int {
439 if table.is_null() {
440 return -1;
441 }
442
443 let t = unsafe { &mut *table };
444
445 let hash = combined_hash(name, name2, name3);
446 let bucket_idx = (hash as usize) % t.buckets.len();
447
448 // Look for existing entry
449 for entry in t.buckets[bucket_idx].iter_mut() {
450 if keys_equal(entry.key1, name)
451 && keys_equal(entry.key2, name2)
452 && keys_equal(entry.key3, name3)
453 {
454 // Replace payload
455 if let Some(dealloc) = f {
456 dealloc(entry.payload, entry.key1 as *mut u8);
457 }
458 entry.payload = userdata;
459 return 0;
460 }
461 }
462
463 // Not found — add new entry
464 hash_add_entry3(table, name, name2, name3, userdata)
465}
466
467/// Look up an entry by single key.
468///
469/// # UPSTREAM-PARITY
470///
471/// ```c
472/// void *xmlHashLookup(xmlHashTablePtr table, const xmlChar *name);
473/// ```
474///
475/// Returns the payload, or NULL if not found.
476///
477/// # SAFETY
478///
479/// - `table` must be a valid pointer to a HashTable, or NULL.
480/// - `name` must be a valid null-terminated string.
481pub unsafe fn hash_lookup(table: *mut HashTable, name: *const xmlChar) -> *mut c_void {
482 hash_lookup3(table, name, ptr::null(), ptr::null())
483}
484
485/// Look up an entry by two keys.
486///
487/// # SAFETY
488///
489/// - `table` must be valid pointers (or NULL
490/// where the upstream C contract allows), obtained from the
491/// matching constructor/owner and not yet freed; the callee may
492/// take or keep ownership exactly as the C API specifies.
493///
494/// - `name`, `name2` must point to valid NUL-terminated
495/// strings (or NULL where the C contract allows) for the lifetime
496/// of the call.
497///
498/// The caller must not race this call with concurrent mutation of the
499/// same objects from other threads (per-object state is not internally
500/// synchronized). Violating any of the above is undefined behavior.
501///
502/// Exercised by the C-API differential courts
503/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
504/// courts; those pass byte-for-byte against the upstream oracle.
505pub unsafe fn hash_lookup2(
506 table: *mut HashTable,
507 name: *const xmlChar,
508 name2: *const xmlChar,
509) -> *mut c_void {
510 hash_lookup3(table, name, name2, ptr::null())
511}
512
513/// Look up an entry by three keys.
514///
515/// # UPSTREAM-PARITY
516///
517/// ```c
518/// void *xmlHashLookup3(xmlHashTablePtr table, const xmlChar *name,
519/// const xmlChar *name2, const xmlChar *name3);
520/// ```
521///
522/// Returns the payload, or NULL if not found.
523///
524/// # SAFETY
525///
526/// - `table` must be a valid pointer to a HashTable, or NULL.
527/// - `name`, `name2`, `name3` must be valid null-terminated strings or NULL.
528pub unsafe fn hash_lookup3(
529 table: *mut HashTable,
530 name: *const xmlChar,
531 name2: *const xmlChar,
532 name3: *const xmlChar,
533) -> *mut c_void {
534 hash_find_entry(table, name, name2, name3)
535}
536
537/// Internal: find an entry's payload.
538///
539/// # Safety
540///
541/// - `table` must be NULL or a valid, initialized `HashTable` that stays
542/// alive for the call; `name`, `name2` and `name3` must be NULL or valid
543/// NUL-terminated strings readable for the hash/key comparisons.
544unsafe fn hash_find_entry(
545 table: *mut HashTable,
546 name: *const xmlChar,
547 name2: *const xmlChar,
548 name3: *const xmlChar,
549) -> *mut c_void {
550 if table.is_null() {
551 return ptr::null_mut();
552 }
553
554 let t = unsafe { &*table };
555
556 let hash = combined_hash(name, name2, name3);
557 let bucket_idx = (hash as usize) % t.buckets.len();
558
559 for entry in &t.buckets[bucket_idx] {
560 if keys_equal(entry.key1, name)
561 && keys_equal(entry.key2, name2)
562 && keys_equal(entry.key3, name3)
563 {
564 return entry.payload;
565 }
566 }
567
568 ptr::null_mut()
569}
570
571/// Get the number of entries in the hash table.
572///
573/// # UPSTREAM-PARITY
574///
575/// ```c
576/// int xmlHashSize(xmlHashTablePtr table);
577/// ```
578pub fn hash_size(table: *mut HashTable) -> c_int {
579 if table.is_null() {
580 return -1;
581 }
582 unsafe { (*table).count as c_int }
583}
584
585/// Remove an entry by single key.
586///
587/// # UPSTREAM-PARITY
588///
589/// ```c
590/// int xmlHashRemoveEntry(xmlHashTablePtr table, const xmlChar *name,
591/// xmlHashDeallocator f);
592/// ```
593///
594/// Returns 0 on success, -1 if not found.
595///
596/// # SAFETY
597///
598/// - `table` must be a valid pointer to a HashTable, or NULL.
599/// - `name` must be a valid null-terminated string.
600/// - `f` may be NULL.
601pub unsafe fn hash_remove_entry(
602 table: *mut HashTable,
603 name: *const xmlChar,
604 f: Option<xmlHashDeallocator>,
605) -> c_int {
606 hash_remove_entry3(table, name, ptr::null(), ptr::null(), f)
607}
608
609/// Remove an entry by two keys.
610///
611/// # SAFETY
612///
613/// - `table` must be valid pointers (or NULL
614/// where the upstream C contract allows), obtained from the
615/// matching constructor/owner and not yet freed; the callee may
616/// take or keep ownership exactly as the C API specifies.
617///
618/// - `name`, `name2` must point to valid NUL-terminated
619/// strings (or NULL where the C contract allows) for the lifetime
620/// of the call.
621///
622/// - `f` must be a valid callback (or None);
623/// the callback is invoked with the documented context pointer and
624/// must itself uphold the same pointer invariants.
625///
626/// The caller must not race this call with concurrent mutation of the
627/// same objects from other threads (per-object state is not internally
628/// synchronized). Violating any of the above is undefined behavior.
629///
630/// Exercised by the C-API differential courts
631/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
632/// courts; those pass byte-for-byte against the upstream oracle.
633pub unsafe fn hash_remove_entry2(
634 table: *mut HashTable,
635 name: *const xmlChar,
636 name2: *const xmlChar,
637 f: Option<xmlHashDeallocator>,
638) -> c_int {
639 hash_remove_entry3(table, name, name2, ptr::null(), f)
640}
641
642/// Remove an entry by three keys.
643///
644/// # UPSTREAM-PARITY
645///
646/// ```c
647/// int xmlHashRemoveEntry3(xmlHashTablePtr table, const xmlChar *name,
648/// const xmlChar *name2, const xmlChar *name3,
649/// xmlHashDeallocator f);
650/// ```
651///
652/// Returns 0 on success, -1 if not found.
653///
654/// # SAFETY
655///
656/// - `table` must be a valid pointer to a HashTable, or NULL.
657/// - `name`, `name2`, `name3` must be valid null-terminated strings or NULL.
658/// - `f` may be NULL.
659pub unsafe fn hash_remove_entry3(
660 table: *mut HashTable,
661 name: *const xmlChar,
662 name2: *const xmlChar,
663 name3: *const xmlChar,
664 f: Option<xmlHashDeallocator>,
665) -> c_int {
666 if table.is_null() {
667 return -1;
668 }
669
670 let t = unsafe { &mut *table };
671
672 let hash = combined_hash(name, name2, name3);
673 let bucket_idx = (hash as usize) % t.buckets.len();
674
675 let bucket = &mut t.buckets[bucket_idx];
676 let pos = bucket.iter().position(|entry| {
677 keys_equal(entry.key1, name)
678 && keys_equal(entry.key2, name2)
679 && keys_equal(entry.key3, name3)
680 });
681
682 if let Some(idx) = pos {
683 let entry = bucket.remove(idx);
684 if let Some(dealloc) = f {
685 dealloc(entry.payload, entry.key1 as *mut u8);
686 }
687 if !t.dict_owned {
688 if !entry.key1.is_null() {
689 allocator::xmlFreeImpl(entry.key1 as *mut c_void);
690 }
691 if !entry.key2.is_null() {
692 allocator::xmlFreeImpl(entry.key2 as *mut c_void);
693 }
694 if !entry.key3.is_null() {
695 allocator::xmlFreeImpl(entry.key3 as *mut c_void);
696 }
697 }
698 t.count -= 1;
699 0
700 } else {
701 -1
702 }
703}
704
705/// Scan all entries with a scanner function.
706///
707/// # UPSTREAM-PARITY
708///
709/// ```c
710/// void xmlHashScan(xmlHashTablePtr table, xmlHashScanner f, void *data);
711/// ```
712///
713/// # SAFETY
714///
715/// - `table` must be a valid pointer to a HashTable, or NULL.
716/// - `f` must be a valid function pointer or NULL.
717/// - `data` may be NULL.
718pub unsafe fn hash_scan(table: *mut HashTable, f: Option<xmlHashScanner>, data: *mut c_void) {
719 if table.is_null() || f.is_none() {
720 return;
721 }
722 let f = f.unwrap();
723
724 let t = unsafe { &*table };
725 for bucket in &t.buckets {
726 for entry in bucket {
727 f(entry.payload, data, entry.key1 as *const xmlChar);
728 }
729 }
730}
731
732/// Scan all entries with a full scanner function (includes all keys).
733///
734/// # UPSTREAM-PARITY
735///
736/// ```c
737/// void xmlHashScanFull(xmlHashTablePtr table, xmlHashScannerFull f, void *data);
738/// ```
739///
740/// # SAFETY
741///
742/// - `table` must be a valid pointer to a HashTable, or NULL.
743/// - `f` must be a valid function pointer or NULL.
744/// - `data` may be NULL.
745pub unsafe fn hash_scan_full(
746 table: *mut HashTable,
747 f: Option<xmlHashScannerFull>,
748 data: *mut c_void,
749) {
750 if table.is_null() || f.is_none() {
751 return;
752 }
753 let f = f.unwrap();
754
755 let t = unsafe { &*table };
756 for bucket in &t.buckets {
757 for entry in bucket {
758 f(entry.payload, data, entry.key1, entry.key2, entry.key3);
759 }
760 }
761}
762
763/// Copy a hash table.
764///
765/// # UPSTREAM-PARITY
766///
767/// ```c
768/// xmlHashTablePtr xmlHashCopy(xmlHashTablePtr table, xmlHashCopier f);
769/// ```
770///
771/// Creates a new hash table with copied entries. The `f` function is called
772/// for each entry to create a copy of the payload.
773///
774/// Returns the new hash table, or NULL on failure.
775///
776/// # SAFETY
777///
778/// - `table` must be a valid pointer to a HashTable, or NULL.
779/// - `f` may be NULL (payloads are copied by pointer).
780pub unsafe fn hash_copy(table: *mut HashTable, f: Option<xmlHashCopier>) -> *mut HashTable {
781 if table.is_null() {
782 return ptr::null_mut();
783 }
784
785 let t = unsafe { &*table };
786 let new_table = hash_create(t.buckets.len() as c_int);
787 if new_table.is_null() {
788 return ptr::null_mut();
789 }
790
791 let new_t = unsafe { &mut *new_table };
792 new_t.deallocator = t.deallocator;
793 new_t.copier = t.copier;
794 new_t.dict_owned = t.dict_owned;
795
796 for bucket in &t.buckets {
797 for entry in bucket {
798 let payload = match f {
799 Some(copier) => copier(entry.payload, entry.key1 as *const xmlChar),
800 None => entry.payload,
801 };
802
803 let k1 = if entry.key1.is_null() {
804 ptr::null()
805 } else {
806 allocator::xmlMemStrdupImpl(entry.key1 as *const c_char) as *const xmlChar
807 };
808 let k2 = if entry.key2.is_null() {
809 ptr::null()
810 } else {
811 allocator::xmlMemStrdupImpl(entry.key2 as *const c_char) as *const xmlChar
812 };
813 let k3 = if entry.key3.is_null() {
814 ptr::null()
815 } else {
816 allocator::xmlMemStrdupImpl(entry.key3 as *const c_char) as *const xmlChar
817 };
818
819 let hash = combined_hash(k1, k2, k3);
820 let bucket_idx = (hash as usize) % new_t.buckets.len();
821
822 new_t.buckets[bucket_idx].push(HashEntry {
823 key1: k1,
824 key2: k2,
825 key3: k3,
826 payload,
827 });
828 new_t.count += 1;
829 }
830 }
831
832 new_table
833}
834
835// ═══════════════════════════════════════════════════════════════════════════════
836// Helper Functions
837// ═══════════════════════════════════════════════════════════════════════════════
838
839/// Compare two xmlChar strings for equality (null-safe).
840const fn keys_equal(a: *const xmlChar, b: *const xmlChar) -> bool {
841 if a.is_null() && b.is_null() {
842 return true;
843 }
844 if a.is_null() || b.is_null() {
845 return false;
846 }
847 // Compare byte by byte
848 let mut i = 0;
849 loop {
850 // SAFETY: Both are null-terminated strings.
851 let ca = unsafe { *a.add(i) };
852 let cb = unsafe { *b.add(i) };
853 if ca != cb {
854 return false;
855 }
856 if ca == 0 {
857 return true;
858 }
859 i += 1;
860 }
861}
862
863// ═══════════════════════════════════════════════════════════════════════════════
864// Tests
865// ═══════════════════════════════════════════════════════════════════════════════
866
867#[cfg(test)]
868mod tests {
869 use super::*;
870
871 /// Allocate a NUL-terminated xmlChar key buffer with the libxml2 allocator.
872 ///
873 /// # Safety
874 ///
875 /// - `s` must be a valid string; the returned pointer is
876 /// allocator-owned and valid for `bytes.len() + 1` bytes, or NULL on
877 /// allocation failure.
878 fn c_str(s: &str) -> *const xmlChar {
879 let bytes = s.as_bytes();
880 let buf = unsafe { allocator::xmlMallocImpl(bytes.len() + 1) as *mut u8 };
881 if !buf.is_null() {
882 unsafe {
883 ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
884 *buf.add(bytes.len()) = 0;
885 }
886 }
887 buf as *const xmlChar
888 }
889
890 /// Create and free a hash table.
891 ///
892 /// # Safety
893 ///
894 /// - `table` is non-NULL (asserted) and valid until freed with
895 /// `hash_free`, which must be called exactly once.
896 #[test]
897 fn test_hash_create_free() {
898 unsafe {
899 let table = hash_create(16);
900 assert!(!table.is_null());
901 hash_free(table, None);
902 }
903 }
904
905 /// Add, look up and reject a duplicate key in a hash table.
906 ///
907 /// # Safety
908 ///
909 /// - `table` is non-NULL (asserted); `key` is a valid NUL-terminated
910 /// string allocated by `c_str` and stays valid until `hash_free`;
911 /// `value` points to a stack `c_int` alive for the whole test;
912 /// `hash_free` releases the table exactly once.
913 #[test]
914 fn test_hash_add_lookup() {
915 unsafe {
916 let table = hash_create(16);
917 let key = c_str("key1");
918 let value = &mut 42 as *mut c_int as *mut c_void;
919
920 let result = hash_add_entry(table, key, value);
921 assert_eq!(result, 0);
922
923 let found = hash_lookup(table, key);
924 assert_eq!(found, value);
925 assert_eq!(*(found as *mut c_int), 42);
926
927 // Duplicate key should fail
928 let result2 = hash_add_entry(table, key, &mut 99 as *mut c_int as *mut c_void);
929 assert_eq!(result2, -1);
930
931 hash_free(table, None);
932 }
933 }
934
935 /// Looking up a missing key returns NULL.
936 ///
937 /// # Safety
938 ///
939 /// - `table` is non-NULL (asserted); the lookup key is a valid
940 /// NUL-terminated string valid for the call; `hash_free` releases
941 /// the table exactly once.
942 #[test]
943 fn test_hash_lookup_not_found() {
944 unsafe {
945 let table = hash_create(16);
946 let found = hash_lookup(table, c_str("nonexistent"));
947 assert!(found.is_null());
948 hash_free(table, None);
949 }
950 }
951
952 /// Remove an entry and check that a second removal fails.
953 ///
954 /// # Safety
955 ///
956 /// - `table` is non-NULL (asserted); `key` is a valid NUL-terminated
957 /// string valid for the calls; `value` stays alive until `hash_free`
958 /// releases the table exactly once.
959 #[test]
960 fn test_hash_remove_entry() {
961 unsafe {
962 let table = hash_create(16);
963 let key = c_str("remove_me");
964 let value = &mut 42 as *mut c_int as *mut c_void;
965
966 hash_add_entry(table, key, value);
967 assert_eq!(hash_size(table), 1);
968
969 let result = hash_remove_entry(table, key, None);
970 assert_eq!(result, 0);
971 assert_eq!(hash_size(table), 0);
972
973 // Removing again should fail
974 let result2 = hash_remove_entry(table, key, None);
975 assert_eq!(result2, -1);
976
977 hash_free(table, None);
978 }
979 }
980
981 /// Update an entry's payload and verify the new value is returned.
982 ///
983 /// # Safety
984 ///
985 /// - `table` is non-NULL (asserted); `key` is a valid NUL-terminated
986 /// string; `val1`/`val2` point to stack `c_int`s alive for the test;
987 /// `hash_free` releases the table exactly once.
988 #[test]
989 fn test_hash_update_entry() {
990 unsafe {
991 let table = hash_create(16);
992 let key = c_str("update_key");
993 let val1 = &mut 1 as *mut c_int as *mut c_void;
994 let val2 = &mut 2 as *mut c_int as *mut c_void;
995
996 hash_update_entry(table, key, val1, None);
997 assert_eq!(hash_lookup(table, key), val1);
998
999 hash_update_entry(table, key, val2, None);
1000 assert_eq!(hash_lookup(table, key), val2);
1001
1002 hash_free(table, None);
1003 }
1004 }
1005
1006 /// Look up an entry keyed by two strings.
1007 ///
1008 /// # Safety
1009 ///
1010 /// - `table` is non-NULL (asserted); both key strings are valid
1011 /// NUL-terminated strings valid for the calls; `value` is a stack
1012 /// `c_int` alive for the test; `hash_free` releases the table
1013 /// exactly once.
1014 #[test]
1015 fn test_hash_two_key_lookup() {
1016 unsafe {
1017 let table = hash_create(16);
1018 let key1 = c_str("ns");
1019 let key2 = c_str("local");
1020 let value = &mut 42 as *mut c_int as *mut c_void;
1021
1022 hash_add_entry2(table, key1, key2, value);
1023 let found = hash_lookup2(table, key1, key2);
1024 assert_eq!(found, value);
1025
1026 // Wrong second key should not find
1027 let not_found = hash_lookup2(table, key1, c_str("wrong"));
1028 assert!(not_found.is_null());
1029
1030 hash_free(table, None);
1031 }
1032 }
1033
1034 /// Look up an entry keyed by three strings.
1035 ///
1036 /// # Safety
1037 ///
1038 /// - `table` is non-NULL (asserted); all three key strings are valid
1039 /// NUL-terminated strings valid for the calls; `value` is a stack
1040 /// `c_int` alive for the test; `hash_free` releases the table
1041 /// exactly once.
1042 #[test]
1043 fn test_hash_three_key_lookup() {
1044 unsafe {
1045 let table = hash_create(16);
1046 let k1 = c_str("a");
1047 let k2 = c_str("b");
1048 let k3 = c_str("c");
1049 let value = &mut 42 as *mut c_int as *mut c_void;
1050
1051 hash_add_entry3(table, k1, k2, k3, value);
1052 let found = hash_lookup3(table, k1, k2, k3);
1053 assert_eq!(found, value);
1054
1055 hash_free(table, None);
1056 }
1057 }
1058
1059 /// Count entries as keys are added.
1060 ///
1061 /// # Safety
1062 ///
1063 /// - `table` is non-NULL (asserted); each `c_str` key is a valid
1064 /// NUL-terminated string valid until `hash_free` releases the table
1065 /// exactly once; NULL payloads are accepted.
1066 #[test]
1067 fn test_hash_size() {
1068 unsafe {
1069 let table = hash_create(16);
1070 assert_eq!(hash_size(table), 0);
1071
1072 hash_add_entry(table, c_str("a"), ptr::null_mut());
1073 assert_eq!(hash_size(table), 1);
1074
1075 hash_add_entry(table, c_str("b"), ptr::null_mut());
1076 assert_eq!(hash_size(table), 2);
1077
1078 hash_add_entry(table, c_str("c"), ptr::null_mut());
1079 assert_eq!(hash_size(table), 3);
1080
1081 hash_free(table, None);
1082 }
1083 }
1084
1085 /// NULL table/key arguments must be tolerated without crashing.
1086 ///
1087 /// # Safety
1088 ///
1089 /// - `hash_lookup`, `hash_size` and `hash_free` handle NULL table
1090 /// pointers as documented no-ops; no pointer is dereferenced.
1091 #[test]
1092 fn test_hash_null_handling() {
1093 unsafe {
1094 assert!(hash_lookup(ptr::null_mut(), ptr::null()).is_null());
1095 assert_eq!(hash_size(ptr::null_mut()), -1);
1096 hash_free(ptr::null_mut(), None); // Should not crash
1097 }
1098 }
1099}