diff --git a/baksmali/src/main/java/org/jf/baksmali/dump.java b/baksmali/src/main/java/org/jf/baksmali/dump.java index 6ea31ddd..32a168ec 100644 --- a/baksmali/src/main/java/org/jf/baksmali/dump.java +++ b/baksmali/src/main/java/org/jf/baksmali/dump.java @@ -32,18 +32,20 @@ import org.jf.dexlib2.dexbacked.DexBackedDexFile; import org.jf.dexlib2.dexbacked.raw.RawDexFile; import org.jf.util.ConsoleUtil; +import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; +import java.io.Writer; public class dump { public static void dump(DexBackedDexFile dexFile, String dumpFileName) throws IOException { if (dumpFileName != null) { - FileWriter writer = null; + Writer writer = null; try { - writer = new FileWriter(dumpFileName); + writer = new BufferedWriter(new FileWriter(dumpFileName)); int consoleWidth = ConsoleUtil.getConsoleWidth(); if (consoleWidth <= 0) { diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/ClassDataItem.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/ClassDataItem.java index 632e56fd..cf8a084a 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/ClassDataItem.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/ClassDataItem.java @@ -54,21 +54,17 @@ public class ClassDataItem { @Override protected void annotateItem(@Nonnull AnnotatedBytes out, @Nonnull RawDexFile dexFile, int itemIndex) { DexReader reader = dexFile.readerAt(out.getCursor()); - int mark = reader.getOffset(); int staticFieldsSize = reader.readSmallUleb128(); - out.annotate(reader.getOffset() - mark, "static_fields_size = %d", staticFieldsSize); + out.annotateTo(reader.getOffset(), "static_fields_size = %d", staticFieldsSize); - mark = reader.getOffset(); int instanceFieldsSize = reader.readSmallUleb128(); - out.annotate(reader.getOffset() - mark, "instance_fields_size = %d", instanceFieldsSize); + out.annotateTo(reader.getOffset(), "instance_fields_size = %d", instanceFieldsSize); - mark = reader.getOffset(); int directMethodsSize = reader.readSmallUleb128(); - out.annotate(reader.getOffset() - mark, "direct_methods_size = %d", directMethodsSize); + out.annotateTo(reader.getOffset(), "direct_methods_size = %d", directMethodsSize); - mark = reader.getOffset(); int virtualMethodsSize = reader.readSmallUleb128(); - out.annotate(reader.getOffset() - mark, "virtual_methods_size = %d", virtualMethodsSize); + out.annotateTo(reader.getOffset(), "virtual_methods_size = %d", virtualMethodsSize); int previousIndex = 0; for (int i=0; i ITEM_SIZE) { - out.annotate(headerSize - ITEM_SIZE, "header padding"); + out.annotateTo(headerSize, "header padding"); } } }; diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/RawDexFile.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/RawDexFile.java index 4a65fd02..6a67cffa 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/RawDexFile.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/RawDexFile.java @@ -108,42 +108,42 @@ public class RawDexFile extends DexBackedDexFile.Impl { int stringCount = headerItem.getStringCount(); if (stringCount > 0) { - annotatedBytes.skipTo(headerItem.getStringOffset()); + annotatedBytes.moveTo(headerItem.getStringOffset()); annotatedBytes.annotate(0, " "); StringIdItem.getAnnotator().annotateSection(annotatedBytes, this, stringCount); } int typeCount = headerItem.getTypeCount(); if (typeCount > 0) { - annotatedBytes.skipTo(headerItem.getTypeOffset()); + annotatedBytes.moveTo(headerItem.getTypeOffset()); annotatedBytes.annotate(0, " "); TypeIdItem.getAnnotator().annotateSection(annotatedBytes, this, typeCount); } int protoCount = headerItem.getProtoCount(); if (protoCount > 0) { - annotatedBytes.skipTo(headerItem.getProtoOffset()); + annotatedBytes.moveTo(headerItem.getProtoOffset()); annotatedBytes.annotate(0, " "); ProtoIdItem.getAnnotator().annotateSection(annotatedBytes, this, protoCount); } int fieldCount = headerItem.getFieldCount(); if (fieldCount > 0) { - annotatedBytes.skipTo(headerItem.getFieldOffset()); + annotatedBytes.moveTo(headerItem.getFieldOffset()); annotatedBytes.annotate(0, " "); FieldIdItem.getAnnotator().annotateSection(annotatedBytes, this, fieldCount); } int methodCount = headerItem.getMethodCount(); if (methodCount > 0) { - annotatedBytes.skipTo(headerItem.getMethodOffset()); + annotatedBytes.moveTo(headerItem.getMethodOffset()); annotatedBytes.annotate(0, " "); MethodIdItem.getAnnotator().annotateSection(annotatedBytes, this, methodCount); } int classCount = headerItem.getClassCount(); if (classCount > 0) { - annotatedBytes.skipTo(headerItem.getClassOffset()); + annotatedBytes.moveTo(headerItem.getClassOffset()); annotatedBytes.annotate(0, " "); ClassDefItem.getAnnotator().annotateSection(annotatedBytes, this, classCount); } @@ -151,7 +151,7 @@ public class RawDexFile extends DexBackedDexFile.Impl { for (MapItem mapItem: getMapItems()) { SectionAnnotator annotator = annotators.get(mapItem.getType()); if (annotator != null) { - annotatedBytes.skipTo(mapItem.getOffset()); + annotatedBytes.moveTo(mapItem.getOffset()); annotator.annotateSection(annotatedBytes, this, mapItem.getItemCount()); } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/SectionAnnotator.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/SectionAnnotator.java index 320f4488..60a2ace6 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/SectionAnnotator.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/SectionAnnotator.java @@ -58,7 +58,7 @@ public abstract class SectionAnnotator { out.annotate(0, ""); for (int i=0; i annotations = Lists.newArrayList(); + /** + * This defines the bytes ranges and their associated range and point annotations. + * + * A range is defined by 2 consecutive keys in the map. The first key is the inclusive start point, the second key + * is the exclusive end point. The range annotation for a range is associated with the first key for that range. + * The point annotations for a point are associated with the key at that point. + */ + @Nonnull private TreeMap annotatations = Maps.newTreeMap(); + private int cursor; private int indentLevel; @@ -62,33 +83,141 @@ public class AnnotatedBytes { } /** - * Skips a portion of the binary output. This is equivalent to calling - * annotate(offset-cursor, ""); + * Moves the cursor to a new location * - * @param offset The offset to skip to + * @param offset The offset to move to */ - public void skipTo(int offset) { - if (offset < cursor) { - throw new IllegalArgumentException("skipTo can only skip forward"); - } - int delta = offset - cursor; - if (delta != 0) { - annotate(delta, ""); - } + public void moveTo(int offset) { + cursor = offset; + } + + /** + * Moves the cursor forward or backward by some amount + * + * @param offset The amount to move the cursor + */ + public void moveBy(int offset) { + cursor += offset; + } + + public void annotateTo(int offset, @Nonnull String msg, Object... formatArgs) { + annotate(offset - cursor, msg, formatArgs); } /** * Add an annotation of the given length at the current location. * + * The location + * + * * @param length the length of data being annotated * @param msg the annotation message * @param formatArgs format arguments to pass to String.format */ public void annotate(int length, @Nonnull String msg, Object... formatArgs) { - annotations.add(new AnnotationItem(cursor, indentLevel, String.format(msg, formatArgs))); + String formattedMsg = String.format(msg, formatArgs); + int exclusiveEndOffset = cursor + length; + + AnnotationEndpoint endPoint = null; + + // Do we have an endpoint at the beginning of this annotation already? + AnnotationEndpoint startPoint = annotatations.get(cursor); + if (startPoint == null) { + // Nope. We need to check that we're not in the middle of an existing range annotation. + Map.Entry previousEntry = annotatations.lowerEntry(cursor); + if (previousEntry != null) { + AnnotationEndpoint previousAnnotations = previousEntry.getValue(); + AnnotationItem previousRangeAnnotation = previousAnnotations.rangeAnnotation; + if (previousRangeAnnotation != null) { + throw new IllegalStateException( + String.format("Cannot add annotation %s, due to existing annotation %s", + formatAnnotation(cursor, cursor + length, formattedMsg), + formatAnnotation(previousEntry.getKey(), previousRangeAnnotation.annotation))); + } + } + } else if (length > 0) { + AnnotationItem existingRangeAnnotation = startPoint.rangeAnnotation; + if (existingRangeAnnotation != null) { + throw new IllegalStateException( + String.format("Cannot add annotation %s, due to existing annotation %s", + formatAnnotation(cursor, cursor + length, formattedMsg), + formatAnnotation(cursor, existingRangeAnnotation.annotation))); + } + } + + if (length > 0) { + // Ensure that there is no later annotation that would intersect with this one + Map.Entry nextEntry = annotatations.higherEntry(cursor); + if (nextEntry != null) { + int nextKey = nextEntry.getKey(); + if (nextKey < exclusiveEndOffset) { + // there is an endpoint that would intersect with this annotation. Find one of the annotations + // associated with the endpoint, to print in the error message + AnnotationEndpoint nextEndpoint = nextEntry.getValue(); + AnnotationItem nextRangeAnnotation = nextEndpoint.rangeAnnotation; + if (nextRangeAnnotation != null) { + throw new IllegalStateException( + String.format("Cannot add annotation %s, due to existing annotation %s", + formatAnnotation(cursor, cursor + length, formattedMsg), + formatAnnotation(nextKey, nextRangeAnnotation.annotation))); + } + if (nextEndpoint.pointAnnotations.size() > 0) { + throw new IllegalStateException( + String.format("Cannot add annotation %s, due to existing annotation %s", + formatAnnotation(cursor, cursor + length, formattedMsg), + formatAnnotation(nextKey, nextKey, + nextEndpoint.pointAnnotations.get(0).annotation))); + } + // There are no annotations on this endpoint. This "shouldn't" happen. We can still throw an exception. + throw new IllegalStateException( + String.format("Cannot add annotation %s, due to existing annotation endpoint at %d", + formatAnnotation(cursor, cursor + length, formattedMsg), + nextKey)); + } + + if (nextKey == exclusiveEndOffset) { + // the next endpoint matches the end of the annotation we are adding + endPoint = nextEntry.getValue(); + } + } + } + + // Now, actually add the annotation + // If startPoint is null, we need to create a new one and add it to annotations. Otherwise, we just need to add + // the annotation to the existing AnnotationEndpoint + // the range annotation + if (startPoint == null) { + startPoint = new AnnotationEndpoint(); + annotatations.put(cursor, startPoint); + } + if (length == 0) { + startPoint.pointAnnotations.add(new AnnotationItem(indentLevel, formattedMsg)); + } else { + startPoint.rangeAnnotation = new AnnotationItem(indentLevel, formattedMsg); + + // If endPoint is null, we need to create a new, empty one and add it to annotations + if (endPoint == null) { + endPoint = new AnnotationEndpoint(); + annotatations.put(exclusiveEndOffset, endPoint); + } + } + cursor += length; } + private String formatAnnotation(int offset, String annotationMsg) { + Integer endOffset = annotatations.higherKey(offset); + return formatAnnotation(offset, endOffset, annotationMsg); + } + + private String formatAnnotation(int offset, Integer endOffset, String annotationMsg) { + if (endOffset != null) { + return String.format("[0x%x, 0x%x) \"%s\"", offset, endOffset, annotationMsg); + } else { + return String.format("[0x%x, ) \"%s\"", offset, annotationMsg); + } + } + public void indent() { indentLevel++; } @@ -104,13 +233,20 @@ public class AnnotatedBytes { return cursor; } + private static class AnnotationEndpoint { + /** Annotations that are associated with a specific point between bytes */ + @Nonnull + public final List pointAnnotations = Lists.newArrayList(); + /** Annotations that are associated with a range of bytes */ + @Nullable + public AnnotationItem rangeAnnotation = null; + } + private static class AnnotationItem { - public final int offset; public final int indentLevel; public final String annotation; - public AnnotationItem(int offset, int indentLevel, String annotation) { - this.offset = offset; + public AnnotationItem(int indentLevel, String annotation) { this.indentLevel = indentLevel; this.annotation = annotation; } @@ -135,52 +271,45 @@ public class AnnotatedBytes { int rightWidth = getAnnotationWidth(); int leftWidth = outputWidth - rightWidth - 1; - StringBuilder padding = new StringBuilder(); - for (int i=0; i<1000; i++) { - padding.append(' '); - } + String padding = Strings.repeat(" ", 1000); TwoColumnOutput twoc = new TwoColumnOutput(out, leftWidth, rightWidth, "|"); - Writer left = twoc.getLeft(); - Writer right = twoc.getRight(); - int leftAt = 0; // left-hand byte output cursor - int rightAt = 0; // right-hand annotation index - int rightSz = annotations.size(); - while ((leftAt < cursor) && (rightAt < rightSz)) { - AnnotationItem a = annotations.get(rightAt); - int start = a.offset; - int end; + Integer[] keys = new Integer[annotatations.size()]; + keys = annotatations.keySet().toArray(keys); - if (rightAt + 1 < annotations.size()) { - end = annotations.get(rightAt+1).offset; - } else { - end = cursor; + AnnotationEndpoint[] values = new AnnotationEndpoint[annotatations.size()]; + values = annotatations.values().toArray(values); + + for (int i=0; i= arr.length) { + arr = enlargeArray(arr, (int)(Math.ceil((arr.length + 1) * 1.5))); + } + + arr[index] = str; + return arr; + } + + private static String[] addString(@Nonnull String[] arr, String str, int index, int newLength) { + if (index >= arr.length) { + arr = enlargeArray(arr, newLength); + } + + arr[index] = str; + return arr; + } + + private static String[] enlargeArray(String[] arr, int newLength) { + String[] newArr = new String[newLength]; + System.arraycopy(arr, 0, newArr, 0, arr.length); + return newArr; + } +} diff --git a/util/src/main/java/org/jf/util/TwoColumnOutput.java b/util/src/main/java/org/jf/util/TwoColumnOutput.java index 6f923d46..a04d9a5d 100644 --- a/util/src/main/java/org/jf/util/TwoColumnOutput.java +++ b/util/src/main/java/org/jf/util/TwoColumnOutput.java @@ -31,7 +31,11 @@ package org.jf.util; -import java.io.*; +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; /** * Class that takes a combined output destination and provides two @@ -45,48 +49,9 @@ public final class TwoColumnOutput { /** > 0; the left column width */ private final int leftWidth; - /** non-null; pending left column output */ - private final StringBuffer leftBuf; + private final int rightWidth; - /** non-null; pending right column output */ - private final StringBuffer rightBuf; - - /** non-null; left column writer */ - private final WrappedIndentingWriter leftColumn; - - /** non-null; right column writer */ - private final WrappedIndentingWriter rightColumn; - - /** - * Turns the given two strings (with widths) and spacer into a formatted - * two-column string. - * - * @param s1 non-null; first string - * @param width1 > 0; width of the first column - * @param spacer non-null; spacer string - * @param s2 non-null; second string - * @param width2 > 0; width of the second column - * @return non-null; an appropriately-formatted string - */ - public static String toString(String s1, int width1, String spacer, - String s2, int width2) { - int len1 = s1.length(); - int len2 = s2.length(); - - StringWriter sw = new StringWriter((len1 + len2) * 3); - TwoColumnOutput twoOut = - new TwoColumnOutput(sw, width1, width2, spacer); - - try { - twoOut.getLeft().write(s1); - twoOut.getRight().write(s2); - } catch (IOException ex) { - throw new RuntimeException("shouldn't happen", ex); - } - - twoOut.flush(); - return sw.toString(); - } + private final String spacer; /** * Constructs an instance. @@ -96,11 +61,8 @@ public final class TwoColumnOutput { * @param rightWidth > 0; width of the right column, in characters * @param spacer non-null; spacer string to sit between the two columns */ - public TwoColumnOutput(Writer out, int leftWidth, int rightWidth, - String spacer) { - if (out == null) { - throw new NullPointerException("out == null"); - } + public TwoColumnOutput(@Nonnull Writer out, int leftWidth, int rightWidth, + @Nonnull String spacer) { if (leftWidth < 1) { throw new IllegalArgumentException("leftWidth < 1"); @@ -110,20 +72,10 @@ public final class TwoColumnOutput { throw new IllegalArgumentException("rightWidth < 1"); } - if (spacer == null) { - throw new NullPointerException("spacer == null"); - } - - StringWriter leftWriter = new StringWriter(1000); - StringWriter rightWriter = new StringWriter(1000); - this.out = out; this.leftWidth = leftWidth; - this.leftBuf = leftWriter.getBuffer(); - this.rightBuf = rightWriter.getBuffer(); - this.leftColumn = new WrappedIndentingWriter(leftWriter, leftWidth); - this.rightColumn = - new WrappedIndentingWriter(rightWriter, rightWidth, spacer); + this.rightWidth = rightWidth; + this.spacer = spacer; } /** @@ -139,114 +91,52 @@ public final class TwoColumnOutput { this(new OutputStreamWriter(out), leftWidth, rightWidth, spacer); } - /** - * Gets the writer to use to write to the left column. - * - * @return non-null; the left column writer - */ - public Writer getLeft() { - return leftColumn; - } + private String[] leftLines = null; + private String[] rightLines = null; + public void write(String left, String right) throws IOException { + leftLines = StringWrapper.wrapString(left, leftWidth, leftLines); + rightLines = StringWrapper.wrapString(right, rightWidth, rightLines); + int leftCount = leftLines.length; + int rightCount = rightLines.length; - /** - * Gets the writer to use to write to the right column. - * - * @return non-null; the right column writer - */ - public Writer getRight() { - return rightColumn; - } + for (int i=0; i 0) { + writeSpaces(out, remaining); + } + + out.write(spacer); + + if (rightLine != null) { + out.write(rightLine); + } + + out.write('\n'); } - - if (rightLen != 0) { - writeSpaces(out, leftWidth - leftLen); - out.write(rightBuf.substring(0, rightLen)); - } - - out.write('\n'); - - leftBuf.delete(0, leftLen + 1); - rightBuf.delete(0, rightLen + 1); - } - } - - /** - * Flushes the left column buffer, printing it and clearing the buffer. - * If the buffer is already empty, this does nothing. - */ - private void flushLeft() throws IOException { - appendNewlineIfNecessary(leftBuf, leftColumn); - - while (leftBuf.length() != 0) { - rightColumn.write('\n'); - outputFullLines(); - } - } - - /** - * Flushes the right column buffer, printing it and clearing the buffer. - * If the buffer is already empty, this does nothing. - */ - private void flushRight() throws IOException { - appendNewlineIfNecessary(rightBuf, rightColumn); - - while (rightBuf.length() != 0) { - leftColumn.write('\n'); - outputFullLines(); - } - } - - /** - * Appends a newline to the given buffer via the given writer, but - * only if it isn't empty and doesn't already end with one. - * - * @param buf non-null; the buffer in question - * @param out non-null; the writer to use - */ - private static void appendNewlineIfNecessary(StringBuffer buf, - Writer out) - throws IOException { - int len = buf.length(); - - if ((len != 0) && (buf.charAt(len - 1) != '\n')) { - out.write('\n'); } } diff --git a/util/src/test/java/org/jf/util/StringWrapperTest.java b/util/src/test/java/org/jf/util/StringWrapperTest.java new file mode 100644 index 00000000..64dca33e --- /dev/null +++ b/util/src/test/java/org/jf/util/StringWrapperTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2013, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.util; + +import org.junit.Assert; +import org.junit.Test; + +public class StringWrapperTest { + @Test + public void testWrapString() { + validateResult( + new String[]{"abc", "abcdef", "abcdef"}, + StringWrapper.wrapString("abc\nabcdefabcdef", 6, null)); + + validateResult( + new String[]{"abc"}, + StringWrapper.wrapString("abc", 6, new String[3])); + + validateResult( + new String[]{"abc"}, + StringWrapper.wrapString("abc", 6, new String[0])); + + validateResult( + new String[]{"abc"}, + StringWrapper.wrapString("abc", 6, new String[1])); + + validateResult( + new String[]{""}, + StringWrapper.wrapString("", 6, new String[3])); + + validateResult( + new String[]{"abcdef"}, + StringWrapper.wrapString("abcdef", 6, new String[3])); + + validateResult( + new String[]{"abcdef", "abcdef"}, + StringWrapper.wrapString("abcdef\nabcdef", 6, new String[3])); + + validateResult( + new String[]{"abc", "", "def"}, + StringWrapper.wrapString("abc\n\ndef", 6, new String[3])); + + validateResult( + new String[]{"", "abcdef"}, + StringWrapper.wrapString("\nabcdef", 6, new String[3])); + + validateResult( + new String[]{"", "", "abcdef"}, + StringWrapper.wrapString("\n\nabcdef", 6, new String[3])); + + validateResult( + new String[]{"", "", "abcdef"}, + StringWrapper.wrapString("\n\nabcdef", 6, new String[4])); + + validateResult( + new String[]{"", "", "abcdef", ""}, + StringWrapper.wrapString("\n\nabcdef\n\n", 6, new String[4])); + + validateResult( + new String[]{"", "", "abcdef", "a", ""}, + StringWrapper.wrapString("\n\nabcdefa\n\n", 6, new String[4])); + + validateResult( + new String[]{"", "", "abcdef", "a", ""}, + StringWrapper.wrapString("\n\nabcdefa\n\n", 6, new String[0])); + + validateResult( + new String[]{"", "", "abcdef", "a", ""}, + StringWrapper.wrapString("\n\nabcdefa\n\n", 6, new String[5])); + + validateResult( + new String[]{"", "", "a", "b", "c", "d", "e", "f", "a", ""}, + StringWrapper.wrapString("\n\nabcdefa\n\n", 1, new String[5])); + } + + public static void validateResult(String[] expected, String[] actual) { + Assert.assertTrue(actual.length >= expected.length); + + int i; + for (i=0; i