ByteArrayAnnotatedOutput.java revision 83b80f81d311b233188c281059aad4a9f5e8b4e6
1/* 2 * [The "BSD licence"] 3 * Copyright (c) 2009 Ben Gruver 4 * All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions 8 * are met: 9 * 1. Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 2. Redistributions in binary form must reproduce the above copyright 12 * notice, this list of conditions and the following disclaimer in the 13 * documentation and/or other materials provided with the distribution. 14 * 3. The name of the author may not be used to endorse or promote products 15 * derived from this software without specific prior written permission. 16 * 17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 18 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 20 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 21 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 22 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 * INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 */ 28 29package org.jf.dexlib.Util; 30 31import java.io.IOException; 32import java.io.Writer; 33import java.util.ArrayList; 34 35/** 36 * Implementation of {@link AnnotatedOutput} which stores the written data 37 * into a <code>byte[]</code>. 38 * 39 * <p><b>Note:</b> As per the {@link Output} interface, multi-byte 40 * writes all use little-endian order.</p> 41 */ 42public final class ByteArrayAnnotatedOutput 43 implements AnnotatedOutput { 44 /** default size for stretchy instances */ 45 private static final int DEFAULT_SIZE = 1000; 46 47 /** 48 * whether the instance is stretchy, that is, whether its array 49 * may be resized to increase capacity 50 */ 51 private final boolean stretchy; 52 53 /** non-null; the data itself */ 54 private byte[] data; 55 56 /** >= 0; current output cursor */ 57 private int cursor; 58 59 /** whether annotations are to be verbose */ 60 private boolean verbose; 61 62 /** 63 * null-ok; list of annotations, or <code>null</code> if this instance 64 * isn't keeping them 65 */ 66 private ArrayList<Annotation> annotations; 67 68 /** >= 40 (if used); the desired maximum annotation width */ 69 private int annotationWidth; 70 71 /** 72 * >= 8 (if used); the number of bytes of hex output to use 73 * in annotations 74 */ 75 private int hexCols; 76 77 private int currentIndent = 0; 78 private int indentAmount = 2; 79 80 /** 81 * Constructs an instance with a fixed maximum size. Note that the 82 * given array is the only one that will be used to store data. In 83 * particular, no reallocation will occur in order to expand the 84 * capacity of the resulting instance. Also, the constructed 85 * instance does not keep annotations by default. 86 * 87 * @param data non-null; data array to use for output 88 */ 89 public ByteArrayAnnotatedOutput(byte[] data) { 90 this(data, false); 91 } 92 93 /** 94 * Constructs a "stretchy" instance. The underlying array may be 95 * reallocated. The constructed instance does not keep annotations 96 * by default. 97 */ 98 public ByteArrayAnnotatedOutput() { 99 this(new byte[DEFAULT_SIZE], true); 100 } 101 102 /** 103 * Internal constructor. 104 * 105 * @param data non-null; data array to use for output 106 * @param stretchy whether the instance is to be stretchy 107 */ 108 private ByteArrayAnnotatedOutput(byte[] data, boolean stretchy) { 109 if (data == null) { 110 throw new NullPointerException("data == null"); 111 } 112 113 this.stretchy = stretchy; 114 this.data = data; 115 this.cursor = 0; 116 this.verbose = false; 117 this.annotations = null; 118 this.annotationWidth = 0; 119 this.hexCols = 0; 120 } 121 122 /** 123 * Gets the underlying <code>byte[]</code> of this instance, which 124 * may be larger than the number of bytes written 125 * 126 * @see #toByteArray 127 * 128 * @return non-null; the <code>byte[]</code> 129 */ 130 public byte[] getArray() { 131 return data; 132 } 133 134 /** 135 * Constructs and returns a new <code>byte[]</code> that contains 136 * the written contents exactly (that is, with no extra unwritten 137 * bytes at the end). 138 * 139 * @see #getArray 140 * 141 * @return non-null; an appropriately-constructed array 142 */ 143 public byte[] toByteArray() { 144 byte[] result = new byte[cursor]; 145 System.arraycopy(data, 0, result, 0, cursor); 146 return result; 147 } 148 149 /** {@inheritDoc} */ 150 public int getCursor() { 151 return cursor; 152 } 153 154 /** {@inheritDoc} */ 155 public void assertCursor(int expectedCursor) { 156 if (cursor != expectedCursor) { 157 throw new ExceptionWithContext("expected cursor " + 158 expectedCursor + "; actual value: " + cursor); 159 } 160 } 161 162 /** {@inheritDoc} */ 163 public void writeByte(int value) { 164 int writeAt = cursor; 165 int end = writeAt + 1; 166 167 if (stretchy) { 168 ensureCapacity(end); 169 } else if (end > data.length) { 170 throwBounds(); 171 return; 172 } 173 174 data[writeAt] = (byte) value; 175 cursor = end; 176 } 177 178 /** {@inheritDoc} */ 179 public void writeShort(int value) { 180 int writeAt = cursor; 181 int end = writeAt + 2; 182 183 if (stretchy) { 184 ensureCapacity(end); 185 } else if (end > data.length) { 186 throwBounds(); 187 return; 188 } 189 190 data[writeAt] = (byte) value; 191 data[writeAt + 1] = (byte) (value >> 8); 192 cursor = end; 193 } 194 195 /** {@inheritDoc} */ 196 public void writeInt(int value) { 197 int writeAt = cursor; 198 int end = writeAt + 4; 199 200 if (stretchy) { 201 ensureCapacity(end); 202 } else if (end > data.length) { 203 throwBounds(); 204 return; 205 } 206 207 data[writeAt] = (byte) value; 208 data[writeAt + 1] = (byte) (value >> 8); 209 data[writeAt + 2] = (byte) (value >> 16); 210 data[writeAt + 3] = (byte) (value >> 24); 211 cursor = end; 212 } 213 214 /** {@inheritDoc} */ 215 public void writeLong(long value) { 216 int writeAt = cursor; 217 int end = writeAt + 8; 218 219 if (stretchy) { 220 ensureCapacity(end); 221 } else if (end > data.length) { 222 throwBounds(); 223 return; 224 } 225 226 int half = (int) value; 227 data[writeAt] = (byte) half; 228 data[writeAt + 1] = (byte) (half >> 8); 229 data[writeAt + 2] = (byte) (half >> 16); 230 data[writeAt + 3] = (byte) (half >> 24); 231 232 half = (int) (value >> 32); 233 data[writeAt + 4] = (byte) half; 234 data[writeAt + 5] = (byte) (half >> 8); 235 data[writeAt + 6] = (byte) (half >> 16); 236 data[writeAt + 7] = (byte) (half >> 24); 237 238 cursor = end; 239 } 240 241 /** {@inheritDoc} */ 242 public int writeUnsignedLeb128(int value) { 243 long remaining = (value & 0xFFFFFFFFL) >> 7; 244 long lValue = value; 245 int count = 0; 246 247 while (remaining != 0) { 248 writeByte((int)(lValue & 0x7f) | 0x80); 249 lValue = remaining; 250 remaining >>= 7; 251 count++; 252 } 253 254 writeByte((int)(lValue & 0x7f)); 255 return count + 1; 256 } 257 258 /** {@inheritDoc} */ 259 public int writeSignedLeb128(int value) { 260 int remaining = value >> 7; 261 int count = 0; 262 boolean hasMore = true; 263 int end = ((value & Integer.MIN_VALUE) == 0) ? 0 : -1; 264 265 while (hasMore) { 266 hasMore = (remaining != end) 267 || ((remaining & 1) != ((value >> 6) & 1)); 268 269 writeByte((value & 0x7f) | (hasMore ? 0x80 : 0)); 270 value = remaining; 271 remaining >>= 7; 272 count++; 273 } 274 275 return count; 276 } 277 278 /** {@inheritDoc} */ 279 public void write(ByteArray bytes) { 280 int blen = bytes.size(); 281 int writeAt = cursor; 282 int end = writeAt + blen; 283 284 if (stretchy) { 285 ensureCapacity(end); 286 } else if (end > data.length) { 287 throwBounds(); 288 return; 289 } 290 291 bytes.getBytes(data, writeAt); 292 cursor = end; 293 } 294 295 /** {@inheritDoc} */ 296 public void write(byte[] bytes, int offset, int length) { 297 int writeAt = cursor; 298 int end = writeAt + length; 299 int bytesEnd = offset + length; 300 301 // twos-complement math trick: ((x < 0) || (y < 0)) <=> ((x|y) < 0) 302 if (((offset | length | end) < 0) || (bytesEnd > bytes.length)) { 303 throw new IndexOutOfBoundsException("bytes.length " + 304 bytes.length + "; " + 305 offset + "..!" + end); 306 } 307 308 if (stretchy) { 309 ensureCapacity(end); 310 } else if (end > data.length) { 311 throwBounds(); 312 return; 313 } 314 315 System.arraycopy(bytes, offset, data, writeAt, length); 316 cursor = end; 317 } 318 319 /** {@inheritDoc} */ 320 public void write(byte[] bytes) { 321 write(bytes, 0, bytes.length); 322 } 323 324 /** {@inheritDoc} */ 325 public void writeZeroes(int count) { 326 if (count < 0) { 327 throw new IllegalArgumentException("count < 0"); 328 } 329 330 int end = cursor + count; 331 332 if (stretchy) { 333 ensureCapacity(end); 334 } else if (end > data.length) { 335 throwBounds(); 336 return; 337 } 338 339 /* 340 * There is no need to actually write zeroes, since the array is 341 * already preinitialized with zeroes. 342 */ 343 344 cursor = end; 345 } 346 347 /** {@inheritDoc} */ 348 public void alignTo(int alignment) { 349 int mask = alignment - 1; 350 351 if ((alignment < 0) || ((mask & alignment) != 0)) { 352 throw new IllegalArgumentException("bogus alignment"); 353 } 354 355 int end = (cursor + mask) & ~mask; 356 357 if (stretchy) { 358 ensureCapacity(end); 359 } else if (end > data.length) { 360 throwBounds(); 361 return; 362 } 363 364 /* 365 * There is no need to actually write zeroes, since the array is 366 * already preinitialized with zeroes. 367 */ 368 369 cursor = end; 370 } 371 372 /** {@inheritDoc} */ 373 public boolean annotates() { 374 return (annotations != null); 375 } 376 377 /** {@inheritDoc} */ 378 public boolean isVerbose() { 379 return verbose; 380 } 381 382 /** {@inheritDoc} */ 383 public void annotate(String msg) { 384 if (annotations == null) { 385 return; 386 } 387 388 endAnnotation(); 389 annotations.add(new Annotation(cursor, msg, currentIndent)); 390 } 391 392 public void indent() { 393 currentIndent++; 394 } 395 396 public void deindent() { 397 currentIndent--; 398 if (currentIndent < 0) { 399 currentIndent = 0; 400 } 401 } 402 403 public void setIndentAmount(int indentAmount) { 404 this.indentAmount = indentAmount; 405 } 406 407 /** {@inheritDoc} */ 408 public void annotate(int amt, String msg) { 409 if (annotations == null) { 410 return; 411 } 412 413 endAnnotation(); 414 415 int asz = annotations.size(); 416 int lastEnd = (asz == 0) ? 0 : annotations.get(asz - 1).getEnd(); 417 int startAt; 418 419 if (lastEnd <= cursor) { 420 startAt = cursor; 421 } else { 422 startAt = lastEnd; 423 } 424 425 annotations.add(new Annotation(startAt, startAt + amt, msg, currentIndent)); 426 } 427 428 /** {@inheritDoc} */ 429 public void endAnnotation() { 430 if (annotations == null) { 431 return; 432 } 433 434 int sz = annotations.size(); 435 436 if (sz != 0) { 437 annotations.get(sz - 1).setEndIfUnset(cursor); 438 } 439 } 440 441 /** {@inheritDoc} */ 442 public int getAnnotationWidth() { 443 int leftWidth = 8 + (hexCols * 2) + (hexCols / 2); 444 445 return annotationWidth - leftWidth; 446 } 447 448 /** 449 * Indicates that this instance should keep annotations. This method may 450 * be called only once per instance, and only before any data has been 451 * written to the it. 452 * 453 * @param annotationWidth >= 40; the desired maximum annotation width 454 * @param verbose whether or not to indicate verbose annotations 455 */ 456 public void enableAnnotations(int annotationWidth, boolean verbose) { 457 if ((annotations != null) || (cursor != 0)) { 458 throw new RuntimeException("cannot enable annotations"); 459 } 460 461 if (annotationWidth < 40) { 462 throw new IllegalArgumentException("annotationWidth < 40"); 463 } 464 465 int hexCols = (((annotationWidth - 7) / 15) + 1) & ~1; 466 if (hexCols < 6) { 467 hexCols = 6; 468 } else if (hexCols > 10) { 469 hexCols = 10; 470 } 471 472 this.annotations = new ArrayList<Annotation>(1000); 473 this.annotationWidth = annotationWidth; 474 this.hexCols = hexCols; 475 this.verbose = verbose; 476 } 477 478 /** 479 * Finishes up annotation processing. This closes off any open 480 * annotations and removes annotations that don't refer to written 481 * data. 482 */ 483 public void finishAnnotating() { 484 // Close off the final annotation, if any. 485 endAnnotation(); 486 487 // Remove annotations that refer to unwritten data. 488 if (annotations != null) { 489 int asz = annotations.size(); 490 while (asz > 0) { 491 Annotation last = annotations.get(asz - 1); 492 if (last.getStart() > cursor) { 493 annotations.remove(asz - 1); 494 asz--; 495 } else if (last.getEnd() > cursor) { 496 last.setEnd(cursor); 497 break; 498 } else { 499 break; 500 } 501 } 502 } 503 } 504 505 /** 506 * Writes the annotated content of this instance to the given writer. 507 * 508 * @param out non-null; where to write to 509 */ 510 public void writeAnnotationsTo(Writer out) throws IOException { 511 int width2 = getAnnotationWidth(); 512 int width1 = annotationWidth - width2 - 1; 513 514 StringBuilder padding = new StringBuilder(); 515 for (int i=0; i<1000; i++) { 516 padding.append(' '); 517 } 518 519 TwoColumnOutput twoc = new TwoColumnOutput(out, width1, width2, "|"); 520 Writer left = twoc.getLeft(); 521 Writer right = twoc.getRight(); 522 int leftAt = 0; // left-hand byte output cursor 523 int rightAt = 0; // right-hand annotation index 524 int rightSz = annotations.size(); 525 526 while ((leftAt < cursor) && (rightAt < rightSz)) { 527 Annotation a = annotations.get(rightAt); 528 int start = a.getStart(); 529 int end; 530 String text; 531 532 if (leftAt < start) { 533 // This is an area with no annotation. 534 end = start; 535 start = leftAt; 536 text = ""; 537 } else { 538 // This is an area with an annotation. 539 end = a.getEnd(); 540 text = padding.substring(0, a.getIndent() * this.indentAmount) + a.getText(); 541 rightAt++; 542 } 543 544 left.write(Hex.dump(data, start, end - start, start, hexCols, 6)); 545 right.write(text); 546 twoc.flush(); 547 leftAt = end; 548 } 549 550 if (leftAt < cursor) { 551 // There is unannotated output at the end. 552 left.write(Hex.dump(data, leftAt, cursor - leftAt, leftAt, 553 hexCols, 6)); 554 } 555 556 while (rightAt < rightSz) { 557 // There are zero-byte annotations at the end. 558 right.write(annotations.get(rightAt).getText()); 559 rightAt++; 560 } 561 562 twoc.flush(); 563 } 564 565 /** 566 * Throws the excpetion for when an attempt is made to write past the 567 * end of the instance. 568 */ 569 private static void throwBounds() { 570 throw new IndexOutOfBoundsException("attempt to write past the end"); 571 } 572 573 /** 574 * Reallocates the underlying array if necessary. Calls to this method 575 * should be guarded by a test of {@link #stretchy}. 576 * 577 * @param desiredSize >= 0; the desired minimum total size of the array 578 */ 579 private void ensureCapacity(int desiredSize) { 580 if (data.length < desiredSize) { 581 byte[] newData = new byte[desiredSize * 2 + 1000]; 582 System.arraycopy(data, 0, newData, 0, cursor); 583 data = newData; 584 } 585 } 586 587 /** 588 * Annotation on output. 589 */ 590 private static class Annotation { 591 /** >= 0; start of annotated range (inclusive) */ 592 private final int start; 593 594 /** 595 * >= 0; end of annotated range (exclusive); 596 * <code>Integer.MAX_VALUE</code> if unclosed 597 */ 598 private int end; 599 600 /** non-null; annotation text */ 601 private final String text; 602 603 private int indent; 604 605 /** 606 * Constructs an instance. 607 * 608 * @param start >= 0; start of annotated range 609 * @param end >= start; end of annotated range (exclusive) or 610 * <code>Integer.MAX_VALUE</code> if unclosed 611 * @param text non-null; annotation text 612 */ 613 public Annotation(int start, int end, String text, int indent) { 614 this.start = start; 615 this.end = end; 616 this.text = text; 617 this.indent = indent; 618 } 619 620 /** 621 * Constructs an instance. It is initally unclosed. 622 * 623 * @param start >= 0; start of annotated range 624 * @param text non-null; annotation text 625 */ 626 public Annotation(int start, String text, int indent) { 627 this(start, Integer.MAX_VALUE, text, indent); 628 } 629 630 /** 631 * Sets the end as given, but only if the instance is unclosed; 632 * otherwise, do nothing. 633 * 634 * @param end >= start; the end 635 */ 636 public void setEndIfUnset(int end) { 637 if (this.end == Integer.MAX_VALUE) { 638 this.end = end; 639 } 640 } 641 642 /** 643 * Sets the end as given. 644 * 645 * @param end >= start; the end 646 */ 647 public void setEnd(int end) { 648 this.end = end; 649 } 650 651 /** 652 * Gets the start. 653 * 654 * @return the start 655 */ 656 public int getStart() { 657 return start; 658 } 659 660 /** 661 * Gets the end. 662 * 663 * @return the end 664 */ 665 public int getEnd() { 666 return end; 667 } 668 669 /** 670 * Gets the text. 671 * 672 * @return non-null; the text 673 */ 674 public String getText() { 675 return text; 676 } 677 678 public int getIndent() { 679 return indent; 680 } 681 } 682} 683