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}