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