Skip to main content

object_hash/
lib.rs

1#![doc = include_str!("../README.md")]
2mod encode;
3mod md5;
4mod sha1;
5mod sha256;
6mod sha512;
7
8use std::fmt;
9
10/// The digest function used to turn the canonical form into a digest.
11///
12/// The default is SHA-1, which is what the reference package uses when no
13/// algorithm is named. The others are here so a digest can match a system that
14/// already speaks one of them.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16#[non_exhaustive]
17pub enum Algorithm {
18    /// SHA-1, 40 hexadecimal characters. The reference's default.
19    #[default]
20    Sha1,
21    /// SHA-256, 64 hexadecimal characters.
22    Sha256,
23    /// SHA-224, 56 hexadecimal characters.
24    Sha224,
25    /// SHA-512, 128 hexadecimal characters.
26    Sha512,
27    /// SHA-384, 96 hexadecimal characters.
28    Sha384,
29    /// MD5, 32 hexadecimal characters.
30    Md5,
31}
32
33/// How the digest is written out.
34///
35/// The reference package calls this `encoding`. Hexadecimal is its default and
36/// the one a cache key is normally written in; base64 is shorter, and raw bytes
37/// are what a caller wants when the digest is fed to something else.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39#[non_exhaustive]
40pub enum Encoding {
41    /// Lowercase hexadecimal, two characters per byte. The reference's default.
42    #[default]
43    Hex,
44    /// Standard base64 with `+` and `/`, padded with `=`.
45    Base64,
46    /// The raw digest bytes.
47    Raw,
48}
49
50/// A value that can be hashed.
51///
52/// This is the set of things the reference can hash, minus the ones that only
53/// exist in JavaScript: there is no case for a function, a symbol, or a `Date`,
54/// because a Rust caller would not pass one.
55///
56/// [`Value::Map`] and [`Value::Object`] are not interchangeable. They are the
57/// reference's `Map` and its plain object, which it encodes differently, so the
58/// same entries produce a different digest under each. Pick the one that matches
59/// what the other side of the conversation is hashing.
60#[derive(Debug, Clone, PartialEq)]
61pub enum Value {
62    /// The absence of a value. Written `Null`, not the empty string.
63    Null,
64    /// A boolean.
65    Bool(bool),
66    /// A number. An integer is written as a float that happens to be integral,
67    /// because the reference has no separate integer case.
68    Number(f64),
69    /// A string.
70    String(String),
71    /// A sequence whose order matters.
72    Array(Vec<Value>),
73    /// A collection whose order does not matter. This is the reference's `Map`.
74    Set(Vec<Value>),
75    /// A mapping from strings to values, written in key order. This is the
76    /// reference's `Map`.
77    Map(Vec<(String, Value)>),
78    /// A mapping from strings to values, written in key order with unquoted
79    /// keys. This is the reference's plain object, and it is encoded differently
80    /// from [`Value::Map`].
81    Object(Vec<(String, Value)>),
82}
83
84/// The choices that change the canonical bytes, mirroring the reference's
85/// options.
86///
87/// [`Options::default`] is what the reference does when nothing is passed, so a
88/// digest computed with the default options matches one the reference computes
89/// with no options.
90///
91/// The reference's `respectType` is deliberately absent. It is on by default
92/// there and makes every plain object carry a preamble describing Node's
93/// prototype chain, which is a fact about the JavaScript runtime rather than
94/// about the value. This crate always encodes the `respectType: false` form; if
95/// the other side hashes plain objects with the default, hash them as
96/// [`Value::Map`] there or turn `respectType` off.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct Options {
99    /// Write only the keys of an object, ignoring the values.
100    ///
101    /// This is the reference's `excludeValues`, and its `keys` and `keysMD5`
102    /// helpers. It applies to [`Value::Object`] only; a [`Value::Map`] keeps its
103    /// values, as the reference's does.
104    pub exclude_values: bool,
105    /// Sort an array's entries, so its order stops mattering.
106    ///
107    /// This is the reference's `unorderedArrays`, off by default.
108    pub unordered_arrays: bool,
109    /// Sort a set's entries.
110    ///
111    /// This is the reference's `unorderedSets`, on by default, and the reason a
112    /// set is unordered at all.
113    pub unordered_sets: bool,
114    /// Sort an object's keys.
115    ///
116    /// This is the reference's `unorderedObjects`, on by default.
117    pub unordered_objects: bool,
118}
119
120impl Default for Options {
121    fn default() -> Self {
122        Self {
123            exclude_values: false,
124            unordered_arrays: false,
125            unordered_sets: true,
126            unordered_objects: true,
127        }
128    }
129}
130
131impl Options {
132    /// The reference's `keys` and `keysMD5`: hash an object's keys, not its
133    /// values.
134    #[must_use]
135    pub fn keys() -> Self {
136        Self {
137            exclude_values: true,
138            ..Self::default()
139        }
140    }
141
142    /// The reference's `unorderedArrays: true`: ignore an array's order.
143    #[must_use]
144    pub fn unordered_arrays(self) -> Self {
145        Self {
146            unordered_arrays: true,
147            ..self
148        }
149    }
150}
151
152impl From<Options> for encode::WriteOptions {
153    fn from(options: Options) -> Self {
154        Self {
155            exclude_values: options.exclude_values,
156            unordered_arrays: options.unordered_arrays,
157            unordered_sets: options.unordered_sets,
158            unordered_objects: options.unordered_objects,
159        }
160    }
161}
162
163impl Value {
164    /// A string value from anything string-like.
165    #[must_use]
166    pub fn string(text: impl Into<String>) -> Self {
167        Self::String(text.into())
168    }
169
170    /// A number value.
171    #[must_use]
172    pub fn number(number: impl Into<f64>) -> Self {
173        Self::Number(number.into())
174    }
175
176    /// Whether this value is a scalar.
177    ///
178    /// Scalars are the values a JavaScript `Set` deduplicates; arrays and maps
179    /// it compares by identity instead. Nothing outside the crate needs this,
180    /// but it is the distinction the encoding turns on.
181    pub(crate) fn is_scalar(&self) -> bool {
182        matches!(
183            self,
184            Self::Null | Self::Bool(_) | Self::Number(_) | Self::String(_)
185        )
186    }
187
188    /// The digest of this value, as lowercase hexadecimal SHA-1.
189    #[must_use]
190    pub fn hash(&self) -> String {
191        hash(self)
192    }
193}
194
195impl fmt::Display for Value {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        f.write_str(&canonical(self))
198    }
199}
200
201impl From<bool> for Value {
202    fn from(value: bool) -> Self {
203        Self::Bool(value)
204    }
205}
206
207impl From<f64> for Value {
208    fn from(value: f64) -> Self {
209        Self::Number(value)
210    }
211}
212
213impl From<i64> for Value {
214    fn from(value: i64) -> Self {
215        Self::Number(value as f64)
216    }
217}
218
219impl From<i32> for Value {
220    fn from(value: i32) -> Self {
221        Self::Number(f64::from(value))
222    }
223}
224
225impl From<u32> for Value {
226    fn from(value: u32) -> Self {
227        Self::Number(f64::from(value))
228    }
229}
230
231impl From<&str> for Value {
232    fn from(value: &str) -> Self {
233        Self::String(value.to_string())
234    }
235}
236
237impl From<String> for Value {
238    fn from(value: String) -> Self {
239        Self::String(value)
240    }
241}
242
243impl<T: Into<Value>> From<Vec<T>> for Value {
244    fn from(value: Vec<T>) -> Self {
245        Self::Array(value.into_iter().map(Into::into).collect())
246    }
247}
248
249/// The canonical form of a value, as text, with the default options.
250///
251/// Two values that are equal have the same canonical form. It is returned rather
252/// than hashed so a caller can use a different digest, or none.
253#[must_use]
254pub fn canonical(value: &Value) -> String {
255    canonical_with(value, &Options::default())
256}
257
258/// The canonical form of a value, as text, with chosen options.
259#[must_use]
260pub fn canonical_with(value: &Value, options: &Options) -> String {
261    encode::canonical(value, &(*options).into())
262}
263
264/// The digest of a value, as lowercase hexadecimal SHA-1.
265///
266/// The digest is stable across runs and machines for equal values. See the
267/// crate documentation for what equal means here.
268///
269/// # Example
270///
271/// ```
272/// use object_hash::{hash, Value};
273///
274/// let left = Value::Map(vec![
275///     ("b".into(), Value::number(2)),
276///     ("a".into(), Value::string("one")),
277/// ]);
278/// // A map's key order does not change the digest.
279/// let right = Value::Map(vec![
280///     ("a".into(), Value::string("one")),
281///     ("b".into(), Value::number(2)),
282/// ]);
283/// assert_eq!(hash(&left), hash(&right));
284///
285/// // An array's order does.
286/// assert_ne!(
287///     hash(&Value::Array(vec![Value::number(1), Value::number(2)])),
288///     hash(&Value::Array(vec![Value::number(2), Value::number(1)])),
289/// );
290/// ```
291#[must_use]
292pub fn hash(value: &Value) -> String {
293    hash_with(value, Algorithm::Sha1)
294}
295
296/// The digest of a value with a chosen algorithm, as lowercase hexadecimal.
297///
298/// The canonical bytes are the same whatever the algorithm, so two algorithms
299/// agree on which values are equal and differ only in the text they produce.
300///
301/// # Example
302///
303/// ```
304/// use object_hash::{Algorithm, Value, hash_with};
305///
306/// let value = Value::string("cipansor");
307/// assert_eq!(hash_with(&value, Algorithm::Sha1).len(), 40);
308/// assert_eq!(hash_with(&value, Algorithm::Sha256).len(), 64);
309/// assert_eq!(hash_with(&value, Algorithm::Sha224).len(), 56);
310/// assert_eq!(hash_with(&value, Algorithm::Md5).len(), 32);
311/// ```
312#[must_use]
313pub fn hash_with(value: &Value, algorithm: Algorithm) -> String {
314    digest(&canonical(value), algorithm)
315}
316
317/// The digest of a value with chosen options, as lowercase hexadecimal SHA-1.
318///
319/// # Example
320///
321/// ```
322/// use object_hash::{Options, Value, hash_with_options};
323///
324/// let object = Value::Object(vec![("a".into(), Value::number(1))]);
325/// // A plain object and a map with the same entry hash differently, which is
326/// // the reference's behaviour and the reason both cases exist.
327/// let map = Value::Map(vec![("a".into(), Value::number(1))]);
328/// assert_ne!(hash_with_options(&object, Options::default()), hash_with_options(&map, Options::default()));
329/// ```
330#[must_use]
331pub fn hash_with_options(value: &Value, options: Options) -> String {
332    hash_with_options_and(value, options, Algorithm::Sha1)
333}
334
335/// The digest of a value with chosen options and algorithm.
336#[must_use]
337pub fn hash_with_options_and(value: &Value, options: Options, algorithm: Algorithm) -> String {
338    digest(&canonical_with(value, &options), algorithm)
339}
340
341/// The digest of a value with chosen options, algorithm and encoding.
342///
343/// This is the full form; the shorter functions fix the encoding to
344/// hexadecimal, which is the reference's default. [`Encoding::Raw`] returns the
345/// digest bytes interpreted as one character each, so a caller that wants the
346/// bytes should use [`raw_hash_with`] instead.
347///
348/// # Example
349///
350/// ```
351/// use object_hash::{hash_with_encoding, Algorithm, Encoding, Value};
352///
353/// let value = Value::Number(1.0);
354/// let hex = hash_with_encoding(&value, Algorithm::Sha256, Encoding::Hex);
355/// assert_eq!(hex.len(), 64);
356/// // Base64 of the same digest is shorter.
357/// let b64 = hash_with_encoding(&value, Algorithm::Sha256, Encoding::Base64);
358/// assert!(b64.len() < hex.len());
359/// ```
360#[must_use]
361pub fn hash_with_encoding(value: &Value, algorithm: Algorithm, encoding: Encoding) -> String {
362    hash_with_options_encoding(value, Options::default(), algorithm, encoding)
363}
364
365/// The digest of a value with every choice named.
366#[must_use]
367pub fn hash_with_options_encoding(
368    value: &Value,
369    options: Options,
370    algorithm: Algorithm,
371    encoding: Encoding,
372) -> String {
373    encode_digest(
374        &raw_digest(canonical_with(value, &options).as_bytes(), algorithm),
375        encoding,
376    )
377}
378
379/// The raw digest bytes of a value.
380///
381/// The reference returns a `Buffer` for `encoding: 'buffer'`; this is the same
382/// digest without a text form in between.
383#[must_use]
384pub fn raw_hash_with(value: &Value, algorithm: Algorithm) -> Vec<u8> {
385    raw_digest(canonical(value).as_bytes(), algorithm)
386}
387
388/// The raw digest bytes of a value with chosen options.
389#[must_use]
390pub fn raw_hash_with_options(value: &Value, options: Options, algorithm: Algorithm) -> Vec<u8> {
391    raw_digest(canonical_with(value, &options).as_bytes(), algorithm)
392}
393
394/// The digest of an already-canonical byte string, as lowercase hexadecimal.
395fn digest(canonical: &str, algorithm: Algorithm) -> String {
396    encode_digest(&raw_digest(canonical.as_bytes(), algorithm), Encoding::Hex)
397}
398
399/// The raw digest bytes of `data` under `algorithm`.
400fn raw_digest(data: &[u8], algorithm: Algorithm) -> Vec<u8> {
401    match algorithm {
402        Algorithm::Sha1 => sha1::digest(data).to_vec(),
403        Algorithm::Sha256 => sha256::digest(data).to_vec(),
404        Algorithm::Sha224 => sha256::digest224(data).to_vec(),
405        Algorithm::Sha512 => sha512::digest(data).to_vec(),
406        Algorithm::Sha384 => sha512::digest384(data).to_vec(),
407        Algorithm::Md5 => md5::digest(data).to_vec(),
408    }
409}
410
411/// Writes out digest bytes in the chosen encoding.
412fn encode_digest(bytes: &[u8], encoding: Encoding) -> String {
413    match encoding {
414        Encoding::Hex => {
415            let mut out = String::with_capacity(bytes.len() * 2);
416            for byte in bytes {
417                out.push(char::from_digit(u32::from(byte >> 4), 16).expect("nibble"));
418                out.push(char::from_digit(u32::from(byte & 0x0f), 16).expect("nibble"));
419            }
420            out
421        }
422        Encoding::Base64 => base64(bytes),
423        Encoding::Raw => bytes.iter().map(|b| char::from(*b)).collect(),
424    }
425}
426
427/// Standard base64 with padding, written out for the same reason as the digests.
428fn base64(bytes: &[u8]) -> String {
429    const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
430    let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
431    for chunk in bytes.chunks(3) {
432        let b0 = u32::from(chunk[0]);
433        let b1 = chunk.get(1).copied().map_or(0, u32::from);
434        let b2 = chunk.get(2).copied().map_or(0, u32::from);
435        let triple = (b0 << 16) | (b1 << 8) | b2;
436        out.push(char::from(ALPHABET[(triple >> 18) as usize & 0x3f]));
437        out.push(char::from(ALPHABET[(triple >> 12) as usize & 0x3f]));
438        // The last group is padded rather than truncated.
439        out.push(if chunk.len() > 1 {
440            char::from(ALPHABET[(triple >> 6) as usize & 0x3f])
441        } else {
442            '='
443        });
444        out.push(if chunk.len() > 2 {
445            char::from(ALPHABET[triple as usize & 0x3f])
446        } else {
447            '='
448        });
449    }
450    out
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456
457    #[test]
458    fn a_digest_is_forty_hexadecimal_characters() {
459        let digest = hash(&Value::Null);
460        assert_eq!(digest.len(), 40);
461        assert!(digest.chars().all(|c| c.is_ascii_hexdigit()));
462    }
463
464    #[test]
465    fn equal_values_hash_the_same_and_different_values_do_not() {
466        assert_eq!(hash(&Value::Number(1.0)), hash(&Value::Number(1.0)));
467        assert_ne!(hash(&Value::Number(1.0)), hash(&Value::Number(2.0)));
468        assert_ne!(hash(&Value::Number(1.0)), hash(&Value::String("1".into())));
469        assert_ne!(hash(&Value::Null), hash(&Value::String("Null".into())));
470    }
471
472    #[test]
473    fn the_conversions_build_the_expected_values() {
474        assert_eq!(Value::from("a"), Value::String("a".into()));
475        assert_eq!(Value::from(1_i32), Value::Number(1.0));
476        assert_eq!(Value::from(true), Value::Bool(true));
477        assert_eq!(
478            Value::from(vec![1_i32, 2]),
479            Value::Array(vec![Value::Number(1.0), Value::Number(2.0)])
480        );
481    }
482
483    #[test]
484    fn the_display_form_is_the_canonical_form() {
485        assert_eq!(Value::Bool(true).to_string(), "bool:true");
486        assert_eq!(Value::Null.to_string(), "Null");
487    }
488}