Our parser for STAR-encoded binary values was wrong, STAR only uses 7 or 11 bytes and can encode negative values as twos complement signaled by the first byte being 0xff rather than 0x80.  COMPRESS-182

git-svn-id: https://svn.apache.org/repos/asf/commons/proper/compress/trunk@1296576 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java b/src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java
index d989b8a..b45b1f2 100644
--- a/src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java
+++ b/src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java
@@ -18,6 +18,8 @@
  */
 package org.apache.commons.compress.archivers.tar;
 
+import java.math.BigInteger;
+
 /**
  * This class provides static utility methods to work with byte streams.
  *
@@ -127,17 +129,52 @@
         if ((buffer[offset] & 0x80) == 0) {
             return parseOctal(buffer, offset, length);
         }
+        final boolean negative = buffer[offset] == (byte) 0xff;
+        if (length < 9) {
+            return parseBinaryLong(buffer, offset, length, negative);
+        }
+        return parseBinaryBigInteger(buffer, offset, length, negative);
+    }
 
-        long val = buffer[offset] & 0x7f;
+    private static long parseBinaryLong(final byte[] buffer, final int offset,
+                                        final int length,
+                                        final boolean negative) {
+        if (length >= 9) {
+            throw new IllegalArgumentException("At offset " + offset + ", "
+                                               + length + " byte binary number"
+                                               + " exceeds maximum signed long"
+                                               + " value");
+        }
+        long val = 0;
         for (int i = 1; i < length; i++) {
-            if (val >= (1L << (63 - 8))) {
-                throw new IllegalArgumentException(
-                    "At offset " + offset + ", " + length + " byte " +
-                    "binary number exceeds maximum signed long value");
-            }
             val = (val << 8) + (buffer[offset + i] & 0xff);
         }
-        return val;
+        if (negative) {
+            // 2's complement
+            val--;
+            val ^= ((long) Math.pow(2, (length - 1) * 8) - 1);
+        }
+        return negative ? -val : val;
+    }
+
+    private static long parseBinaryBigInteger(final byte[] buffer,
+                                              final int offset,
+                                              final int length,
+                                              final boolean negative) {
+        byte[] remainder = new byte[length - 1];
+        System.arraycopy(buffer, offset + 1, remainder, 0, length - 1);
+        BigInteger val = new BigInteger(remainder);
+        if (negative) {
+            // 2's complement
+            val = val.add(BigInteger.valueOf(-1)).not();
+        }
+        if (val.bitLength() > 63) {
+            throw new IllegalArgumentException("At offset " + offset + ", "
+                                               + length + " byte binary number"
+                                               + " exceeds maximum signed long"
+                                               + " value");
+        }
+        return negative ? -val.longValue() : val.longValue();
     }
 
     /**
@@ -325,23 +362,54 @@
         // Check whether we are dealing with UID/GID or SIZE field
         final long maxAsOctalChar = length == TarConstants.UIDLEN ? TarConstants.MAXID : TarConstants.MAXSIZE;
 
-        if (value <= maxAsOctalChar) { // OK to store as octal chars
+        final boolean negative = value < 0;
+        if (!negative && value <= maxAsOctalChar) { // OK to store as octal chars
             return formatLongOctalBytes(value, buf, offset, length);
         }
 
-        long val = value;
+        if (length < 9) {
+            formatLongBinary(value, buf, offset, length, negative);
+        }
+        formatBigIntegerBinary(value, buf, offset, length, negative);
+
+        buf[offset] = (byte) (negative ? 0xff : 0x80);
+        return offset + length;
+    }
+
+    private static void formatLongBinary(final long value, byte[] buf,
+                                         final int offset, final int length,
+                                         final boolean negative) {
+        final int bits = (length - 1) * 8;
+        final long max = (long) Math.pow(2, bits);
+        long val = Math.abs(value);
+        if (val >= max) {
+            throw new IllegalArgumentException("Value " + value +
+                " is too large for " + length + " byte field.");
+        }
+        if (negative) {
+            val ^= max - 1;
+            val |= 0xff << bits;
+            val++;
+        }
         for (int i = offset + length - 1; i >= offset; i--) {
             buf[i] = (byte) val;
             val >>= 8;
         }
+    }
 
-        if (val != 0 || (buf[offset] & 0x80) != 0) {
-            throw new IllegalArgumentException("Value " + value +
-                " is too large for " + length + " byte field.");
+    private static void formatBigIntegerBinary(final long value, byte[] buf,
+                                               final int offset,
+                                               final int length,
+                                               final boolean negative) {
+        BigInteger val = BigInteger.valueOf(value);
+        final byte[] b = val.toByteArray();
+        final int len = b.length;
+        final int off = offset + length - len;
+        System.arraycopy(b, 0, buf, off, len);
+        final byte fill = (byte) (negative ? 0xff : 0);
+        for (int i = offset + 1; i < off; i++) {
+            buf[i] = fill;
         }
-
-        buf[offset] |= 0x80;
-        return offset + length;
     }
 
     /**
diff --git a/src/test/java/org/apache/commons/compress/archivers/tar/TarUtilsTest.java b/src/test/java/org/apache/commons/compress/archivers/tar/TarUtilsTest.java
index 28f11e0..e7ca1b7 100644
--- a/src/test/java/org/apache/commons/compress/archivers/tar/TarUtilsTest.java
+++ b/src/test/java/org/apache/commons/compress/archivers/tar/TarUtilsTest.java
@@ -136,11 +136,18 @@
         assertEquals(value,parseValue);
     }
 
-    public void testRoundTripOctalOrBinary() {
-        checkRoundTripOctalOrBinary(0, 8);
-        checkRoundTripOctalOrBinary(1, 8);
-        checkRoundTripOctalOrBinary(Long.MAX_VALUE, 8); // [0x7f ff ff ff ff ff ff ff
-        checkRoundTripOctalOrBinary(TarConstants.MAXSIZE, 8); // will need binary format
+    public void testRoundTripOctalOrBinary8() {
+        testRoundTripOctalOrBinary(8);
+    }
+    public void testRoundTripOctalOrBinary12() {
+        testRoundTripOctalOrBinary(12);
+    }
+    private void testRoundTripOctalOrBinary(int length) {
+        checkRoundTripOctalOrBinary(0, length);
+        checkRoundTripOctalOrBinary(1, length);
+        checkRoundTripOctalOrBinary(Long.MAX_VALUE >> 7, length); // [0x00 ff ff ff ff ff ff ff
+        checkRoundTripOctalOrBinary(TarConstants.MAXSIZE, length); // will need binary format
+        checkRoundTripOctalOrBinary(-1, length); // will need binary format
     }
     
     // Check correct trailing bytes are generated
@@ -190,4 +197,31 @@
         int len = TarUtils.formatNameBytes(string, buff, 0, buff.length);
         assertEquals(string, TarUtils.parseName(buff, 0, len));
     }
+
+    public void testReadNegativeBinary8Byte() {
+        byte[] b = new byte[] {
+            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+            (byte) 0xff, (byte) 0xff, (byte) 0xf1, (byte) 0xef,
+        };
+        assertEquals(-3601l, TarUtils.parseOctalOrBinary(b, 0, 8));
+    }
+
+    public void testReadNegativeBinary12Byte() {
+        byte[] b = new byte[] {
+            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+            (byte) 0xff, (byte) 0xff, (byte) 0xf1, (byte) 0xef,
+        };
+        assertEquals(-3601l, TarUtils.parseOctalOrBinary(b, 0, 12));
+    }
+
+
+    public void testWriteNegativeBinary8Byte() {
+        byte[] b = new byte[] {
+            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+            (byte) 0xff, (byte) 0xff, (byte) 0xf1, (byte) 0xef,
+        };
+        assertEquals(-3601l, TarUtils.parseOctalOrBinary(b, 0, 8));
+    }
+
 }