azul_core/lib.rs
1//! Shared datatypes for azul-* crates
2//!
3//! `azul-core` provides the platform-independent core types used throughout
4//! the Azul toolkit. Key modules include [`dom`] for DOM construction,
5//! [`callbacks`] for event callback types, [`styled_dom`] for the CSSOM,
6//! and [`window`] for OS windowing abstractions.
7//!
8//! This crate depends on [`azul_css`] for CSS property definitions and is
9//! consumed by `azul-layout`, `azul-dll`, and the platform shell crates.
10//! It supports `no_std` environments via `#![cfg_attr(not(feature = "std"), no_std)]`.
11
12#![cfg_attr(not(feature = "std"), no_std)]
13// Lint policy: deny correctness/safety issues, warn on style
14#![deny(unused_must_use)]
15#![warn(clippy::all)]
16// Extreme-lint lockdown: all clippy groups plus opt-in rustc lints, enforced as
17// -D warnings on library code by the CI clippy job. Test builds are exempt via
18// cfg(not(test)) below since the set is high-noise and low-value on unit and
19// generated tests; clippy::all correctness still applies to test code.
20// (clippy::restriction wholesale + unused_results + box_pointers deliberately
21// omitted — contradictory / overwhelmingly noisy by design.)
22#![cfg_attr(not(test), warn(
23 clippy::pedantic,
24 clippy::nursery,
25 clippy::cargo,
26 // missing_docs, // TODO(docs): re-enable as a dedicated final docs pass; disabled
27 // // for now so the cleanup focuses on code-quality lints, not doc debt.
28 missing_debug_implementations,
29 missing_copy_implementations,
30 unreachable_pub,
31 unused_qualifications,
32 unused_lifetimes,
33 unused_import_braces,
34 unused_macro_rules,
35 unused_crate_dependencies,
36 meta_variable_misuse,
37 trivial_casts,
38 trivial_numeric_casts,
39 elided_lifetimes_in_paths,
40 single_use_lifetimes,
41 variant_size_differences,
42 non_ascii_idents,
43 unsafe_op_in_unsafe_fn,
44 let_underscore_drop,
45))]
46// `multiple_crate_versions` (implied by clippy::cargo) flags transitive
47// dependency-version dups that cannot be resolved in azul's own source:
48// `syn` 1.0.x ↔ 2.0.x (the proc-macro ecosystem is mid-migration; both are
49// pulled in transitively). Documented allow — re-audit when the dep tree aligns.
50#![allow(clippy::multiple_crate_versions)]
51#![allow(
52 clippy::non_canonical_partial_ord_impl,
53 clippy::legacy_numeric_constants,
54 clippy::should_implement_trait,
55 clippy::result_unit_err,
56 clippy::ptr_as_ptr,
57 clippy::too_many_arguments,
58 clippy::type_complexity,
59 unused_imports,
60 unused_variables,
61 unused_mut,
62 unused_parens,
63 dead_code,
64 unused_doc_comments,
65 unused_assignments, // compact_cache_builder incremental updates
66 unexpected_cfgs,
67 unpredictable_function_pointer_comparisons, // intentional in dom callback comparison
68 improper_ctypes_definitions, // xml component fns use Rust fn pointers internally
69 static_mut_refs, // TODO: migrate to OnceLock for Rust 2024
70)]
71
72// `extern crate` + `#[macro_use]` required for `no_std` support:
73// makes `core` and `alloc` macros available without `use` imports.
74#[macro_use]
75extern crate core;
76#[macro_use]
77extern crate alloc;
78#[macro_use]
79extern crate azul_css;
80
81/// Internal macros for `Vec`, `Option`, and callback boilerplate.
82///
83#[macro_use]
84pub mod macros;
85/// Debug logging system with category filtering.
86#[macro_use]
87pub mod debug;
88/// SQL database POD types — `DbValue` + `DbRows` (engine-agnostic). The
89/// `Db` handle + SQLite engine live in `azul_dll` behind `db-sqlite`.
90pub mod db;
91/// Unified `AZ_PROFILE` gate for memory and CPU profiling instrumentation.
92pub mod profile;
93/// `no_std`-friendly synchronization primitives.
94///
95/// In `std` builds these re-export the matching `std::sync` types. In
96/// `no_std` builds they provide minimal spinlock-backed equivalents
97/// implementing only the API surface azul-core relies on.
98pub mod sync {
99 #[cfg(feature = "std")]
100 pub use std::sync::OnceLock;
101
102 #[cfg(not(feature = "std"))]
103 pub use self::nostd::OnceLock;
104
105 #[cfg(not(feature = "std"))]
106 mod nostd {
107 use core::cell::UnsafeCell;
108 use core::sync::atomic::{AtomicU8, Ordering};
109
110 const UNINIT: u8 = 0;
111 const BUSY: u8 = 1;
112 const READY: u8 = 2;
113
114 /// Minimal `no_std` `OnceLock` mirroring the slice of the std API used
115 /// by azul-core (`new`, `get`, `get_or_init`).
116 pub struct OnceLock<T> {
117 state: AtomicU8,
118 value: UnsafeCell<Option<T>>,
119 }
120
121 unsafe impl<T: Send + Sync> Sync for OnceLock<T> {}
122 unsafe impl<T: Send> Send for OnceLock<T> {}
123
124 impl<T> OnceLock<T> {
125 pub const fn new() -> Self {
126 OnceLock {
127 state: AtomicU8::new(UNINIT),
128 value: UnsafeCell::new(None),
129 }
130 }
131
132 pub fn get(&self) -> Option<&T> {
133 if self.state.load(Ordering::Acquire) == READY {
134 unsafe { (*self.value.get()).as_ref() }
135 } else {
136 None
137 }
138 }
139
140 pub fn get_or_init<F: FnOnce() -> T>(&self, f: F) -> &T {
141 if let Some(v) = self.get() {
142 return v;
143 }
144 // Contend for the right to initialize.
145 while self
146 .state
147 .compare_exchange(UNINIT, BUSY, Ordering::Acquire, Ordering::Acquire)
148 .is_err()
149 {
150 if self.state.load(Ordering::Acquire) == READY {
151 return self.get().expect("OnceLock ready");
152 }
153 core::hint::spin_loop();
154 }
155 unsafe {
156 *self.value.get() = Some(f());
157 }
158 self.state.store(READY, Ordering::Release);
159 self.get().expect("OnceLock initialized")
160 }
161 }
162
163 impl<T> Default for OnceLock<T> {
164 fn default() -> Self {
165 Self::new()
166 }
167 }
168
169 impl<T: core::fmt::Debug> core::fmt::Debug for OnceLock<T> {
170 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
171 f.debug_tuple("OnceLock").field(&self.get()).finish()
172 }
173 }
174
175 impl<T: Clone> Clone for OnceLock<T> {
176 fn clone(&self) -> Self {
177 let new = OnceLock::new();
178 if let Some(v) = self.get() {
179 let _ = new.get_or_init(|| v.clone());
180 }
181 new
182 }
183 }
184
185 impl<T: PartialEq> PartialEq for OnceLock<T> {
186 fn eq(&self, other: &Self) -> bool {
187 self.get() == other.get()
188 }
189 }
190 }
191}
192
193/// `no_std`-friendly default hasher used for change-detection hashing.
194///
195/// In `std` builds this re-exports `std::hash::DefaultHasher` so behaviour
196/// is unchanged. In `no_std` builds it provides a small deterministic
197/// `FxHasher`-style hasher implementing `core::hash::Hasher`. The values are
198/// only required to be stable within a single process run (they back diffing /
199/// change detection), not to match `std`'s `SipHash` output.
200pub mod hash {
201 #[cfg(feature = "std")]
202 pub use std::hash::DefaultHasher;
203
204 #[cfg(not(feature = "std"))]
205 pub use self::nostd::DefaultHasher;
206
207 #[cfg(not(feature = "std"))]
208 mod nostd {
209 use core::hash::Hasher;
210
211 const SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95;
212 const ROTATE: u32 = 5;
213
214 /// FxHasher-style `no_std` hasher. Not DoS-resistant; used purely for
215 /// in-process change detection.
216 #[derive(Default)]
217 pub struct DefaultHasher {
218 hash: u64,
219 }
220
221 impl DefaultHasher {
222 pub fn new() -> Self {
223 DefaultHasher { hash: 0 }
224 }
225
226 #[inline]
227 fn add(&mut self, word: u64) {
228 self.hash = (self.hash.rotate_left(ROTATE) ^ word).wrapping_mul(SEED);
229 }
230 }
231
232 impl Hasher for DefaultHasher {
233 #[inline]
234 fn finish(&self) -> u64 {
235 self.hash
236 }
237
238 #[inline]
239 fn write(&mut self, bytes: &[u8]) {
240 for chunk in bytes.chunks(8) {
241 let mut buf = [0u8; 8];
242 buf[..chunk.len()].copy_from_slice(chunk);
243 self.add(u64::from_le_bytes(buf));
244 }
245 }
246
247 #[inline]
248 fn write_u8(&mut self, i: u8) {
249 self.add(i as u64);
250 }
251 #[inline]
252 fn write_u64(&mut self, i: u64) {
253 self.add(i);
254 }
255 #[inline]
256 fn write_usize(&mut self, i: usize) {
257 self.add(i as u64);
258 }
259 }
260 }
261}
262/// Callback types: layout, event, timer, thread, and focus handling.
263#[macro_use]
264pub mod callbacks;
265/// Host-language callback invoker registry.
266///
267/// The C-ABI surface managed-FFI bindings (Lua, Ruby, …) use to register one
268/// per-kind invoker + a single shared releaser, so callbacks can be created via
269/// `_createFromHostHandle` without the host having to generate trampolines for
270/// struct-by-value signatures their FFI library can't handle.
271#[macro_use]
272pub mod host_invoker;
273/// Accessibility types for screen-reader integration (AccessKit).
274pub mod a11y;
275/// Audio POD types — `AudioConfig` (stream format) + `AudioFrame` (interleaved
276/// f32 samples).
277///
278/// The unit captured from the mic, played back, and (P8) shared
279/// over UDP. Backend (rodio / cpal / AVAudioEngine / AAudio) lives dll-side.
280pub mod audio;
281/// Biometric-auth POD types — `BiometricKind` + `BiometricResult` + `BiometricPrompt`.
282///
283/// Stateful manager lives in `azul_layout::managers::biometric`.
284pub mod biometric;
285/// Camera-capture POD types — `CaptureStreamId` + `CameraConfig` +
286/// `CameraFacing` + `StreamState` + … .
287///
288/// The stateful `CameraStream` /
289/// `CameraManager` (which own the shared `ImageRef` texture) live in
290/// `azul_layout::managers::camera`.
291pub mod camera;
292/// Converts `CssPropertyCache` into compact three-tier numeric cache.
293pub mod compact;
294/// Linear-time DOM diffing for incremental updates.
295pub mod diff;
296/// DOM construction: `Dom`, `NodeData`, `NodeType`, and the CSS-in-Rust API.
297pub mod dom;
298/// Drag context for text selection, scrollbar, node, and window drags.
299pub mod drag;
300/// Event filtering: mouse, keyboard, window, and synthetic events.
301pub mod events;
302/// Gamepad POD types — `GamepadId` + `GamepadButton` + `GamepadAxis` +
303/// `GamepadState`.
304///
305/// Stateful manager lives in `azul_layout::managers::gamepad`.
306pub mod gamepad;
307/// Geolocation POD types — `LocationFix` + `GeolocationProbeConfig`.
308///
309/// Stateful manager lives in `azul_layout::managers::geolocation`.
310pub mod geolocation;
311/// Logical and physical coordinate types (`LogicalSize`, `PhysicalPosition`, etc.).
312pub mod geom;
313/// OpenGL context wrappers, shader compilation, and texture cache.
314///
315pub mod gl;
316/// FXAA (Fast Approximate Anti-Aliasing) shader.
317pub mod gl_fxaa;
318/// OpenGL constants (GL 1.1 through GL 4.x).
319pub mod glconst;
320/// GPU value cache for CSS transforms and opacity.
321pub mod gpu;
322/// Hit-test results (which DOM nodes are under the cursor) + the type-safe
323/// hit-test tag system for compositor integration (merged from `hit_test_tag`).
324///
325pub mod hit_test;
326/// Icon provider system for loading icons from fonts, images, or zip packs.
327pub mod icon;
328/// Arena-based node tree storage and hierarchy management.
329pub mod id;
330/// JSON value types for the C API (no serde dependency).
331pub mod json;
332/// System-keyring POD types — `KeyringRequest` + `KeyringResult`.
333///
334/// Stateful manager lives in `azul_layout::managers::keyring`.
335pub mod keyring;
336/// Runtime log filtering: per-level and per-category atomics.
337///
338/// Parsed from `AZ_LOG` but changeable while the process runs. Logging is gated HERE and
339/// never by a cargo feature — see the module docs for the 2026-08-07 incident
340/// that made a compile-time gate delete the one diagnosis that was needed.
341pub mod log_filter;
342/// Menu system: context menus, dropdown menus, and menu bars.
343pub mod menu;
344/// Paged-media primitives: the `FragmentationContext` (continuous vs. paged) and
345/// `PageMargins`. The pagination/slicing logic lives in `azul_layout::solver3`.
346pub mod paged;
347/// SVG `d=""` path data parser.
348pub mod path_parser;
349/// CSS property cache for efficient per-node style resolution.
350pub mod prop_cache;
351/// Type-erased, ref-counted smart pointer with runtime borrow checking.
352pub mod refany;
353/// Resource management: font/image loading, caching, and garbage collection.
354pub mod resources;
355/// Screen-capture POD types — `ScreenCaptureSource` + `ScreenCaptureConfig`.
356///
357/// Symmetric to the camera surface (a "dumb widget" in
358/// `azul_layout::widgets::screencap`); reuses `camera`'s capture status types.
359pub mod screencap;
360/// Text selection and cursor positioning for inline content.
361pub mod selection;
362/// Motion-sensor POD types — `SensorKind` + `SensorReading`.
363///
364/// Stateful manager lives in `azul_layout::managers::sensors`.
365pub mod sensors;
366/// CSS cascade: selector matching, specificity, and property inheritance.
367pub mod style;
368/// `StyledDom` — the result of applying CSS to a DOM tree (the CSSOM).
369pub mod styled_dom;
370/// SVG rendering, path tessellation, and geometric operations.
371pub mod svg;
372/// Timer, thread, and async task management.
373pub mod task;
374/// 3D transform matrix computation for CSS transforms.
375pub mod transform;
376/// Built-in user-agent default stylesheet.
377pub mod ua_css;
378/// Default font/text constants and small geometry helpers for layout.
379pub mod ui_solver;
380/// URL POD type (`Url`/`UrlParseError`); parsing gated behind the `url` feature.
381pub mod url;
382/// Video-playback POD types — `VideoConfig` (source URL + autoplay/loop).
383///
384/// Same "dumb widget" architecture (`azul_layout::widgets::video`); decoded
385/// via vk-video into the shared GL texture.
386pub mod video;
387/// Window configuration, input state, and platform-specific options.
388pub mod window;
389/// XML and XHTML parsing for declarative UI definitions.
390pub mod xml;
391
392/// Ordered map alias used throughout `azul-core`.
393///
394/// This is backed by `BTreeMap` (not a hash map) because the `core` crate
395/// supports `no_std`, where `HashMap` is unavailable. The webrender crates
396/// define their own `FastHashMap` using `HashMap` + `FxHasher`.
397pub type OrderedMap<T, U> = alloc::collections::BTreeMap<T, U>;
398pub type FastBTreeSet<T> = alloc::collections::BTreeSet<T>;
399
400#[cfg(test)]
401#[allow(clippy::pedantic, clippy::nursery)]
402mod autotest_generated {
403 use alloc::{boxed::Box, string::String, vec::Vec};
404 use core::{
405 cell::Cell,
406 hash::{Hash, Hasher},
407 };
408
409 use super::{hash::DefaultHasher, sync::OnceLock, FastBTreeSet, OrderedMap};
410
411 // NOTE: `sync::OnceLock` and `hash::DefaultHasher` are *aliases*: with the
412 // (default) `std` feature they re-export `std::sync::OnceLock` /
413 // `std::hash::DefaultHasher`; without it they resolve to the hand-written
414 // `no_std` shims in this file. Tests below are split accordingly:
415 // * un-gated -> the API contract BOTH impls must satisfy,
416 // * cfg-gated -> behaviour that is specific to one impl.
417 // `DefaultHasher::add` is private to the private `hash::nostd` module, so it
418 // is not nameable from here; `write_u64` forwards to it 1:1 and is used as
419 // the proxy for the numeric/overflow cases.
420
421 // ---------------------------------------------------------------
422 // OnceLock — constructor / getter invariants
423 // ---------------------------------------------------------------
424
425 #[test]
426 fn oncelock_new_is_empty() {
427 let cell: OnceLock<u32> = OnceLock::new();
428 assert!(cell.get().is_none());
429 // getter must stay pure: repeated reads never initialize
430 assert!(cell.get().is_none());
431 }
432
433 #[test]
434 fn oncelock_new_is_usable_in_const_context() {
435 static CELL: OnceLock<u64> = OnceLock::new();
436 assert!(CELL.get().is_none());
437 assert_eq!(*CELL.get_or_init(|| u64::MAX), u64::MAX);
438 assert_eq!(CELL.get().copied(), Some(u64::MAX));
439 }
440
441 #[test]
442 fn oncelock_default_matches_new() {
443 let cell: OnceLock<Vec<u8>> = OnceLock::default();
444 assert!(cell.get().is_none());
445 }
446
447 #[test]
448 fn oncelock_get_or_init_runs_closure_exactly_once() {
449 let calls = Cell::new(0usize);
450 let cell: OnceLock<u32> = OnceLock::new();
451
452 assert_eq!(*cell.get_or_init(|| { calls.set(calls.get() + 1); 7 }), 7);
453 // The second/third call must return the FIRST value and never re-run `f`.
454 assert_eq!(*cell.get_or_init(|| { calls.set(calls.get() + 1); 9 }), 7);
455 assert_eq!(*cell.get_or_init(|| { calls.set(calls.get() + 1); 11 }), 7);
456 assert_eq!(calls.get(), 1);
457 assert_eq!(cell.get().copied(), Some(7));
458 }
459
460 #[test]
461 fn oncelock_get_and_get_or_init_alias_the_same_storage() {
462 let cell: OnceLock<u32> = OnceLock::new();
463 let a: *const u32 = cell.get_or_init(|| 1);
464 let b: *const u32 = cell.get().expect("initialized");
465 let c: *const u32 = cell.get_or_init(|| 2);
466 // The value must never be moved/duplicated by a second init attempt.
467 assert_eq!(a, b);
468 assert_eq!(a, c);
469 }
470
471 #[test]
472 fn oncelock_holds_zero_sized_type() {
473 // ZST: `Option<()>` has no payload bits, so a naive impl can confuse
474 // "initialized" with "None".
475 let cell: OnceLock<()> = OnceLock::new();
476 assert!(cell.get().is_none());
477 cell.get_or_init(|| ());
478 assert!(cell.get().is_some());
479 }
480
481 #[test]
482 fn oncelock_holds_large_payload() {
483 let cell: OnceLock<Box<[u8]>> = OnceLock::new();
484 let v = cell.get_or_init(|| alloc::vec![0xABu8; 1 << 20].into_boxed_slice());
485 assert_eq!(v.len(), 1 << 20);
486 assert!(v.iter().all(|b| *b == 0xAB));
487 assert_eq!(cell.get().map(|b| b.len()), Some(1 << 20));
488 }
489
490 #[test]
491 fn oncelock_holds_nan_without_eq_confusion() {
492 let cell: OnceLock<f64> = OnceLock::new();
493 // `NaN != NaN`, so initialization must be tracked by state, not by
494 // comparing the payload against a sentinel.
495 assert!(cell.get_or_init(|| f64::NAN).is_nan());
496 assert!(cell.get().is_some_and(|f| f.is_nan()));
497 // A second init must not overwrite the stored NaN with 1.0.
498 assert!(cell.get_or_init(|| 1.0).is_nan());
499 }
500
501 #[test]
502 fn oncelock_clone_copies_state_not_aliases_it() {
503 let cell: OnceLock<String> = OnceLock::new();
504
505 let empty = cell.clone();
506 assert!(empty.get().is_none());
507 // Initializing the source must not retro-fill an earlier clone.
508 cell.get_or_init(|| String::from("azul"));
509 assert!(empty.get().is_none());
510
511 let full = cell.clone();
512 assert_eq!(full.get().map(String::as_str), Some("azul"));
513 // Distinct storage: the clone must own its own allocation.
514 assert_ne!(
515 cell.get().expect("init") as *const String,
516 full.get().expect("init") as *const String
517 );
518 }
519
520 #[test]
521 fn oncelock_eq_compares_contents() {
522 let a: OnceLock<u32> = OnceLock::new();
523 let b: OnceLock<u32> = OnceLock::new();
524 assert_eq!(a, b); // both empty
525
526 a.get_or_init(|| 5);
527 assert_ne!(a, b); // Some(5) vs None
528
529 b.get_or_init(|| 5);
530 assert_eq!(a, b);
531
532 let c: OnceLock<u32> = OnceLock::new();
533 c.get_or_init(|| 6);
534 assert_ne!(a, c);
535 }
536
537 #[cfg(feature = "std")]
538 #[test]
539 fn oncelock_concurrent_get_or_init_initializes_exactly_once() {
540 use std::sync::{
541 atomic::{AtomicUsize, Ordering},
542 Barrier,
543 };
544
545 const THREADS: usize = 8;
546
547 let cell: OnceLock<usize> = OnceLock::new();
548 let inits = AtomicUsize::new(0);
549 let gate = Barrier::new(THREADS);
550
551 std::thread::scope(|s| {
552 for id in 0..THREADS {
553 let (cell, inits, gate) = (&cell, &inits, &gate);
554 let _ = s.spawn(move || {
555 gate.wait(); // maximize contention on the CAS
556 let v = *cell.get_or_init(|| {
557 inits.fetch_add(1, Ordering::SeqCst);
558 id
559 });
560 // Every racer must observe the same winner.
561 assert_eq!(v, *cell.get().expect("initialized after get_or_init"));
562 v
563 });
564 }
565 });
566
567 assert_eq!(inits.load(Ordering::SeqCst), 1);
568 let winner = cell.get().copied().expect("initialized");
569 assert!(winner < THREADS);
570 }
571
572 // The `std` OnceLock documents that a panicking `f` leaves the cell
573 // *uninitialized* (and re-initializable) rather than poisoned.
574 //
575 // The `no_std` shim in this file does NOT hold this property: it leaves
576 // `state == BUSY`, so any later `get_or_init` spins forever. This test is
577 // therefore std-gated on purpose — running it under `no_std` would hang the
578 // test binary instead of failing it.
579 #[cfg(feature = "std")]
580 #[test]
581 fn oncelock_panicking_initializer_leaves_cell_reusable() {
582 let cell: OnceLock<u32> = OnceLock::new();
583
584 let prev = std::panic::take_hook();
585 std::panic::set_hook(Box::new(|_| {})); // keep the expected panic quiet
586 let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
587 cell.get_or_init(|| panic!("initializer blew up"));
588 }));
589 std::panic::set_hook(prev);
590
591 assert!(caught.is_err(), "the panic must propagate to the caller");
592 assert!(cell.get().is_none(), "cell must remain uninitialized");
593 assert_eq!(*cell.get_or_init(|| 42), 42, "cell must still be usable");
594 }
595
596 // ---------------------------------------------------------------
597 // DefaultHasher — construction / determinism
598 // ---------------------------------------------------------------
599
600 fn hash_bytes(bytes: &[u8]) -> u64 {
601 let mut h = DefaultHasher::new();
602 h.write(bytes);
603 h.finish()
604 }
605
606 fn hash_u64(word: u64) -> u64 {
607 let mut h = DefaultHasher::new();
608 h.write_u64(word);
609 h.finish()
610 }
611
612 #[test]
613 fn hasher_new_and_default_agree_and_are_deterministic() {
614 assert_eq!(DefaultHasher::new().finish(), DefaultHasher::new().finish());
615 assert_eq!(
616 DefaultHasher::new().finish(),
617 DefaultHasher::default().finish()
618 );
619 }
620
621 #[test]
622 fn hasher_is_deterministic_within_a_run() {
623 assert_eq!(hash_bytes(b"azul"), hash_bytes(b"azul"));
624 assert_eq!(hash_u64(0xDEAD_BEEF_CAFE_F00D), hash_u64(0xDEAD_BEEF_CAFE_F00D));
625 }
626
627 #[test]
628 fn hasher_distinguishes_different_inputs() {
629 assert_ne!(hash_bytes(b"a"), hash_bytes(b"b"));
630 assert_ne!(hash_u64(0), hash_u64(1));
631 }
632
633 #[test]
634 fn hasher_is_order_sensitive() {
635 let mut a = DefaultHasher::new();
636 a.write_u64(1);
637 a.write_u64(2);
638
639 let mut b = DefaultHasher::new();
640 b.write_u64(2);
641 b.write_u64(1);
642
643 assert_ne!(a.finish(), b.finish());
644 }
645
646 #[test]
647 fn hasher_finish_does_not_consume_state() {
648 let mut h = DefaultHasher::new();
649 h.write_u64(7);
650 let first = h.finish();
651 // `finish` must be a pure read: calling it twice returns the same value.
652 assert_eq!(first, h.finish());
653 // ...and further writes must keep mutating the same running state.
654 h.write_u64(7);
655 assert_ne!(first, h.finish());
656 }
657
658 // ---------------------------------------------------------------
659 // DefaultHasher — numeric limits / overflow (exercises the private `add`
660 // via its 1:1 forwarders `write_u64` / `write_usize` / `write_u8`)
661 // ---------------------------------------------------------------
662
663 #[test]
664 fn hasher_handles_integer_limits_without_panicking() {
665 // `add` does a `wrapping_mul`; a debug build must not overflow-panic.
666 for word in [
667 0u64,
668 1,
669 u64::MAX,
670 u64::MAX - 1,
671 i64::MIN as u64, // 0x8000_0000_0000_0000 — "negative" bit pattern
672 i64::MAX as u64,
673 -1i64 as u64,
674 1 << 63,
675 usize::MAX as u64,
676 ] {
677 let h = hash_u64(word);
678 // deterministic + no panic; value itself is impl-defined
679 assert_eq!(h, hash_u64(word));
680 }
681
682 let mut h = DefaultHasher::new();
683 h.write_usize(usize::MAX);
684 h.write_usize(0);
685 h.write_u8(u8::MAX);
686 h.write_u8(0);
687 let _ = h.finish();
688 }
689
690 #[test]
691 fn hasher_repeated_max_words_do_not_overflow_panic() {
692 // Hammer the wrapping rotate/xor/multiply chain: every iteration
693 // overflows u64. Must wrap, never panic (even in a debug profile).
694 let mut h = DefaultHasher::new();
695 for _ in 0..10_000 {
696 h.write_u64(u64::MAX);
697 }
698 let a = h.finish();
699
700 let mut h2 = DefaultHasher::new();
701 for _ in 0..10_000 {
702 h2.write_u64(u64::MAX);
703 }
704 assert_eq!(a, h2.finish(), "overflowing chain must stay deterministic");
705 }
706
707 #[test]
708 fn hasher_zero_words_are_deterministic() {
709 let mut h = DefaultHasher::new();
710 for _ in 0..1_000 {
711 h.write_u64(0);
712 }
713 let a = h.finish();
714
715 let mut h2 = DefaultHasher::new();
716 for _ in 0..1_000 {
717 h2.write_u64(0);
718 }
719 assert_eq!(a, h2.finish());
720 }
721
722 // ---------------------------------------------------------------
723 // DefaultHasher — `write` chunking / boundaries / unicode
724 // ---------------------------------------------------------------
725
726 #[test]
727 fn hasher_write_empty_slice_does_not_panic() {
728 let mut h = DefaultHasher::new();
729 h.write(&[]);
730 h.write(&[]);
731 let a = h.finish();
732
733 let mut h2 = DefaultHasher::new();
734 h2.write(&[]);
735 h2.write(&[]);
736 assert_eq!(a, h2.finish());
737 }
738
739 #[test]
740 fn hasher_write_covers_every_chunk_boundary() {
741 // The `no_std` impl walks `chunks(8)` and zero-pads the tail; lengths
742 // 0..=24 cover empty, short, exact-multiple and ragged-tail cases.
743 let data: Vec<u8> = (0u8..=24).collect();
744 for len in 0..=24usize {
745 let slice = &data[..len];
746 assert_eq!(hash_bytes(slice), hash_bytes(slice), "len {len}");
747 }
748 // A short slice must not collide with the same slice explicitly padded
749 // out past the next 8-byte chunk boundary.
750 assert_ne!(hash_bytes(&[1u8]), hash_bytes(&[1u8, 0, 0, 0, 0, 0, 0, 0, 0]));
751 }
752
753 // `write` must not swallow a trailing zero byte: `[1]` and `[1, 0]` are
754 // different inputs and must hash differently.
755 //
756 // The `no_std` shim FAILS this: it zero-pads the final `chunks(8)` chunk
757 // and mixes in no length, so `[1]` and `[1, 0]` both become the word
758 // `0x0000_0000_0000_0001` — a guaranteed collision for every pair of byte
759 // strings differing only in trailing zeros. Kept as a live assertion for
760 // the (default) `std` build and `ignore`d rather than weakened under
761 // `no_std`; see the autotest report.
762 #[cfg_attr(
763 not(feature = "std"),
764 ignore = "no_std DefaultHasher zero-pads without length mixing: hash([1]) == hash([1, 0])"
765 )]
766 #[test]
767 fn hasher_write_does_not_swallow_trailing_zero_bytes() {
768 assert_ne!(hash_bytes(&[1u8]), hash_bytes(&[1u8, 0]));
769 assert_ne!(hash_bytes(b"az"), hash_bytes(b"az\0"));
770 assert_ne!(hash_bytes(&[]), hash_bytes(&[0u8]));
771 }
772
773 #[test]
774 fn hasher_handles_huge_input() {
775 let big: Vec<u8> = (0..(1 << 16)).map(|i| (i % 251) as u8).collect();
776 let a = hash_bytes(&big);
777 assert_eq!(a, hash_bytes(&big));
778
779 // A single flipped byte in the middle must change the digest.
780 let mut flipped = big.clone();
781 flipped[1 << 15] ^= 0xFF;
782 assert_ne!(a, hash_bytes(&flipped));
783 }
784
785 #[test]
786 fn hasher_handles_unicode_and_nul_bytes() {
787 for s in [
788 "",
789 "\u{0}",
790 "ascii",
791 "héllo wörld",
792 "日本語テキスト",
793 "🦀🔥👨👩👧👦",
794 "a\u{0}b",
795 "\u{FEFF}bom",
796 "\u{10FFFF}",
797 ] {
798 let mut h = DefaultHasher::new();
799 s.hash(&mut h);
800 let a = h.finish();
801
802 let mut h2 = DefaultHasher::new();
803 s.hash(&mut h2);
804 assert_eq!(a, h2.finish(), "unstable hash for {s:?}");
805 }
806
807 // Interior NUL must not truncate the input (C-string style bug).
808 let mut a = DefaultHasher::new();
809 "a\u{0}b".hash(&mut a);
810 let mut b = DefaultHasher::new();
811 "a".hash(&mut b);
812 assert_ne!(a.finish(), b.finish());
813 }
814
815 #[test]
816 fn hasher_respects_eq_hash_contract_for_std_types() {
817 fn digest<T: Hash>(t: &T) -> u64 {
818 let mut h = DefaultHasher::new();
819 t.hash(&mut h);
820 h.finish()
821 }
822
823 // Equal values must hash equal.
824 assert_eq!(digest(&String::from("x")), digest(&String::from("x")));
825 assert_eq!(digest(&alloc::vec![1u64, 2, 3]), digest(&alloc::vec![1u64, 2, 3]));
826 assert_eq!(digest(&(1u8, "a")), digest(&(1u8, "a")));
827
828 // Length must be part of the digest: [1,2] vs [1,2,0] must differ...
829 assert_ne!(digest(&alloc::vec![1u8, 2]), digest(&alloc::vec![1u8, 2, 0]));
830 // ...and prefix-concatenation must not collide ("ab" vs "a"+"b" fields).
831 assert_ne!(digest(&("ab", "")), digest(&("a", "b")));
832 }
833
834 // ---------------------------------------------------------------
835 // `no_std` shim internals: exact FxHasher-style formula of the private
836 // `add`, reached through its 1:1 forwarder `write_u64`.
837 // ---------------------------------------------------------------
838
839 #[cfg(not(feature = "std"))]
840 #[test]
841 fn nostd_hasher_add_matches_documented_formula() {
842 const SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95;
843 const ROTATE: u32 = 5;
844
845 fn expect(words: &[u64]) -> u64 {
846 words
847 .iter()
848 .fold(0u64, |h, w| (h.rotate_left(ROTATE) ^ w).wrapping_mul(SEED))
849 }
850
851 assert_eq!(DefaultHasher::new().finish(), 0, "fresh state must be 0");
852
853 for words in [
854 &[0u64][..],
855 &[1][..],
856 &[u64::MAX][..],
857 &[i64::MIN as u64][..],
858 &[u64::MAX, u64::MAX, u64::MAX][..],
859 &[0, u64::MAX, 0, 1 << 63][..],
860 ] {
861 let mut h = DefaultHasher::new();
862 for w in words {
863 h.write_u64(*w);
864 }
865 assert_eq!(h.finish(), expect(words), "formula drift for {words:?}");
866 }
867 }
868
869 #[cfg(not(feature = "std"))]
870 #[test]
871 fn nostd_hasher_zero_is_an_absorbing_state() {
872 // Documented FxHasher weakness, asserted so it stays *intentional*:
873 // from a zero state, hashing zero words keeps the state at zero
874 // ((0.rotate_left(5) ^ 0) * SEED == 0).
875 let mut h = DefaultHasher::new();
876 for _ in 0..64 {
877 h.write_u64(0);
878 }
879 assert_eq!(h.finish(), 0);
880
881 // Leading zero words are therefore invisible: hash([0, x]) == hash([x]).
882 let mut a = DefaultHasher::new();
883 a.write_u64(0);
884 a.write_u64(0xABCD);
885 let mut b = DefaultHasher::new();
886 b.write_u64(0xABCD);
887 assert_eq!(a.finish(), b.finish());
888 }
889
890 #[cfg(not(feature = "std"))]
891 #[test]
892 fn nostd_hasher_write_empty_slice_is_a_noop() {
893 // `chunks(8)` over an empty slice yields nothing, so the state is untouched.
894 let mut h = DefaultHasher::new();
895 h.write(b"seed");
896 let before = h.finish();
897 h.write(&[]);
898 assert_eq!(h.finish(), before);
899 }
900
901 // ---------------------------------------------------------------
902 // Public type aliases — ordering / dedup invariants
903 // ---------------------------------------------------------------
904
905 #[test]
906 fn ordered_map_iterates_in_key_order() {
907 let mut m: OrderedMap<i64, &str> = OrderedMap::new();
908 for (k, v) in [(i64::MAX, "max"), (0, "zero"), (i64::MIN, "min"), (-1, "neg")] {
909 let _ = m.insert(k, v);
910 }
911 let keys: Vec<i64> = m.keys().copied().collect();
912 assert_eq!(keys, alloc::vec![i64::MIN, -1, 0, i64::MAX]);
913
914 // Re-insert must overwrite, not duplicate.
915 assert_eq!(m.insert(0, "zero2"), Some("zero"));
916 assert_eq!(m.len(), 4);
917 assert_eq!(m.get(&0).copied(), Some("zero2"));
918 }
919
920 #[test]
921 fn fast_btree_set_dedups_and_orders() {
922 let mut s: FastBTreeSet<u32> = FastBTreeSet::new();
923 assert!(s.insert(u32::MAX));
924 assert!(s.insert(0));
925 assert!(!s.insert(0), "duplicate insert must report false");
926 assert!(s.insert(1));
927
928 assert_eq!(s.len(), 3);
929 assert_eq!(s.iter().copied().collect::<Vec<u32>>(), alloc::vec![0, 1, u32::MAX]);
930 assert!(s.contains(&u32::MAX));
931 assert!(!s.contains(&2));
932 }
933}