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