Skip to main content

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}