Skip to main content

Crate object_hash

Crate object_hash 

Source
Expand description

A stable hash of a structured value.

Given a value — a map, an array, a string, a number, nested in any combination — this produces a short digest that is the same every time for values that are equal. Not merely equal within one run of a program: equal across runs, across processes, and across machines.

§Why Hash is not enough

std::hash::Hash looks like the right tool and is not. The default hasher, RandomState, is seeded per process, so the same value hashes differently every time the program starts. That is exactly right for a HashMap, where the seed is what stops an attacker choosing collisions, and exactly wrong for anything that has to be written down or compared across processes. The other half of the problem is that a HashMap iterates in an unspecified order, so even with a fixed hasher two equal maps can hash differently.

So the value has to be normalised first: written into a canonical form that depends only on what the value is. Then the canonical bytes are hashed. This crate does both.

§What it is for

  • Cache keys. A key built from the request parameters has to hit the same entry for the same request.
  • ETags. An entity tag should change when the content changes and not when a field is reordered.
  • Idempotency keys. Recognise a repeated request as the same action.
  • Change detection and deduplication. Compare two digests instead of two documents.

§Example

use object_hash::{Value, hash};

let one = Value::Map(vec![
    ("name".into(), Value::String("cipansor".into())),
    ("year".into(), Value::Number(2026.0)),
]);
// The same entries, inserted the other way round.
let two = Value::Map(vec![
    ("year".into(), Value::Number(2026.0)),
    ("name".into(), Value::String("cipansor".into())),
]);

assert_eq!(hash(&one), hash(&two), "key order does not matter for a map");

let three = Value::Map(vec![
    ("name".into(), Value::String("cipansor".into())),
    ("year".into(), Value::Number(2027.0)),
]);
assert_ne!(hash(&one), hash(&three), "a changed value changes the digest");

§Compatibility

The canonical form is the one the object-hash npm package produces, byte for byte, and the digest is SHA-1 in lowercase hexadecimal, which is its default. A digest computed here equals one computed there for the same value, so a Rust service and a Node service can agree on a cache key without either knowing about the other. That is also why a map is written as an array of pairs and an integral float has no point: the reference writes them that way, and agreeing with it is worth more than tidier output.

The one thing that does not carry over is the reference’s handling of JavaScript objects. An object there is a thing with a prototype and a constructor, and it mixes those into the hash; a Value::Map here is just its entries. That is a deliberate difference: a Rust caller has no prototype to respect, and mixing one in would make the digest depend on the library rather than on the value.

§What it is not

SHA-1 is not used here as a security primitive. The digest names a value; it does not protect it. Do not treat a digest as a signature or as proof that a value came from somewhere. If a stronger digest is wanted, hash the canonical bytes — canonical returns them — with any other function.

Enums§

Value
A value that can be hashed.

Functions§

canonical
The canonical form of a value, as text.
hash
The digest of a value, as lowercase hexadecimal SHA-1.