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