| // SPDX-License-Identifier: LGPL-2.1-or-later |
| // Copyright (c) 2012-2014 Monty Program Ab |
| // Copyright (c) 2015-2021 MariaDB Corporation Ab |
| |
| package org.mariadb.jdbc.client.socket.impl; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.util.zip.DeflaterOutputStream; |
| import org.mariadb.jdbc.client.util.MutableByte; |
| |
| /** |
| * Compression writer handler Permit to wrap standard packet to compressed packet ( 7 byte header). |
| * Driver will compress packet only if packet size is meaningful (1536 bytes) > to one TCP |
| * packet. |
| */ |
| public class CompressOutputStream extends OutputStream { |
| private static final int MIN_COMPRESSION_SIZE = 1536; // TCP-IP single packet |
| private final OutputStream out; |
| private final MutableByte sequence; |
| private final byte[] header = new byte[7]; |
| private byte[] longPacketBuffer = null; |
| |
| /** |
| * Constructor. |
| * |
| * @param out socket output stream |
| * @param compressionSequence compression sequence |
| */ |
| public CompressOutputStream(OutputStream out, MutableByte compressionSequence) { |
| this.out = out; |
| this.sequence = compressionSequence; |
| } |
| |
| /** |
| * Writes <code>len</code> bytes from the specified byte array starting at offset <code>off</code> |
| * to this output stream. The general contract for <code>write(b, off, len)</code> is that some |
| * bytes in the array <code>b</code> are written to the output stream in order; element <code> |
| * b[off]</code> is the first byte written and <code>b[off+len-1]</code> is the last byte written |
| * by this operation. |
| * |
| * <p>The <code>write</code> method of <code>OutputStream</code> calls the write method of one |
| * argument on each of the bytes to be written out. Subclasses are encouraged to override this |
| * method and provide a more efficient implementation. |
| * |
| * <p>If <code>b</code> is <code>null</code>, a <code>NullPointerException</code> is thrown. |
| * |
| * <p>If <code>off</code> is negative, or <code>len</code> is negative, or <code>off+len</code> is |
| * greater than the length of the array <code>b</code>, then an IndexOutOfBoundsException is |
| * thrown. |
| * |
| * @param b the data. |
| * @param off the start offset in the data. |
| * @param len the number of bytes to write. |
| * @throws IOException if an I/O error occurs. In particular, an <code>IOException</code> is |
| * thrown if the output stream is closed. |
| */ |
| @Override |
| public void write(byte[] b, int off, int len) throws IOException { |
| if (len + ((longPacketBuffer != null) ? longPacketBuffer.length : 0) < MIN_COMPRESSION_SIZE) { |
| // ******************************************************************************* |
| // small packet, no compression |
| // ******************************************************************************* |
| |
| if (longPacketBuffer != null) { |
| header[0] = (byte) (len + longPacketBuffer.length); |
| header[1] = (byte) ((len + longPacketBuffer.length) >>> 8); |
| header[2] = 0; |
| header[3] = sequence.incrementAndGet(); |
| header[4] = 0; |
| header[5] = 0; |
| header[6] = 0; |
| out.write(header, 0, 7); |
| out.write(longPacketBuffer, 0, longPacketBuffer.length); |
| out.write(b, off, len); |
| longPacketBuffer = null; |
| return; |
| } |
| |
| header[0] = (byte) len; |
| header[1] = (byte) (len >>> 8); |
| header[2] = 0; |
| header[3] = sequence.incrementAndGet(); |
| header[4] = 0; |
| header[5] = 0; |
| header[6] = 0; |
| out.write(header, 0, 7); |
| out.write(b, off, len); |
| |
| } else { |
| // ******************************************************************************* |
| // compressing packet |
| // ******************************************************************************* |
| int sent = 0; |
| try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { |
| try (DeflaterOutputStream deflater = new DeflaterOutputStream(baos)) { |
| |
| /** |
| * For multi packet, len will be 0x00ffffff + 4 bytes for header. but compression can only |
| * compress up to 0x00ffffff bytes (header initial length size cannot be > 3 bytes) so, |
| * for this specific case, a buffer will save remaining data |
| */ |
| if (longPacketBuffer != null) { |
| deflater.write(longPacketBuffer, 0, longPacketBuffer.length); |
| sent = longPacketBuffer.length; |
| longPacketBuffer = null; |
| } |
| if (len + sent > 0x00ffffff) { |
| int remaining = len + sent - 0x00ffffff; |
| longPacketBuffer = new byte[remaining]; |
| System.arraycopy(b, off + 0x00ffffff - sent, longPacketBuffer, 0, remaining); |
| } |
| |
| int bufLenSent = Math.min(0x00ffffff - sent, len); |
| deflater.write(b, off, bufLenSent); |
| sent += bufLenSent; |
| deflater.finish(); |
| } |
| |
| byte[] compressedBytes = baos.toByteArray(); |
| |
| int compressLen = compressedBytes.length; |
| |
| header[0] = (byte) compressLen; |
| header[1] = (byte) (compressLen >>> 8); |
| header[2] = (byte) (compressLen >>> 16); |
| header[3] = sequence.incrementAndGet(); |
| header[4] = (byte) sent; |
| header[5] = (byte) (sent >>> 8); |
| header[6] = (byte) (sent >>> 16); |
| |
| out.write(header, 0, 7); |
| out.write(compressedBytes, 0, compressLen); |
| out.flush(); |
| } |
| } |
| } |
| |
| /** |
| * Flushes this output stream and forces any buffered output bytes to be written out. The general |
| * contract of <code>flush</code> is that calling it is an indication that, if any bytes |
| * previously written have been buffered by the implementation of the output stream, such bytes |
| * should immediately be written to their intended destination. |
| * |
| * <p>If the intended destination of this stream is an abstraction provided by the underlying |
| * operating system, for example a file, then flushing the stream guarantees only that bytes |
| * previously written to the stream are passed to the operating system for writing; it does not |
| * guarantee that they are actually written to a physical device such as a disk drive. |
| * |
| * <p>The <code>flush</code> method of <code>OutputStream</code> does nothing. |
| * |
| * @throws IOException if an I/O error occurs. |
| */ |
| @Override |
| public void flush() throws IOException { |
| if (longPacketBuffer != null) { |
| byte[] b = longPacketBuffer; |
| longPacketBuffer = null; |
| write(b, 0, b.length); |
| } |
| out.flush(); |
| sequence.set((byte) -1); |
| } |
| |
| /** |
| * Closes this output stream and releases any system resources associated with this stream. The |
| * general contract of <code>close</code> is that it closes the output stream. A closed stream |
| * cannot perform output operations and cannot be reopened. |
| * |
| * <p>The <code>close</code> method of <code>OutputStream</code> does nothing. |
| * |
| * @throws IOException if an I/O error occurs. |
| */ |
| @Override |
| public void close() throws IOException { |
| out.close(); |
| } |
| |
| /** |
| * Writes the specified byte to this output stream. The general contract for <code>write</code> is |
| * that one byte is written to the output stream. The byte to be written is the eight low-order |
| * bits of the argument <code>b</code>. The 24 high-order bits of <code>b</code> are ignored. |
| * |
| * <p>Subclasses of <code>OutputStream</code> must provide an implementation for this method. |
| * |
| * @param b the <code>byte</code>. |
| * @throws IOException if an I/O error occurs. In particular, an <code>IOException</code> may be |
| * thrown if the output stream has been closed. |
| */ |
| @Override |
| public void write(int b) throws IOException { |
| throw new IOException("NOT EXPECTED !"); |
| } |
| } |