pax_global_header00006660000000000000000000000064144317520640014520gustar00rootroot0000000000000052 comment=9da29965c98c3118c6e52da1ce43e913f66c35e8 rdf-canonize-3.4.0/000077500000000000000000000000001443175206400141035ustar00rootroot00000000000000rdf-canonize-3.4.0/.editorconfig000066400000000000000000000003761443175206400165660ustar00rootroot00000000000000# http://editorconfig.org root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.{js,json,jsonld,yaml,yml}] indent_style = space indent_size = 2 [*.{h,cc}] indent_style = space indent_size = 2 rdf-canonize-3.4.0/.eslintrc.js000066400000000000000000000002211443175206400163350ustar00rootroot00000000000000module.exports = { env: { browser: true, commonjs: true, node: true }, extends: 'eslint-config-digitalbazaar', root: true }; rdf-canonize-3.4.0/.github/000077500000000000000000000000001443175206400154435ustar00rootroot00000000000000rdf-canonize-3.4.0/.github/workflows/000077500000000000000000000000001443175206400175005ustar00rootroot00000000000000rdf-canonize-3.4.0/.github/workflows/main.yml000066400000000000000000000037731443175206400211610ustar00rootroot00000000000000name: Node.js CI on: [push] jobs: test-node: runs-on: ubuntu-latest timeout-minutes: 10 strategy: matrix: node-version: [12.x, 14.x, 16.x, 18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - name: Install run: npm install - name: Fetch test suite run: npm run fetch-test-suite - name: Run test with Node.js ${{ matrix.node-version }} run: npm run test-node # test-karma: # runs-on: ubuntu-latest # timeout-minutes: 10 # strategy: # matrix: # node-version: [14.x] # steps: # - uses: actions/checkout@v2 # - name: Use Node.js ${{ matrix.node-version }} # uses: actions/setup-node@v1 # with: # node-version: ${{ matrix.node-version }} # - run: npm install # - name: Run karma tests # run: npm run test-karma lint: runs-on: ubuntu-latest timeout-minutes: 10 strategy: matrix: node-version: [16.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: npm install - name: Run eslint run: npm run lint coverage: # needs: [test-node, test-karma] needs: [test-node] runs-on: ubuntu-latest timeout-minutes: 10 strategy: matrix: node-version: [16.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - name: Install run: npm install - name: Fetch test suite run: npm run fetch-test-suite - name: Generate coverage report run: npm run coverage-ci - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: file: ./coverage/lcov.info fail_ci_if_error: true rdf-canonize-3.4.0/.gitignore000066400000000000000000000004051443175206400160720ustar00rootroot00000000000000*.sublime-project *.sublime-workspace *.sw[nop] *~ .DS_Store .cdtproject .classpath .cproject .nyc_output .project .settings .vscode TAGS build coverage dist node_modules npm-debug.log v8.log // test suites downloaded by the fetch-test-suite script test-suites rdf-canonize-3.4.0/CHANGELOG.md000066400000000000000000000161531443175206400157220ustar00rootroot00000000000000# rdf-canonize ChangeLog ## 3.4.0 - 2023-05-19 ### Added - Allow `canonicalIdMap` to be passed to `canonize` which will be populated by the canonical identifier issuer with the bnode identifier mapping generated by the canonicalization algorithm. This feature is particularly useful when the resulting bnode labels need to be changed for use cases such as selective disclosure. ### Changed - Update for latest [rdf-canon][] changes: test suite location, README, links, and identifiers. ## 3.3.0 - 2022-09-17 ### Added - Add optional `createMessageDigest` factory function for generating an a `MessageDigest` interface. This allows different hash implementations or even different hash algorithms, including HMACs to be used with URDNA2015. Note that using a different hash algorithm from SHA-256 will change the output. ## 3.2.1 - 2022-09-02 ### Fixed - Fix typo in unsupported algorithm error. ## 3.2.0 - 2022-09-02 ### Changed - Test that input is not changed. - Optimize quad processing. ## 3.1.0 - 2022-08-30 ### Added - Allow a maximum number of iterations of the N-Degree Hash Quads algorithm to be set, preventing unusual datasets (and likely meaningless or malicious) from consuming unnecessary CPU cycles. If the set maximum is exceeded then an error will be thrown, terminating the canonize process. This option has only been added to URDNA2015. A future major breaking release is expected to set the maximum number of iterations to a safe value by default; this release is backwards compatible and therefore sets no default. A recommended value is `1`, which will cause, at most, each blank node to have the N-degree algorithm executed on it just once. ## 3.0.0 - 2021-04-07 ### Changed - **BREAKING**: Only support Node.js >= 12. Remove related tests, dependencies, and generated `node6` output. - **BREAKING**: Remove browser bundles. Simplifies package and reduces install size. If you have a use case that requires the bundles, please file an issue. - Fix browser override file path style. ## 2.0.1 - 2021-01-21 ### Fixed - Use `setimmediate` package for `setImmediate` polyfill. The previous custom polyfill was removed. This should allow current projects using this package to stay the same and allow an easy future transition to webpack v5. ## 2.0.0 - 2021-01-20 ### Removed - **BREAKING**: Removed public API for `canonizeSync`. It is still available for testing purposes but does not run in the browser. - **BREAKING**: Removed dependency on `forge` which means that this library will only run in browsers that have support for the WebCrypto API (or an external polyfill for it). - **BREAKING**: Do not expose `existing` on `IdentifierIssuer`. The old IDs can be retrieved in order via `getOldIds`. ### Changed - General optimizations and modernization of the library. ### Added - Add `getOldIds` function to `IdentifierIssuer`. ## 1.2.0 - 2020-09-30 ### Changed - Use node-forge@0.10.0. ## 1.1.0 - 2020-01-17 ### Changed - Optimize away length check on paths. - Update node-forge dependency. - Update semver dependency. ## 1.0.3 - 2019-03-06 ### Changed - Update node-forge dependency. ## 1.0.2 - 2019-02-21 ### Fixed - Fix triple comparator in n-quads parser. ### Added - Add eslint support. ## 1.0.1 - 2019-01-23 ### Changed - Remove use of deprecated `util.isUndefined()`. Avoids unneeded `util` polyfill in webpack build. ## 1.0.0 - 2019-01-23 ### Notes - **WARNING**: This release has a **BREAKING** change that could cause the canonical N-Quads output to differ from previous releases. Specifically, tabs in literals are no longer escaped. No backwards compatibility mode is provided at this time but if you believe it is needed, please file an issue. - If you wish to use the native bindings, you must now install `rdf-canonize-native` yourself. It is no longer a dependency. See below. ### Fixed - **BREAKING**: N-Quad canonical serialized output. - Only escape 4 chars. - Now compatible with https://www.w3.org/TR/n-triples/#canonical-ntriples ### Changed - Improve N-Quads parsing. - Unescape literals. - Handle unicode escapes. - N-Quad serialization optimization. - Varies based on input by roughly ~1-2x. - **BREAKING**: Remove `rdf-canonize-native` as a dependency. The native bindings will still be used if `rdf-canonize-native` can be loaded. This means if you want the native bindings you *must* install them yourself. This change was done due to various problems caused by having any type of dependency involving the native code. With modern runtimes the JavaScript implementation is in many cases *faster*. The native bindings do have overhead but can be useful in cases where you need to offload canonizing into the background. It is recommended to perform benchmarks to determine which method works best in your case. - Update webpack and babel. - **BREAKING**: Remove `usePureJavaScript` option and make the JavaScript implementation the default. Add explicit `useNative` option to force the use of the native implementation from `rdf-canonize-native`. An error will be thrown if native bindings are not available. ## 0.3.0 - 2018-11-01 ### Changed - **BREAKING**: Move native support to optional `rdf-canonize-native` package. If native support is **required** in your environment then *also* add a dependency on the `rdf-canonize-native` package directly. This package only has an *optional* dependency on the native package to allow systems without native binding build tools to use the JavaScript implementation alone. ### Added - Istanbul coverage support. ## 0.2.5 - 2018-11-01 ### Fixed - Accept N-Quads upper case language tag. - Improve acceptable N-Quads blank node labels. ## 0.2.4 - 2018-04-25 ### Fixed - Update for Node.js 10 / OpenSSL 1.1 API. ### Changed - Update nan dependency for Node.js 10 support. ## 0.2.3 - 2017-12-05 ### Fixed - Avoid variable length arrays. Not supported by some C++ compilers. ## 0.2.2 - 2017-12-04 ### Fixed - Use const array initializer sizes. ### Changed - Comment out debug logging. ## 0.2.1 - 2017-10-16 ### Fixed - Distribute `binding.gyp`. ## 0.2.0 - 2017-10-16 ### Added - Benchmark tool using the same manifests as the test system. - Support Node.js 6. - Native Node.js addon support for URDNA2015. Improves performance. - `usePureJavaScript` option to only use JavaScript. ## 0.1.5 - 2017-09-18 ### Changed - **BREAKING**: Remove Node.js 4.x testing and native support. Use a transpiler such as babel if you need further 4.x support. ## 0.1.4 - 2017-09-17 ### Added - Expose `IdentifierIssuer` helper class. ## 0.1.3 - 2017-09-17 ### Fixed - Fix build. ## 0.1.2 - 2017-09-17 ### Changed - Change internals to use ES6. - Return Promise from API for async method. ## 0.1.1 - 2017-08-15 ### Fixed - Move node-forge to dependencies. ## 0.1.0 - 2017-08-15 ### Added - RDF Dataset Normalization async implementation from [jsonld.js][]. - webpack support. - Split messageDigest into Node.js and browser files. - Node.js file uses native crypto module. - Browser file uses forge. - See git history for all changes. [jsonld.js]: https://github.com/digitalbazaar/jsonld.js [rdf-canon]: https://w3c.github.io/rdf-canon/ rdf-canonize-3.4.0/LICENSE000066400000000000000000000030011443175206400151020ustar00rootroot00000000000000New BSD License (3-clause) Copyright (c) 2016-2021, Digital Bazaar, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Digital Bazaar, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL BAZAAR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. rdf-canonize-3.4.0/README.md000066400000000000000000000070251443175206400153660ustar00rootroot00000000000000# rdf-canonize [![Build status](https://img.shields.io/github/workflow/status/digitalbazaar/rdf-canonize/Node.js%20CI)](https://github.com/digitalbazaar/rdf-canonize/actions?query=workflow%3A%22Node.js+CI%22) [![Coverage status](https://img.shields.io/codecov/c/github/digitalbazaar/rdf-canonize)](https://codecov.io/gh/digitalbazaar/rdf-canonize) [![Dependency Status](https://img.shields.io/david/digitalbazaar/rdf-canonize.svg)](https://david-dm.org/digitalbazaar/rdf-canonize) An implementation of the [RDF Dataset Canonicalization Algorithm][] in JavaScript. Introduction ------------ ... Installation ------------ ### Node.js + npm ``` npm install rdf-canonize ``` ```js const canonize = require('rdf-canonize'); ``` ### Node.js + npm + native bindings This package has support for [rdf-canonize-native][]. This package can be useful if your application requires doing many canonizing operations asyncronously in parallel or in the background. It is **highly recommended** that you understand your requirements and benchmark using JavaScript vs native bindings. The native bindings add overhead and the JavaScript implementation may be faster with modern runtimes. The native bindings are not installed by default and must be explicitly installed. ``` npm install rdf-canonize npm install rdf-canonize-native ``` Note that the native code is not automatically used. To use the native bindings you must have them installed and set the `useNative` option to `true`. ```js const canonize = require('rdf-canonize'); ``` ### Browser + npm Install in your project with npm and use your favorite browser bundler tool. Examples -------- ```js const dataset = { // ... }; // canonize a data set with a particular algorithm with async/await const canonical = await canonize.canonize(dataset, {algorithm: 'URDNA2015'}); // canonize a data set with a particular algorithm and force use of the // native implementation const canonical = await canonize.canonize(dataset, { algorithm: 'URDNA2015', useNative: true }); ``` Related Modules --------------- * [jsonld.js][]: An implementation of the [JSON-LD][] specification. Tests ----- This library includes a sample testing utility which may be used to verify that changes to the processor maintain the correct output. The test suite is included in an external repository: https://github.com/w3c/rdf-canon This should be a sibling directory of the rdf-canonize directory or in a `test-suites` dir. To clone shallow copies into the `test-suites` dir you can use the following: npm run fetch-test-suite Node.js tests can be run with a simple command: npm test If you installed the test suites elsewhere, or wish to run other tests, use the `TEST_DIR` environment var: TEST_DIR="/tmp/tests" npm test To generate earl reports: # generate the earl report for node.js EARL=earl-node.jsonld npm test Browser testing with karma is done indirectly through [jsonld.js][]. Benchmark --------- See docs in the [benchmark README](./benchmark/README.md). Source ------ The source code for this library is available at: https://github.com/digitalbazaar/rdf-canonize Commercial Support ------------------ Commercial support for this library is available upon request from [Digital Bazaar][]: support@digitalbazaar.com [Digital Bazaar]: https://digitalbazaar.com/ [JSON-LD]: https://json-ld.org/ [RDF Dataset Canonicalization Algorithm]: https://w3c.github.io/rdf-canon/spec/ [jsonld.js]: https://github.com/digitalbazaar/jsonld.js [rdf-canonize-native]: https://github.com/digitalbazaar/rdf-canonize-native rdf-canonize-3.4.0/bench-nsolid.js000066400000000000000000000100331443175206400170030ustar00rootroot00000000000000/* * Copyright (c) 2016-2021 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const canonize = require('./index')._canonizeSync; //const canonize = require('./index').canonize; const delay = require('delay'); const nsolid = require('nsolid'); const KEYS = ['subject', 'predicate', 'object', 'graph']; (async () => { if(nsolid.profile) { await delay(3000); nsolid.profile(60000); } const quads = [ // uncomment to trigger hash first degree nquads quad('_:x0', 'ex:foo1', 'ex:obj'), quad('_:x1', 'ex:foo2', 'ex:obj'), quad('_:x2', 'ex:foo3', 'ex:obj'), // triggers loop w/simple flag usage quad('_:x3', 'ex:foo1', '_:x4'), quad('_:x4', 'ex:foo2', 'ex:obj1'), quad('_:x5', 'ex:foo1', '_:x6'), quad('_:x6', 'ex:foo2', 'ex:obj2'), // uncomment to trigger hash nquads w/significant permutations // quad('_:b0', 'http://example.org/vocab#p', '_:b1'), // quad('_:b0', 'http://example.org/vocab#p', '_:b2'), // quad('_:b0', 'http://example.org/vocab#p', '_:b3'), // quad('_:b1', 'http://example.org/vocab#p', '_:b0'), // quad('_:b1', 'http://example.org/vocab#p', '_:b3'), // quad('_:b1', 'http://example.org/vocab#p', '_:b4'), // quad('_:b2', 'http://example.org/vocab#p', '_:b0'), // quad('_:b2', 'http://example.org/vocab#p', '_:b4'), // quad('_:b2', 'http://example.org/vocab#p', '_:b5'), // quad('_:b3', 'http://example.org/vocab#p', '_:b0'), // quad('_:b3', 'http://example.org/vocab#p', '_:b1'), // quad('_:b3', 'http://example.org/vocab#p', '_:b5'), // quad('_:b4', 'http://example.org/vocab#p', '_:b1'), // quad('_:b4', 'http://example.org/vocab#p', '_:b2'), // quad('_:b4', 'http://example.org/vocab#p', '_:b5'), // quad('_:b5', 'http://example.org/vocab#p', '_:b3'), // quad('_:b5', 'http://example.org/vocab#p', '_:b2'), // quad('_:b5', 'http://example.org/vocab#p', '_:b4'), // quad('_:b6', 'http://example.org/vocab#p', '_:b7'), // quad('_:b6', 'http://example.org/vocab#p', '_:b8'), // quad('_:b6', 'http://example.org/vocab#p', '_:b9'), // quad('_:b7', 'http://example.org/vocab#p', '_:b6'), // quad('_:b7', 'http://example.org/vocab#p', '_:b10'), // quad('_:b7', 'http://example.org/vocab#p', '_:b11'), // quad('_:b8', 'http://example.org/vocab#p', '_:b6'), // quad('_:b8', 'http://example.org/vocab#p', '_:b10'), // quad('_:b8', 'http://example.org/vocab#p', '_:b11'), // quad('_:b9', 'http://example.org/vocab#p', '_:b6'), // quad('_:b9', 'http://example.org/vocab#p', '_:b10'), // quad('_:b9', 'http://example.org/vocab#p', '_:b11'), // quad('_:b10', 'http://example.org/vocab#p', '_:b7'), // quad('_:b10', 'http://example.org/vocab#p', '_:b8'), // quad('_:b10', 'http://example.org/vocab#p', '_:b9'), // quad('_:b11', 'http://example.org/vocab#p', '_:b7'), // quad('_:b11', 'http://example.org/vocab#p', '_:b8'), // quad('_:b11', 'http://example.org/vocab#p', '_:b9'), ]; try { //const count = 1; //const count = 2000; // ~0.2 secs //const count = 20000; // ~0.6 secs const count = 100000; // ~5.8 secs <-- updated //const count = 200000; // ~5 secs //const count = 600000; // ~10 secs //const count = 1000000; // ~30 secs for(let i = 0; i < count; ++i) { /*const result = */await canonize(quads, { algorithm: 'URDNA2015', usePureJavaScript: true }); //console.log(result); } } catch(e) { console.error(e); } if(nsolid.profile) { nsolid.profileEnd(); } })(); function bnode(v) { return { termType: 'BlankNode', value: v }; } function namednode(v) { return { termType: 'NamedNode', value: v }; } function quad() { const quad = {}; for(let i = 0; i < arguments.length; ++i) { const key = KEYS[i]; const arg = arguments[i]; if(arg.startsWith('_:')) { quad[key] = bnode(arg); } else { quad[key] = namednode(arg); } } if(arguments.length < 4) { quad.graph = { termType: 'DefaultGraph', value: '' }; } return quad; } rdf-canonize-3.4.0/benchmark/000077500000000000000000000000001443175206400160355ustar00rootroot00000000000000rdf-canonize-3.4.0/benchmark/.gitignore000066400000000000000000000000351443175206400200230ustar00rootroot00000000000000block-*-in.nq block-*-out.nq rdf-canonize-3.4.0/benchmark/README.md000066400000000000000000000014651443175206400173220ustar00rootroot00000000000000RDF Dataset Canonicalization Benchmark -------------------------------------- The benchmark system uses the same manifest setup as the test suite. This allows setting up both tests and benchmarks with one setup. In the root dir run **all** test suite tests: npm run benchmark To run a particular dir or file: TEST_DIR=./benchmark npm run benchmark TEST_DIR=./benchmark/m2.jsonld npm run benchmark As an addition to the test suite, tests can be marked with boolean "skip" or "only" flags. The "only" support is a hack and requies the env var "ONLY" to be set: ONLY=1 npm run benchmark ONLY=1 TEST_DIR=./benchmark npm run benchmark Tests are benchmarked with a matrix of {async/sync} {js/native} {x1/x10}. To run large "block" benchmarks, run the builder script first: node make-tests.js rdf-canonize-3.4.0/benchmark/benchmark.js000066400000000000000000000267501443175206400203370ustar00rootroot00000000000000/** * Benchmark runner for rdf-canonize. * * Copyright (c) 2017-2021 Digital Bazaar, Inc. All rights reserved. */ /* eslint-disable indent */ (function() { 'use strict'; // detect node.js (vs. browser) const _nodejs = (typeof process !== 'undefined' && process.versions && process.versions.node); const Benchmark = require('benchmark'); const assert = require('assert'); const fs = require('fs'); let path; if(_nodejs) { path = require('path'); } const canonize = require('..'); const NQuads = require('../lib/NQuads'); // try to load native bindings let rdfCanonizeNative; // try regular load try { rdfCanonizeNative = require('rdf-canonize-native'); } catch(e) { // try peer package try { rdfCanonizeNative = require('../../rdf-canonize-native'); } catch(e) { } } // use native bindings if(rdfCanonizeNative) { canonize._rdfCanonizeNative(rdfCanonizeNative); } else { // skip native tests console.warn('rdf-canonize-native not found'); } const _TEST_SUITE_PATHS = [ process.env.TEST_DIR, '../rdf-canon/tests', './test-suites/rdf-canon/tests', ]; const TEST_SUITE = _TEST_SUITE_PATHS.find(pathExists); if(!TEST_SUITE) { throw new Error('Test suite not found.'); } const ROOT_MANIFEST_DIR = resolvePath(TEST_SUITE); const TEST_TYPES = { 'rdfc:Urgna2012EvalTest': { params: [ parseNQuads(readTestNQuads('action')), createTestOptions({ algorithm: 'URGNA2012', format: 'application/n-quads' }) ] }, 'rdfc:Urdna2015EvalTest': { params: [ parseNQuads(readTestNQuads('action')), createTestOptions({ algorithm: 'URDNA2015', format: 'application/n-quads' }) ] }, }; const SKIP_TESTS = []; // run tests const suite = new Benchmark.Suite; const namepath = []; const filename = joinPath(ROOT_MANIFEST_DIR, 'manifest.jsonld'); const rootManifest = readJson(filename); rootManifest.filename = filename; addManifest(rootManifest); suite .on('start', () => { console.log('Benchmarking...'); }) .on('cycle', event => { console.log(String(event.target)); /* const s = event.target.stats; console.log(` min:${Math.min(...s.sample)} max:${Math.max(...s.sample)}`); console.log(` deviation:${s.deviation} mean:${s.mean}`); console.log(` moe:${s.moe} rme:${s.rme}% sem:${s.sem} var:${s.variance}`); */ }) .on('complete', () => { console.log('Done.'); }) .run({async: true}); /** * Adds the tests for all entries in the given manifest. * * @param manifest the manifest. */ function addManifest(manifest) { namepath.push(manifest.name || manifest.label); // get entries and sequence (alias for entries) const entries = [].concat( getJsonLdValues(manifest, 'entries'), getJsonLdValues(manifest, 'sequence') ); const includes = getJsonLdValues(manifest, 'include'); // add includes to sequence as jsonld files for(let i = 0; i < includes.length; ++i) { entries.push(includes[i] + '.jsonld'); } // process entries for(let i = 0; i < entries.length; ++i) { const entry = readManifestEntry(manifest, entries[i]); if(isJsonLdType(entry, 'mf:Manifest')) { // entry is another manifest addManifest(entry); } else { // assume entry is a test addTest(manifest, entry); } } namepath.pop(); } // i null for random, i number for incremental hashes mode // eslint-disable-next-line no-unused-vars function _bench({description, params, minSamples}) { const options = { name: description, defer: true, fn: function(deferred) { const promise = canonize.canonize.apply(null, params); promise .catch(err => assert.ifError(err)) .then(() => deferred.resolve()); } }; if(minSamples) { options.minSamples = minSamples; } return options; } function addTest(manifest, test) { // skip unknown and explicitly skipped test types const testTypes = Object.keys(TEST_TYPES); const skip = 'skip' in test && test.skip === true; if(skip || !isJsonLdType(test, testTypes) || isJsonLdType(test, SKIP_TESTS)) { const type = [].concat( getJsonLdValues(test, '@type'), getJsonLdValues(test, 'type') ); console.log('Skipping test "' + test.name + '" of type: ' + type); return; } // if ONLY env var set then only run if only is true if(process.env.ONLY && !('only' in test && test.only === true)) { return; } // expand @id and input base const test_id = test['@id'] || test.id; test['@id'] = manifest.baseIri + basename(manifest.filename) + test_id; test.base = manifest.baseIri + test.input; test.manifest = manifest; const description = test_id + ' ' + (test.purpose || test.name); const testInfo = TEST_TYPES[getTestType(test)]; // eslint-disable-next-line no-unused-vars const params = testInfo.params.map(param => param(test)); // custom params for js only async mode const jsParams = testInfo.params.map(param => param(test)); // custom params for native only async mode const nativeParams = testInfo.params.map(param => param(test)); nativeParams[1].useNative = true; // NOTE: the below omit error handling. run manifest with test suite first // number of parallel operations const N = 10; // run async js benchmark suite.add({ name: namepath.concat([description, '(asynchronous js)']).join(' / '), defer: true, fn: function(deferred) { canonize.canonize(...jsParams).then(() => deferred.resolve()); } }); // run async js benchmark x N suite.add({ name: namepath.concat( [description, `(asynchronous js x ${N})`]).join(' / '), defer: true, fn: function(deferred) { const all = []; for(let i = 0; i < N; ++i) { all.push(canonize.canonize(...jsParams)); } Promise.all(all).then(() => deferred.resolve()); } }); /* // run async js benchmark (callback) suite.add({ name: namepath.concat([description, '(asynchronous js / cb)']).join(' / '), defer: true, fn: function(deferred) { canonize.canonize(...jsParams, (err, output) => deferred.resolve()); } }); */ if(rdfCanonizeNative) { // run async native benchmark suite.add({ name: namepath.concat([description, '(asynchronous native)']).join(' / '), defer: true, fn: function(deferred) { canonize.canonize(...nativeParams).then(() => deferred.resolve()); } }); // run async native benchmark x N suite.add({ name: namepath.concat( [description, `(asynchronous native x ${N})`]).join(' / '), defer: true, fn: function(deferred) { const all = []; for(let i = 0; i < N; ++i) { all.push(canonize.canonize(...nativeParams)); } Promise.all(all).then(() => deferred.resolve()); } }); } // run sync js benchmark suite.add({ name: namepath.concat([description, '(synchronous js)']).join(' / '), defer: true, fn: function(deferred) { canonize._canonizeSync(...jsParams); deferred.resolve(); } }); // run sync js benchmark x N suite.add({ name: namepath.concat( [description, `(synchronous js x ${N})`]).join(' / '), defer: true, fn: function(deferred) { const all = []; for(let i = 0; i < N; ++i) { all.push(canonize._canonizeSync(...jsParams)); } Promise.all(all).then(() => deferred.resolve()); } }); if(rdfCanonizeNative) { // run sync native benchmark suite.add({ name: namepath.concat([description, '(synchronous native)']).join(' / '), defer: true, fn: function(deferred) { canonize._canonizeSync(...nativeParams); deferred.resolve(); } }); // run sync native benchmark x N suite.add({ name: namepath.concat( [description, `(synchronous native x ${N})`]).join(' / '), defer: true, fn: function(deferred) { const all = []; for(let i = 0; i < N; ++i) { all.push(canonize._canonizeSync(...nativeParams)); } Promise.all(all).then(() => deferred.resolve()); } }); } /* // run sync js benchmark (try/catch) suite.add({ name: namepath.concat( [description, '(synchronous js / try/catch)']).join(' / '), defer: true, fn: function(deferred) { try { canonize._canonizeSync(...jsParams); } catch(e) {} deferred.resolve(); } }); // run sync js benchmark (non-deferred) suite.add({ name: namepath.concat([description, '(synchronous js nd)']).join(' / '), fn: function() { canonize._canonizeSync(...jsParams); } }); // run sync js benchmark (non-deferred try/catch) suite.add({ name: namepath.concat([description, '(synchronous js nd/tc)']).join(' / '), fn: function() { try { canonize._canonizeSync(...jsParams); } catch(e) {} } }); */ } function getTestType(test) { const types = Object.keys(TEST_TYPES); for(let i = 0; i < types.length; ++i) { if(isJsonLdType(test, types[i])) { return types[i]; } } return null; } function readManifestEntry(manifest, entry) { const dir = dirname(manifest.filename); if(typeof entry === 'string') { const filename = joinPath(dir, entry); entry = readJson(filename); entry.filename = filename; } entry.dirname = dirname(entry.filename || manifest.filename); return entry; } function readTestNQuads(property) { return test => { if(!test[property]) { return null; } const filename = joinPath(test.dirname, test[property]); return readFile(filename); }; } function parseNQuads(fn) { return test => NQuads.parse(fn(test)); } function createTestOptions(opts) { return test => { const testOptions = test.option || {}; const options = Object.assign({}, testOptions); if(opts) { // extend options Object.assign(options, opts); } return options; }; } // find the expected output property or throw error // eslint-disable-next-line no-unused-vars function _getExpectProperty(test) { if('expect' in test) { return 'expect'; } else if('result' in test) { return 'result'; } else { throw Error('No expected output property found'); } } function isJsonLdType(node, type) { const nodeType = [].concat( getJsonLdValues(node, '@type'), getJsonLdValues(node, 'type') ); type = Array.isArray(type) ? type : [type]; for(let i = 0; i < type.length; ++i) { if(nodeType.indexOf(type[i]) !== -1) { return true; } } return false; } function getJsonLdValues(node, property) { let rval = []; if(property in node) { rval = [].concat(node[property]); } return rval; } function readJson(filename) { return JSON.parse(readFile(filename)); } function pathExists(filename) { if(_nodejs) { return fs.existsSync(filename); } return fs.exists(filename); } function readFile(filename) { if(_nodejs) { return fs.readFileSync(filename, 'utf8'); } return fs.read(filename); } function resolvePath(to) { if(_nodejs) { return path.resolve(to); } return fs.absolute(to); } function joinPath() { return (_nodejs ? path : fs).join.apply( null, Array.prototype.slice.call(arguments)); } function dirname(filename) { if(_nodejs) { return path.dirname(filename); } const idx = filename.lastIndexOf(fs.separator); if(idx === -1) { return filename; } return filename.substr(0, idx); } function basename(filename) { if(_nodejs) { return path.basename(filename); } const idx = filename.lastIndexOf(fs.separator); if(idx === -1) { return filename; } return filename.substr(idx + 1); } })(); rdf-canonize-3.4.0/benchmark/block-1.json000066400000000000000000000064631443175206400201710ustar00rootroot00000000000000{ "@context":"https://w3id.org/webledger/v1", "id":"did:ledgertest:eb8c22dc-bde6-4315-92e2-59bd3f3c7d59/blocks/0", "type":"WebLedgerEventBlock", "consensusMethod":"Continuity2017", "event":[ { "@context":"https://w3id.org/webledger/v1", "type":"WebLedgerEvent", "operation":"Create", "input":[ { "@context":"https://w3id.org/test/v1", "id":"https://example.com/events/61a3fd6b-5f2e-41f0-8cb9-d93152caab6b", "type":"Concert", "name":"Big Band Concert in New York City", "startDate":"2017-07-14T21:30", "location":"https://example.org/the-venue", "offers":{ "type":"Offer", "price":"13.00", "priceCurrency":"USD", "url":"https://www.ticketfly.com/purchase/309433" } } ], "signature":{ "type":"LinkedDataSignature2015", "created":"2017-10-12T16:42:14Z", "creator":"https://bedrock.local:18443/consensus/continuity2017/voters/9676bdb5-84c3-4547-9ab6-7d6d1193e1c1#key", "signatureValue":"bOzKLViLLVs29TT7EyyPxzfDT4+lc2Cn+fGbk2v5ZAJhZfcNcrQRdSDIog2z+7nbHrNfvjoARGyrvyJ7l45WalK3jt9AaJrcfj2hXcVt+kBE5Xlo2D4kwGqalqglq7eKwwWXCFzCh0tRmikFdaC9PhyKE/3SM02/uuJpsWTSIjQMmPPYpa4o6UqOSOJF+L7WMRd5uVf22SnE02KwYGo9YQd19UjAii8zNHKPh1b/lZLj8xeNSEZX1vOjEACVSD+XY270oOJNK1QR82HhoYprw38NjdPMwrwgm2UW4ZZYN88oBCoa71iEOIZfRMHDORfHzLsjvDrFMQ9gy8KgHlpaOA==" } } ], "electionResult":[ { "@context":"https://w3id.org/webledger/v1", "blockHeight":0, "manifestHash":"ni:///sha-256;K8uZ8_ht78lQBIvNbci2UJBNshhFRq9v1toFjSlZPkI", "voteRound":1, "voter":"https://bedrock.local:18443/consensus/continuity2017/voters/aa9c36a4-c497-43c8-b004-3e74825b762d", "recommendedElector":[ { "id":"https://bedrock.local:18443/consensus/continuity2017/voters/aa9c36a4-c497-43c8-b004-3e74825b762d" } ], "signature":{ "type":"LinkedDataSignature2015", "created":"2017-10-12T18:36:22Z", "creator":"https://bedrock.local:18443/consensus/continuity2017/voters/aa9c36a4-c497-43c8-b004-3e74825b762d#key", "signatureValue":"QGLNG/ajgFpX0M3eaBbTgmOFdfXsqYLYYU9E+zcIgCW3GE8oBupnl9MFJh30gtesKt86pW/C73oyBXWMa4+yLnoXoLIfdRP6o8o0PFARBPrCq+dOF0gX3v7ZiwZXxWzYx5L4LmFImNMFRCaKInMUuVM6nyV0cO0kJLHA4dXrcEXQY5j6mU2fbzW/AAVSE249zOFTk/v5WPaTWWmkedFbq39RS+VIE+CdqxDGkEPeWyDd1nZhfBh93epUfzoS4R4kvdSxEhbw/YXy9+SRgRS9OuDLDNQ13EBJG25pv1Vz/EKXvceyMJW9grTqoLlG1OzJBAIUPmjbiR7D1NSGlziq2w==" } } ], "blockHeight":0, "publicKey":[ { "id":"https://bedrock.local:18443/consensus/continuity2017/voters/aa9c36a4-c497-43c8-b004-3e74825b762d#key", "type":"CryptographicKey", "owner":"https://bedrock.local:18443/consensus/continuity2017/voters/aa9c36a4-c497-43c8-b004-3e74825b762d", "publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2HZeJEBfiRkcH6ckcS0x\nRwWukyVCbel2A7vq9/rr0KyxrQ5rsiVqp9wl6kCRLvF8+9SjBBGPupZ9o/W+Auwl\nYreV0OqV2RjZkJDqejNZQp7H2iBGdQcQnxRcRMSx/4g5t6aA+US8CQ5g4mgPpSit\nolSq924sI40AEUM4FLRho2d7jkkW+jwBlREL27g9UMXMwC7AbuyTS/NqYjj1RqOa\ncYXO+aCSYmVQTtB445fdpfcP86n5meOFiKaCUGII81Sny7ItJ2/De6OhTmFlq2q1\nQoWPDrvjtr+Y7rnQVZ4g8yQl+qXmVYRkg5ul3tm8Zuie5Sqy56S9iwH0RXBOUkIY\nfwIDAQAB\n-----END PUBLIC KEY-----\n" } ] } rdf-canonize-3.4.0/benchmark/make-tests.js000066400000000000000000000031341443175206400204510ustar00rootroot00000000000000/** * Make tests for benchmarks. * * @author Dave Longley * @author David I. Lehn * * Copyright (c) 2017-2021 Digital Bazaar, Inc. All rights reserved. */ /* eslint-disable indent */ (async () => { const fs = require('fs'); const jsonld = require('../../jsonld.js'); const b1json = fs.readFileSync('./block-1.json'); const b1 = JSON.parse(b1json); const loader = (url, callback) => { if(url === 'https://w3id.org/test/v1') { callback(null, { document: JSON.parse(fs.readFileSync('./test-v1.jsonld')) }); } if(url === 'https://w3id.org/webledger/v1') { return callback(null, { document: JSON.parse(fs.readFileSync('./webledger-v1.jsonld')) }); } return jsonld.loadDocument(url, callback); }; async function bn(b, n) { console.log(`Make block-${n}-{in,out}.nq`); const data = JSON.parse(JSON.stringify(b)); //console.log('nq0', data); const ev = data.event[0]; data.event = []; //console.log('nq1', data); for(let i = 0; i < n; ++i) { // copy and change id const newEv = JSON.parse(JSON.stringify(ev)); newEv.input[0].id = newEv.input[0].id + '-' + i; //console.log('push', newEv); data.event.push(newEv); } //console.log('nq2', data); const nq = await jsonld.toRDF(data, { format: 'application/n-quads', documentLoader: loader }); fs.writeFileSync(`./block-${n}-in.nq`, nq); const can = await jsonld.canonize(data, { documentLoader: loader }); fs.writeFileSync(`./block-${n}-out.nq`, can); } Promise.all([ bn(b1, 1), bn(b1, 2), bn(b1, 10), bn(b1, 100), bn(b1, 1000) ]).catch(e => console.error(e)); })(); rdf-canonize-3.4.0/benchmark/manifest.jsonld000066400000000000000000000045061443175206400210630ustar00rootroot00000000000000{ "@context": { "xsd": "http://www.w3.org/2001/XMLSchema#", "rdfs": "http://www.w3.org/2000/01/rdf-schema#", "mf": "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#", "mq": "http://www.w3.org/2001/sw/DataAccess/tests/test-query#", "rdfc": "https://w3c.github.io/rdf-canon/tests/vocab#", "rdft": "http://www.w3.org/ns/rdftest#", "id": "@id", "type": "@type", "action": { "@id": "mf:action", "@type": "@id" }, "approval": { "@id": "rdft:approval", "@type": "@id" }, "comment": "rdfs:comment", "entries": { "@id": "mf:entries", "@type": "@id", "@container": "@list" }, "label": "rdfs:label", "name": "mf:name", "result": { "@id": "mf:result", "@type": "@id" } }, "id": "", "type": "mf:Manifest", "label": "RDF Dataset Normalization", "comment": "RDF Dataset Normalization benchmarks.", "baseIri": "https://w3c.github.io/rdf-canon/tests/#", "entries": [ { "skip": false, "only": false, "id": "#block-1", "type": "rdfc:Urdna2015EvalTest", "name": "block-1", "comment": null, "approval": "rdft:Proposed", "action": "block-1-in.nq", "result": "block-1-out.nq" }, { "skip": false, "only": false, "id": "#block-2", "type": "rdfc:Urdna2015EvalTest", "name": "block-2", "comment": null, "approval": "rdft:Proposed", "action": "block-2-in.nq", "result": "block-2-out.nq" }, { "skip": false, "only": false, "id": "#block-10", "type": "rdfc:Urdna2015EvalTest", "name": "block-10", "comment": null, "approval": "rdft:Proposed", "action": "block-10-in.nq", "result": "block-10-out.nq" }, { "skip": false, "only": false, "id": "#block-100", "type": "rdfc:Urdna2015EvalTest", "name": "block-100", "comment": null, "approval": "rdft:Proposed", "action": "block-100-in.nq", "result": "block-100-out.nq" }, { "skip": false, "only": false, "id": "#block-1000", "type": "rdfc:Urdna2015EvalTest", "name": "block-1000", "comment": null, "approval": "rdft:Proposed", "action": "block-1000-in.nq", "result": "block-1000-out.nq" } ] } rdf-canonize-3.4.0/benchmark/test-v1.jsonld000066400000000000000000000013331443175206400205530ustar00rootroot00000000000000{ "@context": { "id": "@id", "type": "@type", "cred": "https://w3id.org/credentials#", "dhs": "https://w3id.org/dhs#", "schema": "http://schema.org/", "@vocab": "https://w3id.org/dhs#", "Concert": "schema:Concert", "Offer": "schema:Offer", "Credential": "cred:Credential", "EmergencyResponseCredential": "dhs:EmergencyResponseCredential", "claim": {"@id": "cred:claim", "@type": "@id"}, "emsLicense": {"@id": "dhs:emsLicense", "@type": "@id"}, "location": "schema:location", "name": "schema:name", "offers": "schema:offers", "price": "schema:price", "priceCurrency": "schema:priceCurrency", "startDate": "schema:startDate", "url": "schema:url" } } rdf-canonize-3.4.0/benchmark/webledger-v1.jsonld000066400000000000000000000100721443175206400215340ustar00rootroot00000000000000{ "@context": { "@version": 1.1, "id": "@id", "type": "@type", "dc": "http://purl.org/dc/terms/", "identity": "https://w3id.org/identity#", "rdfs": "http://www.w3.org/2000/01/rdf-schema#", "schema": "http://schema.org/", "sec": "https://w3id.org/security#", "wl": "https://w3id.org/wl#", "xsd": "http://www.w3.org/2001/XMLSchema#", "Config": "wl:Config", "Continuity2017": "wl:Continuity2017", "Continuity2017Peer": "wl:Continuity2017Peer", "Create": "wl:Create", "CryptographicKey": "sec:Key", "EquihashProof2017": "sec:EquihashProof2017", "GraphSignature2012": "sec:GraphSignature2012", "Identity": "identity:Identity", "LinkedDataSignature2015": "sec:LinkedDataSignature2015", "LinkedDataSignature2016": "sec:LinkedDataSignature2016", "ProofOfSignature2017": "wl:ProofOfSignature2017", "ProofOfWork2016": "wl:ProofOfWork2016", "SequentialList": "wl:SequentialList", "UnilateralConsensus2017": "wl:UnilateralConsensus2017", "WebLedgerConfiguration": "wl:WebLedgerConfiguration", "WebLedgerConfigurationEvent": "wl:WebLedgerConfigurationEvent", "WebLedgerEvent": "wl:WebLedgerEvent", "WebLedgerEventBlock": "wl:WebLedgerEventBlock", "approvedSigner": "wl:approvedSigner", "authenticationCredential": {"@id": "sec:authenticationCredential", "@type": "@id", "@container": "@set"}, "blockHeight": "wl:blockHeight", "canonicalizationAlgorithm": "sec:canonicalizationAlgorithm", "comment": "rdfs:comment", "consensusMethod": {"@id": "wl:consensusMethod", "@type": "@vocab"}, "consensusPhase": "wl:consensusPhase", "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, "creator": {"@id": "dc:creator", "@type": "@id"}, "description": "schema:description", "digestAlgorithm": "sec:digestAlgorithm", "digestValue": "sec:digestValue", "domain": "sec:domain", "election": {"@id": "wl:election", "@container": "@set"}, "electionResult": {"@id": "wl:electionResults", "@container": "@set"}, "electorCount": {"@id": "wl:electorCount", "@type": "xsd:integer"}, "equihashParameterK": {"@id": "sec:equihashParameterK", "@type": "xsd:integer"}, "equihashParameterN": {"@id": "sec:equihashParameterN", "@type": "xsd:integer"}, "event": {"@id": "wl:event", "@type": "@id", "@container": ["@graph", "@set"]}, "eventFilter": {"@id": "wl:eventFilter", "@type": "@id"}, "eventHash": "wl:eventHash", "eventType": "wl:eventType", "eventValidator": {"@id": "wl:eventValidator", "@type": "@id"}, "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, "input": {"@id": "wl:input", "@type": "@id", "@container": ["@graph", "@set"]}, "label": "rdfs:label", "ledger": {"@id": "wl:ledger", "@type": "@id"}, "ledgerConfiguration": {"@id": "wl:ledgerConfiguration", "@type": "@id"}, "manifestHash": "wl:manifestHash", "minimumSignaturesRequired": "wl:minimumSignaturesRequired", "name": "schema:name", "nonce": "sec:nonce", "normalizationAlgorithm": "sec:normalizationAlgorithm", "operation": "wl:operation", "owner": {"@id": "sec:owner", "@type": "@id"}, "previousBlock": "wl:previousBlock", "previousBlockHash": "wl:previousBlockHash", "privateKey": {"@id": "sec:privateKey", "@type": "@id"}, "privateKeyPem": "sec:privateKeyPem", "proofAlgorithm": "sec:proofAlgorithm", "proofValue": "sec:proofValue", "publicKey": {"@id": "sec:publicKey", "@type": "@id", "@container": "@set"}, "publicKeyPem": "sec:publicKeyPem", "recommendedElector": {"@id": "wl:recommendedElectors", "@container": "@set"}, "requireEventValidation": "wl:requireEventValidation", "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"}, "seeAlso": {"@id": "rdfs:seeAlso", "@type": "@id"}, "signature": "sec:signature", "signatureAlgorithm": "sec:signatureAlgorithm", "signatureValue": "sec:signatureValue", "supportedEventType": "wl:supportedEventType", "voteRound": {"@id": "wl:voteRound", "@type": "xsd:integer"}, "voter": {"@id": "wl:voteRound", "@type": "@id"} } } rdf-canonize-3.4.0/index.js000066400000000000000000000002721443175206400155510ustar00rootroot00000000000000/** * An implementation of the RDF Dataset Normalization specification. * * @author Dave Longley * * Copyright 2010-2021 Digital Bazaar, Inc. */ module.exports = require('./lib'); rdf-canonize-3.4.0/lib/000077500000000000000000000000001443175206400146515ustar00rootroot00000000000000rdf-canonize-3.4.0/lib/IdentifierIssuer.js000066400000000000000000000037411443175206400204710ustar00rootroot00000000000000/* * Copyright (c) 2016-2021 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; module.exports = class IdentifierIssuer { /** * Creates a new IdentifierIssuer. A IdentifierIssuer issues unique * identifiers, keeping track of any previously issued identifiers. * * @param prefix the prefix to use (''). * @param existing an existing Map to use. * @param counter the counter to use. */ constructor(prefix, existing = new Map(), counter = 0) { this.prefix = prefix; this._existing = existing; this.counter = counter; } /** * Copies this IdentifierIssuer. * * @return a copy of this IdentifierIssuer. */ clone() { const {prefix, _existing, counter} = this; return new IdentifierIssuer(prefix, new Map(_existing), counter); } /** * Gets the new identifier for the given old identifier, where if no old * identifier is given a new identifier will be generated. * * @param [old] the old identifier to get the new identifier for. * * @return the new identifier. */ getId(old) { // return existing old identifier const existing = old && this._existing.get(old); if(existing) { return existing; } // get next identifier const identifier = this.prefix + this.counter; this.counter++; // save mapping if(old) { this._existing.set(old, identifier); } return identifier; } /** * Returns true if the given old identifer has already been assigned a new * identifier. * * @param old the old identifier to check. * * @return true if the old identifier has been assigned a new identifier, * false if not. */ hasId(old) { return this._existing.has(old); } /** * Returns all of the IDs that have been issued new IDs in the order in * which they were issued new IDs. * * @return the list of old IDs that has been issued new IDs in order. */ getOldIds() { return [...this._existing.keys()]; } }; rdf-canonize-3.4.0/lib/MessageDigest-browser.js000066400000000000000000000022461443175206400214200ustar00rootroot00000000000000/*! * Copyright (c) 2016-2022 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; require('setimmediate'); const crypto = self.crypto || self.msCrypto; module.exports = class MessageDigest { /** * Creates a new MessageDigest. * * @param algorithm the algorithm to use. */ constructor(algorithm) { // check if crypto.subtle is available // check is here rather than top-level to only fail if class is used if(!(crypto && crypto.subtle)) { throw new Error('crypto.subtle not found.'); } if(algorithm === 'sha256') { this.algorithm = {name: 'SHA-256'}; } else if(algorithm === 'sha1') { this.algorithm = {name: 'SHA-1'}; } else { throw new Error(`Unsupported algorithm "${algorithm}".`); } this._content = ''; } update(msg) { this._content += msg; } async digest() { const data = new TextEncoder().encode(this._content); const buffer = new Uint8Array( await crypto.subtle.digest(this.algorithm, data)); // return digest in hex let hex = ''; for(let i = 0; i < buffer.length; ++i) { hex += buffer[i].toString(16).padStart(2, '0'); } return hex; } }; rdf-canonize-3.4.0/lib/MessageDigest.js000066400000000000000000000006741443175206400177420ustar00rootroot00000000000000/* * Copyright (c) 2016-2021 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const crypto = require('crypto'); module.exports = class MessageDigest { /** * Creates a new MessageDigest. * * @param algorithm the algorithm to use. */ constructor(algorithm) { this.md = crypto.createHash(algorithm); } update(msg) { this.md.update(msg, 'utf8'); } digest() { return this.md.digest('hex'); } }; rdf-canonize-3.4.0/lib/NQuads.js000066400000000000000000000253121443175206400164050ustar00rootroot00000000000000/*! * Copyright (c) 2016-2022 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; // eslint-disable-next-line no-unused-vars const TERMS = ['subject', 'predicate', 'object', 'graph']; const RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; const RDF_LANGSTRING = RDF + 'langString'; const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string'; const TYPE_NAMED_NODE = 'NamedNode'; const TYPE_BLANK_NODE = 'BlankNode'; const TYPE_LITERAL = 'Literal'; const TYPE_DEFAULT_GRAPH = 'DefaultGraph'; // build regexes const REGEX = {}; (() => { const iri = '(?:<([^:]+:[^>]*)>)'; // https://www.w3.org/TR/turtle/#grammar-production-BLANK_NODE_LABEL const PN_CHARS_BASE = 'A-Z' + 'a-z' + '\u00C0-\u00D6' + '\u00D8-\u00F6' + '\u00F8-\u02FF' + '\u0370-\u037D' + '\u037F-\u1FFF' + '\u200C-\u200D' + '\u2070-\u218F' + '\u2C00-\u2FEF' + '\u3001-\uD7FF' + '\uF900-\uFDCF' + '\uFDF0-\uFFFD'; // TODO: //'\u10000-\uEFFFF'; const PN_CHARS_U = PN_CHARS_BASE + '_'; const PN_CHARS = PN_CHARS_U + '0-9' + '-' + '\u00B7' + '\u0300-\u036F' + '\u203F-\u2040'; const BLANK_NODE_LABEL = '(_:' + '(?:[' + PN_CHARS_U + '0-9])' + '(?:(?:[' + PN_CHARS + '.])*(?:[' + PN_CHARS + ']))?' + ')'; const bnode = BLANK_NODE_LABEL; const plain = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"'; const datatype = '(?:\\^\\^' + iri + ')'; const language = '(?:@([a-zA-Z]+(?:-[a-zA-Z0-9]+)*))'; const literal = '(?:' + plain + '(?:' + datatype + '|' + language + ')?)'; const ws = '[ \\t]+'; const wso = '[ \\t]*'; // define quad part regexes const subject = '(?:' + iri + '|' + bnode + ')' + ws; const property = iri + ws; const object = '(?:' + iri + '|' + bnode + '|' + literal + ')' + wso; const graphName = '(?:\\.|(?:(?:' + iri + '|' + bnode + ')' + wso + '\\.))'; // end of line and empty regexes REGEX.eoln = /(?:\r\n)|(?:\n)|(?:\r)/g; REGEX.empty = new RegExp('^' + wso + '$'); // full quad regex REGEX.quad = new RegExp( '^' + wso + subject + property + object + graphName + wso + '$'); })(); module.exports = class NQuads { /** * Parses RDF in the form of N-Quads. * * @param input the N-Quads input to parse. * * @return an RDF dataset (an array of quads per http://rdf.js.org/). */ static parse(input) { // build RDF dataset const dataset = []; const graphs = {}; // split N-Quad input into lines const lines = input.split(REGEX.eoln); let lineNumber = 0; for(const line of lines) { lineNumber++; // skip empty lines if(REGEX.empty.test(line)) { continue; } // parse quad const match = line.match(REGEX.quad); if(match === null) { throw new Error('N-Quads parse error on line ' + lineNumber + '.'); } // create RDF quad const quad = {subject: null, predicate: null, object: null, graph: null}; // get subject if(match[1] !== undefined) { quad.subject = {termType: TYPE_NAMED_NODE, value: match[1]}; } else { quad.subject = {termType: TYPE_BLANK_NODE, value: match[2]}; } // get predicate quad.predicate = {termType: TYPE_NAMED_NODE, value: match[3]}; // get object if(match[4] !== undefined) { quad.object = {termType: TYPE_NAMED_NODE, value: match[4]}; } else if(match[5] !== undefined) { quad.object = {termType: TYPE_BLANK_NODE, value: match[5]}; } else { quad.object = { termType: TYPE_LITERAL, value: undefined, datatype: { termType: TYPE_NAMED_NODE } }; if(match[7] !== undefined) { quad.object.datatype.value = match[7]; } else if(match[8] !== undefined) { quad.object.datatype.value = RDF_LANGSTRING; quad.object.language = match[8]; } else { quad.object.datatype.value = XSD_STRING; } quad.object.value = _unescape(match[6]); } // get graph if(match[9] !== undefined) { quad.graph = { termType: TYPE_NAMED_NODE, value: match[9] }; } else if(match[10] !== undefined) { quad.graph = { termType: TYPE_BLANK_NODE, value: match[10] }; } else { quad.graph = { termType: TYPE_DEFAULT_GRAPH, value: '' }; } // only add quad if it is unique in its graph if(!(quad.graph.value in graphs)) { graphs[quad.graph.value] = [quad]; dataset.push(quad); } else { let unique = true; const quads = graphs[quad.graph.value]; for(const q of quads) { if(_compareTriples(q, quad)) { unique = false; break; } } if(unique) { quads.push(quad); dataset.push(quad); } } } return dataset; } /** * Converts an RDF dataset to N-Quads. * * @param dataset (array of quads) the RDF dataset to convert. * * @return the N-Quads string. */ static serialize(dataset) { if(!Array.isArray(dataset)) { dataset = NQuads.legacyDatasetToQuads(dataset); } const quads = []; for(const quad of dataset) { quads.push(NQuads.serializeQuad(quad)); } return quads.sort().join(''); } /** * Converts RDF quad components to an N-Quad string (a single quad). * * @param {Object} s - N-Quad subject component. * @param {Object} p - N-Quad predicate component. * @param {Object} o - N-Quad object component. * @param {Object} g - N-Quad graph component. * * @return {string} the N-Quad. */ static serializeQuadComponents(s, p, o, g) { let nquad = ''; // subject can only be NamedNode or BlankNode if(s.termType === TYPE_NAMED_NODE) { nquad += `<${s.value}>`; } else { nquad += `${s.value}`; } // predicate can only be NamedNode nquad += ` <${p.value}> `; // object is NamedNode, BlankNode, or Literal if(o.termType === TYPE_NAMED_NODE) { nquad += `<${o.value}>`; } else if(o.termType === TYPE_BLANK_NODE) { nquad += o.value; } else { nquad += `"${_escape(o.value)}"`; if(o.datatype.value === RDF_LANGSTRING) { if(o.language) { nquad += `@${o.language}`; } } else if(o.datatype.value !== XSD_STRING) { nquad += `^^<${o.datatype.value}>`; } } // graph can only be NamedNode or BlankNode (or DefaultGraph, but that // does not add to `nquad`) if(g.termType === TYPE_NAMED_NODE) { nquad += ` <${g.value}>`; } else if(g.termType === TYPE_BLANK_NODE) { nquad += ` ${g.value}`; } nquad += ' .\n'; return nquad; } /** * Converts an RDF quad to an N-Quad string (a single quad). * * @param quad the RDF quad convert. * * @return the N-Quad string. */ static serializeQuad(quad) { return NQuads.serializeQuadComponents( quad.subject, quad.predicate, quad.object, quad.graph); } /** * Converts a legacy-formatted dataset to an array of quads dataset per * http://rdf.js.org/. * * @param dataset the legacy dataset to convert. * * @return the array of quads dataset. */ static legacyDatasetToQuads(dataset) { const quads = []; const termTypeMap = { 'blank node': TYPE_BLANK_NODE, IRI: TYPE_NAMED_NODE, literal: TYPE_LITERAL }; for(const graphName in dataset) { const triples = dataset[graphName]; triples.forEach(triple => { const quad = {}; for(const componentName in triple) { const oldComponent = triple[componentName]; const newComponent = { termType: termTypeMap[oldComponent.type], value: oldComponent.value }; if(newComponent.termType === TYPE_LITERAL) { newComponent.datatype = { termType: TYPE_NAMED_NODE }; if('datatype' in oldComponent) { newComponent.datatype.value = oldComponent.datatype; } if('language' in oldComponent) { if(!('datatype' in oldComponent)) { newComponent.datatype.value = RDF_LANGSTRING; } newComponent.language = oldComponent.language; } else if(!('datatype' in oldComponent)) { newComponent.datatype.value = XSD_STRING; } } quad[componentName] = newComponent; } if(graphName === '@default') { quad.graph = { termType: TYPE_DEFAULT_GRAPH, value: '' }; } else { quad.graph = { termType: graphName.startsWith('_:') ? TYPE_BLANK_NODE : TYPE_NAMED_NODE, value: graphName }; } quads.push(quad); }); } return quads; } }; /** * Compares two RDF triples for equality. * * @param t1 the first triple. * @param t2 the second triple. * * @return true if the triples are the same, false if not. */ function _compareTriples(t1, t2) { // compare subject and object types first as it is the quickest check if(!(t1.subject.termType === t2.subject.termType && t1.object.termType === t2.object.termType)) { return false; } // compare values if(!(t1.subject.value === t2.subject.value && t1.predicate.value === t2.predicate.value && t1.object.value === t2.object.value)) { return false; } if(t1.object.termType !== TYPE_LITERAL) { // no `datatype` or `language` to check return true; } return ( (t1.object.datatype.termType === t2.object.datatype.termType) && (t1.object.language === t2.object.language) && (t1.object.datatype.value === t2.object.datatype.value) ); } const _escapeRegex = /["\\\n\r]/g; /** * Escape string to N-Quads literal */ function _escape(s) { return s.replace(_escapeRegex, function(match) { switch(match) { case '"': return '\\"'; case '\\': return '\\\\'; case '\n': return '\\n'; case '\r': return '\\r'; } }); } const _unescapeRegex = /(?:\\([tbnrf"'\\]))|(?:\\u([0-9A-Fa-f]{4}))|(?:\\U([0-9A-Fa-f]{8}))/g; /** * Unescape N-Quads literal to string */ function _unescape(s) { return s.replace(_unescapeRegex, function(match, code, u, U) { if(code) { switch(code) { case 't': return '\t'; case 'b': return '\b'; case 'n': return '\n'; case 'r': return '\r'; case 'f': return '\f'; case '"': return '"'; case '\'': return '\''; case '\\': return '\\'; } } if(u) { return String.fromCharCode(parseInt(u, 16)); } if(U) { // FIXME: support larger values throw new Error('Unsupported U escape'); } }); } rdf-canonize-3.4.0/lib/Permuter.js000066400000000000000000000041131443175206400170110ustar00rootroot00000000000000/*! * Copyright (c) 2016-2022 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; module.exports = class Permuter { /** * A Permuter iterates over all possible permutations of the given array * of elements. * * @param list the array of elements to iterate over. */ constructor(list) { // original array this.current = list.sort(); // indicates whether there are more permutations this.done = false; // directional info for permutation algorithm this.dir = new Map(); for(let i = 0; i < list.length; ++i) { this.dir.set(list[i], true); } } /** * Returns true if there is another permutation. * * @return true if there is another permutation, false if not. */ hasNext() { return !this.done; } /** * Gets the next permutation. Call hasNext() to ensure there is another one * first. * * @return the next permutation. */ next() { // copy current permutation to return it const {current, dir} = this; const rval = current.slice(); /* Calculate the next permutation using the Steinhaus-Johnson-Trotter permutation algorithm. */ // get largest mobile element k // (mobile: element is greater than the one it is looking at) let k = null; let pos = 0; const length = current.length; for(let i = 0; i < length; ++i) { const element = current[i]; const left = dir.get(element); if((k === null || element > k) && ((left && i > 0 && element > current[i - 1]) || (!left && i < (length - 1) && element > current[i + 1]))) { k = element; pos = i; } } // no more permutations if(k === null) { this.done = true; } else { // swap k and the element it is looking at const swap = dir.get(k) ? pos - 1 : pos + 1; current[pos] = current[swap]; current[swap] = k; // reverse the direction of all elements larger than k for(const element of current) { if(element > k) { dir.set(element, !dir.get(element)); } } } return rval; } }; rdf-canonize-3.4.0/lib/URDNA2015.js000066400000000000000000000455701443175206400164030ustar00rootroot00000000000000/*! * Copyright (c) 2016-2022 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const IdentifierIssuer = require('./IdentifierIssuer'); const MessageDigest = require('./MessageDigest'); const Permuter = require('./Permuter'); const NQuads = require('./NQuads'); module.exports = class URDNA2015 { constructor({ createMessageDigest = () => new MessageDigest('sha256'), canonicalIdMap = new Map(), maxDeepIterations = Infinity } = {}) { this.name = 'URDNA2015'; this.blankNodeInfo = new Map(); this.canonicalIssuer = new IdentifierIssuer('_:c14n', canonicalIdMap); this.createMessageDigest = createMessageDigest; this.maxDeepIterations = maxDeepIterations; this.quads = null; this.deepIterations = null; } // 4.4) Normalization Algorithm async main(dataset) { this.deepIterations = new Map(); this.quads = dataset; // 1) Create the normalization state. // 2) For every quad in input dataset: for(const quad of dataset) { // 2.1) For each blank node that occurs in the quad, add a reference // to the quad using the blank node identifier in the blank node to // quads map, creating a new entry if necessary. this._addBlankNodeQuadInfo({quad, component: quad.subject}); this._addBlankNodeQuadInfo({quad, component: quad.object}); this._addBlankNodeQuadInfo({quad, component: quad.graph}); } // 3) Create a list of non-normalized blank node identifiers // non-normalized identifiers and populate it using the keys from the // blank node to quads map. // Note: We use a map here and it was generated during step 2. // 4) `simple` flag is skipped -- loop is optimized away. This optimization // is permitted because there was a typo in the hash first degree quads // algorithm in the URDNA2015 spec that was implemented widely making it // such that it could not be fixed; the result was that the loop only // needs to be run once and the first degree quad hashes will never change. // 5.1-5.2 are skipped; first degree quad hashes are generated just once // for all non-normalized blank nodes. // 5.3) For each blank node identifier identifier in non-normalized // identifiers: const hashToBlankNodes = new Map(); const nonNormalized = [...this.blankNodeInfo.keys()]; let i = 0; for(const id of nonNormalized) { // Note: batch hashing first degree quads 100 at a time if(++i % 100 === 0) { await this._yield(); } // steps 5.3.1 and 5.3.2: await this._hashAndTrackBlankNode({id, hashToBlankNodes}); } // 5.4) For each hash to identifier list mapping in hash to blank // nodes map, lexicographically-sorted by hash: const hashes = [...hashToBlankNodes.keys()].sort(); // optimize away second sort, gather non-unique hashes in order as we go const nonUnique = []; for(const hash of hashes) { // 5.4.1) If the length of identifier list is greater than 1, // continue to the next mapping. const idList = hashToBlankNodes.get(hash); if(idList.length > 1) { nonUnique.push(idList); continue; } // 5.4.2) Use the Issue Identifier algorithm, passing canonical // issuer and the single blank node identifier in identifier // list, identifier, to issue a canonical replacement identifier // for identifier. const id = idList[0]; this.canonicalIssuer.getId(id); // Note: These steps are skipped, optimized away since the loop // only needs to be run once. // 5.4.3) Remove identifier from non-normalized identifiers. // 5.4.4) Remove hash from the hash to blank nodes map. // 5.4.5) Set simple to true. } // 6) For each hash to identifier list mapping in hash to blank nodes map, // lexicographically-sorted by hash: // Note: sort optimized away, use `nonUnique`. for(const idList of nonUnique) { // 6.1) Create hash path list where each item will be a result of // running the Hash N-Degree Quads algorithm. const hashPathList = []; // 6.2) For each blank node identifier identifier in identifier list: for(const id of idList) { // 6.2.1) If a canonical identifier has already been issued for // identifier, continue to the next identifier. if(this.canonicalIssuer.hasId(id)) { continue; } // 6.2.2) Create temporary issuer, an identifier issuer // initialized with the prefix _:b. const issuer = new IdentifierIssuer('_:b'); // 6.2.3) Use the Issue Identifier algorithm, passing temporary // issuer and identifier, to issue a new temporary blank node // identifier for identifier. issuer.getId(id); // 6.2.4) Run the Hash N-Degree Quads algorithm, passing // temporary issuer, and append the result to the hash path list. const result = await this.hashNDegreeQuads(id, issuer); hashPathList.push(result); } // 6.3) For each result in the hash path list, // lexicographically-sorted by the hash in result: hashPathList.sort(_stringHashCompare); for(const result of hashPathList) { // 6.3.1) For each blank node identifier, existing identifier, // that was issued a temporary identifier by identifier issuer // in result, issue a canonical identifier, in the same order, // using the Issue Identifier algorithm, passing canonical // issuer and existing identifier. const oldIds = result.issuer.getOldIds(); for(const id of oldIds) { this.canonicalIssuer.getId(id); } } } /* Note: At this point all blank nodes in the set of RDF quads have been assigned canonical identifiers, which have been stored in the canonical issuer. Here each quad is updated by assigning each of its blank nodes its new identifier. */ // 7) For each quad, quad, in input dataset: const normalized = []; for(const quad of this.quads) { // 7.1) Create a copy, quad copy, of quad and replace any existing // blank node identifiers using the canonical identifiers // previously issued by canonical issuer. // Note: We optimize away the copy here. const nQuad = NQuads.serializeQuadComponents( this._componentWithCanonicalId(quad.subject), quad.predicate, this._componentWithCanonicalId(quad.object), this._componentWithCanonicalId(quad.graph) ); // 7.2) Add quad copy to the normalized dataset. normalized.push(nQuad); } // sort normalized output normalized.sort(); // 8) Return the normalized dataset. return normalized.join(''); } // 4.6) Hash First Degree Quads async hashFirstDegreeQuads(id) { // 1) Initialize nquads to an empty list. It will be used to store quads in // N-Quads format. const nquads = []; // 2) Get the list of quads `quads` associated with the reference blank node // identifier in the blank node to quads map. const info = this.blankNodeInfo.get(id); const quads = info.quads; // 3) For each quad `quad` in `quads`: for(const quad of quads) { // 3.1) Serialize the quad in N-Quads format with the following special // rule: // 3.1.1) If any component in quad is an blank node, then serialize it // using a special identifier as follows: const copy = { subject: null, predicate: quad.predicate, object: null, graph: null }; // 3.1.2) If the blank node's existing blank node identifier matches // the reference blank node identifier then use the blank node // identifier _:a, otherwise, use the blank node identifier _:z. copy.subject = this.modifyFirstDegreeComponent( id, quad.subject, 'subject'); copy.object = this.modifyFirstDegreeComponent( id, quad.object, 'object'); copy.graph = this.modifyFirstDegreeComponent( id, quad.graph, 'graph'); nquads.push(NQuads.serializeQuad(copy)); } // 4) Sort nquads in lexicographical order. nquads.sort(); // 5) Return the hash that results from passing the sorted, joined nquads // through the hash algorithm. const md = this.createMessageDigest(); for(const nquad of nquads) { md.update(nquad); } info.hash = await md.digest(); return info.hash; } // 4.7) Hash Related Blank Node async hashRelatedBlankNode(related, quad, issuer, position) { // 1) Set the identifier to use for related, preferring first the canonical // identifier for related if issued, second the identifier issued by issuer // if issued, and last, if necessary, the result of the Hash First Degree // Quads algorithm, passing related. let id; if(this.canonicalIssuer.hasId(related)) { id = this.canonicalIssuer.getId(related); } else if(issuer.hasId(related)) { id = issuer.getId(related); } else { id = this.blankNodeInfo.get(related).hash; } // 2) Initialize a string input to the value of position. // Note: We use a hash object instead. const md = this.createMessageDigest(); md.update(position); // 3) If position is not g, append <, the value of the predicate in quad, // and > to input. if(position !== 'g') { md.update(this.getRelatedPredicate(quad)); } // 4) Append identifier to input. md.update(id); // 5) Return the hash that results from passing input through the hash // algorithm. return md.digest(); } // 4.8) Hash N-Degree Quads async hashNDegreeQuads(id, issuer) { const deepIterations = this.deepIterations.get(id) || 0; if(deepIterations > this.maxDeepIterations) { throw new Error( `Maximum deep iterations (${this.maxDeepIterations}) exceeded.`); } this.deepIterations.set(id, deepIterations + 1); // 1) Create a hash to related blank nodes map for storing hashes that // identify related blank nodes. // Note: 2) and 3) handled within `createHashToRelated` const md = this.createMessageDigest(); const hashToRelated = await this.createHashToRelated(id, issuer); // 4) Create an empty string, data to hash. // Note: We created a hash object `md` above instead. // 5) For each related hash to blank node list mapping in hash to related // blank nodes map, sorted lexicographically by related hash: const hashes = [...hashToRelated.keys()].sort(); for(const hash of hashes) { // 5.1) Append the related hash to the data to hash. md.update(hash); // 5.2) Create a string chosen path. let chosenPath = ''; // 5.3) Create an unset chosen issuer variable. let chosenIssuer; // 5.4) For each permutation of blank node list: const permuter = new Permuter(hashToRelated.get(hash)); let i = 0; while(permuter.hasNext()) { const permutation = permuter.next(); // Note: batch permutations 3 at a time if(++i % 3 === 0) { await this._yield(); } // 5.4.1) Create a copy of issuer, issuer copy. let issuerCopy = issuer.clone(); // 5.4.2) Create a string path. let path = ''; // 5.4.3) Create a recursion list, to store blank node identifiers // that must be recursively processed by this algorithm. const recursionList = []; // 5.4.4) For each related in permutation: let nextPermutation = false; for(const related of permutation) { // 5.4.4.1) If a canonical identifier has been issued for // related, append it to path. if(this.canonicalIssuer.hasId(related)) { path += this.canonicalIssuer.getId(related); } else { // 5.4.4.2) Otherwise: // 5.4.4.2.1) If issuer copy has not issued an identifier for // related, append related to recursion list. if(!issuerCopy.hasId(related)) { recursionList.push(related); } // 5.4.4.2.2) Use the Issue Identifier algorithm, passing // issuer copy and related and append the result to path. path += issuerCopy.getId(related); } // 5.4.4.3) If chosen path is not empty and the length of path // is greater than or equal to the length of chosen path and // path is lexicographically greater than chosen path, then // skip to the next permutation. // Note: Comparing path length to chosen path length can be optimized // away; only compare lexicographically. if(chosenPath.length !== 0 && path > chosenPath) { nextPermutation = true; break; } } if(nextPermutation) { continue; } // 5.4.5) For each related in recursion list: for(const related of recursionList) { // 5.4.5.1) Set result to the result of recursively executing // the Hash N-Degree Quads algorithm, passing related for // identifier and issuer copy for path identifier issuer. const result = await this.hashNDegreeQuads(related, issuerCopy); // 5.4.5.2) Use the Issue Identifier algorithm, passing issuer // copy and related and append the result to path. path += issuerCopy.getId(related); // 5.4.5.3) Append <, the hash in result, and > to path. path += `<${result.hash}>`; // 5.4.5.4) Set issuer copy to the identifier issuer in // result. issuerCopy = result.issuer; // 5.4.5.5) If chosen path is not empty and the length of path // is greater than or equal to the length of chosen path and // path is lexicographically greater than chosen path, then // skip to the next permutation. // Note: Comparing path length to chosen path length can be optimized // away; only compare lexicographically. if(chosenPath.length !== 0 && path > chosenPath) { nextPermutation = true; break; } } if(nextPermutation) { continue; } // 5.4.6) If chosen path is empty or path is lexicographically // less than chosen path, set chosen path to path and chosen // issuer to issuer copy. if(chosenPath.length === 0 || path < chosenPath) { chosenPath = path; chosenIssuer = issuerCopy; } } // 5.5) Append chosen path to data to hash. md.update(chosenPath); // 5.6) Replace issuer, by reference, with chosen issuer. issuer = chosenIssuer; } // 6) Return issuer and the hash that results from passing data to hash // through the hash algorithm. return {hash: await md.digest(), issuer}; } // helper for modifying component during Hash First Degree Quads modifyFirstDegreeComponent(id, component) { if(component.termType !== 'BlankNode') { return component; } /* Note: A mistake in the URDNA2015 spec that made its way into implementations (and therefore must stay to avoid interop breakage) resulted in an assigned canonical ID, if available for `component.value`, not being used in place of `_:a`/`_:z`, so we don't use it here. */ return { termType: 'BlankNode', value: component.value === id ? '_:a' : '_:z' }; } // helper for getting a related predicate getRelatedPredicate(quad) { return `<${quad.predicate.value}>`; } // helper for creating hash to related blank nodes map async createHashToRelated(id, issuer) { // 1) Create a hash to related blank nodes map for storing hashes that // identify related blank nodes. const hashToRelated = new Map(); // 2) Get a reference, quads, to the list of quads in the blank node to // quads map for the key identifier. const quads = this.blankNodeInfo.get(id).quads; // 3) For each quad in quads: let i = 0; for(const quad of quads) { // Note: batch hashing related blank node quads 100 at a time if(++i % 100 === 0) { await this._yield(); } // 3.1) For each component in quad, if component is the subject, object, // and graph name and it is a blank node that is not identified by // identifier: // steps 3.1.1 and 3.1.2 occur in helpers: await Promise.all([ this._addRelatedBlankNodeHash({ quad, component: quad.subject, position: 's', id, issuer, hashToRelated }), this._addRelatedBlankNodeHash({ quad, component: quad.object, position: 'o', id, issuer, hashToRelated }), this._addRelatedBlankNodeHash({ quad, component: quad.graph, position: 'g', id, issuer, hashToRelated }) ]); } return hashToRelated; } async _hashAndTrackBlankNode({id, hashToBlankNodes}) { // 5.3.1) Create a hash, hash, according to the Hash First Degree // Quads algorithm. const hash = await this.hashFirstDegreeQuads(id); // 5.3.2) Add hash and identifier to hash to blank nodes map, // creating a new entry if necessary. const idList = hashToBlankNodes.get(hash); if(!idList) { hashToBlankNodes.set(hash, [id]); } else { idList.push(id); } } _addBlankNodeQuadInfo({quad, component}) { if(component.termType !== 'BlankNode') { return; } const id = component.value; const info = this.blankNodeInfo.get(id); if(info) { info.quads.add(quad); } else { this.blankNodeInfo.set(id, {quads: new Set([quad]), hash: null}); } } async _addRelatedBlankNodeHash( {quad, component, position, id, issuer, hashToRelated}) { if(!(component.termType === 'BlankNode' && component.value !== id)) { return; } // 3.1.1) Set hash to the result of the Hash Related Blank Node // algorithm, passing the blank node identifier for component as // related, quad, path identifier issuer as issuer, and position as // either s, o, or g based on whether component is a subject, object, // graph name, respectively. const related = component.value; const hash = await this.hashRelatedBlankNode( related, quad, issuer, position); // 3.1.2) Add a mapping of hash to the blank node identifier for // component to hash to related blank nodes map, adding an entry as // necessary. const entries = hashToRelated.get(hash); if(entries) { entries.push(related); } else { hashToRelated.set(hash, [related]); } } // canonical ids for 7.1 _componentWithCanonicalId(component) { if(component.termType === 'BlankNode' && !component.value.startsWith(this.canonicalIssuer.prefix)) { // create new BlankNode return { termType: 'BlankNode', value: this.canonicalIssuer.getId(component.value) }; } return component; } async _yield() { return new Promise(resolve => setImmediate(resolve)); } }; function _stringHashCompare(a, b) { return a.hash < b.hash ? -1 : a.hash > b.hash ? 1 : 0; } rdf-canonize-3.4.0/lib/URDNA2015Sync.js000066400000000000000000000445161443175206400172370ustar00rootroot00000000000000/*! * Copyright (c) 2016-2022 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const IdentifierIssuer = require('./IdentifierIssuer'); // FIXME: do not import; convert to requiring a // hash factory const MessageDigest = require('./MessageDigest'); const Permuter = require('./Permuter'); const NQuads = require('./NQuads'); module.exports = class URDNA2015Sync { constructor({ createMessageDigest = () => new MessageDigest('sha256'), canonicalIdMap = new Map(), maxDeepIterations = Infinity } = {}) { this.name = 'URDNA2015'; this.blankNodeInfo = new Map(); this.canonicalIssuer = new IdentifierIssuer('_:c14n', canonicalIdMap); this.createMessageDigest = createMessageDigest; this.maxDeepIterations = maxDeepIterations; this.quads = null; this.deepIterations = null; } // 4.4) Normalization Algorithm main(dataset) { this.deepIterations = new Map(); this.quads = dataset; // 1) Create the normalization state. // 2) For every quad in input dataset: for(const quad of dataset) { // 2.1) For each blank node that occurs in the quad, add a reference // to the quad using the blank node identifier in the blank node to // quads map, creating a new entry if necessary. this._addBlankNodeQuadInfo({quad, component: quad.subject}); this._addBlankNodeQuadInfo({quad, component: quad.object}); this._addBlankNodeQuadInfo({quad, component: quad.graph}); } // 3) Create a list of non-normalized blank node identifiers // non-normalized identifiers and populate it using the keys from the // blank node to quads map. // Note: We use a map here and it was generated during step 2. // 4) `simple` flag is skipped -- loop is optimized away. This optimization // is permitted because there was a typo in the hash first degree quads // algorithm in the URDNA2015 spec that was implemented widely making it // such that it could not be fixed; the result was that the loop only // needs to be run once and the first degree quad hashes will never change. // 5.1-5.2 are skipped; first degree quad hashes are generated just once // for all non-normalized blank nodes. // 5.3) For each blank node identifier identifier in non-normalized // identifiers: const hashToBlankNodes = new Map(); const nonNormalized = [...this.blankNodeInfo.keys()]; for(const id of nonNormalized) { // steps 5.3.1 and 5.3.2: this._hashAndTrackBlankNode({id, hashToBlankNodes}); } // 5.4) For each hash to identifier list mapping in hash to blank // nodes map, lexicographically-sorted by hash: const hashes = [...hashToBlankNodes.keys()].sort(); // optimize away second sort, gather non-unique hashes in order as we go const nonUnique = []; for(const hash of hashes) { // 5.4.1) If the length of identifier list is greater than 1, // continue to the next mapping. const idList = hashToBlankNodes.get(hash); if(idList.length > 1) { nonUnique.push(idList); continue; } // 5.4.2) Use the Issue Identifier algorithm, passing canonical // issuer and the single blank node identifier in identifier // list, identifier, to issue a canonical replacement identifier // for identifier. const id = idList[0]; this.canonicalIssuer.getId(id); // Note: These steps are skipped, optimized away since the loop // only needs to be run once. // 5.4.3) Remove identifier from non-normalized identifiers. // 5.4.4) Remove hash from the hash to blank nodes map. // 5.4.5) Set simple to true. } // 6) For each hash to identifier list mapping in hash to blank nodes map, // lexicographically-sorted by hash: // Note: sort optimized away, use `nonUnique`. for(const idList of nonUnique) { // 6.1) Create hash path list where each item will be a result of // running the Hash N-Degree Quads algorithm. const hashPathList = []; // 6.2) For each blank node identifier identifier in identifier list: for(const id of idList) { // 6.2.1) If a canonical identifier has already been issued for // identifier, continue to the next identifier. if(this.canonicalIssuer.hasId(id)) { continue; } // 6.2.2) Create temporary issuer, an identifier issuer // initialized with the prefix _:b. const issuer = new IdentifierIssuer('_:b'); // 6.2.3) Use the Issue Identifier algorithm, passing temporary // issuer and identifier, to issue a new temporary blank node // identifier for identifier. issuer.getId(id); // 6.2.4) Run the Hash N-Degree Quads algorithm, passing // temporary issuer, and append the result to the hash path list. const result = this.hashNDegreeQuads(id, issuer); hashPathList.push(result); } // 6.3) For each result in the hash path list, // lexicographically-sorted by the hash in result: hashPathList.sort(_stringHashCompare); for(const result of hashPathList) { // 6.3.1) For each blank node identifier, existing identifier, // that was issued a temporary identifier by identifier issuer // in result, issue a canonical identifier, in the same order, // using the Issue Identifier algorithm, passing canonical // issuer and existing identifier. const oldIds = result.issuer.getOldIds(); for(const id of oldIds) { this.canonicalIssuer.getId(id); } } } /* Note: At this point all blank nodes in the set of RDF quads have been assigned canonical identifiers, which have been stored in the canonical issuer. Here each quad is updated by assigning each of its blank nodes its new identifier. */ // 7) For each quad, quad, in input dataset: const normalized = []; for(const quad of this.quads) { // 7.1) Create a copy, quad copy, of quad and replace any existing // blank node identifiers using the canonical identifiers // previously issued by canonical issuer. // Note: We optimize away the copy here. const nQuad = NQuads.serializeQuadComponents( this._componentWithCanonicalId({component: quad.subject}), quad.predicate, this._componentWithCanonicalId({component: quad.object}), this._componentWithCanonicalId({component: quad.graph}) ); // 7.2) Add quad copy to the normalized dataset. normalized.push(nQuad); } // sort normalized output normalized.sort(); // 8) Return the normalized dataset. return normalized.join(''); } // 4.6) Hash First Degree Quads hashFirstDegreeQuads(id) { // 1) Initialize nquads to an empty list. It will be used to store quads in // N-Quads format. const nquads = []; // 2) Get the list of quads `quads` associated with the reference blank node // identifier in the blank node to quads map. const info = this.blankNodeInfo.get(id); const quads = info.quads; // 3) For each quad `quad` in `quads`: for(const quad of quads) { // 3.1) Serialize the quad in N-Quads format with the following special // rule: // 3.1.1) If any component in quad is an blank node, then serialize it // using a special identifier as follows: const copy = { subject: null, predicate: quad.predicate, object: null, graph: null }; // 3.1.2) If the blank node's existing blank node identifier matches // the reference blank node identifier then use the blank node // identifier _:a, otherwise, use the blank node identifier _:z. copy.subject = this.modifyFirstDegreeComponent( id, quad.subject, 'subject'); copy.object = this.modifyFirstDegreeComponent( id, quad.object, 'object'); copy.graph = this.modifyFirstDegreeComponent( id, quad.graph, 'graph'); nquads.push(NQuads.serializeQuad(copy)); } // 4) Sort nquads in lexicographical order. nquads.sort(); // 5) Return the hash that results from passing the sorted, joined nquads // through the hash algorithm. const md = this.createMessageDigest(); for(const nquad of nquads) { md.update(nquad); } info.hash = md.digest(); return info.hash; } // 4.7) Hash Related Blank Node hashRelatedBlankNode(related, quad, issuer, position) { // 1) Set the identifier to use for related, preferring first the canonical // identifier for related if issued, second the identifier issued by issuer // if issued, and last, if necessary, the result of the Hash First Degree // Quads algorithm, passing related. let id; if(this.canonicalIssuer.hasId(related)) { id = this.canonicalIssuer.getId(related); } else if(issuer.hasId(related)) { id = issuer.getId(related); } else { id = this.blankNodeInfo.get(related).hash; } // 2) Initialize a string input to the value of position. // Note: We use a hash object instead. const md = this.createMessageDigest(); md.update(position); // 3) If position is not g, append <, the value of the predicate in quad, // and > to input. if(position !== 'g') { md.update(this.getRelatedPredicate(quad)); } // 4) Append identifier to input. md.update(id); // 5) Return the hash that results from passing input through the hash // algorithm. return md.digest(); } // 4.8) Hash N-Degree Quads hashNDegreeQuads(id, issuer) { const deepIterations = this.deepIterations.get(id) || 0; if(deepIterations > this.maxDeepIterations) { throw new Error( `Maximum deep iterations (${this.maxDeepIterations}) exceeded.`); } this.deepIterations.set(id, deepIterations + 1); // 1) Create a hash to related blank nodes map for storing hashes that // identify related blank nodes. // Note: 2) and 3) handled within `createHashToRelated` const md = this.createMessageDigest(); const hashToRelated = this.createHashToRelated(id, issuer); // 4) Create an empty string, data to hash. // Note: We created a hash object `md` above instead. // 5) For each related hash to blank node list mapping in hash to related // blank nodes map, sorted lexicographically by related hash: const hashes = [...hashToRelated.keys()].sort(); for(const hash of hashes) { // 5.1) Append the related hash to the data to hash. md.update(hash); // 5.2) Create a string chosen path. let chosenPath = ''; // 5.3) Create an unset chosen issuer variable. let chosenIssuer; // 5.4) For each permutation of blank node list: const permuter = new Permuter(hashToRelated.get(hash)); while(permuter.hasNext()) { const permutation = permuter.next(); // 5.4.1) Create a copy of issuer, issuer copy. let issuerCopy = issuer.clone(); // 5.4.2) Create a string path. let path = ''; // 5.4.3) Create a recursion list, to store blank node identifiers // that must be recursively processed by this algorithm. const recursionList = []; // 5.4.4) For each related in permutation: let nextPermutation = false; for(const related of permutation) { // 5.4.4.1) If a canonical identifier has been issued for // related, append it to path. if(this.canonicalIssuer.hasId(related)) { path += this.canonicalIssuer.getId(related); } else { // 5.4.4.2) Otherwise: // 5.4.4.2.1) If issuer copy has not issued an identifier for // related, append related to recursion list. if(!issuerCopy.hasId(related)) { recursionList.push(related); } // 5.4.4.2.2) Use the Issue Identifier algorithm, passing // issuer copy and related and append the result to path. path += issuerCopy.getId(related); } // 5.4.4.3) If chosen path is not empty and the length of path // is greater than or equal to the length of chosen path and // path is lexicographically greater than chosen path, then // skip to the next permutation. // Note: Comparing path length to chosen path length can be optimized // away; only compare lexicographically. if(chosenPath.length !== 0 && path > chosenPath) { nextPermutation = true; break; } } if(nextPermutation) { continue; } // 5.4.5) For each related in recursion list: for(const related of recursionList) { // 5.4.5.1) Set result to the result of recursively executing // the Hash N-Degree Quads algorithm, passing related for // identifier and issuer copy for path identifier issuer. const result = this.hashNDegreeQuads(related, issuerCopy); // 5.4.5.2) Use the Issue Identifier algorithm, passing issuer // copy and related and append the result to path. path += issuerCopy.getId(related); // 5.4.5.3) Append <, the hash in result, and > to path. path += `<${result.hash}>`; // 5.4.5.4) Set issuer copy to the identifier issuer in // result. issuerCopy = result.issuer; // 5.4.5.5) If chosen path is not empty and the length of path // is greater than or equal to the length of chosen path and // path is lexicographically greater than chosen path, then // skip to the next permutation. // Note: Comparing path length to chosen path length can be optimized // away; only compare lexicographically. if(chosenPath.length !== 0 && path > chosenPath) { nextPermutation = true; break; } } if(nextPermutation) { continue; } // 5.4.6) If chosen path is empty or path is lexicographically // less than chosen path, set chosen path to path and chosen // issuer to issuer copy. if(chosenPath.length === 0 || path < chosenPath) { chosenPath = path; chosenIssuer = issuerCopy; } } // 5.5) Append chosen path to data to hash. md.update(chosenPath); // 5.6) Replace issuer, by reference, with chosen issuer. issuer = chosenIssuer; } // 6) Return issuer and the hash that results from passing data to hash // through the hash algorithm. return {hash: md.digest(), issuer}; } // helper for modifying component during Hash First Degree Quads modifyFirstDegreeComponent(id, component) { if(component.termType !== 'BlankNode') { return component; } /* Note: A mistake in the URDNA2015 spec that made its way into implementations (and therefore must stay to avoid interop breakage) resulted in an assigned canonical ID, if available for `component.value`, not being used in place of `_:a`/`_:z`, so we don't use it here. */ return { termType: 'BlankNode', value: component.value === id ? '_:a' : '_:z' }; } // helper for getting a related predicate getRelatedPredicate(quad) { return `<${quad.predicate.value}>`; } // helper for creating hash to related blank nodes map createHashToRelated(id, issuer) { // 1) Create a hash to related blank nodes map for storing hashes that // identify related blank nodes. const hashToRelated = new Map(); // 2) Get a reference, quads, to the list of quads in the blank node to // quads map for the key identifier. const quads = this.blankNodeInfo.get(id).quads; // 3) For each quad in quads: for(const quad of quads) { // 3.1) For each component in quad, if component is the subject, object, // or graph name and it is a blank node that is not identified by // identifier: // steps 3.1.1 and 3.1.2 occur in helpers: this._addRelatedBlankNodeHash({ quad, component: quad.subject, position: 's', id, issuer, hashToRelated }); this._addRelatedBlankNodeHash({ quad, component: quad.object, position: 'o', id, issuer, hashToRelated }); this._addRelatedBlankNodeHash({ quad, component: quad.graph, position: 'g', id, issuer, hashToRelated }); } return hashToRelated; } _hashAndTrackBlankNode({id, hashToBlankNodes}) { // 5.3.1) Create a hash, hash, according to the Hash First Degree // Quads algorithm. const hash = this.hashFirstDegreeQuads(id); // 5.3.2) Add hash and identifier to hash to blank nodes map, // creating a new entry if necessary. const idList = hashToBlankNodes.get(hash); if(!idList) { hashToBlankNodes.set(hash, [id]); } else { idList.push(id); } } _addBlankNodeQuadInfo({quad, component}) { if(component.termType !== 'BlankNode') { return; } const id = component.value; const info = this.blankNodeInfo.get(id); if(info) { info.quads.add(quad); } else { this.blankNodeInfo.set(id, {quads: new Set([quad]), hash: null}); } } _addRelatedBlankNodeHash( {quad, component, position, id, issuer, hashToRelated}) { if(!(component.termType === 'BlankNode' && component.value !== id)) { return; } // 3.1.1) Set hash to the result of the Hash Related Blank Node // algorithm, passing the blank node identifier for component as // related, quad, path identifier issuer as issuer, and position as // either s, o, or g based on whether component is a subject, object, // graph name, respectively. const related = component.value; const hash = this.hashRelatedBlankNode(related, quad, issuer, position); // 3.1.2) Add a mapping of hash to the blank node identifier for // component to hash to related blank nodes map, adding an entry as // necessary. const entries = hashToRelated.get(hash); if(entries) { entries.push(related); } else { hashToRelated.set(hash, [related]); } } // canonical ids for 7.1 _componentWithCanonicalId({component}) { if(component.termType === 'BlankNode' && !component.value.startsWith(this.canonicalIssuer.prefix)) { // create new BlankNode return { termType: 'BlankNode', value: this.canonicalIssuer.getId(component.value) }; } return component; } }; function _stringHashCompare(a, b) { return a.hash < b.hash ? -1 : a.hash > b.hash ? 1 : 0; } rdf-canonize-3.4.0/lib/URGNA2012.js000066400000000000000000000057311443175206400163760ustar00rootroot00000000000000/*! * Copyright (c) 2016-2022 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const MessageDigest = require('./MessageDigest'); const URDNA2015 = require('./URDNA2015'); module.exports = class URDNA2012 extends URDNA2015 { constructor() { super(); this.name = 'URGNA2012'; this.createMessageDigest = () => new MessageDigest('sha1'); } // helper for modifying component during Hash First Degree Quads modifyFirstDegreeComponent(id, component, key) { if(component.termType !== 'BlankNode') { return component; } if(key === 'graph') { return { termType: 'BlankNode', value: '_:g' }; } return { termType: 'BlankNode', value: (component.value === id ? '_:a' : '_:z') }; } // helper for getting a related predicate getRelatedPredicate(quad) { return quad.predicate.value; } // helper for creating hash to related blank nodes map async createHashToRelated(id, issuer) { // 1) Create a hash to related blank nodes map for storing hashes that // identify related blank nodes. const hashToRelated = new Map(); // 2) Get a reference, quads, to the list of quads in the blank node to // quads map for the key identifier. const quads = this.blankNodeInfo.get(id).quads; // 3) For each quad in quads: let i = 0; for(const quad of quads) { // 3.1) If the quad's subject is a blank node that does not match // identifier, set hash to the result of the Hash Related Blank Node // algorithm, passing the blank node identifier for subject as related, // quad, path identifier issuer as issuer, and p as position. let position; let related; if(quad.subject.termType === 'BlankNode' && quad.subject.value !== id) { related = quad.subject.value; position = 'p'; } else if( quad.object.termType === 'BlankNode' && quad.object.value !== id) { // 3.2) Otherwise, if quad's object is a blank node that does not match // identifier, to the result of the Hash Related Blank Node algorithm, // passing the blank node identifier for object as related, quad, path // identifier issuer as issuer, and r as position. related = quad.object.value; position = 'r'; } else { // 3.3) Otherwise, continue to the next quad. continue; } // Note: batch hashing related blank nodes 100 at a time if(++i % 100 === 0) { await this._yield(); } // 3.4) Add a mapping of hash to the blank node identifier for the // component that matched (subject or object) to hash to related blank // nodes map, adding an entry as necessary. const hash = await this.hashRelatedBlankNode( related, quad, issuer, position); const entries = hashToRelated.get(hash); if(entries) { entries.push(related); } else { hashToRelated.set(hash, [related]); } } return hashToRelated; } }; rdf-canonize-3.4.0/lib/URGNA2012Sync.js000066400000000000000000000055051443175206400172320ustar00rootroot00000000000000/*! * Copyright (c) 2016-2021 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const MessageDigest = require('./MessageDigest'); const URDNA2015Sync = require('./URDNA2015Sync'); module.exports = class URDNA2012Sync extends URDNA2015Sync { constructor() { super(); this.name = 'URGNA2012'; this.createMessageDigest = () => new MessageDigest('sha1'); } // helper for modifying component during Hash First Degree Quads modifyFirstDegreeComponent(id, component, key) { if(component.termType !== 'BlankNode') { return component; } if(key === 'graph') { return { termType: 'BlankNode', value: '_:g' }; } return { termType: 'BlankNode', value: (component.value === id ? '_:a' : '_:z') }; } // helper for getting a related predicate getRelatedPredicate(quad) { return quad.predicate.value; } // helper for creating hash to related blank nodes map createHashToRelated(id, issuer) { // 1) Create a hash to related blank nodes map for storing hashes that // identify related blank nodes. const hashToRelated = new Map(); // 2) Get a reference, quads, to the list of quads in the blank node to // quads map for the key identifier. const quads = this.blankNodeInfo.get(id).quads; // 3) For each quad in quads: for(const quad of quads) { // 3.1) If the quad's subject is a blank node that does not match // identifier, set hash to the result of the Hash Related Blank Node // algorithm, passing the blank node identifier for subject as related, // quad, path identifier issuer as issuer, and p as position. let position; let related; if(quad.subject.termType === 'BlankNode' && quad.subject.value !== id) { related = quad.subject.value; position = 'p'; } else if( quad.object.termType === 'BlankNode' && quad.object.value !== id) { // 3.2) Otherwise, if quad's object is a blank node that does not match // identifier, to the result of the Hash Related Blank Node algorithm, // passing the blank node identifier for object as related, quad, path // identifier issuer as issuer, and r as position. related = quad.object.value; position = 'r'; } else { // 3.3) Otherwise, continue to the next quad. continue; } // 3.4) Add a mapping of hash to the blank node identifier for the // component that matched (subject or object) to hash to related blank // nodes map, adding an entry as necessary. const hash = this.hashRelatedBlankNode(related, quad, issuer, position); const entries = hashToRelated.get(hash); if(entries) { entries.push(related); } else { hashToRelated.set(hash, [related]); } } return hashToRelated; } }; rdf-canonize-3.4.0/lib/index.js000066400000000000000000000163361443175206400163270ustar00rootroot00000000000000/** * An implementation of the RDF Dataset Normalization specification. * This library works in the browser and node.js. * * BSD 3-Clause License * Copyright (c) 2016-2023 Digital Bazaar, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Digital Bazaar, Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ 'use strict'; const URDNA2015 = require('./URDNA2015'); const URGNA2012 = require('./URGNA2012'); const URDNA2015Sync = require('./URDNA2015Sync'); const URGNA2012Sync = require('./URGNA2012Sync'); // optional native support let rdfCanonizeNative; try { rdfCanonizeNative = require('rdf-canonize-native'); } catch(e) {} // return a dataset from input dataset or legacy dataset function _inputToDataset(input/*, options*/) { // back-compat with legacy dataset if(!Array.isArray(input)) { return exports.NQuads.legacyDatasetToQuads(input); } return input; } // expose helpers exports.NQuads = require('./NQuads'); exports.IdentifierIssuer = require('./IdentifierIssuer'); /** * Get or set native API. * * @param api the native API. * * @return the currently set native API. */ exports._rdfCanonizeNative = function(api) { if(api) { rdfCanonizeNative = api; } return rdfCanonizeNative; }; /** * Asynchronously canonizes an RDF dataset. * * @param {Array|object|string} input - The input to canonize given as a * dataset or legacy dataset. * @param {object} options - The options to use: * {string} algorithm - The canonicalization algorithm to use, `URDNA2015` or * `URGNA2012`. * {Function} [createMessageDigest] - A factory function for creating a * `MessageDigest` interface that overrides the built-in message digest * implementation used by the canonize algorithm; note that using a hash * algorithm (or HMAC algorithm) that differs from the one specified by * the canonize algorithm will result in different output. * {Map} [canonicalIdMap] - An optional Map to be populated by the canonical * identifier issuer with the bnode identifier mapping generated by the * canonicalization algorithm. * {boolean} [useNative=false] - Use native implementation. * {number} [maxDeepIterations=Infinity] - The maximum number of times to run * deep comparison algorithms (such as the N-Degree Hash Quads algorithm * used in URDNA2015) before bailing out and throwing an error; this is a * useful setting for preventing wasted CPU cycles or DoS when canonizing * meaningless or potentially malicious datasets, a recommended value is * `1`. * * @return a Promise that resolves to the canonicalized RDF Dataset. */ exports.canonize = async function(input, options) { const dataset = _inputToDataset(input, options); if(options.useNative) { if(!rdfCanonizeNative) { throw new Error('rdf-canonize-native not available'); } if(options.createMessageDigest) { throw new Error( '"createMessageDigest" cannot be used with "useNative".'); } return new Promise((resolve, reject) => rdfCanonizeNative.canonize(dataset, options, (err, canonical) => err ? reject(err) : resolve(canonical))); } if(options.algorithm === 'URDNA2015') { return new URDNA2015(options).main(dataset); } if(options.algorithm === 'URGNA2012') { if(options.createMessageDigest) { throw new Error( '"createMessageDigest" cannot be used with "URGNA2012".'); } return new URGNA2012(options).main(dataset); } if(!('algorithm' in options)) { throw new Error('No RDF Dataset Canonicalization algorithm specified.'); } throw new Error( 'Invalid RDF Dataset Canonicalization algorithm: ' + options.algorithm); }; /** * This method is no longer available in the public API, it is for testing * only. It synchronously canonizes an RDF dataset and does not work in the * browser. * * @param {Array|object|string} input - The input to canonize given as a * dataset or legacy dataset. * @param {object} options - The options to use: * {string} algorithm - The canonicalization algorithm to use, `URDNA2015` or * `URGNA2012`. * {Function} [createMessageDigest] - A factory function for creating a * `MessageDigest` interface that overrides the built-in message digest * implementation used by the canonize algorithm; note that using a hash * algorithm (or HMAC algorithm) that differs from the one specified by * the canonize algorithm will result in different output. * {boolean} [useNative=false] - Use native implementation. * {number} [maxDeepIterations=Infinity] - The maximum number of times to run * deep comparison algorithms (such as the N-Degree Hash Quads algorithm * used in URDNA2015) before bailing out and throwing an error; this is a * useful setting for preventing wasted CPU cycles or DoS when canonizing * meaningless or potentially malicious datasets, a recommended value is * `1`. * * @return the RDF dataset in canonical form. */ exports._canonizeSync = function(input, options) { const dataset = _inputToDataset(input, options); if(options.useNative) { if(!rdfCanonizeNative) { throw new Error('rdf-canonize-native not available'); } if(options.createMessageDigest) { throw new Error( '"createMessageDigest" cannot be used with "useNative".'); } return rdfCanonizeNative.canonizeSync(dataset, options); } if(options.algorithm === 'URDNA2015') { return new URDNA2015Sync(options).main(dataset); } if(options.algorithm === 'URGNA2012') { if(options.createMessageDigest) { throw new Error( '"createMessageDigest" cannot be used with "URGNA2012".'); } return new URGNA2012Sync(options).main(dataset); } if(!('algorithm' in options)) { throw new Error('No RDF Dataset Canonicalization algorithm specified.'); } throw new Error( 'Invalid RDF Dataset Canonicalization algorithm: ' + options.algorithm); }; rdf-canonize-3.4.0/package.json000066400000000000000000000035751443175206400164030ustar00rootroot00000000000000{ "name": "rdf-canonize", "version": "3.4.0", "description": "An implementation of the RDF Dataset Normalization Algorithm in JavaScript", "homepage": "https://github.com/digitalbazaar/rdf-canonize", "author": { "name": "Digital Bazaar, Inc.", "email": "support@digitalbazaar.com", "url": "https://digitalbazaar.com/" }, "contributors": [ "Dave Longley " ], "repository": { "type": "git", "url": "https://github.com/digitalbazaar/rdf-canonize" }, "bugs": { "url": "https://github.com/digitalbazaar/rdf-canonize/issues", "email": "support@digitalbazaar.com" }, "license": "BSD-3-Clause", "main": "index.js", "files": [ "index.js", "lib/*.js" ], "dependencies": { "setimmediate": "^1.0.5" }, "devDependencies": { "benchmark": "^2.1.4", "chai": "^4.2.0", "delay": "^5.0.0", "eslint": "^7.23.0", "eslint-config-digitalbazaar": "^2.6.1", "mocha": "^8.3.2", "mocha-lcov-reporter": "^1.3.0", "nsolid": "0.0.0", "nyc": "^15.1.0" }, "engines": { "node": ">=12" }, "keywords": [ "JSON", "Linked Data", "JSON-LD", "RDF", "Semantic Web", "jsonld" ], "scripts": { "fetch-test-suite": "if [ ! -e test-suites/rdf-canon ]; then git clone --depth 1 https://github.com/w3c/rdf-canon.git test-suites/rdf-canon; fi", "test": "npm run test-node", "test-node": "NODE_ENV=test mocha -R spec --check-leaks", "benchmark": "node benchmark/benchmark.js", "coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text-summary npm test", "coverage-ci": "NODE_ENV=test nyc --reporter=lcovonly npm run test", "coverage-report": "nyc report", "lint": "eslint '*.js' 'lib/*.js' 'test/*.js' 'benchmark/*.js'" }, "browser": { "./lib/MessageDigest.js": "./lib/MessageDigest-browser.js", "rdf-canonize-native": false } } rdf-canonize-3.4.0/test/000077500000000000000000000000001443175206400150625ustar00rootroot00000000000000rdf-canonize-3.4.0/test/.eslintrc.js000066400000000000000000000001271443175206400173210ustar00rootroot00000000000000module.exports = { env: { mocha: true }, globals: { phantom: true } }; rdf-canonize-3.4.0/test/EarlReport.js000066400000000000000000000055371443175206400175110ustar00rootroot00000000000000/** * Copyright (c) 2016-2021 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const fs = require('fs'); module.exports = class EarlReport { constructor(implementation) { this.implementation = 'node.js'; let today = new Date(); today = today.getFullYear() + '-' + (today.getMonth() < 9 ? '0' + (today.getMonth() + 1) : today.getMonth() + 1) + '-' + (today.getDate() < 10 ? '0' + today.getDate() : today.getDate()); this.report = { '@context': { doap: 'http://usefulinc.com/ns/doap#', foaf: 'http://xmlns.com/foaf/0.1/', dc: 'http://purl.org/dc/terms/', earl: 'http://www.w3.org/ns/earl#', xsd: 'http://www.w3.org/2001/XMLSchema#', 'doap:homepage': {'@type': '@id'}, 'doap:license': {'@type': '@id'}, 'dc:creator': {'@type': '@id'}, 'foaf:homepage': {'@type': '@id'}, subjectOf: {'@reverse': 'earl:subject'}, 'earl:assertedBy': {'@type': '@id'}, 'earl:mode': {'@type': '@id'}, 'earl:test': {'@type': '@id'}, 'earl:outcome': {'@type': '@id'}, 'dc:date': {'@type': 'xsd:date'} }, '@id': 'https://github.com/digitalbazaar/jsonld.js', '@type': [ 'doap:Project', 'earl:TestSubject', 'earl:Software' ], 'doap:name': 'jsonld.js', 'dc:title': 'jsonld.js', 'doap:homepage': 'https://github.com/digitalbazaar/rdf-canonize', 'doap:license': 'https://github.com/digitalbazaar/rdf-canonize/blob/master/LICENSE', 'doap:description': 'A JSON-LD processor for JavaScript', 'doap:programming-language': 'JavaScript', 'dc:creator': 'https://github.com/dlongley', 'doap:developer': { '@id': 'https://github.com/dlongley', '@type': [ 'foaf:Person', 'earl:Assertor' ], 'foaf:name': 'Dave Longley', 'foaf:homepage': 'https://github.com/dlongley' }, 'dc:date': { '@value': today, '@type': 'xsd:date' }, subjectOf: [] }; this.report['@id'] += '#' + implementation; this.report['doap:name'] += ' ' + implementation; this.report['dc:title'] += ' ' + implementation; } addAssertion(test, pass) { this.report.subjectOf.push({ '@type': 'earl:Assertion', 'earl:assertedBy': this.report['doap:developer']['@id'], 'earl:mode': 'earl:automatic', 'earl:test': test['@id'], 'earl:result': { '@type': 'earl:TestResult', 'dc:date': new Date().toISOString(), 'earl:outcome': pass ? 'earl:passed' : 'earl:failed' } }); return this; } write(filename) { const json = JSON.stringify(this.report, null, 2); if(this.implementation === 'node.js') { fs.writeFileSync(filename, json); } else { fs.write(filename, json, 'w'); } return this; } }; rdf-canonize-3.4.0/test/misc.js000066400000000000000000000015661443175206400163630ustar00rootroot00000000000000/** * Misc tests. */ // disable so tests can be copy & pasted /* eslint-disable quotes, quote-props */ const rdfCanonize = require('..'); const assert = require('assert'); const {NQuads} = rdfCanonize; describe('API tests', () => { it('should set canonicalIdMap data', async () => { const input = `\ _:b0 _:b1 . _:b1 "v1" . `; const expected = `\ _:c14n0 _:c14n1 . _:c14n1 "v1" . `; const expectIdMap = new Map(Object.entries({ '_:b0': '_:c14n0', '_:b1': '_:c14n1' })); const canonicalIdMap = new Map(); const dataset = NQuads.parse(input); const output = await rdfCanonize.canonize(dataset, { algorithm: 'URDNA2015', format: 'application/n-quads', canonicalIdMap }); assert.deepStrictEqual(output, expected); assert.deepStrictEqual(canonicalIdMap, expectIdMap); }); }); rdf-canonize-3.4.0/test/test.js000066400000000000000000000257731443175206400164150ustar00rootroot00000000000000/** * Test runner for rdf-canonize. * * @author Dave Longley * * Copyright (c) 2016-2021 Digital Bazaar, Inc. All rights reserved. */ /* eslint-disable indent */ (function() { 'use strict'; // detect node.js (vs. phantomJS) const _nodejs = (typeof process !== 'undefined' && process.versions && process.versions.node); const fs = require('fs'); let program; let assert; let path; if(_nodejs) { path = require('path'); assert = require('assert'); /* program = require('commander'); program .option('--earl [filename]', 'Output an earl report') .option('--bail', 'Bail when a test fails') .option('--test-dir', 'Test directory') .parse(process.argv); */ program = {}; program.earl = process.env.EARL; program.bail = process.env.BAIL === 'true'; program.testDir = process.env.TEST_DIR; } else { const system = require('system'); require('./setImmediate'); window.Promise = require('es6-promise').Promise; assert = require('chai').assert; require('mocha/mocha'); require('mocha-phantomjs/lib/mocha-phantomjs/core_extensions'); program = {}; for(let i = 0; i < system.args.length; ++i) { const arg = system.args[i]; if(arg.indexOf('--') === 0) { const argname = arg.substr(2); switch(argname) { case 'earl': program[argname] = system.args[i + 1]; ++i; break; default: program[argname] = true; } } } mocha.setup({ reporter: 'spec', ui: 'bdd' }); } const canonize = require('..'); const EarlReport = require('./EarlReport'); const NQuads = require('../lib/NQuads'); // try to load native bindings let rdfCanonizeNative; // try regular load try { rdfCanonizeNative = require('rdf-canonize-native'); } catch(e) { // try peer package try { rdfCanonizeNative = require('../../rdf-canonize-native'); } catch(e) { } } // use native bindings if(rdfCanonizeNative) { canonize._rdfCanonizeNative(rdfCanonizeNative); } else { // skip native tests console.warn('rdf-canonize-native not found'); } const _TEST_SUITE_PATHS = [ program.testDir, '../rdf-canon/tests', './test-suites/rdf-canon/tests', ]; const TEST_SUITE = _TEST_SUITE_PATHS.find(pathExists); if(!TEST_SUITE) { throw new Error('Test suite not found.'); } const ROOT_MANIFEST_DIR = resolvePath(TEST_SUITE); const TEST_TYPES = { 'rdfc:Urgna2012EvalTest': { params: [ parseNQuads(readTestNQuads('action')), createTestOptions({ algorithm: 'URGNA2012', format: 'application/n-quads' }) ], compare: compareExpectedNQuads }, 'rdfc:Urdna2015EvalTest': { params: [ parseNQuads(readTestNQuads('action')), createTestOptions({ algorithm: 'URDNA2015', format: 'application/n-quads' }) ], compare: compareExpectedNQuads }, }; const SKIP_TESTS = []; // create earl report const earl = new EarlReport(_nodejs ? 'node.js' : 'browser'); // run tests describe('rdf-canonize', function() { const filename = joinPath(ROOT_MANIFEST_DIR, 'manifest.jsonld'); const rootManifest = readJson(filename); rootManifest.filename = filename; addManifest(rootManifest); if(program.earl) { const filename = resolvePath(program.earl); describe('Writing EARL report to: ' + filename, function() { it('should print the earl report', function(done) { earl.write(filename); done(); }); }); } }); if(!_nodejs) { mocha.run(() => phantom.exit()); } /** * Adds the tests for all entries in the given manifest. * * @param manifest the manifest. */ function addManifest(manifest) { describe(manifest.name || manifest.label, function() { // get entries and sequence (alias for entries) const entries = [].concat( getJsonLdValues(manifest, 'entries'), getJsonLdValues(manifest, 'sequence') ); const includes = getJsonLdValues(manifest, 'include'); // add includes to sequence as jsonld files for(let i = 0; i < includes.length; ++i) { entries.push(includes[i] + '.jsonld'); } // process entries for(let i = 0; i < entries.length; ++i) { const entry = readManifestEntry(manifest, entries[i]); if(isJsonLdType(entry, 'mf:Manifest')) { // entry is another manifest addManifest(entry); } else { // assume entry is a test addTest(manifest, entry); } } }); } function _clone(json) { return JSON.parse(JSON.stringify(json)); } function addTest(manifest, test) { // skip unknown and explicitly skipped test types const testTypes = Object.keys(TEST_TYPES); if(!isJsonLdType(test, testTypes) || isJsonLdType(test, SKIP_TESTS)) { const type = [].concat( getJsonLdValues(test, '@type'), getJsonLdValues(test, 'type') ); console.log('Skipping test "' + test.name + '" of type: ' + type); } // expand @id and input base const test_id = test['@id'] || test.id; test['@id'] = manifest.baseIri + basename(manifest.filename) + test_id; test.base = manifest.baseIri + test.input; test.manifest = manifest; const description = test_id + ' ' + (test.purpose || test.name); const testInfo = TEST_TYPES[getTestType(test)]; const params = testInfo.params.map(param => param(test)); // custom params for js only async mode const jsParams = testInfo.params.map(param => param(test)); // copy used to check inputs do not change const jsParamsOrig = _clone(jsParams); // custom params for native only async mode const nativeParams = testInfo.params.map(param => param(test)); nativeParams[1].useNative = true; // copy used to check inputs do not change const nativeParamsOrig = _clone(nativeParams); const createCallback = done => (err, result) => { try { if(err) { throw err; } testInfo.compare(test, result); earl.addAssertion(test, true); return done(); } catch(ex) { if(program.bail) { if(ex.name !== 'AssertionError') { console.log('\nError: ', JSON.stringify(ex, null, 2)); } if(_nodejs) { process.exit(); } else { phantom.exit(); } } earl.addAssertion(test, false); return done(ex); } }; // run async js test it(description + ' (asynchronous js)', function(done) { this.timeout(5000); const callback = createCallback(done); const promise = canonize.canonize.apply(null, jsParams); promise .then(function(data) { // check input not changed assert.deepStrictEqual(jsParamsOrig, jsParams); return data; }) .then(callback.bind(null, null), callback); }); if(rdfCanonizeNative && params[1].algorithm === 'URDNA2015') { // run async native test it(description + ' (asynchronous native)', function(done) { this.timeout(5000); const callback = createCallback(done); const promise = canonize.canonize.apply(null, nativeParams); promise .then(function(data) { // check input not changed assert.deepStrictEqual(nativeParamsOrig, nativeParams); return data; }) .then(callback.bind(null, null), callback); }); } // run sync test it(description + ' (synchronous js)', function(done) { this.timeout(5000); const callback = createCallback(done); let result; try { result = canonize._canonizeSync.apply(null, jsParams); // check input not changed assert.deepStrictEqual(jsParamsOrig, jsParams); } catch(e) { return callback(e); } callback(null, result); }); if(rdfCanonizeNative && params[1].algorithm === 'URDNA2015') { // run sync test it(description + ' (synchronous native)', function(done) { this.timeout(5000); const callback = createCallback(done); let result; try { result = canonize._canonizeSync.apply(null, nativeParams); // check input not changed assert.deepStrictEqual(nativeParamsOrig, nativeParams); } catch(e) { return callback(e); } callback(null, result); }); } } function getTestType(test) { const types = Object.keys(TEST_TYPES); for(let i = 0; i < types.length; ++i) { if(isJsonLdType(test, types[i])) { return types[i]; } } return null; } function readManifestEntry(manifest, entry) { const dir = dirname(manifest.filename); if(typeof entry === 'string') { const filename = joinPath(dir, entry); entry = readJson(filename); entry.filename = filename; } entry.dirname = dirname(entry.filename || manifest.filename); return entry; } function readTestNQuads(property) { return test => { if(!test[property]) { return null; } const filename = joinPath(test.dirname, test[property]); return readFile(filename); }; } function parseNQuads(fn) { return test => NQuads.parse(fn(test)); } function createTestOptions(opts) { return test => { const testOptions = test.option || {}; const options = Object.assign({}, testOptions); if(opts) { // extend options Object.assign(options, opts); } return options; }; } // find the expected output property or throw error function _getExpectProperty(test) { if('expect' in test) { return 'expect'; } else if('result' in test) { return 'result'; } else { throw Error('No expected output property found'); } } function compareExpectedNQuads(test, result) { let expect; try { expect = readTestNQuads(_getExpectProperty(test))(test); assert.strictEqual(result, expect); } catch(ex) { if(program.bail) { console.log('\nTEST FAILED\n'); console.log('EXPECTED:\n' + expect); console.log('ACTUAL:\n' + result); } throw ex; } } function isJsonLdType(node, type) { const nodeType = [].concat( getJsonLdValues(node, '@type'), getJsonLdValues(node, 'type') ); type = Array.isArray(type) ? type : [type]; for(let i = 0; i < type.length; ++i) { if(nodeType.indexOf(type[i]) !== -1) { return true; } } return false; } function getJsonLdValues(node, property) { let rval = []; if(property in node) { rval = [].concat(node[property]); } return rval; } function readJson(filename) { return JSON.parse(readFile(filename)); } function pathExists(filename) { if(_nodejs) { return fs.existsSync(filename); } return fs.exists(filename); } function readFile(filename) { if(_nodejs) { return fs.readFileSync(filename, 'utf8'); } return fs.read(filename); } function resolvePath(to) { if(_nodejs) { return path.resolve(to); } return fs.absolute(to); } function joinPath() { return (_nodejs ? path : fs).join.apply( null, Array.prototype.slice.call(arguments)); } function dirname(filename) { if(_nodejs) { return path.dirname(filename); } const idx = filename.lastIndexOf(fs.separator); if(idx === -1) { return filename; } return filename.substr(0, idx); } function basename(filename) { if(_nodejs) { return path.basename(filename); } const idx = filename.lastIndexOf(fs.separator); if(idx === -1) { return filename; } return filename.substr(idx + 1); } })();