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