Skip to main content

object_hash/
lib.rs

1//! A stable hash of a structured value.
2//!
3//! Given a value — a map, an array, a string, a number, nested in any
4//! combination — this produces a short digest that is the same every time for
5//! values that are equal. Not merely equal within one run of a program: equal
6//! across runs, across processes, and across machines.
7//!
8//! # Why `Hash` is not enough
9//!
10//! `std::hash::Hash` looks like the right tool and is not. The default hasher,
11//! `RandomState`, is seeded per process, so the same value hashes differently
12//! every time the program starts. That is exactly right for a `HashMap`, where
13//! the seed is what stops an attacker choosing collisions, and exactly wrong for
14//! anything that has to be written down or compared across processes. The other
15//! half of the problem is that a `HashMap` iterates in an unspecified order, so
16//! even with a fixed hasher two equal maps can hash differently.
17//!
18//! So the value has to be normalised first: written into a canonical form that
19//! depends only on what the value is. Then the canonical bytes are hashed. This
20//! crate does both.
21//!
22//! # What it is for
23//!
24//! - **Cache keys.** A key built from the request parameters has to hit the same
25//!   entry for the same request.
26//! - **ETags.** An entity tag should change when the content changes and not
27//!   when a field is reordered.
28//! - **Idempotency keys.** Recognise a repeated request as the same action.
29//! - **Change detection and deduplication.** Compare two digests instead of two
30//!   documents.
31//!
32//! # Example
33//!
34//! ```
35//! use object_hash::{Value, hash};
36//!
37//! let one = Value::Map(vec![
38//!     ("name".into(), Value::String("cipansor".into())),
39//!     ("year".into(), Value::Number(2026.0)),
40//! ]);
41//! // The same entries, inserted the other way round.
42//! let two = Value::Map(vec![
43//!     ("year".into(), Value::Number(2026.0)),
44//!     ("name".into(), Value::String("cipansor".into())),
45//! ]);
46//!
47//! assert_eq!(hash(&one), hash(&two), "key order does not matter for a map");
48//!
49//! let three = Value::Map(vec![
50//!     ("name".into(), Value::String("cipansor".into())),
51//!     ("year".into(), Value::Number(2027.0)),
52//! ]);
53//! assert_ne!(hash(&one), hash(&three), "a changed value changes the digest");
54//! ```
55//!
56//! # Compatibility
57//!
58//! The canonical form is the one the `object-hash` npm package produces, byte
59//! for byte, and the digest is SHA-1 in lowercase hexadecimal, which is its
60//! default. A digest computed here equals one computed there for the same value,
61//! so a Rust service and a Node service can agree on a cache key without either
62//! knowing about the other. That is also why a map is written as an array of
63//! pairs and an integral float has no point: the reference writes them that way,
64//! and agreeing with it is worth more than tidier output.
65//!
66//! The one thing that does not carry over is the reference's handling of
67//! JavaScript objects. An object there is a thing with a prototype and a
68//! constructor, and it mixes those into the hash; a `Value::Map` here is just
69//! its entries. That is a deliberate difference: a Rust caller has no prototype
70//! to respect, and mixing one in would make the digest depend on the library
71//! rather than on the value.
72//!
73//! # What it is not
74//!
75//! SHA-1 is not used here as a security primitive. The digest names a value; it
76//! does not protect it. Do not treat a digest as a signature or as proof that a
77//! value came from somewhere. If a stronger digest is wanted, hash the canonical
78//! bytes — [`canonical`] returns them — with any other function.
79
80/// The README, compiled as a doctest so its example cannot drift from the API.
81#[cfg(doctest)]
82#[doc = include_str!("../README.md")]
83pub struct ReadmeDoctests;
84
85mod encode;
86mod sha1;
87
88use std::fmt;
89
90/// A value that can be hashed.
91///
92/// This is the set of things the reference can hash, minus the ones that only
93/// exist in JavaScript: there is no case for a function, a symbol, or a `Date`,
94/// because a Rust caller would not pass one. A `Map` here stands for the
95/// reference's plain object and its `Map`, which it treats the same way.
96#[derive(Debug, Clone, PartialEq)]
97pub enum Value {
98    /// The absence of a value. Written `Null`, not the empty string.
99    Null,
100    /// A boolean.
101    Bool(bool),
102    /// A number. An integer is written as a float that happens to be integral,
103    /// because the reference has no separate integer case.
104    Number(f64),
105    /// A string.
106    String(String),
107    /// A sequence whose order matters.
108    Array(Vec<Value>),
109    /// A collection whose order does not matter.
110    Set(Vec<Value>),
111    /// A mapping from strings to values, written in key order.
112    Map(Vec<(String, Value)>),
113}
114
115impl Value {
116    /// A string value from anything string-like.
117    #[must_use]
118    pub fn string(text: impl Into<String>) -> Self {
119        Self::String(text.into())
120    }
121
122    /// A number value.
123    #[must_use]
124    pub fn number(number: impl Into<f64>) -> Self {
125        Self::Number(number.into())
126    }
127
128    /// Whether this value is a scalar.
129    ///
130    /// Scalars are the values a JavaScript `Set` deduplicates; arrays and maps
131    /// it compares by identity instead. Nothing outside the crate needs this,
132    /// but it is the distinction the encoding turns on.
133    pub(crate) fn is_scalar(&self) -> bool {
134        matches!(
135            self,
136            Self::Null | Self::Bool(_) | Self::Number(_) | Self::String(_)
137        )
138    }
139
140    /// The digest of this value, as lowercase hexadecimal SHA-1.
141    #[must_use]
142    pub fn hash(&self) -> String {
143        hash(self)
144    }
145}
146
147impl fmt::Display for Value {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        f.write_str(&canonical(self))
150    }
151}
152
153impl From<bool> for Value {
154    fn from(value: bool) -> Self {
155        Self::Bool(value)
156    }
157}
158
159impl From<f64> for Value {
160    fn from(value: f64) -> Self {
161        Self::Number(value)
162    }
163}
164
165impl From<i64> for Value {
166    fn from(value: i64) -> Self {
167        Self::Number(value as f64)
168    }
169}
170
171impl From<i32> for Value {
172    fn from(value: i32) -> Self {
173        Self::Number(f64::from(value))
174    }
175}
176
177impl From<u32> for Value {
178    fn from(value: u32) -> Self {
179        Self::Number(f64::from(value))
180    }
181}
182
183impl From<&str> for Value {
184    fn from(value: &str) -> Self {
185        Self::String(value.to_string())
186    }
187}
188
189impl From<String> for Value {
190    fn from(value: String) -> Self {
191        Self::String(value)
192    }
193}
194
195impl<T: Into<Value>> From<Vec<T>> for Value {
196    fn from(value: Vec<T>) -> Self {
197        Self::Array(value.into_iter().map(Into::into).collect())
198    }
199}
200
201/// The canonical form of a value, as text.
202///
203/// Two values that are equal have the same canonical form. It is returned rather
204/// than hashed so a caller can use a different digest, or none.
205#[must_use]
206pub fn canonical(value: &Value) -> String {
207    encode::canonical(value)
208}
209
210/// The digest of a value, as lowercase hexadecimal SHA-1.
211///
212/// The digest is stable across runs and machines for equal values. See the
213/// crate documentation for what equal means here.
214///
215/// # Example
216///
217/// ```
218/// use object_hash::{hash, Value};
219///
220/// let left = Value::Map(vec![
221///     ("b".into(), Value::number(2)),
222///     ("a".into(), Value::string("one")),
223/// ]);
224/// // A map's key order does not change the digest.
225/// let right = Value::Map(vec![
226///     ("a".into(), Value::string("one")),
227///     ("b".into(), Value::number(2)),
228/// ]);
229/// assert_eq!(hash(&left), hash(&right));
230///
231/// // An array's order does.
232/// assert_ne!(
233///     hash(&Value::Array(vec![Value::number(1), Value::number(2)])),
234///     hash(&Value::Array(vec![Value::number(2), Value::number(1)])),
235/// );
236/// ```
237#[must_use]
238pub fn hash(value: &Value) -> String {
239    sha1::hex(canonical(value).as_bytes())
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn a_digest_is_forty_hexadecimal_characters() {
248        let digest = hash(&Value::Null);
249        assert_eq!(digest.len(), 40);
250        assert!(digest.chars().all(|c| c.is_ascii_hexdigit()));
251    }
252
253    #[test]
254    fn equal_values_hash_the_same_and_different_values_do_not() {
255        assert_eq!(hash(&Value::Number(1.0)), hash(&Value::Number(1.0)));
256        assert_ne!(hash(&Value::Number(1.0)), hash(&Value::Number(2.0)));
257        assert_ne!(hash(&Value::Number(1.0)), hash(&Value::String("1".into())));
258        assert_ne!(hash(&Value::Null), hash(&Value::String("Null".into())));
259    }
260
261    #[test]
262    fn the_conversions_build_the_expected_values() {
263        assert_eq!(Value::from("a"), Value::String("a".into()));
264        assert_eq!(Value::from(1_i32), Value::Number(1.0));
265        assert_eq!(Value::from(true), Value::Bool(true));
266        assert_eq!(
267            Value::from(vec![1_i32, 2]),
268            Value::Array(vec![Value::Number(1.0), Value::Number(2.0)])
269        );
270    }
271
272    #[test]
273    fn the_display_form_is_the_canonical_form() {
274        assert_eq!(Value::Bool(true).to_string(), "bool:true");
275        assert_eq!(Value::Null.to_string(), "Null");
276    }
277}