/* * Copyright 2014 GoDataDriven B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.divolte.server.js; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Optional; import java.util.zip.CRC32; import java.util.zip.Deflater; import javax.annotation.ParametersAreNonnullByDefault; /** * Utility class for compressing data. * * This is necessary because {@link java.util.zip.GZIPOutputStream} doesn't let us * control the compression level. */ @ParametersAreNonnullByDefault class Gzip { // Magic value indicating start of Gzip header. private static final int GZIP_MAGIC = 0x8b1f; // Pre-calculated Gzip header. private static final byte[] GZIP_HEADER = { (byte) GZIP_MAGIC, // Magic number (short) (byte)(GZIP_MAGIC >> 8), // Magic number (short) Deflater.DEFLATED, // Compression method (CM) 0, // Flags (FLG) 0, // Modification time MTIME (int) 0, // Modification time MTIME (int) 0, // Modification time MTIME (int) 0, // Modification time MTIME (int) 0, // Extra flags (XFLG) 0 // Operating system (OS) }; // The size of the Gzip header. private static final int GZIP_HEADER_LENGTH = GZIP_HEADER.length; // The size of the Gzip footer. private static final int GZIP_FOOTER_SIZE = 8; // The amount of overhead imposed by the Gzip container. private static final int GZIP_OVERHEAD = GZIP_HEADER_LENGTH + GZIP_FOOTER_SIZE; /** * Compress some data as much as possible using the Gzip algorithm. * @param input the data to compress. * @return the compressed data (including Gzip container), or empty * if the compressed data wasn't smaller than the input. */ public static Optional<ByteBuffer> compress(final byte[] input) { return compress(input, 0, input.length); } /** * Compress some data as much as possible using the Gzip algorithm. * @param input the data to compress. * @return the compressed data (including Gzip container), or empty * if the compressed data wasn't smaller than the input. */ public static Optional<ByteBuffer> compress(final ByteBuffer input) { final byte[] b = input.array(); return compress(b, input.arrayOffset(), input.remaining()); } /** * Compress some data as much as possible using the Gzip algorithm. * @param input the data to compress. * @param offset the offset within <code>input</code> of the data to compress. * @param length the length of the data within <code>input</code> to compress. * @return the compressed data (including Gzip container), or empty * if the compressed data wasn't smaller than the input. */ public static Optional<ByteBuffer> compress(final byte[] input, final int offset, final int length) { // Step 1: Perform the actual compression. final Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION, true); deflater.setInput(input, offset, length); deflater.finish(); final byte[] payload = new byte[input.length - GZIP_FOOTER_SIZE]; final int payloadSize = deflater.deflate(payload); deflater.end(); // Step 2: Calculate the CRC for the trailer. final CRC32 checksum = new CRC32(); checksum.update(input); // Step 3: Assemble the Gzip container with the compressed data. final Optional<ByteBuffer> output; final int outputSize = GZIP_OVERHEAD + payloadSize; if (outputSize < input.length) { final ByteBuffer bb = ByteBuffer.allocate(outputSize) .order(ByteOrder.LITTLE_ENDIAN); bb.put(GZIP_HEADER) .put(payload, 0, payloadSize) .putInt((int)checksum.getValue()) .putInt(input.length); bb.flip(); output = Optional.of(bb); } else { // The output wasn't smaller than the input. output = Optional.empty(); } return output; } }