cbor
Version:
Encode and parse CBOR documents.
368 lines (331 loc) • 10.6 kB
text/coffeescript
# jslint node: true
assert = require 'assert'
stream = require 'stream'
async = require 'async'
BufferStream = require '../lib/BufferStream'
utils = require '../lib/utils'
Simple = require '../lib/simple'
constants = require './constants'
MT = constants.MT
# @nodoc
# Never executed, just an atom to compare against that can never happen in
# a real stream.
`// istanbul ignore next`
BREAK = () ->
"BREAK"
# A SAX-style syntactic parser for CBOR. Either pipe another
# stream into an instance of this class, or pass a string, Buffer, or
# BufferStream into `options.input`, assign callbacks, then call `start()`.
#
# In the event callbacks, `kind` is one of the following strings:
# - 'value': an atomic value was detected
# - 'array-first': the first element of an array
# - 'array': an item after the first in an array
# - 'key-first': the first key in a map
# - 'key': a key other than the first in a map
# - 'stream-first': the first item in an indefinite encoding
# - 'stream': an item other than the first in an indefinite encoding
# - null: the end of a top-level CBOR item
#
# @event value(val,tags,kind) an atomic item (not a map or array) was detected
# @param val [any] the value
# @param tags [Array] an array of tags that preceded the value
# @param kind [String] see above
#
# @event array-start(count,tags,kind) the start of an array has been read.
# @param count [Integer] the number of items in the array. -1 if indefinite length.
# @param tags [Array] an array of tags that preceded the list.
# @param kind [String] see above
#
# @event array-stop(count,tags,kind) the end of an array has been reached.
# @param count [Integer] the actual number of items in the array.
# @param tags [Array] an array of tags that preceded the list.
# @param kind [String] see above
#
# @event map-start(count,tags,kind) the start of a map has been read.
# @param count [Integer] the number of pairs in the map. -1 if indefinite length.
# @param tags [Array] an array of tags that preceded the list
# @param kind [String] see above
#
# @event map-stop(count,tags,kind) the end of a map has been reached
# @param count [Integer] the actual number of pairs in the map.
# @param tags [Array] an array of tags that preceded the list
# @param kind [String] see above
#
# @event stream-start(mt,tags,kind) The start of a CBOR indefinite length
# bytestring or utf8-string.
# @param mt [] the major type for all of the items
# @param tags [Array] an array of tags that preceded the list
# @param kind [String] see above
#
# @event stream-stop(count,mt,tags,kind) we got to the end of a CBOR indefinite
# length bytestring or utf8-string.
# @param count [Integer] the number of constituent items
# @param mt [] the major type for all of the items
# @param tags [Array] an array of tags that preceded the list
# @param kind [String] see above
#
# @event end() the end of the input
#
# @event error(er) parse error such as invalid input
# @param er [Error]
#
module.exports = class Evented extends stream.Writable
# Create an event-based CBOR parser.
# @param options [Object] options for the parser
# @option options input [Buffer,String,BufferStream] optional input
# @option options encoding [String] encoding of a String `input` (default: 'hex')
# @option options offset [Integer] *byte* offset into the input from which to start
constructor: (options) ->
super() # TODO: pass subset of options
@options = utils.extend
max_depth: 512 # on my machine, the max was 674
input: null
offset: 0
encoding: 'hex'
, options
@bs = null
@tags = []
@kind = null
@depth = 0
@last_err = null
@on 'finish', ()->
@bs.end()
if @options.input?
input = @options.input
buf = null
if Buffer.isBuffer(input)
buf = input
else if typeof(input) == 'string'
if @options.encoding == 'hex'
input = input.replace /^0x/, ''
buf = new Buffer(input, @options.encoding)
else if BufferStream.isBufferStream(input)
@bs = input
return @_start
else
throw new Error "input must be Buffer, string, or BufferStream"
if @options.offset
buf = buf.slice @options.offset
@bs = new BufferStream
bsInit: buf
@_start
else
@bs = new BufferStream
@_pump()
# All events have been hooked, start parsing the input.
#
# @note This MUST NOT be called if you're piping a Readable stream in.
start: ()->
@_pump()
error: (er)->
@last_er = er
# @nodoc
_write: (chunk, enc, next)->
@bs.write chunk, enc, next
# @nodoc
_drainState: () ->
tags = @tags.slice()
@tags.length = 0
[@kind, kind] = [null, @kind]
[tags, kind]
# @nodoc
_val: (val,cb) ->
[tags, kind] = @_drainState()
@emit 'value', val, tags, kind
cb.call this, @last_er, val
# @nodoc
_readBuf: (len,cb) ->
@bs.wait len, (er,buf) =>
return cb.call(this,er) if er
@_val buf, cb
# @nodoc
_readStr: (len,cb) ->
@bs.wait len, (er,buf) =>
return cb.call(this,er) if er
@_val buf.toString('utf8'), cb
# @nodoc
_readArray: (count,cb) ->
[tags, kind] = @_drainState()
@emit 'array-start', count, tags, kind
async.timesSeries count, (n,done) =>
@kind = if n then 'array' else 'array-first'
@_unpack done
, (er) =>
return cb.call(this, er) if er
@emit 'array-stop', count, tags, kind
@mt = MT.ARRAY
cb.call this
# @nodoc
_readMap: (count,cb) ->
[tags, kind] = @_drainState()
@emit 'map-start', count, tags, kind
up = @_unpack.bind(this)
async.timesSeries count, (n,done) =>
async.series [
(cb) =>
@kind = if n then 'key' else 'key-first'
up cb
, (cb) =>
@kind = 'value'
up cb
], done
, (er) =>
return cb.call(this, er) if er
@emit 'map-stop', count, tags, kind
@mt = MT.MAP
cb.call this
# @nodoc
_readTag: (val,cb) ->
@tags.push val
@_unpack cb
# @nodoc
_readSimple: (val,cb) ->
switch val
when 20 then @_val false, cb
when 21 then @_val true, cb
when 22 then @_val null, cb
when 23 then @_val undefined, cb
else @_val new Simple(val), cb
# @nodoc
_getVal: (val,cb) ->
switch @mt
when MT.POS_INT then @_val val, cb
when MT.NEG_INT then @_val -1-val, cb
when MT.BYTE_STRING then @_readBuf val, cb
when MT.UTF8_STRING then @_readStr val, cb
when MT.ARRAY then @_readArray val, cb
when MT.MAP then @_readMap val, cb
when MT.TAG then @_readTag val, cb
when MT.SIMPLE_FLOAT then @_readSimple val, cb
else
# really should never happen, since the above are all of the cases
# of three bits
`// istanbul ignore next`
cb.call this, new Error("Unknown major type(#{@mt}): #{val}")
# @nodoc
_stream_stringy: (cb) ->
mt = @mt
[tags, kind] = @_drainState()
count = 0
@emit 'stream-start', mt, tags, kind
keep_going = true
async.doWhilst (done) =>
@kind = if count then 'stream' else 'stream-first'
@_unpack (er,val) =>
return done(er) if er
if val == BREAK
keep_going = false
else
if @mt != mt
done(new Error "Invalid stream major type: #{@mt}, when anticipating only #{mt}")
return
count++
done()
, () ->
keep_going
, (er) =>
return cb.call(this, er) if er
@emit 'stream-stop', count, mt, tags, kind
cb.call this
# @nodoc
_stream_array: (cb) ->
mt = @mt
[tags, kind] = @_drainState()
count = 0
@emit 'array-start', -1, tags, kind
keep_going = true
async.doWhilst (done) =>
@kind = if count then 'array' else 'array-first'
@_unpack (er,val) =>
return done(er) if er
if val == BREAK
keep_going = false
else
count++
done()
, () ->
keep_going
, (er) =>
return cb.call(this, er) if er
@emit 'array-stop', count, tags, kind
cb.call this
# @nodoc
_stream_map: (cb) ->
mt = @mt
[tags, kind] = @_drainState()
count = 0
@emit 'map-start', -1, tags, kind
keep_going = true
async.doWhilst (done) =>
@kind = if count then 'key' else 'key-first'
@_unpack (er,val) ->
return done(er) if er
if val == BREAK
keep_going = false
done()
else
count++
@kind = 'value'
@_unpack done
, () ->
keep_going
, (er) =>
return cb.call(this, er) if er
@emit 'map-stop', count, tags, kind
cb.call this
# @nodoc
_stream: (cb) ->
switch @mt
when MT.BYTE_STRING, MT.UTF8_STRING then @_stream_stringy cb
when MT.ARRAY then @_stream_array cb
when MT.MAP then @_stream_map cb
when MT.SIMPLE_FLOAT
[tags, kind] = @_drainState()
cb.call this, null, BREAK
else
# really should never happen, since the above are all of the cases
# of three bits
`// istanbul ignore next`
cb.call this, new Error("Invalid stream major type: #{@mt}")
# @nodoc
_unpack: (cb) ->
@bs.wait 1, (er,buf) =>
return cb(er) if er
@depth++
if @depth > @options.max_depth
return cb.call this, new Error("Maximum depth exceeded: #{@depth}")
@octet = buf[0]
@mt = @octet >> 5
@ai = @octet & 0x1f
decrement = (er)=>
unless er instanceof Error
@depth--
cb.apply this, arguments
switch @ai
when 24,25,26,27
@bs.wait 1<<(@ai-24), (er,buf) =>
return cb er if er
if @mt == MT.SIMPLE_FLOAT # floating point or high simple
if @ai == 24
@_readSimple utils.parseInt(@ai, buf), decrement
else
@_val utils.parseFloat(@ai, buf), decrement
else
@_getVal utils.parseInt(@ai, buf), decrement
when 28,29,30
return cb(new Error("Additional info not implemented: #{@ai}"))
when 31 then @_stream decrement
else @_getVal @ai, decrement
# @nodoc
_pump: (er)=>
if er
if BufferStream.isEOFError(er) && (@depth == 0)
return @emit 'end'
else
return @emit 'error', er
async.nextTick ()=>
if @bs.isEOF()
@emit 'end'
else
@_unpack @_pump