UNPKG

cbor

Version:

Encode and parse CBOR documents.

449 lines (394 loc) 13.2 kB
# jslint node: true assert = require 'assert' util = require 'util' stream = require 'stream' # @nodoc EMPTY = new Buffer 0 # @nodoc createEOF = () -> e = new Error('EOF') e.BufferStreamEOF = true e # A buffer that grows in chunks, and allows waiting for a given number of # bytes to be available. # # @method #writeUInt8(val) # Write an unsigned byte into the stream # @param val [Integer] the byte to write # # @method #writeUInt16LE(val) # Write an unsigned 16 bit integer into the stream in little endian order # to the stream # @param val [Integer] # # @method #writeUInt16BE(val) # Write an unsigned 16 bit integer into the stream in big endian order # to the stream # @param val [Integer] # # @method #writeUInt32LE(val) # Write an unsigned 32 bit integer into the stream in little endian order # to the stream # @param val [Integer] # # @method #writeUInt32BE(val) # Write an unsigned 32 bit integer into the stream in big endian order # to the stream # @param val [Integer] # # @method #writeInt8(val) # Write a signed 8 bit integer into the stream # to the stream # @param val [Integer] # # @method #writeInt16LE(val) # Write a signed 16 bit integer into the stream in little endian order # to the stream # @param val [Integer] # # @method #writeInt16BE(val) # Write a signed 16 bit integer into the stream in big endian order # to the stream # @param val [Integer] # # @method #writeInt32LE(val) # Write a signed 32 bit integer into the stream in little endian order # to the stream # @param val [Integer] # # @method #writeInt32BE(val) # Write a signed 32 bit integer into the stream in big endian order # to the stream # @param val [Integer] # # @method #writeFloatLE(val) # Write a single-precision IEEE 754 floating point number in little endian order # to the stream # @param val [Integer] # # @method #writeFloatBE(val) # Write a single-precision IEEE 754 floating point number in big endian order # to the stream # @param val [Integer] # # @method #writeDoubleLE(val) # Write a double-precision IEEE 754 floating point number in little endian order # to the stream # @param val [Integer] # # @method #writeDoubleBE(val) # Write a double-precision IEEE 754 floating point number in big endian order # to the stream # @param val [Integer] class BufferStream extends stream.Writable # @param options [Object] can be as for stream, but also: # @option options [Buffer] bsInit initial buffer # @option options [Integer] bsGrowSize the number of bytes to grow when needed (default: 512) # @option options [Boolean] bsStartEmpty don't initialize with a growSize buffer (default: false) # @option options [Boolean] bsStartEnded after bsInit is appended, should we end the stream? # if bsInit, defaults to `true`, else ignored # @option options [Boolean] bsZero whenever a new Buffer is added to the list, # initialize it to zero (default: false) constructor: (options={})-> @clear() @_resetCB() super options @growSize = options.bsGrowSize ? 512 @zero = options.bsZero ? false @on "finish", ()=> @_resetCB createEOF() # Sometimes we'll know the buffer we're working with, # and it will never grow. buf = options.bsInit if Buffer.isBuffer buf @append buf startEnded = options.bsStartEnded ? true if !!startEnded @end() else if !options.bsStartEmpty @grow() # Is the given object a BufferStream? # @param obj [Object] the object to check # @return [Boolean] @isBufferStream: (obj)-> obj instanceof BufferStream # Is the given Error an End Of File indicator? # @param er [Error] the error object to check # @return [Boolean] @isEOFError: (er)-> er and (er instanceof Error) and (er.BufferStreamEOF == true) # When this object gets passed to an Encoder, render it as a byte string. # @nodoc encodeCBOR: (enc)-> enc._packBuffer enc, @flatten() # Is this BufferStream valid? # Checks `@length`, `@left`, and the internal buffer list for consistency # @return [Boolean] isValid: ()-> len = @bufs.reduce (prev, cur)-> prev + cur.length , 0 len -= @left len == @length # @nodoc # Leave this in for debugging, please. _bufSizes: ()-> @bufs.map (b)-> b.length # @nodoc _write: (chunk, encoding, cb)-> unless Buffer.isBuffer chunk cb(new Error 'String encoding not supported') else @append chunk cb() # @nodoc _resetCB: (args...)-> [cb, @waitingCB, @waitingLen] = [@waitingCB, null, Number.MAX_VALUE] if cb and args.length cb.apply @, args cb # @nodoc _notifyWaiter: ()-> assert.ok @waitingCB buf = @read(@waitingLen); @_resetCB null, buf # Read from the BufferStream # # @param length [Integer] number of bytes to read, up to `@length`. # @option length [Integer] -1 get the first buffer # @option length [Integer] 0 read all read: (length)-> buf = null if @length == 0 return EMPTY lenZ = @bufs[0].length length ?= 0 lenW = switch length when 0 then @length when -1 then Math.min(@length, lenZ) else Math.min(@length, length) # TODO: don't keep slicing; maintain a start offset. # Note to self: none of the paths here need to modify @left # You don't need to check again. if lenZ == lenW # hey, it might happen, often if length=-1 buf = @bufs.shift() else if lenZ > lenW # just return part of the first buf buf = @bufs[0].slice(0, lenW) @bufs[0] = @bufs[0].slice(lenW) else if lenW == @length # the guts of @flatten. We know there's more than one buffer needed, # so skip some of the checks there. Also don't destroy left. local_left = null if @left != 0 lastbuf = @bufs[@bufs.length-1] if @left == lastbuf.length # grown, but not added to yet. This will break Buffer.concat. local_left = @bufs.pop() else local_left = lastbuf.slice(lastbuf.length - @left) buf = Buffer.concat @bufs, @length @bufs = if local_left then [local_left] else [] else # We're going to need more than one. some = [] got = 0 while got < lenW b = @bufs.shift() some.push b got += b.length buf = Buffer.concat some, lenW if got > lenW # put the unread bytes back. Those bytes will ALWAYS be in the last # chunk of some. last = some[some.length-1] left = got-lenW @bufs.unshift last.slice(last.length - left) @length -= lenW buf # Are we at the End of File? # @return [Boolean] isEOF: ()-> (@length == 0) and @_writableState.finished # Wait for a given number of bytes to be available, then call the callback # @param length [Integer] number of bytes to wait for # @param cb [function] callback(error, buffer), where if the error is empty, # the buffer will be exactly `length` bytes # @return [void] # @throw [Error] invalid state, a second wait() while one was already pending # @throw [Error] invalid `length` # @throw [Error] no callback specified wait: (length, cb)-> # TODO: should these be asserts? if @waitingCB throw new Error 'Invalid state. Cannot wait while already waiting.' unless (typeof(length) == 'number') and (length >= 0) throw new Error "length required, must be non-negative number: #{length}" unless typeof(cb) == 'function' throw new Error 'cb required, must be function' if length == 0 # I totally waited. Really. process.nextTick ()=> cb.call @, null, EMPTY else @waitingCB = cb @waitingLen = length if @length >= length @_notifyWaiter() else if @_writableState.ended # never gonna fill you up # Damn you, me from months ago. You just rolled yourself. @_resetCB createEOF() # Clear all bytes from the stream, without notifying any pending waits # @return [void] clear: ()-> # if someone is waiting, they will have to keep waiting; # everything currently read is tossed @bufs = [] @length = 0 @left = 0 # Trim the last buffer in the list, so that there are no unused bytes # @nodoc _trimLast: ()-> # set left to 0, keeping any relevant info in the last buffer old = @left if @left > 0 last = @bufs.pop() if @left != last.length @bufs.push(last.slice(0, last.length-@left)) @left = 0 old # @nodoc _lengthen: (size)-> assert.ok size>0 @length += size len = @length if @length >= @waitingLen @_notifyWaiter() len # @nodoc grow: (size)-> @_trimLast() s = size ? @growSize b = new Buffer(s) if @zero b.fill 0 @bufs.push(b) @left = s b # Append a buffer to the stream # @param buf [Buffer] the buffer to add # @return [Integer] the number of bytes added append: (buf)-> assert.ok Buffer.isBuffer(buf) len = buf.length return if len == 0 if @left == 0 @bufs.push buf else if len > @left @_trimLast() # left always 0 @bufs.push buf # still nothing left else lastbuf = @bufs[@bufs.length-1] buf.copy lastbuf, lastbuf.length - @left @left -= len @_lengthen len # Smoosh everything into one buffer, with nothing left over # This probably should never be called aside from internally or from the # unit tests. May be changed to _flatten in the future. # # @note This does not fire any waiting callbacks, or remove bytes from the stream. # @return [Buffer] the concatenated set of all bytes flatten: ()-> if @length == 0 @left = 0 @bufs = [] return EMPTY b = null switch @bufs.length # Note: this really is an assert, since it's protected by the # @length == 0 above when 0 then assert.fail @length, "Invalid state. No buffers when length>0." when 1 if @left == 0 # already flat b = @bufs[0] else b = @bufs[0].slice 0, @length @bufs = [b] @left = 0 else if @left == @bufs[@bufs.length-1].length # grown, but not added to yet. This will break Buffer.concat, # so just drop it as unused @bufs.pop() b = Buffer.concat(@bufs, @length); @bufs = [b]; @left = 0; b # Generate a buffer cropped by `start` and `end`. Negative indexes start from # the end of the bytes currently in the stream. # # @note This does not fire any waiting callbacks, or remove bytes from the stream. # @param start [Integer] start offset (default: 0) # @param end [Integer] end offset (default: `@length`) # @return [Buffer] the generated slice slice: (start, end)-> @flatten().slice start, end # Fill the buffer with a given value. # @param val [Integer] the value to put in each byte # @param offset [Integer] beginning offset to modify (default: 0) # @param end [Integer] end offset (default: `@length`) fill: (val, offset, end)-> @flatten().fill val, offset, end # Convert the bytes to JSON, as a list of integers. # @return [String] JSON representation toJSON: ()-> @flatten().toJSON() # Convert the bytes to a string, in the given encoding. # @param encoding [String] encoding name (default: 'hex') # @return [String] string representation toString: (encoding='hex')-> @flatten().toString(encoding) # Make sure that there are `len` bytes available for writing. # @nodoc ensure: (len)-> if @left < len @grow Math.max(@growSize, len) else @bufs[@bufs.length-1] # Write a string into the stream # @param value [String] the string to write # @param length [Integer] the number of *bytes* to write (default: byte # length of string in the given encoding) # @param encoding [String] The encoding to use for the string (default: 'utf8') writeString: (value, length, encoding='utf8')-> length ?= Buffer.byteLength value, encoding return if length == 0 b = @ensure length b.write value, b.length - @left, length, encoding @left -= length @_lengthen length # @nodoc @_write_gen: (meth, len)-> (val)-> b = @ensure len b[meth].call b, val, b.length - @left, true @left -= len @_lengthen len writeUInt8: @_write_gen 'writeUInt8', 1 writeUInt16LE: @_write_gen 'writeUInt16LE', 2 writeUInt16BE: @_write_gen 'writeUInt16BE', 2 writeUInt32LE: @_write_gen 'writeUInt32LE', 4 writeUInt32BE: @_write_gen 'writeUInt32BE', 4 writeInt8: @_write_gen 'writeInt8', 1 writeInt16LE: @_write_gen 'writeInt16LE', 2 writeInt16BE: @_write_gen 'writeInt16BE', 2 writeInt32LE: @_write_gen 'writeInt32LE', 4 writeInt32BE: @_write_gen 'writeInt32BE', 4 writeFloatLE: @_write_gen 'writeFloatLE', 4 writeFloatBE: @_write_gen 'writeFloatBE', 4 writeDoubleLE: @_write_gen 'writeDoubleLE', 8 writeDoubleBE: @_write_gen 'writeDoubleBE', 8 module.exports = BufferStream;