COMPRESS-313 add auto-detection for LZMA streams

git-svn-id: https://svn.apache.org/repos/asf/commons/proper/compress/trunk@1670129 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 5e2bd81..f3ae88a 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -54,6 +54,9 @@
 This also changes the superclass of ZCompressorInputStream.    
 ">
 
+      <action issue="COMPRESS-313" type="add" date="2015-03-30">
+        CompressorStreamFactory can now auto-detect LZMA streams.
+      </action>
       <action issue="COMPRESS-312" type="fix" date="2015-03-28">
         TarArchiveEntry's constructor with a File and a String arg
         didn't normalize the name.
diff --git a/src/main/java/org/apache/commons/compress/compressors/CompressorStreamFactory.java b/src/main/java/org/apache/commons/compress/compressors/CompressorStreamFactory.java
index 4bc1810..adca670 100644
--- a/src/main/java/org/apache/commons/compress/compressors/CompressorStreamFactory.java
+++ b/src/main/java/org/apache/commons/compress/compressors/CompressorStreamFactory.java
@@ -29,6 +29,7 @@
 import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
 import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
 import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
+import org.apache.commons.compress.compressors.lzma.LZMAUtils;
 import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
 import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream;
 import org.apache.commons.compress.compressors.xz.XZUtils;
@@ -241,6 +242,11 @@
                 return new XZCompressorInputStream(in, decompressConcatenated);
             }
 
+            if (LZMAUtils.matches(signature, signatureLength) &&
+                LZMAUtils.isLZMACompressionAvailable()) {
+                return new LZMACompressorInputStream(in);
+            }
+
         } catch (IOException e) {
             throw new CompressorException("Failed to detect Compressor from InputStream.", e);
         }
diff --git a/src/main/java/org/apache/commons/compress/compressors/lzma/LZMACompressorInputStream.java b/src/main/java/org/apache/commons/compress/compressors/lzma/LZMACompressorInputStream.java
index 142b617..f9a8eae 100644
--- a/src/main/java/org/apache/commons/compress/compressors/lzma/LZMACompressorInputStream.java
+++ b/src/main/java/org/apache/commons/compress/compressors/lzma/LZMACompressorInputStream.java
@@ -81,4 +81,36 @@
     public void close() throws IOException {
         in.close();
     }
+
+    /**
+     * Checks if the signature matches what is expected for an lzma file.
+     * 
+     * @param signature
+     *            the bytes to check
+     * @param length
+     *            the number of bytes to check
+     * @return true, if this stream is an lzma  compressed stream, false otherwise
+     * 
+     * @since 1.10
+     */
+    public static boolean matches(byte[] signature, int length) {
+
+        if (signature == null || length < 3) {
+            return false;
+        }
+
+        if (signature[0] != 0x5d) {
+            return false;
+        }
+
+        if (signature[1] != 0) {
+            return false;
+        }
+
+        if (signature[2] != 0) {
+            return false;
+        }
+
+        return true;
+    }
 }
diff --git a/src/main/java/org/apache/commons/compress/compressors/lzma/LZMAUtils.java b/src/main/java/org/apache/commons/compress/compressors/lzma/LZMAUtils.java
new file mode 100644
index 0000000..7d3e1be
--- /dev/null
+++ b/src/main/java/org/apache/commons/compress/compressors/lzma/LZMAUtils.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.commons.compress.compressors.lzma;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.commons.compress.compressors.FileNameUtil;
+
+/**
+ * Utility code for the lzma compression format.
+ * @ThreadSafe
+ * @since 1.10
+ */
+public class LZMAUtils {
+
+    private static final FileNameUtil fileNameUtil;
+
+    /**
+     * LZMA Header Magic Bytes begin a LZMA file.
+     */
+    private static final byte[] HEADER_MAGIC = {
+        (byte) 0x5D, 0, 0
+    };
+
+    static enum CachedAvailability {
+        DONT_CACHE, CACHED_AVAILABLE, CACHED_UNAVAILABLE
+    }
+
+    private static volatile CachedAvailability cachedLZMAAvailability;
+
+    static {
+        Map<String, String> uncompressSuffix = new HashMap<String, String>();
+        uncompressSuffix.put(".lzma", "");
+        uncompressSuffix.put("-lzma", "");
+        fileNameUtil = new FileNameUtil(uncompressSuffix, ".lzma");
+        cachedLZMAAvailability = CachedAvailability.DONT_CACHE;
+        try {
+            Class.forName("org.osgi.framework.BundleEvent");
+        } catch (Exception ex) {
+            setCacheLZMAAvailablity(true);
+        }
+    }
+
+    /** Private constructor to prevent instantiation of this utility class. */
+    private LZMAUtils() {
+    }
+
+    /**
+     * Checks if the signature matches what is expected for a .lzma file.
+     *
+     * @param   signature     the bytes to check
+     * @param   length        the number of bytes to check
+     * @return  true if signature matches the .lzma magic bytes, false otherwise
+     */
+    public static boolean matches(byte[] signature, int length) {
+        if (length < HEADER_MAGIC.length) {
+            return false;
+        }
+
+        for (int i = 0; i < HEADER_MAGIC.length; ++i) {
+            if (signature[i] != HEADER_MAGIC[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Are the classes required to support LZMA compression available?
+     */
+    public static boolean isLZMACompressionAvailable() {
+        final CachedAvailability cachedResult = cachedLZMAAvailability;
+        if (cachedResult != CachedAvailability.DONT_CACHE) {
+            return cachedResult == CachedAvailability.CACHED_AVAILABLE;
+        }
+        return internalIsLZMACompressionAvailable();
+    }
+
+    private static boolean internalIsLZMACompressionAvailable() {
+        try {
+            LZMACompressorInputStream.matches(null, 0);
+            return true;
+        } catch (NoClassDefFoundError error) {
+            return false;
+        }
+    }
+
+    /**
+     * Detects common lzma suffixes in the given filename.
+     *
+     * @param filename name of a file
+     * @return {@code true} if the filename has a common lzma suffix,
+     *         {@code false} otherwise
+     */
+    public static boolean isCompressedFilename(String filename) {
+        return fileNameUtil.isCompressedFilename(filename);
+    }
+
+    /**
+     * Maps the given name of a lzma-compressed file to the name that
+     * the file should have after uncompression.  Any filenames with
+     * the generic ".lzma" suffix (or any other generic lzma suffix)
+     * is mapped to a name without that suffix. If no lzma suffix is
+     * detected, then the filename is returned unmapped.
+     *
+     * @param filename name of a file
+     * @return name of the corresponding uncompressed file
+     */
+    public static String getUncompressedFilename(String filename) {
+        return fileNameUtil.getUncompressedFilename(filename);
+    }
+
+    /**
+     * Maps the given filename to the name that the file should have after
+     * compression with lzma.
+     *
+     * @param filename name of a file
+     * @return name of the corresponding compressed file
+     */
+    public static String getCompressedFilename(String filename) {
+        return fileNameUtil.getCompressedFilename(filename);
+    }
+
+    /**
+     * Whether to cache the result of the LZMA check.
+     *
+     * <p>This defaults to {@code false} in an OSGi environment and {@code true} otherwise.</p>
+     * @param doCache whether to cache the result
+     */
+    public static void setCacheLZMAAvailablity(boolean doCache) {
+        if (!doCache) {
+            cachedLZMAAvailability = CachedAvailability.DONT_CACHE;
+        } else if (cachedLZMAAvailability == CachedAvailability.DONT_CACHE) {
+            final boolean hasLzma = internalIsLZMACompressionAvailable();
+            cachedLZMAAvailability = hasLzma ? CachedAvailability.CACHED_AVAILABLE
+                : CachedAvailability.CACHED_UNAVAILABLE;
+        }
+    }
+
+    // only exists to support unit tests
+    static CachedAvailability getCachedLZMAAvailability() {
+        return cachedLZMAAvailability;
+    }
+}
diff --git a/src/site/xdoc/index.xml b/src/site/xdoc/index.xml
index f738f76..0f095b0 100644
--- a/src/site/xdoc/index.xml
+++ b/src/site/xdoc/index.xml
@@ -68,6 +68,7 @@
               <li>Added support for parallel ZIP compression.</li>
               <li>Added support for raw transfer of entries from one ZIP file to another without uncompress/compress.</li>
               <li>Performance improvements for creating ZIP files with lots of small entries.</li>
+              <li>Added auto-detection for LZMA.</li>
             </ul>
           </subsection>
         </section>
diff --git a/src/test/java/org/apache/commons/compress/compressors/LZMATestCase.java b/src/test/java/org/apache/commons/compress/compressors/LZMATestCase.java
index cdc00f5..56b5c71 100644
--- a/src/test/java/org/apache/commons/compress/compressors/LZMATestCase.java
+++ b/src/test/java/org/apache/commons/compress/compressors/LZMATestCase.java
@@ -18,9 +18,11 @@
  */
 package org.apache.commons.compress.compressors;
 
+import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 
 import org.apache.commons.compress.AbstractTestCase;
@@ -37,18 +39,36 @@
         final InputStream is = new FileInputStream(input);
         try {
             final CompressorInputStream in = new LZMACompressorInputStream(is);
-            FileOutputStream out = null;
-            try {
-                out = new FileOutputStream(output);
-                IOUtils.copy(in, out);
-            } finally {
-                if (out != null) {
-                    out.close();
-                }
-                in.close();
-            }
+            copy(in, output);
         } finally {
             is.close();
         }
     }
+
+    @Test
+    public void testLZMAUnarchiveWithAutodetection() throws Exception {
+        final File input = getFile("bla.tar.lzma");
+        final File output = new File(dir, "bla.tar");
+        final InputStream is = new BufferedInputStream(new FileInputStream(input));
+        try {
+            final CompressorInputStream in = new CompressorStreamFactory()
+                .createCompressorInputStream(is);
+            copy(in, output);
+        } finally {
+            is.close();
+        }
+    }
+
+    private void copy(InputStream in, File output) throws IOException {
+        FileOutputStream out = null;
+        try {
+            out = new FileOutputStream(output);
+            IOUtils.copy(in, out);
+        } finally {
+            if (out != null) {
+                out.close();
+            }
+            in.close();
+        }
+    }
 }
diff --git a/src/test/java/org/apache/commons/compress/compressors/lzma/LZMAUtilsTestCase.java b/src/test/java/org/apache/commons/compress/compressors/lzma/LZMAUtilsTestCase.java
new file mode 100644
index 0000000..357c9fb
--- /dev/null
+++ b/src/test/java/org/apache/commons/compress/compressors/lzma/LZMAUtilsTestCase.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.commons.compress.compressors.lzma;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class LZMAUtilsTestCase {
+
+    @Test
+    public void testIsCompressedFilename() {
+        assertFalse(LZMAUtils.isCompressedFilename(""));
+        assertFalse(LZMAUtils.isCompressedFilename(".lzma"));
+
+        assertTrue(LZMAUtils.isCompressedFilename("x.lzma"));
+        assertTrue(LZMAUtils.isCompressedFilename("x-lzma"));
+
+        assertFalse(LZMAUtils.isCompressedFilename("xxgz"));
+        assertFalse(LZMAUtils.isCompressedFilename("lzmaz"));
+        assertFalse(LZMAUtils.isCompressedFilename("xaz"));
+
+        assertFalse(LZMAUtils.isCompressedFilename("x.lzma "));
+        assertFalse(LZMAUtils.isCompressedFilename("x.lzma\n"));
+        assertFalse(LZMAUtils.isCompressedFilename("x.lzma.y"));
+    }
+
+    @Test
+    public void testGetUncompressedFilename() {
+        assertEquals("", LZMAUtils.getUncompressedFilename(""));
+        assertEquals(".lzma", LZMAUtils.getUncompressedFilename(".lzma"));
+
+        assertEquals("x", LZMAUtils.getUncompressedFilename("x.lzma"));
+        assertEquals("x", LZMAUtils.getUncompressedFilename("x-lzma"));
+
+        assertEquals("x.lzma ", LZMAUtils.getUncompressedFilename("x.lzma "));
+        assertEquals("x.lzma\n", LZMAUtils.getUncompressedFilename("x.lzma\n"));
+        assertEquals("x.lzma.y", LZMAUtils.getUncompressedFilename("x.lzma.y"));
+    }
+
+    @Test
+    public void testGetCompressedFilename() {
+        assertEquals(".lzma", LZMAUtils.getCompressedFilename(""));
+        assertEquals("x.lzma", LZMAUtils.getCompressedFilename("x"));
+
+        assertEquals("x.wmf .lzma", LZMAUtils.getCompressedFilename("x.wmf "));
+        assertEquals("x.wmf\n.lzma", LZMAUtils.getCompressedFilename("x.wmf\n"));
+        assertEquals("x.wmf.y.lzma", LZMAUtils.getCompressedFilename("x.wmf.y"));
+    }
+
+    @Test
+    public void testMatches() {
+        byte[] data = {
+            (byte) 0x5D, 0, 0,
+        };
+        assertFalse(LZMAUtils.matches(data, 2));
+        assertTrue(LZMAUtils.matches(data, 3));
+        assertTrue(LZMAUtils.matches(data, 4));
+        data[2] = '0';
+        assertFalse(LZMAUtils.matches(data, 3));
+    }
+
+    @Test
+    public void testCachingIsEnabledByDefaultAndLZMAIsPresent() {
+        assertEquals(LZMAUtils.CachedAvailability.CACHED_AVAILABLE, LZMAUtils.getCachedLZMAAvailability());
+        assertTrue(LZMAUtils.isLZMACompressionAvailable());
+    }
+
+    @Test
+    public void testCanTurnOffCaching() {
+        try {
+            LZMAUtils.setCacheLZMAAvailablity(false);
+            assertEquals(LZMAUtils.CachedAvailability.DONT_CACHE, LZMAUtils.getCachedLZMAAvailability());
+            assertTrue(LZMAUtils.isLZMACompressionAvailable());
+        } finally {
+            LZMAUtils.setCacheLZMAAvailablity(true);
+        }
+    }
+
+    @Test
+    public void testTurningOnCachingReEvaluatesAvailability() {
+        try {
+            LZMAUtils.setCacheLZMAAvailablity(false);
+            assertEquals(LZMAUtils.CachedAvailability.DONT_CACHE, LZMAUtils.getCachedLZMAAvailability());
+            LZMAUtils.setCacheLZMAAvailablity(true);
+            assertEquals(LZMAUtils.CachedAvailability.CACHED_AVAILABLE, LZMAUtils.getCachedLZMAAvailability());
+        } finally {
+            LZMAUtils.setCacheLZMAAvailablity(true);
+        }
+    }
+
+}