Skip to main content

object_hash/
lib.rs

1#![doc = include_str!("../README.md")]
2mod encode;
3mod md5;
4mod sha1;
5mod sha256;
6
7use std::fmt;
8
9/// The digest function used to turn the canonical form into a digest.
10///
11/// The default is SHA-1, which is what the reference package uses when no
12/// algorithm is named. The others are here so a digest can match a system that
13/// already speaks one of them.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15#[non_exhaustive]
16pub enum Algorithm {
17    /// SHA-1, 40 hexadecimal characters. The reference's default.
18    #[default]
19    Sha1,
20    /// SHA-256, 64 hexadecimal characters.
21    Sha256,
22    /// MD5, 32 hexadecimal characters.
23    Md5,
24}
25
26/// A value that can be hashed.
27///
28/// This is the set of things the reference can hash, minus the ones that only
29/// exist in JavaScript: there is no case for a function, a symbol, or a `Date`,
30/// because a Rust caller would not pass one. A `Map` here stands for the
31/// reference's plain object and its `Map`, which it treats the same way.
32#[derive(Debug, Clone, PartialEq)]
33pub enum Value {
34    /// The absence of a value. Written `Null`, not the empty string.
35    Null,
36    /// A boolean.
37    Bool(bool),
38    /// A number. An integer is written as a float that happens to be integral,
39    /// because the reference has no separate integer case.
40    Number(f64),
41    /// A string.
42    String(String),
43    /// A sequence whose order matters.
44    Array(Vec<Value>),
45    /// A collection whose order does not matter.
46    Set(Vec<Value>),
47    /// A mapping from strings to values, written in key order.
48    Map(Vec<(String, Value)>),
49}
50
51impl Value {
52    /// A string value from anything string-like.
53    #[must_use]
54    pub fn string(text: impl Into<String>) -> Self {
55        Self::String(text.into())
56    }
57
58    /// A number value.
59    #[must_use]
60    pub fn number(number: impl Into<f64>) -> Self {
61        Self::Number(number.into())
62    }
63
64    /// Whether this value is a scalar.
65    ///
66    /// Scalars are the values a JavaScript `Set` deduplicates; arrays and maps
67    /// it compares by identity instead. Nothing outside the crate needs this,
68    /// but it is the distinction the encoding turns on.
69    pub(crate) fn is_scalar(&self) -> bool {
70        matches!(
71            self,
72            Self::Null | Self::Bool(_) | Self::Number(_) | Self::String(_)
73        )
74    }
75
76    /// The digest of this value, as lowercase hexadecimal SHA-1.
77    #[must_use]
78    pub fn hash(&self) -> String {
79        hash(self)
80    }
81}
82
83impl fmt::Display for Value {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        f.write_str(&canonical(self))
86    }
87}
88
89impl From<bool> for Value {
90    fn from(value: bool) -> Self {
91        Self::Bool(value)
92    }
93}
94
95impl From<f64> for Value {
96    fn from(value: f64) -> Self {
97        Self::Number(value)
98    }
99}
100
101impl From<i64> for Value {
102    fn from(value: i64) -> Self {
103        Self::Number(value as f64)
104    }
105}
106
107impl From<i32> for Value {
108    fn from(value: i32) -> Self {
109        Self::Number(f64::from(value))
110    }
111}
112
113impl From<u32> for Value {
114    fn from(value: u32) -> Self {
115        Self::Number(f64::from(value))
116    }
117}
118
119impl From<&str> for Value {
120    fn from(value: &str) -> Self {
121        Self::String(value.to_string())
122    }
123}
124
125impl From<String> for Value {
126    fn from(value: String) -> Self {
127        Self::String(value)
128    }
129}
130
131impl<T: Into<Value>> From<Vec<T>> for Value {
132    fn from(value: Vec<T>) -> Self {
133        Self::Array(value.into_iter().map(Into::into).collect())
134    }
135}
136
137/// The canonical form of a value, as text.
138///
139/// Two values that are equal have the same canonical form. It is returned rather
140/// than hashed so a caller can use a different digest, or none.
141#[must_use]
142pub fn canonical(value: &Value) -> String {
143    encode::canonical(value)
144}
145
146/// The digest of a value, as lowercase hexadecimal SHA-1.
147///
148/// The digest is stable across runs and machines for equal values. See the
149/// crate documentation for what equal means here.
150///
151/// # Example
152///
153/// ```
154/// use object_hash::{hash, Value};
155///
156/// let left = Value::Map(vec![
157///     ("b".into(), Value::number(2)),
158///     ("a".into(), Value::string("one")),
159/// ]);
160/// // A map's key order does not change the digest.
161/// let right = Value::Map(vec![
162///     ("a".into(), Value::string("one")),
163///     ("b".into(), Value::number(2)),
164/// ]);
165/// assert_eq!(hash(&left), hash(&right));
166///
167/// // An array's order does.
168/// assert_ne!(
169///     hash(&Value::Array(vec![Value::number(1), Value::number(2)])),
170///     hash(&Value::Array(vec![Value::number(2), Value::number(1)])),
171/// );
172/// ```
173#[must_use]
174pub fn hash(value: &Value) -> String {
175    hash_with(value, Algorithm::Sha1)
176}
177
178/// The digest of a value with a chosen algorithm, as lowercase hexadecimal.
179///
180/// The canonical bytes are the same whatever the algorithm, so two algorithms
181/// agree on which values are equal and differ only in the text they produce.
182///
183/// # Example
184///
185/// ```
186/// use object_hash::{Algorithm, Value, hash_with};
187///
188/// let value = Value::string("cipansor");
189/// assert_eq!(hash_with(&value, Algorithm::Sha1).len(), 40);
190/// assert_eq!(hash_with(&value, Algorithm::Sha256).len(), 64);
191/// assert_eq!(hash_with(&value, Algorithm::Md5).len(), 32);
192/// ```
193#[must_use]
194pub fn hash_with(value: &Value, algorithm: Algorithm) -> String {
195    let bytes = canonical(value);
196    match algorithm {
197        Algorithm::Sha1 => sha1::hex(bytes.as_bytes()),
198        Algorithm::Sha256 => sha256::hex(bytes.as_bytes()),
199        Algorithm::Md5 => md5::hex(bytes.as_bytes()),
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn a_digest_is_forty_hexadecimal_characters() {
209        let digest = hash(&Value::Null);
210        assert_eq!(digest.len(), 40);
211        assert!(digest.chars().all(|c| c.is_ascii_hexdigit()));
212    }
213
214    #[test]
215    fn equal_values_hash_the_same_and_different_values_do_not() {
216        assert_eq!(hash(&Value::Number(1.0)), hash(&Value::Number(1.0)));
217        assert_ne!(hash(&Value::Number(1.0)), hash(&Value::Number(2.0)));
218        assert_ne!(hash(&Value::Number(1.0)), hash(&Value::String("1".into())));
219        assert_ne!(hash(&Value::Null), hash(&Value::String("Null".into())));
220    }
221
222    #[test]
223    fn the_conversions_build_the_expected_values() {
224        assert_eq!(Value::from("a"), Value::String("a".into()));
225        assert_eq!(Value::from(1_i32), Value::Number(1.0));
226        assert_eq!(Value::from(true), Value::Bool(true));
227        assert_eq!(
228            Value::from(vec![1_i32, 2]),
229            Value::Array(vec![Value::Number(1.0), Value::Number(2.0)])
230        );
231    }
232
233    #[test]
234    fn the_display_form_is_the_canonical_form() {
235        assert_eq!(Value::Bool(true).to_string(), "bool:true");
236        assert_eq!(Value::Null.to_string(), "Null");
237    }
238}