erdtman/canonicalize

Hello ๐Ÿ‘‹๐Ÿป : FYI @truestamp/truestamp-canonify port

grempe opened this issue ยท 2 comments

Hello, this is not an issue, but just to say thanks for the code which served as part of the base for our Typescript port.

https://github.com/truestamp/truestamp-canonify

You library, along with the reference code, was super helpful.

We ported this to typescript as it is important for us to not only gain the security advantages, but to further flesh out the test suite and allow use of the library in not only Node.js but also in Deno and the browser.

We'd love for you to take a look, and if there are any comments about our friendly fork please do let me know.

Out of curiosity I also copied your most current code over and ran our test suite against it. The following was the output (and will show a couple of the differences in how we're handling certain cases. I went through a number of manual test cases to see how JSON.stringify() is documented to work and tried to get the output to align closely with that.

Some of the differences are the handling of:

  • BigInt values should throw an Error as JSON.stringify() does. The user would need to call .toString() on the BigInt.
  • Serialization of function values to in Arrays/Objects to null, not undefined which is not valid JSON
  • Removal of Object key:value where the value is undefined (e.g. Symbol() values)

Here's the test output.

โฏ npm t 

> @truestamp/canonify@1.0.0 test
> jest

 PASS  tests/testdata.spec.ts
 FAIL  tests/basics.spec.ts
  โ— serializing โ€บ should behave like JSON.stringify() for โ€บ BigInt should throw a TypeError

    expect(received).toThrow(expected)

    Expected substring: "BigInt value can't be serialized in JSON"
    Received message:   "Do not know how to serialize a BigInt"

          2 | export default function canonify(object: any): string | undefined {
          3 |   if (object === null || typeof object !== 'object') {
        > 4 |     return JSON.stringify(object);
            |                 ^
          5 |   }
          6 |
          7 |   if (object.toJSON instanceof Function) {

          at canonify (src/index.ts:4:17)
          at t (tests/basics.spec.ts:69:17)
          at Object.<anonymous> (node_modules/expect/build/toThrowMatchers.js:83:11)
          at Object.throwingMatcher [as toThrow] (node_modules/expect/build/index.js:382:21)
          at Object.<anonymous> (tests/basics.spec.ts:72:17)

      70 |       };
      71 |       expect(t).toThrow(TypeError);
    > 72 |       expect(t).toThrow("BigInt value can't be serialized in JSON");
         |                 ^
      73 |     });
      74 |
      75 |     // JSON.stringify('foo')

      at Object.<anonymous> (tests/basics.spec.ts:72:17)

  โ— serializing โ€บ should behave like JSON.stringify() for โ€บ Array

    expect(received).toEqual(expected) // deep equality

    Expected: "[null,null,true,false,\"foo\",42,\"42\",null,null]"
    Received: "[null,null,true,false,\"foo\",42,\"42\",null,undefined]"

      93 |     test('Array', () => {
      94 |       const a = [undefined, null, true, false, "foo", 42, BigInt(42).toString(), Symbol('hello'), () => { }]
    > 95 |       expect(canonify(a)).toEqual('[null,null,true,false,"foo",42,"42",null,null]')
         |                           ^
      96 |     })
      97 |
      98 |     test('Array with String keys', () => {

      at Object.<anonymous> (tests/basics.spec.ts:95:27)

  โ— serializing โ€บ should behave like JSON.stringify() for โ€บ Object

    expect(received).toEqual(expected) // deep equality

    Expected: "{\"big\":\"42\",\"f\":false,\"n\":null,\"num\":42,\"s\":\"string\",\"t\":true}"
    Received: "{\"big\":\"42\",\"f\":false,\"fun\":undefined,\"n\":null,\"num\":42,\"s\":\"string\",\"t\":true}"

      107 |     test('Object', () => {
      108 |       const o = { big: BigInt(42).toString(), f: false, fun: () => { }, n: null, num: 42, s: "string", sym: Symbol('hello'), t: true, u: undefined }
    > 109 |       expect(canonify(o)).toEqual('{"big":"42","f":false,"n":null,"num":42,"s":"string","t":true}')
          |                           ^
      110 |     })
      111 |
      112 |     // Standard data structures

      at Object.<anonymous> (tests/basics.spec.ts:109:27)

  โ— serializing โ€บ should behave like JSON.stringify() for โ€บ Symbols

    expect(received).toEqual(expected) // deep equality

    Expected: "{}"
    Received: "{\"y\":undefined}"

      146 |       // @ts-ignore-next-line
      147 |       const e1 = { x: undefined, y: Object, z: Symbol('') }
    > 148 |       expect(canonify(e1)).toEqual('{}')
          |                            ^
      149 |
      150 |       // @ts-ignore-next-line
      151 |       const e2 = { [Symbol('foo')]: 'foo' }

      at Object.<anonymous> (tests/basics.spec.ts:148:28)

  โ— serializing โ€บ arrays should handle โ€บ a one element function array

    expect(received).toEqual(expected) // deep equality

    Expected: "[null]"
    Received: "[undefined]"

      232 |     test('a one element function array', () => {
      233 |       let f = function foo() { }
    > 234 |       expect(canonify([f])).toEqual('[null]');
          |                             ^
      235 |     });
      236 |
      237 |     test('a nested array', () => {

      at Object.<anonymous> (tests/basics.spec.ts:234:29)

  โ— serializing โ€บ objects should handle โ€บ an object with a function value

    expect(received).toEqual(expected) // deep equality

    Expected: "{}"
    Received: "{\"test\":undefined}"

      291 |     test('an object with a function value', () => {
      292 |       let f = function foo() { }
    > 293 |       expect(canonify({ test: f })).toEqual('{}');
          |                                     ^
      294 |     });
      295 |
      296 |     test('an object with a toJSON serializer function value', () => {

      at Object.<anonymous> (tests/basics.spec.ts:293:37)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |     100 |      100 |     100 |     100 |                   
 index.ts |     100 |      100 |     100 |     100 |                   
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 failed, 1 passed, 2 total
Tests:       6 failed, 47 passed, 53 total
Snapshots:   0 total
Time:        4.451 s
Ran all test suites.

Cheers.

Glenn

Thanks for reaching out I will try to make some time to look into this! Happy that the code was of use :)

To any future people who were confused, like me, truestamp's canonify github repo is no longer accessible (I don't know why). Their NPM entry for it is still public: https://www.npmjs.com/package/@truestamp/canonify