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.
538unsafe fn hash_find_entry(
539 table: *mut HashTable,
540 name: *const xmlChar,
541 name2: *const xmlChar,
542 name3: *const xmlChar,
543) -> *mut c_void {
544 if table.is_null() {
545 return ptr::null_mut();
546 }
547
548 let t = unsafe { &*table };
549
550 let hash = combined_hash(name, name2, name3);
551 let bucket_idx = (hash as usize) % t.buckets.len();
552
553 for entry in &t.buckets[bucket_idx] {
554 if keys_equal(entry.key1, name)
555 && keys_equal(entry.key2, name2)
556 && keys_equal(entry.key3, name3)
557 {
558 return entry.payload;
559 }
560 }
561
562 ptr::null_mut()
563}
564
565/// Get the number of entries in the hash table.
566///
567/// # UPSTREAM-PARITY
568///
569/// ```c
570/// int xmlHashSize(xmlHashTablePtr table);
571/// ```
572pub fn hash_size(table: *mut HashTable) -> c_int {
573 if table.is_null() {
574 return -1;
575 }
576 unsafe { (*table).count as c_int }
577}
578
579/// Remove an entry by single key.
580///
581/// # UPSTREAM-PARITY
582///
583/// ```c
584/// int xmlHashRemoveEntry(xmlHashTablePtr table, const xmlChar *name,
585/// xmlHashDeallocator f);
586/// ```
587///
588/// Returns 0 on success, -1 if not found.
589///
590/// # SAFETY
591///
592/// - `table` must be a valid pointer to a HashTable, or NULL.
593/// - `name` must be a valid null-terminated string.
594/// - `f` may be NULL.
595pub unsafe fn hash_remove_entry(
596 table: *mut HashTable,
597 name: *const xmlChar,
598 f: Option<xmlHashDeallocator>,
599) -> c_int {
600 hash_remove_entry3(table, name, ptr::null(), ptr::null(), f)
601}
602
603/// Remove an entry by two keys.
604///
605/// # SAFETY
606///
607/// - `table` must be valid pointers (or NULL
608/// where the upstream C contract allows), obtained from the
609/// matching constructor/owner and not yet freed; the callee may
610/// take or keep ownership exactly as the C API specifies.
611///
612/// - `name`, `name2` must point to valid NUL-terminated
613/// strings (or NULL where the C contract allows) for the lifetime
614/// of the call.
615///
616/// - `f` must be a valid callback (or None);
617/// the callback is invoked with the documented context pointer and
618/// must itself uphold the same pointer invariants.
619///
620/// The caller must not race this call with concurrent mutation of the
621/// same objects from other threads (per-object state is not internally
622/// synchronized). Violating any of the above is undefined behavior.
623///
624/// Exercised by the C-API differential courts
625/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
626/// courts; those pass byte-for-byte against the upstream oracle.
627pub unsafe fn hash_remove_entry2(
628 table: *mut HashTable,
629 name: *const xmlChar,
630 name2: *const xmlChar,
631 f: Option<xmlHashDeallocator>,
632) -> c_int {
633 hash_remove_entry3(table, name, name2, ptr::null(), f)
634}
635
636/// Remove an entry by three keys.
637///
638/// # UPSTREAM-PARITY
639///
640/// ```c
641/// int xmlHashRemoveEntry3(xmlHashTablePtr table, const xmlChar *name,
642/// const xmlChar *name2, const xmlChar *name3,
643/// xmlHashDeallocator f);
644/// ```
645///
646/// Returns 0 on success, -1 if not found.
647///
648/// # SAFETY
649///
650/// - `table` must be a valid pointer to a HashTable, or NULL.
651/// - `name`, `name2`, `name3` must be valid null-terminated strings or NULL.
652/// - `f` may be NULL.
653pub unsafe fn hash_remove_entry3(
654 table: *mut HashTable,
655 name: *const xmlChar,
656 name2: *const xmlChar,
657 name3: *const xmlChar,
658 f: Option<xmlHashDeallocator>,
659) -> c_int {
660 if table.is_null() {
661 return -1;
662 }
663
664 let t = unsafe { &mut *table };
665
666 let hash = combined_hash(name, name2, name3);
667 let bucket_idx = (hash as usize) % t.buckets.len();
668
669 let bucket = &mut t.buckets[bucket_idx];
670 let pos = bucket.iter().position(|entry| {
671 keys_equal(entry.key1, name)
672 && keys_equal(entry.key2, name2)
673 && keys_equal(entry.key3, name3)
674 });
675
676 if let Some(idx) = pos {
677 let entry = bucket.remove(idx);
678 if let Some(dealloc) = f {
679 dealloc(entry.payload, entry.key1 as *mut u8);
680 }
681 if !t.dict_owned {
682 if !entry.key1.is_null() {
683 allocator::xmlFreeImpl(entry.key1 as *mut c_void);
684 }
685 if !entry.key2.is_null() {
686 allocator::xmlFreeImpl(entry.key2 as *mut c_void);
687 }
688 if !entry.key3.is_null() {
689 allocator::xmlFreeImpl(entry.key3 as *mut c_void);
690 }
691 }
692 t.count -= 1;
693 0
694 } else {
695 -1
696 }
697}
698
699/// Scan all entries with a scanner function.
700///
701/// # UPSTREAM-PARITY
702///
703/// ```c
704/// void xmlHashScan(xmlHashTablePtr table, xmlHashScanner f, void *data);
705/// ```
706///
707/// # SAFETY
708///
709/// - `table` must be a valid pointer to a HashTable, or NULL.
710/// - `f` must be a valid function pointer or NULL.
711/// - `data` may be NULL.
712pub unsafe fn hash_scan(table: *mut HashTable, f: Option<xmlHashScanner>, data: *mut c_void) {
713 if table.is_null() || f.is_none() {
714 return;
715 }
716 let f = f.unwrap();
717
718 let t = unsafe { &*table };
719 for bucket in &t.buckets {
720 for entry in bucket {
721 f(entry.payload, data, entry.key1 as *const xmlChar);
722 }
723 }
724}
725
726/// Scan all entries with a full scanner function (includes all keys).
727///
728/// # UPSTREAM-PARITY
729///
730/// ```c
731/// void xmlHashScanFull(xmlHashTablePtr table, xmlHashScannerFull f, void *data);
732/// ```
733///
734/// # SAFETY
735///
736/// - `table` must be a valid pointer to a HashTable, or NULL.
737/// - `f` must be a valid function pointer or NULL.
738/// - `data` may be NULL.
739pub unsafe fn hash_scan_full(
740 table: *mut HashTable,
741 f: Option<xmlHashScannerFull>,
742 data: *mut c_void,
743) {
744 if table.is_null() || f.is_none() {
745 return;
746 }
747 let f = f.unwrap();
748
749 let t = unsafe { &*table };
750 for bucket in &t.buckets {
751 for entry in bucket {
752 f(entry.payload, data, entry.key1, entry.key2, entry.key3);
753 }
754 }
755}
756
757/// Copy a hash table.
758///
759/// # UPSTREAM-PARITY
760///
761/// ```c
762/// xmlHashTablePtr xmlHashCopy(xmlHashTablePtr table, xmlHashCopier f);
763/// ```
764///
765/// Creates a new hash table with copied entries. The `f` function is called
766/// for each entry to create a copy of the payload.
767///
768/// Returns the new hash table, or NULL on failure.
769///
770/// # SAFETY
771///
772/// - `table` must be a valid pointer to a HashTable, or NULL.
773/// - `f` may be NULL (payloads are copied by pointer).
774pub unsafe fn hash_copy(table: *mut HashTable, f: Option<xmlHashCopier>) -> *mut HashTable {
775 if table.is_null() {
776 return ptr::null_mut();
777 }
778
779 let t = unsafe { &*table };
780 let new_table = hash_create(t.buckets.len() as c_int);
781 if new_table.is_null() {
782 return ptr::null_mut();
783 }
784
785 let new_t = unsafe { &mut *new_table };
786 new_t.deallocator = t.deallocator;
787 new_t.copier = t.copier;
788 new_t.dict_owned = t.dict_owned;
789
790 for bucket in &t.buckets {
791 for entry in bucket {
792 let payload = match f {
793 Some(copier) => copier(entry.payload, entry.key1 as *const xmlChar),
794 None => entry.payload,
795 };
796
797 let k1 = if entry.key1.is_null() {
798 ptr::null()
799 } else {
800 allocator::xmlMemStrdupImpl(entry.key1 as *const c_char) as *const xmlChar
801 };
802 let k2 = if entry.key2.is_null() {
803 ptr::null()
804 } else {
805 allocator::xmlMemStrdupImpl(entry.key2 as *const c_char) as *const xmlChar
806 };
807 let k3 = if entry.key3.is_null() {
808 ptr::null()
809 } else {
810 allocator::xmlMemStrdupImpl(entry.key3 as *const c_char) as *const xmlChar
811 };
812
813 let hash = combined_hash(k1, k2, k3);
814 let bucket_idx = (hash as usize) % new_t.buckets.len();
815
816 new_t.buckets[bucket_idx].push(HashEntry {
817 key1: k1,
818 key2: k2,
819 key3: k3,
820 payload,
821 });
822 new_t.count += 1;
823 }
824 }
825
826 new_table
827}
828
829// ═══════════════════════════════════════════════════════════════════════════════
830// Helper Functions
831// ═══════════════════════════════════════════════════════════════════════════════
832
833/// Compare two xmlChar strings for equality (null-safe).
834const fn keys_equal(a: *const xmlChar, b: *const xmlChar) -> bool {
835 if a.is_null() && b.is_null() {
836 return true;
837 }
838 if a.is_null() || b.is_null() {
839 return false;
840 }
841 // Compare byte by byte
842 let mut i = 0;
843 loop {
844 // SAFETY: Both are null-terminated strings.
845 let ca = unsafe { *a.add(i) };
846 let cb = unsafe { *b.add(i) };
847 if ca != cb {
848 return false;
849 }
850 if ca == 0 {
851 return true;
852 }
853 i += 1;
854 }
855}
856
857// ═══════════════════════════════════════════════════════════════════════════════
858// Tests
859// ═══════════════════════════════════════════════════════════════════════════════
860
861#[cfg(test)]
862mod tests {
863 use super::*;
864
865 fn c_str(s: &str) -> *const xmlChar {
866 let bytes = s.as_bytes();
867 let buf = unsafe { allocator::xmlMallocImpl(bytes.len() + 1) as *mut u8 };
868 if !buf.is_null() {
869 unsafe {
870 ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
871 *buf.add(bytes.len()) = 0;
872 }
873 }
874 buf as *const xmlChar
875 }
876
877 #[test]
878 fn test_hash_create_free() {
879 unsafe {
880 let table = hash_create(16);
881 assert!(!table.is_null());
882 hash_free(table, None);
883 }
884 }
885
886 #[test]
887 fn test_hash_add_lookup() {
888 unsafe {
889 let table = hash_create(16);
890 let key = c_str("key1");
891 let value = &mut 42 as *mut c_int as *mut c_void;
892
893 let result = hash_add_entry(table, key, value);
894 assert_eq!(result, 0);
895
896 let found = hash_lookup(table, key);
897 assert_eq!(found, value);
898 assert_eq!(*(found as *mut c_int), 42);
899
900 // Duplicate key should fail
901 let result2 = hash_add_entry(table, key, &mut 99 as *mut c_int as *mut c_void);
902 assert_eq!(result2, -1);
903
904 hash_free(table, None);
905 }
906 }
907
908 #[test]
909 fn test_hash_lookup_not_found() {
910 unsafe {
911 let table = hash_create(16);
912 let found = hash_lookup(table, c_str("nonexistent"));
913 assert!(found.is_null());
914 hash_free(table, None);
915 }
916 }
917
918 #[test]
919 fn test_hash_remove_entry() {
920 unsafe {
921 let table = hash_create(16);
922 let key = c_str("remove_me");
923 let value = &mut 42 as *mut c_int as *mut c_void;
924
925 hash_add_entry(table, key, value);
926 assert_eq!(hash_size(table), 1);
927
928 let result = hash_remove_entry(table, key, None);
929 assert_eq!(result, 0);
930 assert_eq!(hash_size(table), 0);
931
932 // Removing again should fail
933 let result2 = hash_remove_entry(table, key, None);
934 assert_eq!(result2, -1);
935
936 hash_free(table, None);
937 }
938 }
939
940 #[test]
941 fn test_hash_update_entry() {
942 unsafe {
943 let table = hash_create(16);
944 let key = c_str("update_key");
945 let val1 = &mut 1 as *mut c_int as *mut c_void;
946 let val2 = &mut 2 as *mut c_int as *mut c_void;
947
948 hash_update_entry(table, key, val1, None);
949 assert_eq!(hash_lookup(table, key), val1);
950
951 hash_update_entry(table, key, val2, None);
952 assert_eq!(hash_lookup(table, key), val2);
953
954 hash_free(table, None);
955 }
956 }
957
958 #[test]
959 fn test_hash_two_key_lookup() {
960 unsafe {
961 let table = hash_create(16);
962 let key1 = c_str("ns");
963 let key2 = c_str("local");
964 let value = &mut 42 as *mut c_int as *mut c_void;
965
966 hash_add_entry2(table, key1, key2, value);
967 let found = hash_lookup2(table, key1, key2);
968 assert_eq!(found, value);
969
970 // Wrong second key should not find
971 let not_found = hash_lookup2(table, key1, c_str("wrong"));
972 assert!(not_found.is_null());
973
974 hash_free(table, None);
975 }
976 }
977
978 #[test]
979 fn test_hash_three_key_lookup() {
980 unsafe {
981 let table = hash_create(16);
982 let k1 = c_str("a");
983 let k2 = c_str("b");
984 let k3 = c_str("c");
985 let value = &mut 42 as *mut c_int as *mut c_void;
986
987 hash_add_entry3(table, k1, k2, k3, value);
988 let found = hash_lookup3(table, k1, k2, k3);
989 assert_eq!(found, value);
990
991 hash_free(table, None);
992 }
993 }
994
995 #[test]
996 fn test_hash_size() {
997 unsafe {
998 let table = hash_create(16);
999 assert_eq!(hash_size(table), 0);
1000
1001 hash_add_entry(table, c_str("a"), ptr::null_mut());
1002 assert_eq!(hash_size(table), 1);
1003
1004 hash_add_entry(table, c_str("b"), ptr::null_mut());
1005 assert_eq!(hash_size(table), 2);
1006
1007 hash_add_entry(table, c_str("c"), ptr::null_mut());
1008 assert_eq!(hash_size(table), 3);
1009
1010 hash_free(table, None);
1011 }
1012 }
1013
1014 #[test]
1015 fn test_hash_null_handling() {
1016 unsafe {
1017 assert!(hash_lookup(ptr::null_mut(), ptr::null()).is_null());
1018 assert_eq!(hash_size(ptr::null_mut()), -1);
1019 hash_free(ptr::null_mut(), None); // Should not crash
1020 }
1021 }
1022}