Skip to main content

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}