//  SPDX-License-Identifier: LGPL-2.1-or-later
//  Copyright (c) 2015-2024 MariaDB Corporation Ab

'use strict';

const PacketNodeEncoded = require('./packet-node-encoded');
const PacketIconvEncoded = require('./packet-node-iconv');
const Collations = require('../const/collations');
const Utils = require('../misc/utils');
const Errors = require('../misc/errors');

/**
 * MySQL packet parser
 * see : https://mariadb.com/kb/en/library/0-packet/
 */
class PacketInputStream {
  constructor(unexpectedPacket, receiveQueue, out, opts, info, fatalError) {
    this.unexpectedPacket = unexpectedPacket;
    this.opts = opts;
    this.receiveQueue = receiveQueue;
    this.info = info;
    this.out = out;
    this.fatalError = fatalError;

    //in case packet is not complete
    this.header = Buffer.allocUnsafe(4);
    this.headerLen = 0;
    this.packetLen = null;
    this.remainingLen = null;

    this.parts = null;
    this.partsTotalLen = 0;
    this.multiPacketAllowed = false;
    this.changeEncoding(this.opts.collation ? this.opts.collation : Collations.fromIndex(224));
    this.changeDebug(this.opts.debug);
    this.opts.on('collation', this.changeEncoding.bind(this));
    this.opts.on('debug', this.changeDebug.bind(this));
  }

  changeEncoding(collation) {
    this.encoding = collation.charset;
    this.packet = Buffer.isEncoding(this.encoding)
      ? new PacketNodeEncoded(this.encoding)
      : new PacketIconvEncoded(this.encoding);
  }

  changeDebug(debug) {
    this.receivePacket = debug ? this.receivePacketDebug : this.receivePacketBasic;
  }

  receivePacketDebug(packet) {
    let cmd = this.currentCmd();
    this.header[0] = this.packetLen;
    this.header[1] = this.packetLen >> 8;
    this.header[2] = this.packetLen >> 16;
    this.header[3] = this.sequenceNo;
    if (packet) {
      this.opts.logger.network(
        `<== conn:${this.info.threadId ? this.info.threadId : -1} ${
          cmd
            ? cmd.onPacketReceive
              ? cmd.constructor.name + '.' + cmd.onPacketReceive.name
              : cmd.constructor.name
            : 'no command'
        } (${packet.pos},${packet.end})\n${Utils.log(this.opts, packet.buf, packet.pos, packet.end, this.header)}`
      );
    }

    if (!cmd) {
      this.unexpectedPacket(packet);
      return;
    }

    cmd.sequenceNo = this.sequenceNo;
    cmd.onPacketReceive(packet, this.out, this.opts, this.info);
    if (!cmd.onPacketReceive) {
      this.receiveQueue.shift();
    }
  }

  receivePacketBasic(packet) {
    let cmd = this.currentCmd();
    if (!cmd) {
      this.unexpectedPacket(packet);
      return;
    }
    cmd.sequenceNo = this.sequenceNo;
    cmd.onPacketReceive(packet, this.out, this.opts, this.info);
    if (!cmd.onPacketReceive) this.receiveQueue.shift();
  }

  resetHeader() {
    this.remainingLen = null;
    this.headerLen = 0;
  }

  currentCmd() {
    let cmd;
    while ((cmd = this.receiveQueue.peek())) {
      if (cmd.onPacketReceive) return cmd;
      this.receiveQueue.shift();
    }
    return null;
  }

  /**
   * A packet whose length is exactly 0xffffff signals a multi-part packet (more fragments follow).
   * Before authentication completes this can only be a malicious/MitM server attempting to exhaust
   * client memory through unbounded reassembly, so refuse it and tear the connection down.
   */
  rejectPrematureMultiPacket() {
    this.parts = null;
    this.partsTotalLen = 0;
    this.fatalError(
      Errors.createFatalError(
        'Multi-part packet (16MB fragment) received before authentication completed. This is not ' +
          'legitimate server behaviour and is refused to prevent unbounded memory reassembly.',
        Errors.ER_UNEXPECTED_PACKET,
        this.info
      ),
      true
    );
  }

  onData(chunk) {
    let pos = 0;
    let length;
    const chunkLen = chunk.length;

    do {
      //read header
      if (this.remainingLen) {
        length = this.remainingLen;
      } else if (this.headerLen === 0 && chunkLen - pos >= 4) {
        this.packetLen = chunk[pos] + (chunk[pos + 1] << 8) + (chunk[pos + 2] << 16);
        this.sequenceNo = chunk[pos + 3];
        pos += 4;
        length = this.packetLen;
      } else {
        length = null;
        while (chunkLen - pos > 0) {
          this.header[this.headerLen++] = chunk[pos++];
          if (this.headerLen === 4) {
            this.packetLen = this.header[0] + (this.header[1] << 8) + (this.header[2] << 16);
            this.sequenceNo = this.header[3];
            length = this.packetLen;
            break;
          }
        }
      }

      if (length) {
        if (chunkLen - pos >= length) {
          pos += length;
          if (!this.parts) {
            if (this.packetLen < 0xffffff) {
              this.receivePacket(this.packet.update(chunk, pos - length, pos));
              // fast path, knowing there is no parts
              // loop can be simplified until reaching the end of the packet.
              while (pos + 4 < chunkLen) {
                this.packetLen = chunk[pos] + (chunk[pos + 1] << 8) + (chunk[pos + 2] << 16);
                this.sequenceNo = chunk[pos + 3];
                pos += 4;
                if (chunkLen - pos >= this.packetLen) {
                  pos += this.packetLen;
                  if (this.packetLen < 0xffffff) {
                    this.receivePacket(this.packet.update(chunk, pos - this.packetLen, pos));
                  } else {
                    this.parts = [chunk.subarray(pos - this.packetLen, pos)];
                    this.partsTotalLen = this.packetLen;
                    break;
                  }
                } else {
                  const buf = chunk.subarray(pos, chunkLen);
                  if (!this.parts) {
                    this.parts = [buf];
                    this.partsTotalLen = chunkLen - pos;
                  } else {
                    this.parts.push(buf);
                    this.partsTotalLen += chunkLen - pos;
                  }
                  this.remainingLen = this.packetLen - (chunkLen - pos);
                  return;
                }
              }
            } else {
              this.parts = [chunk.subarray(pos - length, pos)];
              this.partsTotalLen = length;
            }
          } else {
            this.parts.push(chunk.subarray(pos - length, pos));
            this.partsTotalLen += length;

            if (this.packetLen < 0xffffff) {
              let buf = Buffer.concat(this.parts, this.partsTotalLen);
              this.parts = null;
              this.receivePacket(this.packet.update(buf, 0, this.partsTotalLen));
            } else if (!this.multiPacketAllowed) {
              // a multi-part packet is being reassembled before authentication completed
              // refuse it to prevent unbounded memory growth (CONJS-358)
              return this.rejectPrematureMultiPacket();
            }
          }
          this.resetHeader();
        } else {
          const buf = chunk.subarray(pos, chunkLen);
          if (!this.parts) {
            this.parts = [buf];
            this.partsTotalLen = chunkLen - pos;
          } else {
            this.parts.push(buf);
            this.partsTotalLen += chunkLen - pos;
          }
          this.remainingLen = length - (chunkLen - pos);
          return;
        }
      }
    } while (pos < chunkLen);
  }
}

module.exports = PacketInputStream;
