LZ4Factory.java

package net.jpountz.lz4;

/*
 * Copyright 2020 Adrien Grand and the lz4-java contributors.
 *
 * 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.
 */

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;

import net.jpountz.util.Native;
import net.jpountz.util.Utils;

import static net.jpountz.lz4.LZ4Constants.DEFAULT_COMPRESSION_LEVEL;
import static net.jpountz.lz4.LZ4Constants.MAX_COMPRESSION_LEVEL;
import static net.jpountz.lz4.LZ4Constants.MIN_ACCELERATION;
import static net.jpountz.lz4.LZ4Constants.MAX_ACCELERATION;

/**
 * Entry point for the LZ4 API.
 * <p>
 * This class has 3 instances<ul>
 * <li>a {@link #nativeInstance() native} instance which is a JNI binding to
 * <a href="https://github.com/lz4/lz4">the original LZ4 C implementation</a>.
 * <li>a {@link #safeInstance() safe Java} instance which is a pure Java port
 * of the original C library,</li>
 * <li>an {@link #unsafeInstance() unsafe Java} instance which is a Java port
 * using the unofficial {@link sun.misc.Unsafe} API.
 * </ul>
 * <p>
 * Only the {@link #safeInstance() safe instance} is guaranteed to work on your
 * JVM, as a consequence it is advised to use the {@link #fastestInstance()} or
 * {@link #fastestJavaInstance()} to pull a {@link LZ4Factory} instance.
 * <p>
 * All methods from this class are very costly, so you should get an instance
 * once, and then reuse it whenever possible. This is typically done by storing
 * a {@link LZ4Factory} instance in a static field.
 */
public final class LZ4Factory {

  private static LZ4Factory instance(String impl, boolean insecureFastDecompressor) {
    try {
      return new LZ4Factory(impl, insecureFastDecompressor);
    } catch (Exception e) {
      throw new AssertionError(e);
    }
  }

  private static LZ4Factory NATIVE_INSTANCE,
                            NATIVE_INSECURE_INSTANCE,
                            JAVA_UNSAFE_INSTANCE,
                            JAVA_UNSAFE_INSECURE_INSTANCE,
                            JAVA_SAFE_INSTANCE;

  /**
   * Returns a {@link LZ4Factory} instance that returns compressors and
   * decompressors that are native bindings to the original C library.
   * <p>
   * Please note that this instance has some traps you should be aware of:<ol>
   * <li>Upon loading this instance, files will be written to the temporary
   * directory of the system. Although these files are supposed to be deleted
   * when the JVM exits, they might remain on systems that don't support
   * removal of files being used such as Windows.
   * <li>The instance can only be loaded once per JVM. This can be a problem
   * if your application uses multiple class loaders (such as most servlet
   * containers): this instance will only be available to the children of the
   * class loader which has loaded it. As a consequence, it is advised to
   * either not use this instance in webapps or to put this library in the lib
   * directory of your servlet container so that it is loaded by the system
   * class loader.
   * <li>From lz4-java version 1.6.0, a {@link LZ4FastDecompressor} instance
   * returned by {@link #fastDecompressor()} of this instance is SLOWER
   * than a {@link LZ4SafeDecompressor} instance returned by
   * {@link #safeDecompressor()}, due to a change in the original LZ4
   * C implementation. The corresponding C API function is deprecated.
   * Hence use of {@link #fastDecompressor()} is deprecated
   * for this instance.
   * </ol>
   *
   * @return a {@link LZ4Factory} instance that returns compressors and
   * decompressors that are native bindings to the original C library
   */
  public static synchronized LZ4Factory nativeInstance() {
    if (NATIVE_INSTANCE == null) {
      NATIVE_INSTANCE = instance("JNI", false);
    }
    return NATIVE_INSTANCE;
  }

  /**
   * Insecure variant of {@link #nativeInstance()}. The JNI-based {@link LZ4FastDecompressor} is not secure for
   * untrusted inputs, so {@link #nativeInstance()} will instead return the slower safe java implementation from
   * {@link #fastDecompressor()}. If that implementation is too slow for you, it is recommended to move to
   * {@link #safeDecompressor()}, which is actually faster even than the JNI {@link #fastDecompressor()}. Only if that
   * is not an option for you, and you can guarantee no untrusted inputs will be decompressed, should you use this
   * method.
   *
   * @return An insecure, JNI-backed LZ4Factory
   * @deprecated Never decompress untrusted inputs with this instance. Prefer {@link #nativeInstance()}.
   */
  @Deprecated
  public static synchronized LZ4Factory nativeInsecureInstance() {
    if (NATIVE_INSECURE_INSTANCE == null) {
      NATIVE_INSECURE_INSTANCE = instance("JNI", true);
    }
    return NATIVE_INSECURE_INSTANCE;
  }

  /**
   * Returns a {@link LZ4Factory} instance that returns compressors and
   * decompressors that are written with Java's official API.
   *
   * @return a {@link LZ4Factory} instance that returns compressors and
   * decompressors that are written with Java's official API.
   */
  public static synchronized LZ4Factory safeInstance() {
    if (JAVA_SAFE_INSTANCE == null) {
      JAVA_SAFE_INSTANCE = instance("JavaSafe", false);
    }
    return JAVA_SAFE_INSTANCE;
  }

  /**
   * Returns a {@link LZ4Factory} instance that returns compressors and
   * decompressors that may use {@link sun.misc.Unsafe} to speed up compression
   * and decompression.
   *
   * @return a {@link LZ4Factory} instance that returns compressors and
   * decompressors that may use {@link sun.misc.Unsafe} to speed up compression
   * and decompression.
   *
   * @deprecated Note: It is not yet clear which Unsafe-based implementations are secure. Out of caution, this method
   * currently returns the {@link #safeInstance()}. In a future version, when security has been assessed, this method
   * may return to Unsafe.
   */
  @Deprecated
  public static synchronized LZ4Factory unsafeInstance() {
    if (JAVA_UNSAFE_INSTANCE == null) {
      // TODO: move back to `instance("JavaUnsafe", false)` once we know more about the security of the Unsafe implementation
      JAVA_UNSAFE_INSTANCE = safeInstance();
    }
    return JAVA_UNSAFE_INSTANCE;
  }

  /**
   * Insecure variant of {@link #unsafeInstance()}. The Unsafe-based {@link LZ4FastDecompressor} is not secure for
   * untrusted inputs, so {@link #unsafeInstance()} will instead return the slower safe java implementation from
   * {@link #fastDecompressor()}. If that implementation is too slow for you, it is recommended to move to
   * {@link #safeDecompressor()}. Only if that is not an option for you, and you can guarantee no untrusted inputs will
   * be decompressed, should you use this method.
   *
   * @return An insecure, Unsafe-backed LZ4Factory
   * @deprecated Never decompress untrusted inputs with this instance. Prefer {@link #unsafeInstance()}.
   */
  @Deprecated
  public static synchronized LZ4Factory unsafeInsecureInstance() {
    if (JAVA_UNSAFE_INSECURE_INSTANCE == null) {
      JAVA_UNSAFE_INSECURE_INSTANCE = instance("JavaUnsafe", true);
    }
    return JAVA_UNSAFE_INSECURE_INSTANCE;
  }

  /**
   * Returns the fastest available {@link LZ4Factory} instance which does not
   * rely on JNI bindings. It first tries to load the
   * {@link #unsafeInstance() unsafe instance}, and then the
   * {@link #safeInstance() safe Java instance} if the JVM doesn't have a
   * working {@link sun.misc.Unsafe}.
   *
   * @return the fastest available {@link LZ4Factory} instance which does not
   * rely on JNI bindings.
   */
  public static LZ4Factory fastestJavaInstance() {
    if (Utils.isUnalignedAccessAllowed()) {
      try {
        return unsafeInstance();
      } catch (Throwable t) {
        return safeInstance();
      }
    } else {
      return safeInstance();
    }
  }

  /**
   * Returns the fastest available {@link LZ4Factory} instance. If the class
   * loader is the system class loader and if the
   * {@link #nativeInstance() native instance} loads successfully, then the
   * {@link #nativeInstance() native instance} is returned, otherwise the
   * {@link #fastestJavaInstance() fastest Java instance} is returned.
   * <p>
   * Please read {@link #nativeInstance() javadocs of nativeInstance()} before
   * using this method.
   *
   * @return the fastest available {@link LZ4Factory} instance
   */
  public static LZ4Factory fastestInstance() {
    if (Native.isLoaded()
        || Native.class.getClassLoader() == ClassLoader.getSystemClassLoader()) {
      try {
        return nativeInstance();
      } catch (Throwable t) {
        return fastestJavaInstance();
      }
    } else {
      return fastestJavaInstance();
    }
  }

  @SuppressWarnings("unchecked")
  private static <T> T classInstance(String cls) throws NoSuchFieldException, SecurityException, ClassNotFoundException, IllegalArgumentException, IllegalAccessException {
    ClassLoader loader = LZ4Factory.class.getClassLoader();
    loader = loader == null ? ClassLoader.getSystemClassLoader() : loader;
    final Class<?> c = loader.loadClass(cls);
    Field f = c.getField("INSTANCE");
    return (T) f.get(null);
  }

  private final String impl;
  private final LZ4Compressor fastCompressor;
  private final LZ4Compressor highCompressor;
  private final LZ4FastDecompressor fastDecompressor;
  private final LZ4SafeDecompressor safeDecompressor;
  private final LZ4Compressor[] highCompressors = new LZ4Compressor[MAX_COMPRESSION_LEVEL + 1];

  private LZ4Factory(String impl, boolean insecureFastDecompressor) throws ClassNotFoundException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException {
    this.impl = impl;
    fastCompressor = classInstance("net.jpountz.lz4.LZ4" + impl + "Compressor");
    highCompressor = classInstance("net.jpountz.lz4.LZ4HC" + impl + "Compressor");
    if (insecureFastDecompressor) {
      fastDecompressor = classInstance("net.jpountz.lz4.LZ4" + impl + "FastDecompressor");
    } else {
      fastDecompressor = LZ4JavaSafeFastDecompressor.INSTANCE;
    }
    safeDecompressor = classInstance("net.jpountz.lz4.LZ4" + impl + "SafeDecompressor");
    Constructor<? extends LZ4Compressor> highConstructor = highCompressor.getClass().getDeclaredConstructor(int.class);
    highCompressors[DEFAULT_COMPRESSION_LEVEL] = highCompressor;
    for (int level = 1; level <= MAX_COMPRESSION_LEVEL; level++) {
      if (level == DEFAULT_COMPRESSION_LEVEL) continue;
      highCompressors[level] = highConstructor.newInstance(level);
    }

    // quickly test that everything works as expected
    final byte[] original = new byte[] {'a','b','c','d',' ',' ',' ',' ',' ',' ','a','b','c','d','e','f','g','h','i','j'};
    for (LZ4Compressor compressor : Arrays.asList(fastCompressor, highCompressor)) {
      final int maxCompressedLength = compressor.maxCompressedLength(original.length);
      final byte[] compressed = new byte[maxCompressedLength];
      final int compressedLength = compressor.compress(original, 0, original.length, compressed, 0, maxCompressedLength);
      final byte[] restored = new byte[original.length];
      fastDecompressor.decompress(compressed, 0, restored, 0, original.length);
      if (!Arrays.equals(original, restored)) {
        throw new AssertionError();
      }
      Arrays.fill(restored, (byte) 0);
      final int decompressedLength = safeDecompressor.decompress(compressed, 0, compressedLength, restored, 0);
      if (decompressedLength != original.length || !Arrays.equals(original, restored)) {
        throw new AssertionError();
      }
    }

  }

  /**
   * Returns a blazing fast {@link LZ4Compressor}.
   *
   * @return a blazing fast {@link LZ4Compressor}
   */
  public LZ4Compressor fastCompressor() {
    return fastCompressor;
  }

  /**
   * Returns a {@link LZ4Compressor} which requires more memory than
   * {@link #fastCompressor()} and is slower but compresses more efficiently.
   *
   * @return a {@link LZ4Compressor} which requires more memory than
   * {@link #fastCompressor()} and is slower but compresses more efficiently.
   */
  public LZ4Compressor highCompressor() {
    return highCompressor;
  }

  /**
   * Returns a {@link LZ4Compressor} which requires more memory than
   * {@link #fastCompressor()} and is slower but compresses more efficiently.
   * The compression level can be customized.
   * <p>For current implementations, the following is true about compression level:<ol>
   *   <li>It should be in range [1, 17]</li>
   *   <li>A compression level higher than 17 would be treated as 17.</li>
   *   <li>A compression level lower than 1 would be treated as 9.</li>
   * </ol>
   * Note that compression levels from different implementations
   * (native, unsafe Java, and safe Java) cannot be compared with one another.
   * Specifically, the native implementation of a high compression level
   * is not necessarily faster than the safe/unsafe Java implementation
   * of the same compression level.
   * <p>Higher levels search harder for matches. In the safe and unsafe Java
   * implementations, the maximum match-search effort per position doubles with
   * each level ({@code 2^(level-1)} attempts, up to 65536 at level 17). The
   * cost stays linear in the input size, but on highly repetitive or
   * deliberately crafted input, high levels can be orders of magnitude slower
   * than on typical data. When compressing untrusted input and CPU time
   * matters, use the default level (9) or lower.
   *
   * @param compressionLevel the compression level between [1, 17]; the higher the level, the higher the compression ratio
   * @return a {@link LZ4Compressor} which requires more memory than
   * {@link #fastCompressor()} and is slower but compresses more efficiently.
   */
  public LZ4Compressor highCompressor(int compressionLevel) {
    if (compressionLevel > MAX_COMPRESSION_LEVEL) {
      compressionLevel = MAX_COMPRESSION_LEVEL;
    } else if (compressionLevel < 1) {
      compressionLevel = DEFAULT_COMPRESSION_LEVEL;
    }
    return highCompressors[compressionLevel];
  }

  /**
   * Creates a new {@link LZ4JNIFastResetCompressor} with default acceleration (1).
   * <p>
   * This compressor pre-allocates native state once and uses
   * {@code LZ4_compress_fast_extState_fastReset} for each compression, avoiding
   * the expensive full state initialization that the default {@link #fastCompressor()}
   * performs on every call.
   * <p>
   * Unlike {@link #fastCompressor()}, this compressor is <b>NOT</b> thread-safe and
   * holds native resources that must be freed. Always use try-with-resources or
   * call {@link LZ4JNIFastResetCompressor#close() close()} explicitly.
   * <p>
   * Only available for the native instance.
   *
   * @return a new fast-reset compressor
   * @throws UnsupportedOperationException if this is not the native instance
   * @see LZ4JNIFastResetCompressor
   * @see #fastCompressor()
   * @see #fastResetCompressor(int)
   */
  public LZ4JNIFastResetCompressor fastResetCompressor() {
    enforceNativeInstance("fastResetCompressor");
    return new LZ4JNIFastResetCompressor();
  }

  /**
   * Creates a new {@link LZ4JNIFastResetCompressor} with the specified acceleration.
   * <p>
   * This compressor pre-allocates native state once and uses
   * {@code LZ4_compress_fast_extState_fastReset} for each compression, avoiding
   * the expensive full state initialization that the default {@link #fastCompressor()}
   * performs on every call.
   * <p>
   * Unlike {@link #fastCompressor()}, this compressor is <b>NOT</b> thread-safe and
   * holds native resources that must be freed. Always use try-with-resources or
   * call {@link LZ4JNIFastResetCompressor#close() close()} explicitly.
   * <p>
   * Only available for the native instance.
   *
   * <p>Acceleration follows the upstream LZ4 contract:<ol>
   *   <li>It should be in range [1, 65537].</li>
   *   <li>A value lower than 1 is treated as 1.</li>
   *   <li>A value greater than 65537 is treated as 65537.</li>
   * </ol>
   *
   * @param acceleration acceleration factor (1 = default, higher = faster but less compression)
   * @return a new fast-reset compressor
   * @throws UnsupportedOperationException if this is not the native instance
   * @see LZ4JNIFastResetCompressor
   * @see #fastCompressor()
   */
  public LZ4JNIFastResetCompressor fastResetCompressor(int acceleration) {
    enforceNativeInstance("fastResetCompressor");
    if (acceleration < MIN_ACCELERATION) {
      acceleration = MIN_ACCELERATION;
    } else if (acceleration > MAX_ACCELERATION) {
      acceleration = MAX_ACCELERATION;
    }
    return new LZ4JNIFastResetCompressor(acceleration);
  }

  /**
   * Creates a new {@link LZ4JNIHCFastResetCompressor} with default compression level (9).
   * <p>
   * This compressor pre-allocates native HC state once and uses
   * {@code LZ4_compress_HC_extStateHC_fastReset} for each compression, avoiding
   * the expensive full state initialization that the default {@link #highCompressor()}
   * performs on every call.
   * <p>
   * Unlike {@link #highCompressor()}, this compressor is <b>NOT</b> thread-safe and
   * holds native resources that must be freed. Always use try-with-resources or
   * call {@link LZ4JNIHCFastResetCompressor#close() close()} explicitly.
   * <p>
   * Only available for the native instance.
   *
   * @return a new HC fast-reset compressor
   * @throws UnsupportedOperationException if this is not the native instance
   * @see LZ4JNIHCFastResetCompressor
   * @see #highCompressor()
   * @see #highFastResetCompressor(int)
   */
  public LZ4JNIHCFastResetCompressor highFastResetCompressor() {
    enforceNativeInstance("highFastResetCompressor");
    return new LZ4JNIHCFastResetCompressor();
  }

  /**
   * Creates a new {@link LZ4JNIHCFastResetCompressor} with the specified compression level.
   * <p>
   * This compressor pre-allocates native HC state once and uses
   * {@code LZ4_compress_HC_extStateHC_fastReset} for each compression, avoiding
   * the expensive full state initialization that the default {@link #highCompressor()}
   * performs on every call.
   * <p>
   * Unlike {@link #highCompressor()}, this compressor is <b>NOT</b> thread-safe and
   * holds native resources that must be freed. Always use try-with-resources or
   * call {@link LZ4JNIHCFastResetCompressor#close() close()} explicitly.
   * <p>
   * Only available for the native instance.
   *
   * @param compressionLevel compression level (1-17, higher = better compression)
   * @return a new HC fast-reset compressor
   * @throws UnsupportedOperationException if this is not the native instance
   * @see LZ4JNIHCFastResetCompressor
   * @see #highCompressor(int)
   */
  public LZ4JNIHCFastResetCompressor highFastResetCompressor(int compressionLevel) {
    enforceNativeInstance("highFastResetCompressor");
    if (compressionLevel > MAX_COMPRESSION_LEVEL) {
      compressionLevel = MAX_COMPRESSION_LEVEL;
    } else if (compressionLevel < 1) {
      compressionLevel = DEFAULT_COMPRESSION_LEVEL;
    }
    return new LZ4JNIHCFastResetCompressor(compressionLevel);
  }

  /**
   * Returns a {@link LZ4FastDecompressor} instance.
   * Use of this method is deprecated for the {@link #nativeInstance() native instance}.
   *
   * @return a {@link LZ4FastDecompressor} instance
   *
   * @see #nativeInstance()
   */
  public LZ4FastDecompressor fastDecompressor() {
    return fastDecompressor;
  }

  /**
   * Returns a {@link LZ4SafeDecompressor} instance.
   *
   * @return a {@link LZ4SafeDecompressor} instance
   */
  public LZ4SafeDecompressor safeDecompressor() {
    return safeDecompressor;
  }

  /**
   * Returns a {@link LZ4UnknownSizeDecompressor} instance.
   * @deprecated use {@link #safeDecompressor()}
   *
   * @return a {@link LZ4UnknownSizeDecompressor} instance
   */
  public LZ4UnknownSizeDecompressor unknownSizeDecompressor() {
    return safeDecompressor();
  }

  /**
   * Returns a {@link LZ4Decompressor} instance.
   * @deprecated use {@link #fastDecompressor()}
   *
   * @return a {@link LZ4Decompressor} instance
   */
  public LZ4Decompressor decompressor() {
    return fastDecompressor();
  }

  /**
   * Prints the fastest instance.
   *
   * @param args no argument required
   */
  public static void main(String[] args) {
    System.out.println("Fastest instance is " + fastestInstance());
    System.out.println("Fastest Java instance is " + fastestJavaInstance());
  }

  @Override
  public String toString() {
    return getClass().getSimpleName() + ":" + impl;
  }

  private void enforceNativeInstance(String methodName) throws UnsupportedOperationException {
    if (!"JNI".equals(impl)) {
      throw new UnsupportedOperationException(
        methodName + "() is only available for the native instance. Use LZ4Factory.nativeInstance() to get a native factory");
    }
  }
}