From b30e82bbee1439d81fe3333d73bd3f432abcdda2 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Tue, 7 Jul 2026 10:45:12 -0400 Subject: [PATCH] perf: add optional `fast_hash` feature for a faster (non-DoS-resistant) hasher --- Cargo.toml | 3 +++ src/lib.rs | 15 +++++++++++++++ src/parse_to_value.rs | 4 ++-- src/value.rs | 14 +++++++++----- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c889b7e..8433c35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,15 @@ all-features = true [dependencies] indexmap = { version = "2.2.6", optional = true } +rustc-hash = { version = "2", optional = true } serde = { version = "1.0", optional = true, features = ["derive"] } serde_json = { version = "1.0", optional = true } unicode-width = { version = "0.2.0", optional = true } [features] cst = [] +# use a faster (non-DoS-resistant) hasher for parsed objects +fast_hash = ["dep:rustc-hash"] preserve_order = ["indexmap"] serde = ["dep:serde"] serde_json = ["dep:serde_json"] diff --git a/src/lib.rs b/src/lib.rs index 1f3bf39..aa5e026 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -126,6 +126,21 @@ //! which will pull in and use the [unicode-width](https://crates.io/crates/unicode-width) dependency internally. //! Otherwise it will use the character count, which isn't as accurate of a number, but will probably be good enough //! in most cases. +//! +//! ## Faster hashing +//! +//! Parsed objects are stored in a hash map that uses the standard library's default (DoS-resistant but slower) +//! hasher. When parsing trusted input, enable the `fast_hash` cargo feature to instead use the faster +//! [rustc-hash](https://crates.io/crates/rustc-hash) hasher, which can noticeably speed up parsing of +//! object-heavy documents: +//! +//! ```toml +//! # in Cargo.toml +//! jsonc-parser = { version = "...", features = ["fast_hash"] } +//! ``` +//! +//! This hasher is not resistant to hash-collision denial-of-service attacks, so avoid it when parsing +//! untrusted input. It composes with the `preserve_order` feature. #![deny(clippy::print_stderr)] #![deny(clippy::print_stdout)] diff --git a/src/parse_to_value.rs b/src/parse_to_value.rs index 3015714..5e9f3f9 100644 --- a/src/parse_to_value.rs +++ b/src/parse_to_value.rs @@ -50,7 +50,7 @@ fn parse_value<'a>(parser: &mut JsoncParser<'a>, token: Token<'a>) -> Result(parser: &mut JsoncParser<'a>) -> Result, ParseError> { parser.enter_container()?; - let mut props = Map::new(); + let mut props = Map::default(); let mut first = true; loop { @@ -131,7 +131,7 @@ mod tests { .unwrap() .unwrap(); - let mut object_map = Map::new(); + let mut object_map = Map::default(); object_map.insert(Cow::Borrowed("a"), JsonValue::Null); object_map.insert( Cow::Borrowed("b"), diff --git a/src/value.rs b/src/value.rs index 5499ff3..d4aa458 100644 --- a/src/value.rs +++ b/src/value.rs @@ -12,10 +12,14 @@ pub enum JsonValue<'a> { Null, } -#[cfg(not(feature = "preserve_order"))] +#[cfg(all(not(feature = "preserve_order"), not(feature = "fast_hash")))] pub type Map = std::collections::HashMap; -#[cfg(feature = "preserve_order")] +#[cfg(all(not(feature = "preserve_order"), feature = "fast_hash"))] +pub type Map = std::collections::HashMap; +#[cfg(all(feature = "preserve_order", not(feature = "fast_hash")))] pub type Map = indexmap::IndexMap; +#[cfg(all(feature = "preserve_order", feature = "fast_hash"))] +pub type Map = indexmap::IndexMap; /// A JSON object. #[derive(Clone, PartialEq, Debug)] @@ -82,7 +86,7 @@ impl<'a> JsonObject<'a> { /// Creates a new JsonObject with the specified capacity. pub fn with_capacity(capacity: usize) -> JsonObject<'a> { - JsonObject(Map::with_capacity(capacity)) + JsonObject(Map::with_capacity_and_hasher(capacity, Default::default())) } /// Drops the object returning the inner map. @@ -230,7 +234,7 @@ mod test { #[test] fn it_should_take() { - let mut inner = Map::new(); + let mut inner = Map::default(); inner.insert(Cow::Borrowed("prop"), JsonValue::String(Cow::Borrowed("asdf"))); inner.insert(Cow::Borrowed("other"), JsonValue::String(Cow::Borrowed("text"))); let mut obj = JsonObject::new(inner); @@ -250,7 +254,7 @@ mod test { #[test] fn it_should_get() { - let mut inner = Map::new(); + let mut inner = Map::default(); inner.insert(Cow::Borrowed("prop"), JsonValue::String(Cow::Borrowed("asdf"))); let obj = JsonObject::new(inner);