/* Copyright (c) 2015, 2025, Oracle and/or its affiliates.

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License, version 2.0,
   as published by the Free Software Foundation.

   This program is designed to work with certain software (including
   but not limited to OpenSSL) that is licensed under separate terms,
   as designated in a particular file or component or in included license
   documentation.  The authors of MySQL hereby grant you an additional
   permission to link the program and your derivative works with the
   separately licensed software that they have either included with
   the program or referenced in the documentation.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License, version 2.0, for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA */

#include "mysql/binlog/event/trx_boundary_parser.h"

#include <string.h>

#include "m_string.h"
#include "my_byteorder.h"
#include "mysql/binlog/event/binlog_event.h"
#include "string_with_len.h"

#if !defined(NDEBUG) && defined(HAVE_MYSYS)
/* Event parser state names */
static const char *event_parser_state_names[] = {"None", "GTID", "DDL", "DML",
                                                 "Error"};
#endif

/*
  -----------------------------------------
  Transaction_boundary_parser class methods
  -----------------------------------------
*/

namespace mysql::binlog::event {

Transaction_boundary_parser::~Transaction_boundary_parser() = default;

/**
   Reset the transaction boundary parser.

   This method initialize the boundary parser state.
*/
void Transaction_boundary_parser::reset() {
  BAPI_PRINT("info", ("transaction boundary parser is changing state "
                      "from '%s' to '%s'",
                      event_parser_state_names[current_parser_state],
                      event_parser_state_names[EVENT_PARSER_NONE]));
  current_parser_state = EVENT_PARSER_NONE;
  last_parser_state = EVENT_PARSER_NONE;
  m_current_boundary_state = EVENT_BOUNDARY_TYPE_ERROR;
}

bool Transaction_boundary_parser::feed_event(
    mysql::binlog::event::Log_event_basic_info log_event_info,
    bool throw_warnings) {
  BAPI_TRACE;

  m_current_boundary_state =
      get_event_boundary_type(log_event_info, throw_warnings);
  return update_state(m_current_boundary_state, throw_warnings);
}

bool Transaction_boundary_parser::check_row_logging_constraints(
    mysql::binlog::event::Log_event_basic_info log_event_info) {
  BAPI_TRACE;

  // If the boundary parser is in error state return true
  if (EVENT_BOUNDARY_TYPE_ERROR == m_current_boundary_state) return true;

  // INTVAR_EVENT, RAND_EVENT, USER_VAR_EVENT
  if (EVENT_BOUNDARY_TYPE_PRE_STATEMENT == m_current_boundary_state) {
    // An USER_VAR_EVENT can appear with RBR when associated to DDL events
    return log_event_info.event_type != mysql::binlog::event::USER_VAR_EVENT ||
           current_parser_state != EVENT_PARSER_DDL;
  }

  // DDL or TRANSACTION_PAYLOAD_EVENT
  if (EVENT_BOUNDARY_TYPE_STATEMENT == m_current_boundary_state ||
      current_parser_state == EVENT_PARSER_NONE) {
    if (log_event_info.query_length > 16 &&
        (!native_strncasecmp(log_event_info.query,
                             STRING_WITH_LEN("CREATE TEMPORARY")) ||
         !native_strncasecmp(log_event_info.query,
                             STRING_WITH_LEN("DROP TEMPORARY")))) {
      return true;
    }
  }

  // Defend against injected LOAD events
  if (log_event_info.event_type ==
          mysql::binlog::event::BEGIN_LOAD_QUERY_EVENT ||
      log_event_info.event_type ==
          mysql::binlog::event::EXECUTE_LOAD_QUERY_EVENT ||
      log_event_info.event_type == mysql::binlog::event::APPEND_BLOCK_EVENT ||
      log_event_info.event_type == mysql::binlog::event::DELETE_FILE_EVENT)
    return true;

  // DML using non row events
  if (EVENT_BOUNDARY_TYPE_STATEMENT == m_current_boundary_state &&
      current_parser_state == EVENT_PARSER_DML)
    if (log_event_info.event_type != mysql::binlog::event::TABLE_MAP_EVENT &&
        log_event_info.event_type != mysql::binlog::event::WRITE_ROWS_EVENT &&
        log_event_info.event_type != mysql::binlog::event::UPDATE_ROWS_EVENT &&
        log_event_info.event_type != mysql::binlog::event::DELETE_ROWS_EVENT &&
        log_event_info.event_type !=
            mysql::binlog::event::PARTIAL_UPDATE_ROWS_EVENT &&
        log_event_info.event_type !=
            mysql::binlog::event::ROWS_QUERY_LOG_EVENT &&
        log_event_info.event_type != mysql::binlog::event::VIEW_CHANGE_EVENT) {
      // We allow XA control statements
      if (log_event_info.event_type == mysql::binlog::event::QUERY_EVENT) {
        if (!native_strncasecmp(log_event_info.query,
                                STRING_WITH_LEN("XA END")) ||
            !native_strncasecmp(log_event_info.query,
                                STRING_WITH_LEN("XA PREPARE")) ||
            !native_strncasecmp(log_event_info.query,
                                STRING_WITH_LEN("SAVEPOINT")) ||
            !native_strncasecmp(log_event_info.query,
                                STRING_WITH_LEN("CREATE TABLE")))
          return false;
      }
      return true;
    }

  return false;
}

Transaction_boundary_parser::enum_event_boundary_type
Transaction_boundary_parser::get_event_boundary_type(
    mysql::binlog::event::Log_event_basic_info event_info,
    bool throw_warnings) {
  BAPI_TRACE;

  enum_event_boundary_type boundary_type;

  switch (event_info.event_type) {
    case mysql::binlog::event::GTID_LOG_EVENT:
    case mysql::binlog::event::ANONYMOUS_GTID_LOG_EVENT:
    case mysql::binlog::event::GTID_TAGGED_LOG_EVENT:
      boundary_type = EVENT_BOUNDARY_TYPE_GTID;
      break;

    /*
      There are four types of queries that we have to deal with: BEGIN, COMMIT,
      ROLLBACK and the rest.
    */
    case mysql::binlog::event::QUERY_EVENT: {
      if (event_info.query_length == 0) {
        /* purecov: begin inspected */
        BAPI_ASSERT(event_info.query == nullptr);
        boundary_type = EVENT_BOUNDARY_TYPE_ERROR;
        break;
        /* purecov: end */
      }

      /*
        BEGIN is always the begin of a DML transaction.
      */
      if (!strncmp(event_info.query, "BEGIN", event_info.query_length) ||
          !strncmp(event_info.query, STRING_WITH_LEN("XA START")))
        boundary_type = EVENT_BOUNDARY_TYPE_BEGIN_TRX;
      /*
        COMMIT and ROLLBACK are always the end of a transaction.
      */
      else if (!strncmp(event_info.query, "COMMIT", event_info.query_length) ||
               (!native_strncasecmp(event_info.query,
                                    STRING_WITH_LEN("ROLLBACK")) &&
                native_strncasecmp(event_info.query,
                                   STRING_WITH_LEN("ROLLBACK TO "))))
        boundary_type = EVENT_BOUNDARY_TYPE_END_TRX;
      /*
        XA ROLLBACK is always the end of a XA transaction.
      */
      else if (!native_strncasecmp(event_info.query,
                                   STRING_WITH_LEN("XA ROLLBACK")))
        boundary_type = EVENT_BOUNDARY_TYPE_END_XA_TRX;
      /*
        If the query is not (BEGIN | XA START | COMMIT | [XA] ROLLBACK), it can
        be considered an ordinary statement.
      */
      else
        boundary_type = EVENT_BOUNDARY_TYPE_STATEMENT;

      break;
    }

    /*
      XID events are always the end of a transaction.
      XA_prepare event ends XA-prepared group of events (prepared XA
      transaction).
    */
    case mysql::binlog::event::XID_EVENT:
    case mysql::binlog::event::XA_PREPARE_LOG_EVENT:
      boundary_type = EVENT_BOUNDARY_TYPE_END_TRX;
      break;

    /*
      Intvar, Rand and User_var events are always considered as pre-statements.
    */
    case mysql::binlog::event::INTVAR_EVENT:
    case mysql::binlog::event::RAND_EVENT:
    case mysql::binlog::event::USER_VAR_EVENT:
      boundary_type = EVENT_BOUNDARY_TYPE_PRE_STATEMENT;
      break;

    /*
      The following event types are always considered as statements
      because they will always be wrapped between BEGIN/COMMIT.
    */
    case mysql::binlog::event::EXECUTE_LOAD_QUERY_EVENT:
    case mysql::binlog::event::TABLE_MAP_EVENT:
    case mysql::binlog::event::APPEND_BLOCK_EVENT:
    case mysql::binlog::event::BEGIN_LOAD_QUERY_EVENT:
    case mysql::binlog::event::ROWS_QUERY_LOG_EVENT:
    case mysql::binlog::event::WRITE_ROWS_EVENT:
    case mysql::binlog::event::UPDATE_ROWS_EVENT:
    case mysql::binlog::event::DELETE_ROWS_EVENT:
    case mysql::binlog::event::VIEW_CHANGE_EVENT:
    case mysql::binlog::event::PARTIAL_UPDATE_ROWS_EVENT:
      boundary_type = EVENT_BOUNDARY_TYPE_STATEMENT;
      break;

    /*
      Incident events have their own boundary type.
    */
    case mysql::binlog::event::INCIDENT_EVENT:
      boundary_type = EVENT_BOUNDARY_TYPE_INCIDENT;
      break;

    /*
      Rotate, Format_description and Heartbeat should be ignored.
      Also, any other kind of event not listed in the "cases" above
      will be ignored.
    */
    case mysql::binlog::event::ROTATE_EVENT:
    case mysql::binlog::event::FORMAT_DESCRIPTION_EVENT:
    case mysql::binlog::event::HEARTBEAT_LOG_EVENT:
    case mysql::binlog::event::HEARTBEAT_LOG_EVENT_V2:
    case mysql::binlog::event::PREVIOUS_GTIDS_LOG_EVENT:
    case mysql::binlog::event::STOP_EVENT:
    case mysql::binlog::event::SLAVE_EVENT:
    case mysql::binlog::event::DELETE_FILE_EVENT:
    case mysql::binlog::event::TRANSACTION_CONTEXT_EVENT:
      boundary_type = EVENT_BOUNDARY_TYPE_IGNORE;
      break;

    case mysql::binlog::event::TRANSACTION_PAYLOAD_EVENT:
      boundary_type = EVENT_BOUNDARY_TYPE_TRANSACTION_PAYLOAD;
      break;

    /*
      If the event is none of above supported event types, this is probably
      an event type unsupported by this server version. So, we must check if
      this event is ignorable or not.
    */
    default:
      if (event_info.ignorable_event)
        boundary_type = EVENT_BOUNDARY_TYPE_IGNORE;
      else {
        boundary_type = EVENT_BOUNDARY_TYPE_ERROR;
        if (throw_warnings)
          log_server_warning(
              ER_TRX_BOUND_UNSUPPORTED_UNIGNORABLE_EVENT_IN_STREAM, nullptr);
      }
  } /* End of switch(event_type) */

  return boundary_type;
}

/**
   Update the boundary parser state based on a given boundary type.

   @param event_boundary_type The event boundary type of the event used to
                              fed the boundary parser.
   @param throw_warnings      If the function should throw warnings while
                              updating the boundary parser state.
                              Please see comments on this at feed_event().

   @return  false State updated successfully.
            true  There was an error updating the state.
*/
bool Transaction_boundary_parser::update_state(
    enum_event_boundary_type event_boundary_type, bool throw_warnings) {
  BAPI_TRACE;

  enum_event_parser_state new_parser_state = EVENT_PARSER_NONE;

  bool error = false;

  switch (event_boundary_type) {
    /*
      GTIDs are always the start of a transaction stream.
    */
    case EVENT_BOUNDARY_TYPE_GTID:
      /* In any case, we will update the state to GTID */
      new_parser_state = EVENT_PARSER_GTID;
      /* The following switch is mostly to differentiate the warning messages */
      switch (current_parser_state) {
        case EVENT_PARSER_GTID:
        case EVENT_PARSER_DDL:
        case EVENT_PARSER_DML:
          /*
            We don't consider an unexpected GTID appearing in the applier as
            being an error. In this context it can mean several things:
            - A transaction was logged partially in an old relay log
            - A transaction is retrying after an error
          */
          if (m_trx_boundary_parser_context != TRX_BOUNDARY_PARSER_APPLIER) {
            if (throw_warnings) {
              if (current_parser_state == EVENT_PARSER_GTID)
                log_server_warning(
                    ER_TRX_BOUND_GTID_LOG_EVENT_IN_STREAM,
                    "after a GTID_LOG_EVENT, GTID_TAGGED_LOG_EVENT or an "
                    "ANONYMOUS_GTID_LOG_EVENT");
              else {
                if (current_parser_state == EVENT_PARSER_DDL)
                  log_server_warning(ER_TRX_BOUND_GTID_LOG_EVENT_IN_STREAM,
                                     "in the middle of a DDL");
                else
                  log_server_warning(ER_TRX_BOUND_GTID_LOG_EVENT_IN_STREAM,
                                     "in the middle of a DML");
              }
            }
            error = true;
          }
          break;
        case EVENT_PARSER_ERROR: /* we probably threw a warning before */
          error = true;
          /* FALL THROUGH */
        case EVENT_PARSER_NONE:
          break;
      }
      break;

    /*
      There are four types of queries that we have to deal with: BEGIN, COMMIT,
      ROLLBACK and the rest.
    */
    case EVENT_BOUNDARY_TYPE_BEGIN_TRX:
      /* In any case, we will update the state to DML */
      new_parser_state = EVENT_PARSER_DML;
      /* The following switch is mostly to differentiate the warning messages */
      switch (current_parser_state) {
        case EVENT_PARSER_DDL:
        case EVENT_PARSER_DML:
          if (throw_warnings) {
            if (current_parser_state == EVENT_PARSER_DDL)
              log_server_warning(ER_TRX_BOUND_UNEXPECTED_BEGIN_IN_STREAM,
                                 "DDL");
            else
              log_server_warning(ER_TRX_BOUND_UNEXPECTED_BEGIN_IN_STREAM,
                                 "DML");
          }
          error = true;
          break;
        case EVENT_PARSER_ERROR: /* we probably threw a warning before */
          error = true;
          /* FALL THROUGH */
        case EVENT_PARSER_NONE:
        case EVENT_PARSER_GTID:
          break;
      }
      break;

    case EVENT_BOUNDARY_TYPE_END_TRX:
      /* In any case, we will update the state to NONE */
      new_parser_state = EVENT_PARSER_NONE;
      /* The following switch is mostly to differentiate the warning messages */
      switch (current_parser_state) {
        case EVENT_PARSER_NONE:
        case EVENT_PARSER_GTID:
        case EVENT_PARSER_DDL:
          if (throw_warnings) {
            if (current_parser_state == EVENT_PARSER_NONE)
              log_server_warning(
                  ER_TRX_BOUND_UNEXPECTED_COMMIT_ROLLBACK_OR_XID_LOG_EVENT_IN_STREAM,
                  "outside a transaction");
            else {
              if (current_parser_state == EVENT_PARSER_GTID)
                log_server_warning(
                    ER_TRX_BOUND_UNEXPECTED_COMMIT_ROLLBACK_OR_XID_LOG_EVENT_IN_STREAM,
                    "after a GTID_LOG_EVENT");
              else
                log_server_warning(
                    ER_TRX_BOUND_UNEXPECTED_COMMIT_ROLLBACK_OR_XID_LOG_EVENT_IN_STREAM,
                    "in the middle of a DDL");
            }
          } /* EVENT_PARSER_DDL */
          error = true;
          break;
        case EVENT_PARSER_ERROR: /* we probably threw a warning before */
          error = true;
          /* FALL THROUGH */
        case EVENT_PARSER_DML:
          break;
      }
      break;

    case EVENT_BOUNDARY_TYPE_END_XA_TRX:
      /* In any case, we will update the state to NONE */
      new_parser_state = EVENT_PARSER_NONE;
      /* The following switch is mostly to differentiate the warning messages */
      switch (current_parser_state) {
        case EVENT_PARSER_NONE:
        case EVENT_PARSER_DDL:
          if (throw_warnings) {
            if (current_parser_state == EVENT_PARSER_NONE) {
              log_server_warning(ER_TRX_BOUND_UNEXPECTED_XA_ROLLBACK_IN_STREAM,
                                 "outside a transaction");
            } else {
              log_server_warning(ER_TRX_BOUND_UNEXPECTED_XA_ROLLBACK_IN_STREAM,
                                 "in the middle of a DDL");
            }
          } /* EVENT_PARSER_DDL */
          error = true;
          break;
        case EVENT_PARSER_ERROR: /* we probably threw a warning before */
          error = true;
          /* FALL THROUGH */
        case EVENT_PARSER_DML:
        /* XA ROLLBACK can appear after a GTID event */
        case EVENT_PARSER_GTID:
          break;
      }
      break;

    case EVENT_BOUNDARY_TYPE_STATEMENT:
      switch (current_parser_state) {
        case EVENT_PARSER_NONE:
          new_parser_state = EVENT_PARSER_NONE;
          break;
        case EVENT_PARSER_GTID:
        case EVENT_PARSER_DDL:
          new_parser_state = EVENT_PARSER_NONE;
          break;
        case EVENT_PARSER_DML:
          new_parser_state = current_parser_state;
          break;
        case EVENT_PARSER_ERROR: /* we probably threw a warning before */
          error = true;
          break;
      }
      break;

    case EVENT_BOUNDARY_TYPE_TRANSACTION_PAYLOAD:
      switch (current_parser_state) {
        case EVENT_PARSER_GTID:
          new_parser_state = EVENT_PARSER_NONE;
          break;
        case EVENT_PARSER_NONE:
        case EVENT_PARSER_DML:
        case EVENT_PARSER_DDL:
        case EVENT_PARSER_ERROR: /* we probably threw a warning before */
          error = true;
          break;
      }
      break;

    /*
      Intvar, Rand and User_var events might be inside of a transaction stream
      if any Intvar, Rand and User_var was fed before, if BEGIN was fed before
      or if GTID was fed before. In the case of no GTID, no BEGIN and no
      previous Intvar, Rand or User_var it will be considered the start of a
      transaction stream.
    */
    case EVENT_BOUNDARY_TYPE_PRE_STATEMENT:
      switch (current_parser_state) {
        case EVENT_PARSER_NONE:
        case EVENT_PARSER_GTID:
          new_parser_state = EVENT_PARSER_DDL;
          break;
        case EVENT_PARSER_DDL:
        case EVENT_PARSER_DML:
          new_parser_state = current_parser_state;
          break;
        case EVENT_PARSER_ERROR: /* we probably threw a warning before */
          error = true;
          break;
      }
      break;

    /*
      Incident events can happen without a GTID (before BUG#19594845 fix) or
      with its own GTID in order to be skipped. In any case, it should always
      mark "the end" of a transaction.
    */
    case EVENT_BOUNDARY_TYPE_INCIDENT:
      /* In any case, we will update the state to NONE */
      new_parser_state = EVENT_PARSER_NONE;
      break;

    /*
      Rotate, Format_description and Heartbeat should be ignored.
      The rotate might be fake, like when the IO thread receives from dump
      thread Previous_gtid and Heartbeat events due to reconnection/auto
      positioning.
    */
    case EVENT_BOUNDARY_TYPE_IGNORE:
      new_parser_state = current_parser_state;
      break;

    case EVENT_BOUNDARY_TYPE_ERROR:
      error = true;
      new_parser_state = EVENT_PARSER_ERROR;
      break;
  }

  BAPI_PRINT("info", ("transaction boundary parser is changing state "
                      "from '%s' to '%s'",
                      event_parser_state_names[current_parser_state],
                      event_parser_state_names[new_parser_state]));

  last_parser_state = current_parser_state;
  current_parser_state = new_parser_state;

  return error;
}

void Transaction_boundary_parser::log_server_warning(int, const char *) {}

}  // namespace mysql::binlog::event
