Reuse the underlying byte array for ByteArray

Reuse the byte array for dex file output.

When calculating signature and checksum for a DEX
file, consider the size of the DEX file, instead of
the array. An array bigger than the file can be
allocated, and this would lead to wrong signature
and checksum calculation.

Because we reuse the output array
between seprate dx invocations, we
must write zeros to array. Otherwise, we might
end up picking up the output from
previous invocation.

The BaseDumper now tracks the number of read
bytes instead of the position.

Test: existing
Change-Id: I17126cfc02330bb459ae48be781e83ea997e4137
diff --git a/dx/junit-tests/com/android/dx/util/ByteArrayAnnotatedOutputTest.java b/dx/junit-tests/com/android/dx/util/ByteArrayAnnotatedOutputTest.java
new file mode 100644
index 0000000..da5a1df
--- /dev/null
+++ b/dx/junit-tests/com/android/dx/util/ByteArrayAnnotatedOutputTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 com.android.dx.util;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+import org.junit.Test;
+
+public final class ByteArrayAnnotatedOutputTest {
+    @Test
+    public void testArrayZeroedOut() {
+        int length = 100;
+        byte[] data = new byte[length];
+        Arrays.fill(data, (byte) 0xFF);
+
+        ByteArrayAnnotatedOutput output = new ByteArrayAnnotatedOutput(data);
+
+        output.writeZeroes(length);
+
+        for (int i = 0; i < length; i++) {
+            assertEquals("Position " + i + " has not been zeroed out", 0, data[i]);
+        }
+    }
+
+    @Test
+    public void testArrayAligned() {
+        int length = 16;
+        byte[] data = new byte[length];
+        Arrays.fill(data, (byte) 0xFF);
+
+        ByteArrayAnnotatedOutput output = new ByteArrayAnnotatedOutput(data);
+
+        // write at least one byte, so alignment is not correct
+        output.writeByte(0);
+        output.alignTo(length);
+
+        for (int i = 0; i < length; i++) {
+            assertEquals("Position " + i + " has not been zeroed out", 0, data[i]);
+        }
+    }
+}
diff --git a/dx/src/com/android/dx/command/dump/BaseDumper.java b/dx/src/com/android/dx/command/dump/BaseDumper.java
index e1ed000..b2bc5bb 100644
--- a/dx/src/com/android/dx/command/dump/BaseDumper.java
+++ b/dx/src/com/android/dx/command/dump/BaseDumper.java
@@ -63,8 +63,8 @@
     /** {@code non-null;} the current column separator string */
     private String separator;
 
-    /** the offset of the next byte to dump */
-    private int at;
+    /** the number of read bytes */
+    private int readBytes;
 
     /** commandline parsedArgs */
     protected Args args;
@@ -91,7 +91,7 @@
         this.strictParse = args.strictParse;
         this.indent = 0;
         this.separator = rawBytes ? "|" : "";
-        this.at = 0;
+        this.readBytes = 0;
         this.args = args;
 
         this.dexOptions = new DexOptions();
@@ -130,22 +130,13 @@
     /** {@inheritDoc} */
     @Override
     public void parsed(ByteArray bytes, int offset, int len, String human) {
-        offset = bytes.underlyingOffset(offset, getBytes());
+        offset = bytes.underlyingOffset(offset);
 
         boolean rawBytes = getRawBytes();
 
-        if (offset < at) {
-            println("<dump skipped backwards to " + Hex.u4(offset) + ">");
-            at = offset;
-        } else if (offset > at) {
-            String hex = rawBytes ? hexDump(at, offset - at) : "";
-            print(twoColumns(hex, "<skipped to " + Hex.u4(offset) + ">"));
-            at = offset;
-        }
-
         String hex = rawBytes ? hexDump(offset, len) : "";
         print(twoColumns(hex, human));
-        at += len;
+        readBytes += len;
     }
 
     /** {@inheritDoc} */
@@ -163,23 +154,12 @@
     }
 
     /**
-     * Gets the current dump cursor (that is, the offset of the expected
-     * next byte to dump).
+     * Gets the current number of read bytes.
      *
      * @return {@code >= 0;} the dump cursor
      */
-    protected final int getAt() {
-        return at;
-    }
-
-    /**
-     * Sets the dump cursor to the indicated offset in the given array.
-     *
-     * @param arr {@code non-null;} array in question
-     * @param offset {@code >= 0;} offset into the array
-     */
-    protected final void setAt(ByteArray arr, int offset) {
-        at = arr.underlyingOffset(offset, bytes);
+    protected final int getReadBytes() {
+        return readBytes;
     }
 
     /**
diff --git a/dx/src/com/android/dx/command/dump/BlockDumper.java b/dx/src/com/android/dx/command/dump/BlockDumper.java
index 8dce7fc..21a2de9 100644
--- a/dx/src/com/android/dx/command/dump/BlockDumper.java
+++ b/dx/src/com/android/dx/command/dump/BlockDumper.java
@@ -159,9 +159,6 @@
             return;
         }
 
-        // Reset the dump cursor to the start of the method.
-        setAt(bytes, offset);
-
         suppressDump = false;
 
         if (first) {
@@ -213,9 +210,6 @@
         int sz = list.size();
         CodeObserver codeObserver = new CodeObserver(bytes, BlockDumper.this);
 
-        // Reset the dump cursor to the start of the bytecode.
-        setAt(bytes, 0);
-
         suppressDump = false;
 
         int byteAt = 0;
@@ -344,7 +338,6 @@
         }
 
         suppressDump = false;
-        setAt(bytes, 0);
         parsed(bytes, 0, bytes.size(), sb.toString());
         suppressDump = true;
     }
diff --git a/dx/src/com/android/dx/command/dump/ClassDumper.java b/dx/src/com/android/dx/command/dump/ClassDumper.java
index 9723c62..22f13bd 100644
--- a/dx/src/com/android/dx/command/dump/ClassDumper.java
+++ b/dx/src/com/android/dx/command/dump/ClassDumper.java
@@ -65,9 +65,9 @@
         cf.setObserver(this);
         cf.getMagic(); // Force parsing to happen.
 
-        int at = getAt();
-        if (at != bytes.length) {
-            parsed(ba, at, bytes.length - at, "<extra data at end of file>");
+        int readBytes = getReadBytes();
+        if (readBytes != bytes.length) {
+            parsed(ba, readBytes, bytes.length - readBytes, "<extra data at end of file>");
         }
     }
 }
diff --git a/dx/src/com/android/dx/command/dump/SsaDumper.java b/dx/src/com/android/dx/command/dump/SsaDumper.java
index 45e69ec..58b8461 100644
--- a/dx/src/com/android/dx/command/dump/SsaDumper.java
+++ b/dx/src/com/android/dx/command/dump/SsaDumper.java
@@ -172,7 +172,6 @@
         }
 
         suppressDump = false;
-        setAt(bytes, 0);
         parsed(bytes, 0, bytes.size(), sb.toString());
         suppressDump = true;
     }
diff --git a/dx/src/com/android/dx/dex/file/DexFile.java b/dx/src/com/android/dx/dex/file/DexFile.java
index 577e603..04babb3 100644
--- a/dx/src/com/android/dx/dex/file/DexFile.java
+++ b/dx/src/com/android/dx/dex/file/DexFile.java
@@ -36,6 +36,8 @@
 import java.security.DigestException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 import java.util.zip.Adler32;
 
 /**
@@ -216,8 +218,23 @@
      */
     public void writeTo(OutputStream out, Writer humanOut, boolean verbose)
         throws IOException {
+        writeTo(out, null /* storage */, humanOut, verbose);
+    }
+
+
+    /**
+     * Writes the contents of this instance as either a binary or a
+     * human-readable form, or both.
+     *
+     * @param out {@code null-ok;} where to write to
+     * @param storage temporary storage for storing dexing.
+     * @param humanOut {@code null-ok;} where to write human-oriented output to
+     * @param verbose whether to be verbose when writing human-oriented output
+     */
+    public void writeTo(OutputStream out, Storage storage, Writer humanOut, boolean verbose)
+            throws IOException {
         boolean annotate = (humanOut != null);
-        ByteArrayAnnotatedOutput result = toDex0(annotate, verbose);
+        ByteArrayAnnotatedOutput result = toDex0(annotate, verbose, storage);
 
         if (out != null) {
             out.write(result.getArray());
@@ -229,6 +246,17 @@
     }
 
     /**
+     * Writes the contents of this instance as a binary.
+     *
+     * @param storage temporary storage for storing dexing.
+     * @return the stored content.
+     */
+    public ByteArrayAnnotatedOutput writeTo(Storage storage) {
+        return toDex0(false, false, storage);
+    }
+
+
+    /**
      * Returns the contents of this instance as a {@code .dex} file,
      * in {@code byte[]} form.
      *
@@ -239,7 +267,7 @@
     public byte[] toDex(Writer humanOut, boolean verbose)
         throws IOException {
         boolean annotate = (humanOut != null);
-        ByteArrayAnnotatedOutput result = toDex0(annotate, verbose);
+        ByteArrayAnnotatedOutput result = toDex0(annotate, verbose, null);
 
         if (annotate) {
             result.writeAnnotationsTo(humanOut);
@@ -548,6 +576,29 @@
     }
 
     /**
+     * Holder for a byte[] that can grow on demand.
+     */
+    public static final class Storage {
+        byte[] storage;
+        public Storage(byte[] storage) {
+            this.storage = storage;
+        }
+
+        public byte[] getStorage(int requestedLength) {
+            if (storage.length < requestedLength) {
+                Logger.getAnonymousLogger().log(
+                        Level.FINER,
+                        "DexFile storage too small  "
+                                + storage.length
+                                + " vs "
+                                + requestedLength);
+                storage = new byte[requestedLength];
+            }
+            return storage;
+        }
+    }
+
+    /**
      * Returns the contents of this instance as a {@code .dex} file,
      * in a {@link ByteArrayAnnotatedOutput} instance.
      *
@@ -556,7 +607,8 @@
      * @return {@code non-null;} a {@code .dex} file for this instance
      */
     private ByteArrayAnnotatedOutput toDex0(boolean annotate,
-            boolean verbose) {
+            boolean verbose,
+            Storage storage) {
         /*
          * The following is ordered so that the prepare() calls which
          * add items happen before the calls to the sections that get
@@ -634,7 +686,8 @@
         // Write out all the sections.
 
         fileSize = offset;
-        byte[] barr = new byte[fileSize];
+        byte[] barr = storage == null ? new byte[fileSize] : storage.getStorage(fileSize);
+
         ByteArrayAnnotatedOutput out = new ByteArrayAnnotatedOutput(barr);
 
         if (annotate) {
@@ -672,8 +725,8 @@
 
         // Perform final bookkeeping.
 
-        calcSignature(barr);
-        calcChecksum(barr);
+        calcSignature(barr, out.getCursor());
+        calcChecksum(barr, out.getCursor());
 
         if (annotate) {
             wordData.writeIndexAnnotation(out, ItemType.TYPE_CODE_ITEM,
@@ -705,8 +758,9 @@
      * given array, and modify the array to contain it.
      *
      * @param bytes {@code non-null;} the bytes of the file
+     * @param len length of {@code .dex} file encoded in the array
      */
-    private static void calcSignature(byte[] bytes) {
+    private static void calcSignature(byte[] bytes, int len) {
         MessageDigest md;
 
         try {
@@ -715,13 +769,13 @@
             throw new RuntimeException(ex);
         }
 
-        md.update(bytes, 32, bytes.length - 32);
+        md.update(bytes, 32, len - 32);
 
         try {
             int amt = md.digest(bytes, 12, 20);
             if (amt != 20) {
                 throw new RuntimeException("unexpected digest write: " + amt +
-                                           " bytes");
+                        " bytes");
             }
         } catch (DigestException ex) {
             throw new RuntimeException(ex);
@@ -733,11 +787,12 @@
      * given array, and modify the array to contain it.
      *
      * @param bytes {@code non-null;} the bytes of the file
+     * @param len length of {@code .dex} file encoded in the array
      */
-    private static void calcChecksum(byte[] bytes) {
+    private static void calcChecksum(byte[] bytes, int len) {
         Adler32 a32 = new Adler32();
 
-        a32.update(bytes, 12, bytes.length - 12);
+        a32.update(bytes, 12, len - 12);
 
         int sum = (int) a32.getValue();
 
diff --git a/dx/src/com/android/dx/util/ByteArray.java b/dx/src/com/android/dx/util/ByteArray.java
index aeab048..be5d776 100644
--- a/dx/src/com/android/dx/util/ByteArray.java
+++ b/dx/src/com/android/dx/util/ByteArray.java
@@ -19,6 +19,7 @@
 import java.io.DataInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Arrays;
 
 /**
  * Wrapper for a {@code byte[]}, which provides read-only access and
@@ -95,7 +96,8 @@
      */
     public ByteArray slice(int start, int end) {
         checkOffsets(start, end);
-        return new ByteArray(bytes, start + this.start, end + this.start);
+        byte[] slicedOut = Arrays.copyOfRange(bytes, start, end);
+        return new ByteArray(slicedOut);
     }
 
     /**
@@ -103,16 +105,11 @@
      * offset into this instance.
      *
      * @param offset offset into this instance
-     * @param bytes {@code non-null;} (alleged) underlying array
      * @return corresponding offset into {@code bytes}
      * @throws IllegalArgumentException thrown if {@code bytes} is
      * not the underlying array of this instance
      */
-    public int underlyingOffset(int offset, byte[] bytes) {
-        if (bytes != this.bytes) {
-            throw new IllegalArgumentException("wrong bytes");
-        }
-
+    public int underlyingOffset(int offset) {
         return start + offset;
     }
 
@@ -282,7 +279,7 @@
          *
          * @return {@code 0..size();} the cursor
          */
-        public int getCursor();
+        int getCursor();
     }
 
     /**
diff --git a/dx/src/com/android/dx/util/ByteArrayAnnotatedOutput.java b/dx/src/com/android/dx/util/ByteArrayAnnotatedOutput.java
index 226f1a1..55a3ffd 100644
--- a/dx/src/com/android/dx/util/ByteArrayAnnotatedOutput.java
+++ b/dx/src/com/android/dx/util/ByteArrayAnnotatedOutput.java
@@ -22,6 +22,7 @@
 import java.io.IOException;
 import java.io.Writer;
 import java.util.ArrayList;
+import java.util.Arrays;
 
 /**
  * Implementation of {@link AnnotatedOutput} which stores the written data
@@ -329,9 +330,9 @@
         }
 
         /*
-         * There is no need to actually write zeroes, since the array is
-         * already preinitialized with zeroes.
+         * We need to write zeroes, since the array might be reused across different dx invocations.
          */
+        Arrays.fill(data, cursor, end, (byte) 0);
 
         cursor = end;
     }
@@ -355,9 +356,9 @@
         }
 
         /*
-         * There is no need to actually write zeroes, since the array is
-         * already preinitialized with zeroes.
+         * We need to write zeroes, since the array might be reused across different dx invocations.
          */
+        Arrays.fill(data, cursor, end, (byte) 0);
 
         cursor = end;
     }