lib/reader.js

'use strict'

const UnderflowError = require('../errors/underflow-error')
const ParseError = require('../errors/parse-error')

class Reader {
  constructor (buffer) {
    this.buffer = buffer
    this.cursor = 0
    this.bookmarks = []
  }

  /**
   * Create a Reader from a source of bytes.
   *
   * Currently, this method only allows the creation of a Reader from a Buffer.
   *
   * If the object provided is already a Reader, that reader is returned as is.
   *
   * @param {Reader|Buffer} source Source of binary data.
   * @return {Reader} Instance of Reader
   */
  static from (source) {
    if (Buffer.isBuffer(source)) {
      return new Reader(source)
    } else if (source instanceof Reader) {
      return source
    } else {
      throw new Error('Reader must be given a Buffer')
    }
  }

  /**
   * Store the current cursor position on a stack.
   */
  bookmark () {
    this.bookmarks.push(this.cursor)
  }

  /**
   * Pop the most recently bookmarked cursor position off the stack.
   */
  restore () {
    this.cursor = this.bookmarks.pop()
  }

  /**
   * Ensure this number of bytes is buffered.
   *
   * This method checks that the given number of bytes is buffered and available
   * for reading. If insufficient bytes are available, the method throws an
   * `UnderflowError`.
   *
   * @param {Number} bytes Number of bytes that should be available.
   */
  ensureAvailable (bytes) {
    if (this.buffer.length < (this.cursor + bytes)) {
      throw new UnderflowError('Tried to read ' + bytes + ' bytes, but only ' +
        (this.buffer.length - this.cursor) + ' bytes available')
    }
  }

  /**
   * Read a fixed-length big-endian integer.
   *
   * @param {Number} length Length of the integer in bytes.
   * @return {Number} Contents of next byte.
   */
  readUInt (length) {
    if (length > Reader.MAX_INT_BYTES) {
      throw new Error('Tried too read too large integer (requested: ' +
        length + ', max: ' + Reader.MAX_INT_BYTES + ')')
    }
    this.ensureAvailable(length)
    const value = this.buffer.readUIntBE(this.cursor, length)
    this.cursor += length
    return value
  }

  /**
   * Look at a fixed-length integer, but don't advance the cursor.
   *
   * @param {Number} length Length of the integer in bytes.
   * @return {Number} Contents of the next byte.
   */
  peekUInt (length) {
    if (length > Reader.MAX_INT_BYTES) {
      throw new Error('Tried too read too large integer (requested: ' +
        length + ', max: ' + Reader.MAX_INT_BYTES + ')')
    }
    this.ensureAvailable(length)
    const value = this.buffer.readUIntBE(this.cursor, length)
    return value
  }

  /**
   * Advance cursor by length bytes.
   */
  skipUInt (length) {
    this.skip(length)
  }

  /**
   * Read a variable-length integer at the cursor position.
   *
   * Return the integer as a number and advance the cursor accordingly.
   *
   * @return {Number} Value of the integer.
   */
  readVarUInt () {
    const buffer = this.readVarOctetString()
    if (buffer.length > Reader.MAX_INT_BYTES) {
      throw new ParseError('UInt of length ' + buffer.length +
        'too large to parse as integer (max: ' + Reader.MAX_INT_BYTES + ')')
    }

    return buffer.readUIntBE(0, buffer.length)
  }

  /**
   * Read the next variable-length integer, but don't advance the cursor.
   *
   * @return {Number} Integer at the cursor position.
   */
  peekVarUInt () {
    this.bookmark()
    const value = this.readVarUInt()
    this.restore()

    return value
  }

  /**
   * Skip past the variable-length integer at the cursor position.
   *
   * Since variable integers are encoded the same way as octet strings, this
   * method is equivalent to skipVarOctetString.
   */
  skipVarUInt () {
    this.skipVarOctetString()
  }

  /**
   * Read a fixed-length octet string.
   *
   * @param {Number} length Length of the octet string.
   */
  readOctetString (length) {
    return this.read(length)
  }

  /**
   * Peek at a fixed length octet string.
   *
   * @param {Number} length Length of the octet string.
   */
  peekOctetString (length) {
    return this.peek(length)
  }

  /**
   * Skip a fixed length octet string.
   *
   * @param {Number} length Length of the octet string.
   */
  skipOctetString (length) {
    return this.skip(length)
  }

  /**
   * Read a length prefix.
   *
   * You shouldn't need this. Length prefixes are used internally by
   * variable-length octet strings and integers.
   *
   * @return {Number} Length value.
   */
  readLengthPrefix () {
    const length = this.readUInt8()

    if (length & Reader.HIGH_BIT) {
      return this.readUInt(length & Reader.LOWER_SEVEN_BITS)
    }

    return length
  }

  /**
   * Read a variable-length octet string.
   *
   * A variable-length octet string is a length-prefixed set of arbitrary bytes.
   *
   * @return {Buffer} Contents of the octet string.
   */
  readVarOctetString () {
    const length = this.readLengthPrefix()

    return this.read(length)
  }

  /**
   * Read a variable-length buffer, but do not advance cursor position.
   *
   * @return {Buffer} Contents of the buffer.
   */
  peekVarOctetString () {
    this.bookmark()
    const value = this.readVarOctetString()
    this.restore()

    return value
  }

  /**
   * Skip a variable-length buffer.
   */
  skipVarOctetString () {
    const length = this.readLengthPrefix()

    return this.skip(length)
  }

  /**
   * Read a given number of bytes.
   *
   * Returns this many bytes starting at the cursor position and advances the
   * cursor.
   *
   * @param {Number} bytes Number of bytes to read.
   * @return {Buffer} Contents of bytes read.
   */
  read (bytes) {
    this.ensureAvailable(bytes)

    const value = this.buffer.slice(this.cursor, this.cursor + bytes)
    this.cursor += bytes

    return value
  }

  /**
   * Read bytes, but do not advance cursor.
   *
   * @param {Number} bytes Number of bytes to read.
   * @return {Buffer} Contents of bytes read.
   */
  peek (bytes) {
    this.ensureAvailable(bytes)

    return this.buffer.slice(this.cursor, this.cursor + bytes)
  }

  /**
   * Skip a number of bytes.
   *
   * Advances the cursor by this many bytes.
   *
   * @param {Number} bytes Number of bytes to advance the cursor by.
   */
  skip (bytes) {
    this.ensureAvailable(bytes)

    this.cursor += bytes
  }
}

// Most significant bit in a byte
Reader.HIGH_BIT = 0x80

// Other bits in a byte
Reader.LOWER_SEVEN_BITS = 0x7F

// Largest integer (in bytes) that is safely representable in JavaScript
// => Math.floor(Number.MAX_SAFE_INTEGER.toString(2).length / 8)
Reader.MAX_INT_BYTES = 6

// Create {read,peek,skip}UInt{8,16,32,64} shortcuts
;['read', 'peek', 'skip'].forEach((verb) => {
  ;[1, 2, 4, 8].forEach((bytes) => {
    Reader.prototype[verb + 'UInt' + bytes * 8] = function () {
      return this[verb + 'UInt'](bytes)
    }
  })
})

module.exports = Reader