feat(json-hash): extract into own package

This commit is contained in:
Julien Fontanet
2024-08-05 14:37:33 +02:00
parent ddfdce9da5
commit 14325fe992
8 changed files with 158 additions and 36 deletions

View File

@@ -0,0 +1,16 @@
The use for this librariy is to create a composite key from a JSON value, ignoring objects identity and properties order.
The hash algorithm used is intended to be fast and with low collisions and is not guaranteed to be be secure.
```js
import { jsonHash } from '@vates/json-hash'
console.log(jsonHash('foo'))
// → "siEyldVkkW+JpqQkVVZ8h8P0gPzXocFeIg8X1xaaeQs="
// order of properties is ignored
console.log(jsonHash({ foo: 0, bar: 1 }))
// → "JckoRSMIBjNlgEWIXhgpBOuyLQYqABZqvf1ccb3BPg0="
console.log(jsonHash({ bar: 1, foo: 0 }))
// → "JckoRSMIBjNlgEWIXhgpBOuyLQYqABZqvf1ccb3BPg0="
```

1
@vates/json-hash/.npmignore Symbolic link
View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -0,0 +1,49 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/json-hash
[![Package Version](https://badgen.net/npm/v/@vates/json-hash)](https://npmjs.org/package/@vates/json-hash) ![License](https://badgen.net/npm/license/@vates/json-hash) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/json-hash)](https://bundlephobia.com/result?p=@vates/json-hash) [![Node compatibility](https://badgen.net/npm/node/@vates/json-hash)](https://npmjs.org/package/@vates/json-hash)
> Compute a stable hash from a JSON-ifiable value
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/json-hash):
```sh
npm install --save @vates/json-hash
```
## Usage
The use for this librariy is to create a composite key from a JSON value, ignoring objects identity and properties order.
The hash algorithm used is intended to be fast and with low collisions and is not guaranteed to be be secure.
```js
import { jsonHash } from '@vates/json-hash'
console.log(jsonHash('foo'))
// → "siEyldVkkW+JpqQkVVZ8h8P0gPzXocFeIg8X1xaaeQs="
// order of properties is ignored
console.log(jsonHash({ foo: 0, bar: 1 }))
// → "JckoRSMIBjNlgEWIXhgpBOuyLQYqABZqvf1ccb3BPg0="
console.log(jsonHash({ bar: 1, foo: 0 }))
// → "JckoRSMIBjNlgEWIXhgpBOuyLQYqABZqvf1ccb3BPg0="
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

38
@vates/json-hash/index.js Normal file
View File

@@ -0,0 +1,38 @@
'use strict'
const { createHash } = require('node:crypto')
function updateJsonHash(value, hash) {
if (value !== null && typeof value === 'object') {
if (Array.isArray(value)) {
hash.update('[')
for (const item of value) {
updateJsonHash(item, hash)
// trailing is not a big deal because it does not need to be valid JSON
hash.update(',')
}
hash.update(']')
} else {
hash.update('{')
for (const key of Object.keys(value).sort()) {
updateJsonHash(key, hash)
hash.update(':')
updateJsonHash(value[key], hash)
// trailing is not a big deal because it does not need to be valid JSON
hash.update(',')
}
hash.update('}')
}
} else {
hash.update(JSON.stringify(value))
}
}
exports.jsonHash = function jsonHash(value) {
// this hash does not need to be secure, it just needs to be fast and with low collisions
const hash = createHash('sha256')
updateJsonHash(value, hash)
return hash.digest('base64')
}

View File

@@ -0,0 +1,19 @@
const { jsonHash } = require('@vates/json-hash')
const assert = require('node:assert/strict')
const test = require('test')
const cases = [
[null, 'dCNOmK/nSY+12vHzasLXiswzlGT5UHA7jAGYkvmCuQs='],
[42, 'c0dctApWjo2ooEXO0RATfhWfiQrE2og7axfcZRs6gEk='],
['foo', 'siEyldVkkW+JpqQkVVZ8h8P0gPzXocFeIg8X1xaaeQs='],
[[], 'T1PNoYwrqgwDVLtfmj7L5e0Sq02OEbqHPC8RFhICuUU='],
[{}, 'RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o='],
[{ a: 1, b: 2 }, 'LMjVxyj2vtMZ3W1cZ2cJrQij5zn7FBOlPiyBB38OmFE='],
[{ b: 2, a: 1 }, 'LMjVxyj2vtMZ3W1cZ2cJrQij5zn7FBOlPiyBB38OmFE='],
]
for (const [value, hash] of cases) {
test(JSON.stringify(value), function () {
assert.equal(jsonHash(value), hash)
})
}

View File

@@ -0,0 +1,33 @@
{
"private": false,
"name": "@vates/json-hash",
"description": "Compute a stable hash from a JSON-ifiable value",
"keywords": [
"hash",
"json",
"stable"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/json-hash",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/json-hash",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.0.0",
"engines": {
"node": ">=8.10"
},
"devDependencies": {
"test": "^3.3.0"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
}
}

View File

@@ -10,10 +10,10 @@ import { Agent, ProxyAgent, request } from 'undici'
import { coalesceCalls } from '@vates/coalesce-calls'
import { Collection } from 'xo-collection'
import { compose } from '@vates/compose'
import { createHash } from 'node:crypto'
import { createLogger } from '@xen-orchestra/log'
import { EventEmitter } from 'events'
import { Index } from 'xo-collection/index.js'
import { jsonHash } from '@vates/json-hash'
import { cancelable, defer, fromCallback, ignoreErrors, pDelay, pRetry, pTimeout } from 'promise-toolbox'
import { limitConcurrency } from 'limit-concurrency-decorator'
import { decorateClass } from '@vates/decorate-with'
@@ -90,41 +90,6 @@ const addSyncStackTrace = async promise => {
}
}
function updateJsonHash(value, hash) {
if (value !== null && typeof value === 'object') {
if (Array.isArray(value)) {
hash.update('[')
for (const item of value) {
updateJsonHash(item, hash)
// trailing is not a big deal because it does not need to be valid JSON
hash.update(',')
}
hash.update(']')
} else {
hash.update('{')
for (const key of Object.keys(value).sort()) {
updateJsonHash(key, hash)
hash.update(':')
updateJsonHash(value[key], hash)
// trailing is not a big deal because it does not need to be valid JSON
hash.update(',')
}
hash.update('}')
}
} else {
hash.update(JSON.stringify(value))
}
}
export function jsonHash(value) {
// this hash does not need to be secure, it just needs to be fast and with low collisions
const hash = createHash('sha256')
updateJsonHash(value, hash)
return hash.digest('base64')
}
// -------------------------------------------------------------------
export class Xapi extends EventEmitter {

View File

@@ -34,6 +34,7 @@
"@vates/coalesce-calls": "^0.1.0",
"@vates/compose": "^2.1.0",
"@vates/decorate-with": "^2.1.0",
"@vates/json-hash": "^0.0.0",
"@vates/obfuscate": "^0.1.0",
"@vates/xml": "^2.0.0",
"@vates/xml-rpc": "^1.0.0",