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