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