Skip to content

Commit

Permalink
fix: catch full range of NaN, Infinity and -Infinity for allow*=false
Browse files Browse the repository at this point in the history
  • Loading branch information
rvagg committed Jan 11, 2021
1 parent 9217d5d commit f424741
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 21 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ cborg is designed with deterministic encoding forms as a primary feature. It is

* Varying number sizes and no strict requirement for their encoding - e.g. a `1` may be encoded as `0x01`, `0x1801`, `0x190001`, `1a00000001` or `1b0000000000000001`.
* Varying int sizes used as lengths for lengthed objects (maps, arrays, strings, bytes) - e.g. a single entry array could specify its length using any of the above forms for `1`. Tags can also vary in size and still represent the same number.
* IEEE 754 allows for `NaN`, `Infinity` and `-Infinity` to be represented in many different ways, meaning it is possible to represent the same data using many different byte forms.
* Indefinite length items where the length is omitted from the additional item of the entity token and a "break" is inserted to indicate the end of of the object. This provides two ways to encode the same object.
* Tags that can allow alternative representations of objects - e.g. using the bigint or negative bigint tags to represent standard size integers.
* Map ordering is flexible by default, so a single map can be represented in many different forms by shuffling the keys.
Expand All @@ -258,6 +259,7 @@ By default, cborg will always **encode** objects to the same bytes by applying s
By default, cborg allows for some flexibility on **decode** of objects, which will present some challenges if users wish to impose strictness requirements at both serialization _and_ deserialization. Options that can be provided to `decode()` to impose some strictness requirements are:

* `strict: true` to impose strict sizing rules for int, negative ints and lengths of lengthed objects
* `allowNaN: false` and `allowInfinity` to prevent decoding of any value that would resolve to `NaN`, `Infinity` or `-Infinity`, using CBOR tokens or IEEE 754 representation—as long as your application can do without these symbols.
* `allowIndefinite: false` to disallow indefinite lengthed objects and the "break" tag
* Not providing any tag decoders, or ensuring that tag decoders are strict about their forms (e.g. a bigint decoder could reject bigints that could have fit into a standard major 0 64-bit integer).
* Overriding type decoders where they may introduce undesired flexibility.
Expand Down
33 changes: 18 additions & 15 deletions lib/7float.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,28 @@ export function decodeBreak (data, pos, minor, options) {
return new Token(Type.break, undefined, 1)
}

function createToken (value, bytes, options) {
if (options) {
if (options.allowNaN === false && Number.isNaN(value)) {
throw new Error(`${decodeErrPrefix} NaN values are not supported`)
}
if (options.allowInfinity === false && (value === Infinity || value === -Infinity)) {
throw new Error(`${decodeErrPrefix} Infinity values are not supported`)
}
}
return new Token(Type.float, value, bytes)
}

export function decodeFloat16 (data, pos, minor, options) {
return new Token(Type.float, readFloat16(data, pos + 1, options), 3)
return createToken(readFloat16(data, pos + 1), 3, options)
}

export function decodeFloat32 (data, pos, minor, options) {
return new Token(Type.float, readFloat32(data, pos + 1), 5)
return createToken(readFloat32(data, pos + 1), 5, options)
}

export function decodeFloat64 (data, pos, minor, options) {
return new Token(Type.float, readFloat64(data, pos + 1), 9)
return createToken(readFloat64(data, pos + 1), 9, options)
}

export function encodeFloat (buf, token, options) {
Expand Down Expand Up @@ -61,7 +73,7 @@ export function encodeFloat (buf, token, options) {
let decoded
if (!options || options.float64 !== true) {
encodeFloat16(float)
decoded = readFloat16(ui8a, 1) // no options, `allowInfinity` and `allowNaN` are for decode
decoded = readFloat16(ui8a, 1)
if (float === decoded || Number.isNaN(float)) {
ui8a[0] = 0xf9
buf.copyTo(0, ui8a.slice(0, 3))
Expand Down Expand Up @@ -94,7 +106,7 @@ encodeFloat.encodedSize = function encodedSize (token, options) {
let decoded
if (!options || options.float64 !== true) {
encodeFloat16(float)
decoded = readFloat16(ui8a, 1) // no options, `allowInfinity` and `allowNaN` are for decode
decoded = readFloat16(ui8a, 1)
if (float === decoded || Number.isNaN(float)) {
return 3
}
Expand Down Expand Up @@ -154,28 +166,19 @@ function encodeFloat16 (inp) {
}
}

function readFloat16 (ui8a, pos, options) {
function readFloat16 (ui8a, pos) {
if (ui8a.length - pos < 2) {
throw new Error(`${decodeErrPrefix} not enough data for float16`)
}

const half = (ui8a[pos] << 8) + ui8a[pos + 1]
if (half === 0x7c00) {
if (options && options.allowInfinity === false) {
throw new Error(`${decodeErrPrefix} Infinity values are not supported`)
}
return Infinity
}
if (half === 0xfc00) {
if (options && options.allowInfinity === false) {
throw new Error(`${decodeErrPrefix} Infinity values are not supported`)
}
return -Infinity
}
if (half === 0x7e00) {
if (options && options.allowNaN === false) {
throw new Error(`${decodeErrPrefix} NaN values are not supported`)
}
return NaN
}
const exp = (half >> 10) & 0x1f
Expand Down
26 changes: 20 additions & 6 deletions test/test-7float.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,15 @@ const fixtures = [
{ data: 'fb8000000000000002', expected: -1e-323, type: 'float64' },
{ data: 'fb3fefffffffffffff', expected: 0.9999999999999999, type: 'float64' },
{ data: 'fbbfefffffffffffff', expected: -0.9999999999999999, type: 'float64' },
{ data: 'f97c00', expected: Infinity, type: 'Infinity' },
{ data: 'f9fc00', expected: -Infinity, type: '-Infinity' },
{ data: 'f97e00', expected: NaN, type: 'NaN' },
{ data: 'f97c00', expected: Infinity, type: 'Infinity' }, // special CBOR token for -Infinity
{ data: 'fb7ff0000000000000', expected: Infinity, type: 'Infinity', strict: false }, // an IEEE 754 representation of Infinity
{ data: 'f9fc00', expected: -Infinity, type: '-Infinity' }, // special CBOR token for -Infinity
{ data: 'fbfff0000000000000', expected: -Infinity, type: '-Infinity', strict: false }, // an IEEE 754 representation of Infinity
{ data: 'f97e00', expected: NaN, type: 'NaN' }, // special CBOR token for NaN
{ data: 'f97ff8', expected: NaN, type: 'NaN', strict: false }, // one of the many IEEE 754 representations of NaN
{ data: 'fa7ff80000', expected: NaN, type: 'NaN', strict: false },
{ data: 'fb7ff8000000000000', expected: NaN, type: 'NaN', strict: false },
{ data: 'fb7ff8cafedeadbeef', expected: NaN, type: 'NaN', strict: false }, // yep, that's NaN too
{ data: 'fb40f4241a31a5a515', expected: 82497.63712086187, type: 'float64' }
]

Expand All @@ -47,9 +53,11 @@ describe('float', () => {

describe('encode', () => {
for (const fixture of fixtures) {
it(`should encode ${fixture.type}=${fixture.expected}`, () => {
assert.strictEqual(toHex(encode(fixture.expected)), fixture.data, `encode ${fixture.type}`)
})
if (fixture.strict !== false) {
it(`should encode ${fixture.type}=${fixture.expected}`, () => {
assert.strictEqual(toHex(encode(fixture.expected)), fixture.data, `encode ${fixture.type}`)
})
}
}
})

Expand Down Expand Up @@ -98,11 +106,17 @@ describe('float', () => {
assert.deepStrictEqual(decode(fromHex('830102f9fc00')), [1, 2, -Infinity])
assert.throws(() => decode(fromHex('830102f97c00'), { allowInfinity: false }), /Infinity/)
assert.throws(() => decode(fromHex('830102f9fc00'), { allowInfinity: false }), /Infinity/)
for (const fixture of fixtures.filter((f) => f.type.endsWith('Infinity'))) {
assert.throws(() => decode(fromHex(fixture.data), { allowInfinity: false }), /Infinity/)
}
})

it('can switch off NaN support', () => {
assert.deepStrictEqual(decode(fromHex('830102f97e00')), [1, 2, NaN])
assert.throws(() => decode(fromHex('830102f97e00'), { allowNaN: false }), /NaN/)
for (const fixture of fixtures.filter((f) => f.type === 'NaN')) {
assert.throws(() => decode(fromHex(fixture.data), { allowNaN: false }), /NaN/)
}
})
})
})

0 comments on commit f424741

Please sign in to comment.