cbor
Version:
Encode and parse CBOR documents.
259 lines (224 loc) • 6.74 kB
text/coffeescript
stream = require 'stream'
url = require 'url'
bignumber = require 'bignumber.js'
BufferStream = require '../lib/BufferStream'
Tagged = require '../lib/tagged'
Simple = require '../lib/simple'
constants = require './constants'
# TODO: replace these with constants and add unit tests to verify
MT = constants.MT
NUM_BYTES = constants.NUM_BYTES
TAG = constants.TAG
SHIFT32 = Math.pow 2, 32
DOUBLE = (MT.SIMPLE_FLOAT<<5)|NUM_BYTES.EIGHT
TRUE = (MT.SIMPLE_FLOAT<<5)|constants.SIMPLE.TRUE
FALSE = (MT.SIMPLE_FLOAT<<5)|constants.SIMPLE.FALSE
UNDEFINED = (MT.SIMPLE_FLOAT<<5)|constants.SIMPLE.UNDEFINED
NULL = (MT.SIMPLE_FLOAT<<5)|constants.SIMPLE.NULL
# A Readable stream of CBOR bytes. Call `write` to get JSON objects translated
# into the stream.
module.exports = class Encoder extends stream.Readable
# Create an encoder
# @param options [Object] options for the encoder
# @option options genTypes [Array] array of pairs of `type`, `function(Encoder)`
# for semantic types to be encoded. (default: [Array, arrayFunc,
# Date, dateFunc, Buffer, bufferFunc, RegExp, regexFunc,
# url.Url, urlFunc, bignumber, bignumberFunc]
constructor: (options={})->
super options
@bs = new BufferStream options
@going = false
@sendEOF = false
@semanticTypes = [
Array, @_packArray
Date, @_packDate
Buffer, @_packBuffer
RegExp, @_packRegexp
url.Url, @_packUrl
bignumber, @_packBigNumber
]
addTypes = options.genTypes ? []
for typ,i in addTypes by 2
@addSemanticType typ, addTypes[i+1]
# Add an encoding function to the list of supported semantic types. This is
# useful for objects for which you can't add an encodeCBOR method
# @return [function(Encoder)] if this type already exists in the semantic type
# list, replace that function with the new one, and return the old one.
# Return null if this is a new type.
addSemanticType: (type, fun) ->
for typ,i in @semanticTypes by 2
if typ == type
[old, @semanticTypes[i+1]] = [@semanticTypes[i+1], fun]
return old
@semanticTypes.push type, fun
null
# @nodoc
_read: (size)->
@going = true
while @going
x = @bs.read()
if x.length
@going = @push x
else
if @sendEOF
@going = @push null
break
# @nodoc
_packNaN: ()->
@bs.write 'f97e00', 'hex' # Half-NaN
# @nodoc
_packInfinity: (obj)->
half = if obj < 0 then 'f9fc00' else 'f97c00'
@bs.write half, 'hex'
# @nodoc
_packFloat: (obj)->
# TODO: see if we can write smaller ones.
@bs.writeUInt8 DOUBLE
@bs.writeDoubleBE obj
# @nodoc
_packInt: (obj,mt)->
mt = mt << 5
switch
when obj < 24 then @bs.writeUInt8 mt|obj
when obj <= 0xff
@bs.writeUInt8 mt|NUM_BYTES.ONE
@bs.writeUInt8 obj
when obj <= 0xffff
@bs.writeUInt8 mt|NUM_BYTES.TWO
@bs.writeUInt16BE obj
when obj <= 0xffffffff
@bs.writeUInt8 mt|NUM_BYTES.FOUR
@bs.writeUInt32BE obj
when obj < 0x20000000000000
@bs.writeUInt8 mt|NUM_BYTES.EIGHT
@bs.writeUInt32BE Math.floor(obj / SHIFT32)
@bs.writeUInt32BE (obj % SHIFT32)
else
@_packFloat obj
# @nodoc
_packNumber: (obj)->
switch
when isNaN(obj) then @_packNaN obj
when !isFinite(obj) then @_packInfinity obj
when Math.round(obj) == obj #int
if obj<0
@_packInt -obj-1, MT.NEG_INT
else
@_packInt obj, MT.POS_INT
else
@_packFloat obj
# @nodoc
_packString: (obj)->
len = Buffer.byteLength obj, 'utf8'
@_packInt len, MT.UTF8_STRING
@bs.writeString obj, len, 'utf8'
# @nodoc
_packBoolean: (obj)->
@bs.writeUInt8 if obj then TRUE else FALSE
# @nodoc
_packUndefined: (obj)->
@bs.writeUInt8 UNDEFINED
# @nodoc
_packArray: (gen, obj)->
len = obj.length
@_packInt len, MT.ARRAY
for x in obj
@_pack x
# @nodoc
_packTag: (tag)->
@_packInt tag, MT.TAG
# @nodoc
_packDate: (gen, obj)->
@_packTag TAG.DATE_EPOCH
@_pack obj / 1000
# @nodoc
_packBuffer: (gen, obj)->
@_packInt obj.length, MT.BYTE_STRING
@bs.append obj
# @nodoc
_packRegexp: (gen, obj)->
@_packTag TAG.REGEXP
@_pack obj.source
# @nodoc
_packUrl: (gen, obj)->
@_packTag TAG.URI
@_pack obj.format()
# @nodoc
_packBigint: (obj)->
if obj.isNegative()
obj = obj.negated().minus(1)
tag = TAG.NEG_BIGINT
else
tag = TAG.POS_BIGINT
str = obj.toString(16)
if str.length % 2
str = '0'+str
buf = new Buffer str, 'hex'
@_packTag tag
@_packBuffer this, buf, @bs
# @nodoc
_packBigNumber: (gen, obj)->
if obj.isNaN()
return @_packNaN()
unless obj.isFinite()
return @_packInfinity if obj.isNegative() then -Infinity else Infinity
# if integer, just write a bigint.
if obj.c.length < (obj.e + 2)
return @_packBigint obj
@_packTag TAG.DECIMAL_FRAC
@_packInt 2, MT.ARRAY
slide = new bignumber obj
@_packInt slide.e, MT.POS_INT
slide.e = slide.c.length - 1
@_packBigint slide
# @nodoc
_packObject: (obj)->
unless obj then return @bs.writeUInt8 NULL
for typ,i in @semanticTypes by 2
if obj instanceof typ
return @semanticTypes[i+1].call(this, this, obj)
f = obj.encodeCBOR
if typeof f == 'function'
return f.call(obj, this)
keys = Object.keys obj
len = keys.length
@_packInt len, MT.MAP
for k in keys
@_pack k
@_pack obj[k]
# @nodoc
_pack: (obj)->
switch typeof(obj)
when 'number' then @_packNumber obj
when 'string' then @_packString obj
when 'boolean' then @_packBoolean obj
when 'undefined' then @_packUndefined obj
when 'object' then @_packObject obj
else
# e.g. function
throw new Error('Unknown type: ' + typeof(obj));
# Encode one or more JavaScript objects into the stream.
# @param objs... [Object+] the objects to encode
write: (objs...)->
for o in objs
@_pack o
if @going
x = @bs.read()
if x.length
@going = @push x
# Encode zero or more JavaScript objects into the stream, then end the stream.
# @param objs... [Object*] the objects to encode
end: (objs...)->
if objs.length then @write objs...
if @going
# assert.ok(@bs.length == 0)
@going = @push null
else
@sendEOF = true
# Encode one or more JavaScript objects, and return a Buffer containing the
# CBOR bytes.
# @param objs... [Object+] the objects to encode
@encode: (objs...)->
g = new Encoder
g.end objs...
g.read()