UNPKG

cbor

Version:

Encode and parse CBOR documents.

368 lines (331 loc) 10.6 kB
# 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