text: 'use strict';
  
  const { Writable } = require('stream');
  
  const PerMessageDeflate = require('./permessage-deflate');
  const {
    BINARY_TYPES,
    EMPTY_BUFFER,
    kStatusCode,
    kWebSocket
  } = require('./constants');
  const { concat, toArrayBuffer, unmask } = require('./buffer-util');
  const { isValidStatusCode, isValidUTF8 } = require('./validation');
  
  const GET_INFO = 0;
  const GET_PAYLOAD_LENGTH_16 = 1;
  const GET_PAYLOAD_LENGTH_64 = 2;
  const GET_MASK = 3;
  const GET_DATA = 4;
  const INFLATING = 5;
  
  /**
   * HyBi Receiver implementation.
   *
   * @extends stream.Writable
   */
  class Receiver extends Writable {
    /**
     * Creates a Receiver instance.
     *
     * @param {String} binaryType The type for binary data
     * @param {Object} extensions An object containing the negotiated extensions
     * @param {Number} maxPayload The maximum allowed message length
     */
    constructor(binaryType, extensions, maxPayload) {
      super();
  
      this._binaryType = binaryType || BINARY_TYPES[0];
      this[kWebSocket] = undefined;
      this._extensions = extensions || {};
      this._maxPayload = maxPayload | 0;
  
      this._bufferedBytes = 0;
      this._buffers = [];
  
      this._compressed = false;
      this._payloadLength = 0;
      this._mask = undefined;
      this._fragmented = 0;
      this._masked = false;
      this._fin = false;
      this._opcode = 0;
  
      this._totalPayloadLength = 0;
      this._messageLength = 0;
      this._fragments = [];
  
      this._state = GET_INFO;
      this._loop = false;
    }
  
    /**
     * Implements `Writable.prototype._write()`.
     *
     * @param {Buffer} chunk The chunk of data to write
     * @param {String} encoding The character encoding of `chunk`
     * @param {Function} cb Callback
     */
    _write(chunk, encoding, cb) {
      if (this._opcode === 0x08 && this._state == GET_INFO) return cb();
  
      this._bufferedBytes += chunk.length;
      this._buffers.push(chunk);
      this.startLoop(cb);
    }
  
    /**
     * Consumes `n` bytes from the buffered data.
     *
     * @param {Number} n The number of bytes to consume
     * @return {Buffer} The consumed bytes
     * @private
     */
    consume(n) {
      this._bufferedBytes -= n;
  
      if (n === this._buffers[0].length) return this._buffers.shift();
  
      if (n < this._buffers[0].length) {
        const buf = this._buffers[0];
        this._buffers[0] = buf.slice(n);
        return buf.slice(0, n);
      }
  
      const dst = Buffer.allocUnsafe(n);
  
      do {
        const buf = this._buffers[0];
        const offset = dst.length - n;
  
        if (n >= buf.length) {
          dst.set(this._buffers.shift(), offset);
        } else {
          dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset);
          this._buffers[0] = buf.slice(n);
        }
  
        n -= buf.length;
      } while (n > 0);
  
      return dst;
    }
  
    /**
     * Starts the parsing loop.
     *
     * @param {Function} cb Callback
     * @private
     */
    startLoop(cb) {
      let err;
      this._loop = true;
  
      do {
        switch (this._state) {
          case GET_INFO:
            err = this.getInfo();
            break;
          case GET_PAYLOAD_LENGTH_16:
            err = this.getPayloadLength16();
            break;
          case GET_PAYLOAD_LENGTH_64:
            err = this.getPayloadLength64();
            break;
          case GET_MASK:
            this.getMask();
            break;
          case GET_DATA:
            err = this.getData(cb);
            break;
          default:
            // `INFLATING`
            this._loop = false;
            return;
        }
      } while (this._loop);
  
      cb(err);
    }
  
    /**
     * Reads the first two bytes of a frame.
     *
     * @return {(RangeError|undefined)} A possible error
     * @private
     */
    getInfo() {
      if (this._bufferedBytes < 2) {
        this._loop = false;
        return;
      }
  
      const buf = this.consume(2);
  
      if ((buf[0] & 0x30) !== 0x00) {
        this._loop = false;
        return error(RangeError, 'RSV2 and RSV3 must be clear', true, 1002);
      }
  
      const compressed = (buf[0] & 0x40) === 0x40;
  
      if (compressed && !this._extensions[PerMessageDeflate.extensionName]) {
        this._loop = false;
        return error(RangeError, 'RSV1 must be clear', true, 1002);
      }
  
      this._fin = (buf[0] & 0x80) === 0x80;
      this._opcode = buf[0] & 0x0f;
      this._payloadLength = buf[1] & 0x7f;
  
      if (this._opcode === 0x00) {
        if (compressed) {
          this._loop = false;
          return error(RangeError, 'RSV1 must be clear', true, 1002);
        }
  
        if (!this._fragmented) {
          this._loop = false;
          return error(RangeError, 'invalid opcode 0', true, 1002);
        }
  
        this._opcode = this._fragmented;
      } else if (this._opcode === 0x01 || this._opcode === 0x02) {
        if (this._fragmented) {
          this._loop = false;
          return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002);
        }
  
        this._compressed = compressed;
      } else if (this._opcode > 0x07 && this._opcode < 0x0b) {
        if (!this._fin) {
          this._loop = false;
          return error(RangeError, 'FIN must be set', true, 1002);
        }
  
        if (compressed) {
          this._loop = false;
          return error(RangeError, 'RSV1 must be clear', true, 1002);
        }
  
        if (this._payloadLength > 0x7d) {
          this._loop = false;
          return error(
            RangeError,
            `invalid payload length ${this._payloadLength}`,
            true,
            1002
          );
        }
      } else {
        this._loop = false;
        return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002);
      }
  
      if (!this._fin && !this._fragmented) this._fragmented = this._opcode;
      this._masked = (buf[1] & 0x80) === 0x80;
  
      if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16;
      else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64;
      else return this.haveLength();
    }
  
    /**
     * Gets extended payload length (7+16).
     *
     * @return {(RangeError|undefined)} A possible error
     * @private
     */
    getPayloadLength16() {
      if (this._bufferedBytes < 2) {
        this._loop = false;
        return;
      }
  
      this._payloadLength = this.consume(2).readUInt16BE(0);
      return this.haveLength();
    }
  
    /**
     * Gets extended payload length (7+64).
     *
     * @return {(RangeError|undefined)} A possible error
     * @private
     */
    getPayloadLength64() {
      if (this._bufferedBytes < 8) {
        this._loop = false;
        return;
      }
  
      const buf = this.consume(8);
      const num = buf.readUInt32BE(0);
  
      //
      // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned
      // if payload length is greater than this number.
      //
      if (num > Math.pow(2, 53 - 32) - 1) {
        this._loop = false;
        return error(
          RangeError,
          'Unsupported WebSocket frame: payload length > 2^53 - 1',
          false,
          1009
        );
      }
  
      this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4);
      return this.haveLength();
    }
  
    /**
     * Payload length has been read.
     *
     * @return {(RangeError|undefined)} A possible error
     * @private
     */
    haveLength() {
      if (this._payloadLength && this._opcode < 0x08) {
        this._totalPayloadLength += this._payloadLength;
        if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) {
          this._loop = false;
          return error(RangeError, 'Max payload size exceeded', false, 1009);
        }
      }
  
      if (this._masked) this._state = GET_MASK;
      else this._state = GET_DATA;
    }
  
    /**
     * Reads mask bytes.
     *
     * @private
     */
    getMask() {
      if (this._bufferedBytes < 4) {
        this._loop = false;
        return;
      }
  
      this._mask = this.consume(4);
      this._state = GET_DATA;
    }
  
    /**
     * Reads data bytes.
     *
     * @param {Function} cb Callback
     * @return {(Error|RangeError|undefined)} A possible error
     * @private
     */
    getData(cb) {
      let data = EMPTY_BUFFER;
  
      if (this._payloadLength) {
        if (this._bufferedBytes < this._payloadLength) {
          this._loop = false;
          return;
        }
  
        data = this.consume(this._payloadLength);
        if (this._masked) unmask(data, this._mask);
      }
  
      if (this._opcode > 0x07) return this.controlMessage(data);
  
      if (this._compressed) {
        this._state = INFLATING;
        this.decompress(data, cb);
        return;
      }
  
      if (data.length) {
        //
        // This message is not compressed so its lenght is the sum of the payload
        // length of all fragments.
        //
        this._messageLength = this._totalPayloadLength;
        this._fragments.push(data);
      }
  
      return this.dataMessage();
    }
  
    /**
     * Decompresses data.
     *
     * @param {Buffer} data Compressed data
     * @param {Function} cb Callback
     * @private
     */
    decompress(data, cb) {
      const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
  
      perMessageDeflate.decompress(data, this._fin, (err, buf) => {
        if (err) return cb(err);
  
        if (buf.length) {
          this._messageLength += buf.length;
          if (this._messageLength > this._maxPayload && this._maxPayload > 0) {
            return cb(
              error(RangeError, 'Max payload size exceeded', false, 1009)
            );
          }
  
          this._fragments.push(buf);
        }
  
        const er = this.dataMessage();
        if (er) return cb(er);
  
        this.startLoop(cb);
      });
    }
  
    /**
     * Handles a data message.
     *
     * @return {(Error|undefined)} A possible error
     * @private
     */
    dataMessage() {
      if (this._fin) {
        const messageLength = this._messageLength;
        const fragments = this._fragments;
  
        this._totalPayloadLength = 0;
        this._messageLength = 0;
        this._fragmented = 0;
        this._fragments = [];
  
        if (this._opcode === 2) {
          let data;
  
          if (this._binaryType === 'nodebuffer') {
            data = concat(fragments, messageLength);
          } else if (this._binaryType === 'arraybuffer') {
            data = toArrayBuffer(concat(fragments, messageLength));
          } else {
            data = fragments;
          }
  
          this.emit('message', data);
        } else {
          const buf = concat(fragments, messageLength);
  
          if (!isValidUTF8(buf)) {
            this._loop = false;
            return error(Error, 'invalid UTF-8 sequence', true, 1007);
          }
  
          this.emit('message', buf.toString());
        }
      }
  
      this._state = GET_INFO;
    }
  
    /**
     * Handles a control message.
     *
     * @param {Buffer} data Data to handle
     * @return {(Error|RangeError|undefined)} A possible error
     * @private
     */
    controlMessage(data) {
      if (this._opcode === 0x08) {
        this._loop = false;
  
        if (data.length === 0) {
          this.emit('conclude', 1005, '');
          this.end();
        } else if (data.length === 1) {
          return error(RangeError, 'invalid payload length 1', true, 1002);
        } else {
          const code = data.readUInt16BE(0);
  
          if (!isValidStatusCode(code)) {
            return error(RangeError, `invalid status code ${code}`, true, 1002);
          }
  
          const buf = data.slice(2);
  
          if (!isValidUTF8(buf)) {
            return error(Error, 'invalid UTF-8 sequence', true, 1007);
          }
  
          this.emit('conclude', code, buf.toString());
          this.end();
        }
      } else if (this._opcode === 0x09) {
        this.emit('ping', data);
      } else {
        this.emit('pong', data);
      }
  
      this._state = GET_INFO;
    }
  }
  
  module.exports = Receiver;
  
  /**
   * Builds an error object.
   *
   * @param {(Error|RangeError)} ErrorCtor The error constructor
   * @param {String} message The error message
   * @param {Boolean} prefix Specifies whether or not to add a default prefix to
   *     `message`
   * @param {Number} statusCode The status code
   * @return {(Error|RangeError)} The error
   * @private
   */
  function error(ErrorCtor, message, prefix, statusCode) {
    const err = new ErrorCtor(
      prefix ? `Invalid WebSocket frame: ${message}` : message
    );
  
    Error.captureStackTrace(err, error);
    err[kStatusCode] = statusCode;
    return err;
  }
  