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}