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