content: /*
   * Copyright (c) 2015, 2020, 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 also distributed 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 included with MySQL.
   *
   * Without limiting anything contained in the foregoing, this file, which is
   * part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
   * version 1.0, a copy of which can be found at
   * http://oss.oracle.com/licenses/universal-foss-exception.
   *
   * 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
   */
  
  package com.mysql.cj.protocol.a;
  
  import java.io.BufferedOutputStream;
  import java.io.IOException;
  import java.util.zip.Deflater;
  
  import com.mysql.cj.protocol.MessageSender;
  
  /**
   * A {@link MessageSender} for the compressed protocol.
   * 
   * TODO: add support for pre-allocated buffer for large packets (if there's a demonstrable perf improvement)
   */
  public class CompressedPacketSender implements MessageSender<NativePacketPayload> {
      private BufferedOutputStream outputStream;
      private Deflater deflater = new Deflater();
      /** Buffer to compress data to. Used only across one send() invocation. */
      private byte compressedPacket[];
      /** Sequence id of compressed packet. Used only across one send() invocation. */
      private byte compressedSequenceId = 0;
      /** Length of current compressed packet. */
      private int compressedPayloadLen = 0;
  
      public static final int COMP_HEADER_LENGTH = 7;
      public static final int MIN_COMPRESS_LEN = 50;
  
      public CompressedPacketSender(BufferedOutputStream outputStream) {
          this.outputStream = outputStream;
      }
  
      /**
       * Shut down this packet sender and deallocate any resources.
       */
      public void stop() {
          this.deflater.end();
          this.deflater = null;
      }
  
      private void resetPacket() {
          this.compressedPayloadLen = 0;
          this.deflater.reset();
      }
  
      /**
       * Add and compress the header for the raw packet into the compressed packet.
       * 
       * @param packetSequence
       *            sequence id
       * @param uncompressedPacketLen
       *            uncompressed packet length
       */
      private void addUncompressedHeader(byte packetSequence, int uncompressedPacketLen) {
          byte uncompressedHeader[] = new byte[NativeConstants.HEADER_LENGTH];
          NativeUtils.encodeMysqlThreeByteInteger(uncompressedPacketLen, uncompressedHeader, 0);
          uncompressedHeader[3] = packetSequence;
          this.deflater.setInput(uncompressedHeader);
          this.compressedPayloadLen += this.deflater.deflate(this.compressedPacket, this.compressedPayloadLen,
                  this.compressedPacket.length - this.compressedPayloadLen);
      }
  
      /**
       * Add and compress the payload into the compressed packet.
       * 
       * @param payload
       *            payload bytes
       * @param payloadOffset
       *            offset
       * @param payloadLen
       *            length
       */
      private void addPayload(byte[] payload, int payloadOffset, int payloadLen) {
          this.deflater.setInput(payload, payloadOffset, payloadLen);
          this.compressedPayloadLen += this.deflater.deflate(this.compressedPacket, this.compressedPayloadLen,
                  this.compressedPacket.length - this.compressedPayloadLen);
      }
  
      /**
       * Complete compression of the current payload contents to the compressed packet.
       */
      private void completeCompression() {
          this.deflater.finish();
          this.compressedPayloadLen += this.deflater.deflate(this.compressedPacket, this.compressedPayloadLen,
                  this.compressedPacket.length - this.compressedPayloadLen);
      }
  
      /**
       * Write the compressed packet header.
       * 
       * @param compLen
       *            compressed data length
       * @param seq
       *            sequence id
       * @param uncompLen
       *            uncompressed data length
       * @throws IOException
       *             if write exception occurs
       */
      private void writeCompressedHeader(int compLen, byte seq, int uncompLen) throws IOException {
          this.outputStream.write(NativeUtils.encodeMysqlThreeByteInteger(compLen));
          this.outputStream.write(seq);
          this.outputStream.write(NativeUtils.encodeMysqlThreeByteInteger(uncompLen));
      }
  
      /**
       * Write an uncompressed packet header.
       * 
       * @param packetLen
       *            packet length
       * @param packetSequence
       *            sequence id
       * @throws IOException
       *             if write exception occurs
       */
      private void writeUncompressedHeader(int packetLen, byte packetSequence) throws IOException {
          this.outputStream.write(NativeUtils.encodeMysqlThreeByteInteger(packetLen));
          this.outputStream.write(packetSequence);
      }
  
      /**
       * Send a compressed packet.
       * 
       * @param uncompressedPayloadLen
       *            uncompressed data length
       * @throws IOException
       *             if write exception occurs
       */
      private void sendCompressedPacket(int uncompressedPayloadLen) throws IOException {
          writeCompressedHeader(this.compressedPayloadLen, this.compressedSequenceId++, uncompressedPayloadLen);
  
          // compressed payload
          this.outputStream.write(this.compressedPacket, 0, this.compressedPayloadLen);
      }
  
      /**
       * Packet sender implementation for the compressed MySQL protocol. For compressed transmission of multi-packets, split the packets up in the same way as the
       * uncompressed protocol. We fit up to MAX_PACKET_SIZE bytes of split uncompressed packet, including the header, into an compressed packet. The first packet
       * of the multi-packet is 4 bytes of header and MAX_PACKET_SIZE - 4 bytes of the payload. The next packet must send the remaining four bytes of the payload
       * followed by a new header and payload. If the second split packet is also around MAX_PACKET_SIZE in length, then only MAX_PACKET_SIZE - 4 (from the
       * previous packet) - 4 (for the new header) can be sent. This means the payload will be limited by 8 bytes and this will continue to increase by 4 at every
       * iteration.
       * 
       * @param packet
       *            data bytes
       * @param packetLen
       *            packet length
       * @param packetSequence
       *            sequence id
       * @throws IOException
       *             if i/o exception occurs
       */
      public void send(byte[] packet, int packetLen, byte packetSequence) throws IOException {
          this.compressedSequenceId = packetSequence;
  
          // short-circuit send small packets without compression and return
          if (packetLen < MIN_COMPRESS_LEN) {
              writeCompressedHeader(packetLen + NativeConstants.HEADER_LENGTH, this.compressedSequenceId, 0);
              writeUncompressedHeader(packetLen, packetSequence);
              this.outputStream.write(packet, 0, packetLen);
              this.outputStream.flush();
              return;
          }
  
          if (packetLen + NativeConstants.HEADER_LENGTH > NativeConstants.MAX_PACKET_SIZE) {
              this.compressedPacket = new byte[NativeConstants.MAX_PACKET_SIZE];
          } else {
              this.compressedPacket = new byte[NativeConstants.HEADER_LENGTH + packetLen];
          }
  
          PacketSplitter packetSplitter = new PacketSplitter(packetLen);
  
          int unsentPayloadLen = 0;
          int unsentOffset = 0;
          // loop over constructing and sending compressed packets
          while (true) {
              this.compressedPayloadLen = 0;
  
              if (packetSplitter.nextPacket()) {
                  // rest of previous packet
                  if (unsentPayloadLen > 0) {
                      addPayload(packet, unsentOffset, unsentPayloadLen);
                  }
  
                  // current packet
                  int remaining = NativeConstants.MAX_PACKET_SIZE - unsentPayloadLen;
                  // if remaining is 0 then we are sending a very huge packet such that are 4-byte header-size carryover from last packet accumulated to the size
                  // of a whole packet itself. We don't handle this. Would require 4 million packet segments (64 gigs in one logical packet)
                  int len = Math.min(remaining, NativeConstants.HEADER_LENGTH + packetSplitter.getPacketLen());
                  int lenNoHdr = len - NativeConstants.HEADER_LENGTH;
                  addUncompressedHeader(packetSequence, packetSplitter.getPacketLen());
                  addPayload(packet, packetSplitter.getOffset(), lenNoHdr);
  
                  completeCompression();
                  // don't send payloads with incompressible data
                  if (this.compressedPayloadLen >= len) {
                      // combine the unsent and current packet in an uncompressed packet
                      writeCompressedHeader(unsentPayloadLen + len, this.compressedSequenceId++, 0);
                      this.outputStream.write(packet, unsentOffset, unsentPayloadLen);
                      writeUncompressedHeader(lenNoHdr, packetSequence);
                      this.outputStream.write(packet, packetSplitter.getOffset(), lenNoHdr);
                  } else {
                      sendCompressedPacket(len + unsentPayloadLen);
                  }
  
                  packetSequence++;
                  unsentPayloadLen = packetSplitter.getPacketLen() - lenNoHdr;
                  unsentOffset = packetSplitter.getOffset() + lenNoHdr;
                  resetPacket();
              } else if (unsentPayloadLen > 0) {
                  // no more packets, send remaining unsent data
                  addPayload(packet, unsentOffset, unsentPayloadLen);
                  completeCompression();
                  if (this.compressedPayloadLen >= unsentPayloadLen) {
                      writeCompressedHeader(unsentPayloadLen, this.compressedSequenceId, 0);
                      this.outputStream.write(packet, unsentOffset, unsentPayloadLen);
                  } else {
                      sendCompressedPacket(unsentPayloadLen);
                  }
                  resetPacket();
                  break;
              } else {
                  // nothing left to send (only happens on boundaries)
                  break;
              }
          }
  
          this.outputStream.flush();
  
          // release reference to (possibly large) compressed packet buffer
          this.compressedPacket = null;
      }
  
      @Override
      public MessageSender<NativePacketPayload> undecorateAll() {
          return this;
      }
  
      @Override
      public MessageSender<NativePacketPayload> undecorate() {
          return this;
      }
  }
  