Source.java revision 1734:bf76248bbe51
1/* 2 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26package jdk.nashorn.internal.runtime; 27 28import java.io.ByteArrayOutputStream; 29import java.io.File; 30import java.io.FileNotFoundException; 31import java.io.FileOutputStream; 32import java.io.IOError; 33import java.io.IOException; 34import java.io.InputStream; 35import java.io.PrintWriter; 36import java.io.Reader; 37import java.lang.ref.WeakReference; 38import java.net.MalformedURLException; 39import java.net.URI; 40import java.net.URISyntaxException; 41import java.net.URL; 42import java.net.URLConnection; 43import java.nio.charset.Charset; 44import java.nio.charset.StandardCharsets; 45import java.nio.file.Files; 46import java.nio.file.Path; 47import java.nio.file.Paths; 48import java.security.MessageDigest; 49import java.security.NoSuchAlgorithmException; 50import java.time.LocalDateTime; 51import java.util.Arrays; 52import java.util.Base64; 53import java.util.Objects; 54import java.util.WeakHashMap; 55import jdk.nashorn.api.scripting.URLReader; 56import jdk.nashorn.internal.parser.Token; 57import jdk.nashorn.internal.runtime.logging.DebugLogger; 58import jdk.nashorn.internal.runtime.logging.Loggable; 59import jdk.nashorn.internal.runtime.logging.Logger; 60/** 61 * Source objects track the origin of JavaScript entities. 62 */ 63@Logger(name="source") 64public final class Source implements Loggable { 65 private static final int BUF_SIZE = 8 * 1024; 66 private static final Cache CACHE = new Cache(); 67 68 // Message digest to file name encoder 69 private final static Base64.Encoder BASE64 = Base64.getUrlEncoder().withoutPadding(); 70 71 /** 72 * Descriptive name of the source as supplied by the user. Used for error 73 * reporting to the user. For example, SyntaxError will use this to print message. 74 * Used to implement __FILE__. Also used for SourceFile in .class for debugger usage. 75 */ 76 private final String name; 77 78 /** 79 * Base path or URL of this source. Used to implement __DIR__, which can be 80 * used to load scripts relative to the location of the current script. 81 * This will be null when it can't be computed. 82 */ 83 private final String base; 84 85 /** Source content */ 86 private final Data data; 87 88 /** Cached hash code */ 89 private int hash; 90 91 /** Base64-encoded SHA1 digest of this source object */ 92 private volatile byte[] digest; 93 94 /** source URL set via //@ sourceURL or //# sourceURL directive */ 95 private String explicitURL; 96 97 // Do *not* make this public, ever! Trusts the URL and content. 98 private Source(final String name, final String base, final Data data) { 99 this.name = name; 100 this.base = base; 101 this.data = data; 102 } 103 104 private static synchronized Source sourceFor(final String name, final String base, final URLData data) throws IOException { 105 try { 106 final Source newSource = new Source(name, base, data); 107 final Source existingSource = CACHE.get(newSource); 108 if (existingSource != null) { 109 // Force any access errors 110 data.checkPermissionAndClose(); 111 return existingSource; 112 } 113 114 // All sources in cache must be fully loaded 115 data.load(); 116 CACHE.put(newSource, newSource); 117 118 return newSource; 119 } catch (final RuntimeException e) { 120 final Throwable cause = e.getCause(); 121 if (cause instanceof IOException) { 122 throw (IOException) cause; 123 } 124 throw e; 125 } 126 } 127 128 private static class Cache extends WeakHashMap<Source, WeakReference<Source>> { 129 public Source get(final Source key) { 130 final WeakReference<Source> ref = super.get(key); 131 return ref == null ? null : ref.get(); 132 } 133 134 public void put(final Source key, final Source value) { 135 assert !(value.data instanceof RawData); 136 put(key, new WeakReference<>(value)); 137 } 138 } 139 140 /* package-private */ 141 DebuggerSupport.SourceInfo getSourceInfo() { 142 return new DebuggerSupport.SourceInfo(getName(), data.hashCode(), data.url(), data.array()); 143 } 144 145 // Wrapper to manage lazy loading 146 private static interface Data { 147 148 URL url(); 149 150 int length(); 151 152 long lastModified(); 153 154 char[] array(); 155 156 boolean isEvalCode(); 157 } 158 159 private static class RawData implements Data { 160 private final char[] array; 161 private final boolean evalCode; 162 private int hash; 163 164 private RawData(final char[] array, final boolean evalCode) { 165 this.array = Objects.requireNonNull(array); 166 this.evalCode = evalCode; 167 } 168 169 private RawData(final String source, final boolean evalCode) { 170 this.array = Objects.requireNonNull(source).toCharArray(); 171 this.evalCode = evalCode; 172 } 173 174 private RawData(final Reader reader) throws IOException { 175 this(readFully(reader), false); 176 } 177 178 @Override 179 public int hashCode() { 180 int h = hash; 181 if (h == 0) { 182 h = hash = Arrays.hashCode(array) ^ (evalCode? 1 : 0); 183 } 184 return h; 185 } 186 187 @Override 188 public boolean equals(final Object obj) { 189 if (this == obj) { 190 return true; 191 } 192 if (obj instanceof RawData) { 193 final RawData other = (RawData)obj; 194 return Arrays.equals(array, other.array) && evalCode == other.evalCode; 195 } 196 return false; 197 } 198 199 @Override 200 public String toString() { 201 return new String(array()); 202 } 203 204 @Override 205 public URL url() { 206 return null; 207 } 208 209 @Override 210 public int length() { 211 return array.length; 212 } 213 214 @Override 215 public long lastModified() { 216 return 0; 217 } 218 219 @Override 220 public char[] array() { 221 return array; 222 } 223 224 225 @Override 226 public boolean isEvalCode() { 227 return evalCode; 228 } 229 } 230 231 private static class URLData implements Data { 232 private final URL url; 233 protected final Charset cs; 234 private int hash; 235 protected char[] array; 236 protected int length; 237 protected long lastModified; 238 239 private URLData(final URL url, final Charset cs) { 240 this.url = Objects.requireNonNull(url); 241 this.cs = cs; 242 } 243 244 @Override 245 public int hashCode() { 246 int h = hash; 247 if (h == 0) { 248 h = hash = url.hashCode(); 249 } 250 return h; 251 } 252 253 @Override 254 public boolean equals(final Object other) { 255 if (this == other) { 256 return true; 257 } 258 if (!(other instanceof URLData)) { 259 return false; 260 } 261 262 final URLData otherData = (URLData) other; 263 264 if (url.equals(otherData.url)) { 265 // Make sure both have meta data loaded 266 try { 267 if (isDeferred()) { 268 // Data in cache is always loaded, and we only compare to cached data. 269 assert !otherData.isDeferred(); 270 loadMeta(); 271 } else if (otherData.isDeferred()) { 272 otherData.loadMeta(); 273 } 274 } catch (final IOException e) { 275 throw new RuntimeException(e); 276 } 277 278 // Compare meta data 279 return this.length == otherData.length && this.lastModified == otherData.lastModified; 280 } 281 return false; 282 } 283 284 @Override 285 public String toString() { 286 return new String(array()); 287 } 288 289 @Override 290 public URL url() { 291 return url; 292 } 293 294 @Override 295 public int length() { 296 return length; 297 } 298 299 @Override 300 public long lastModified() { 301 return lastModified; 302 } 303 304 @Override 305 public char[] array() { 306 assert !isDeferred(); 307 return array; 308 } 309 310 @Override 311 public boolean isEvalCode() { 312 return false; 313 } 314 315 boolean isDeferred() { 316 return array == null; 317 } 318 319 @SuppressWarnings("try") 320 protected void checkPermissionAndClose() throws IOException { 321 try (InputStream in = url.openStream()) { 322 // empty 323 } 324 debug("permission checked for ", url); 325 } 326 327 protected void load() throws IOException { 328 if (array == null) { 329 final URLConnection c = url.openConnection(); 330 try (InputStream in = c.getInputStream()) { 331 array = cs == null ? readFully(in) : readFully(in, cs); 332 length = array.length; 333 lastModified = c.getLastModified(); 334 debug("loaded content for ", url); 335 } 336 } 337 } 338 339 protected void loadMeta() throws IOException { 340 if (length == 0 && lastModified == 0) { 341 final URLConnection c = url.openConnection(); 342 length = c.getContentLength(); 343 lastModified = c.getLastModified(); 344 debug("loaded metadata for ", url); 345 } 346 } 347 } 348 349 private static class FileData extends URLData { 350 private final File file; 351 352 private FileData(final File file, final Charset cs) { 353 super(getURLFromFile(file), cs); 354 this.file = file; 355 356 } 357 358 @Override 359 protected void checkPermissionAndClose() throws IOException { 360 if (!file.canRead()) { 361 throw new FileNotFoundException(file + " (Permission Denied)"); 362 } 363 debug("permission checked for ", file); 364 } 365 366 @Override 367 protected void loadMeta() { 368 if (length == 0 && lastModified == 0) { 369 length = (int) file.length(); 370 lastModified = file.lastModified(); 371 debug("loaded metadata for ", file); 372 } 373 } 374 375 @Override 376 protected void load() throws IOException { 377 if (array == null) { 378 array = cs == null ? readFully(file) : readFully(file, cs); 379 length = array.length; 380 lastModified = file.lastModified(); 381 debug("loaded content for ", file); 382 } 383 } 384 } 385 386 private static void debug(final Object... msg) { 387 final DebugLogger logger = getLoggerStatic(); 388 if (logger != null) { 389 logger.info(msg); 390 } 391 } 392 393 private char[] data() { 394 return data.array(); 395 } 396 397 /** 398 * Returns a Source instance 399 * 400 * @param name source name 401 * @param content contents as char array 402 * @param isEval does this represent code from 'eval' call? 403 * @return source instance 404 */ 405 public static Source sourceFor(final String name, final char[] content, final boolean isEval) { 406 return new Source(name, baseName(name), new RawData(content, isEval)); 407 } 408 409 /** 410 * Returns a Source instance 411 * 412 * @param name source name 413 * @param content contents as char array 414 * 415 * @return source instance 416 */ 417 public static Source sourceFor(final String name, final char[] content) { 418 return sourceFor(name, content, false); 419 } 420 421 /** 422 * Returns a Source instance 423 * 424 * @param name source name 425 * @param content contents as string 426 * @param isEval does this represent code from 'eval' call? 427 * @return source instance 428 */ 429 public static Source sourceFor(final String name, final String content, final boolean isEval) { 430 return new Source(name, baseName(name), new RawData(content, isEval)); 431 } 432 433 /** 434 * Returns a Source instance 435 * 436 * @param name source name 437 * @param content contents as string 438 * @return source instance 439 */ 440 public static Source sourceFor(final String name, final String content) { 441 return sourceFor(name, content, false); 442 } 443 444 /** 445 * Constructor 446 * 447 * @param name source name 448 * @param url url from which source can be loaded 449 * 450 * @return source instance 451 * 452 * @throws IOException if source cannot be loaded 453 */ 454 public static Source sourceFor(final String name, final URL url) throws IOException { 455 return sourceFor(name, url, null); 456 } 457 458 /** 459 * Constructor 460 * 461 * @param name source name 462 * @param url url from which source can be loaded 463 * @param cs Charset used to convert bytes to chars 464 * 465 * @return source instance 466 * 467 * @throws IOException if source cannot be loaded 468 */ 469 public static Source sourceFor(final String name, final URL url, final Charset cs) throws IOException { 470 return sourceFor(name, baseURL(url), new URLData(url, cs)); 471 } 472 473 /** 474 * Constructor 475 * 476 * @param name source name 477 * @param file file from which source can be loaded 478 * 479 * @return source instance 480 * 481 * @throws IOException if source cannot be loaded 482 */ 483 public static Source sourceFor(final String name, final File file) throws IOException { 484 return sourceFor(name, file, null); 485 } 486 487 /** 488 * Constructor 489 * 490 * @param name source name 491 * @param path path from which source can be loaded 492 * 493 * @return source instance 494 * 495 * @throws IOException if source cannot be loaded 496 */ 497 public static Source sourceFor(final String name, final Path path) throws IOException { 498 File file = null; 499 try { 500 file = path.toFile(); 501 } catch (final UnsupportedOperationException uoe) { 502 } 503 504 if (file != null) { 505 return sourceFor(name, file); 506 } else { 507 return sourceFor(name, Files.newBufferedReader(path)); 508 } 509 } 510 511 /** 512 * Constructor 513 * 514 * @param name source name 515 * @param file file from which source can be loaded 516 * @param cs Charset used to convert bytes to chars 517 * 518 * @return source instance 519 * 520 * @throws IOException if source cannot be loaded 521 */ 522 public static Source sourceFor(final String name, final File file, final Charset cs) throws IOException { 523 final File absFile = file.getAbsoluteFile(); 524 return sourceFor(name, dirName(absFile, null), new FileData(file, cs)); 525 } 526 527 /** 528 * Returns an instance 529 * 530 * @param name source name 531 * @param reader reader from which source can be loaded 532 * 533 * @return source instance 534 * 535 * @throws IOException if source cannot be loaded 536 */ 537 public static Source sourceFor(final String name, final Reader reader) throws IOException { 538 // Extract URL from URLReader to defer loading and reuse cached data if available. 539 if (reader instanceof URLReader) { 540 final URLReader urlReader = (URLReader) reader; 541 return sourceFor(name, urlReader.getURL(), urlReader.getCharset()); 542 } 543 return new Source(name, baseName(name), new RawData(reader)); 544 } 545 546 @Override 547 public boolean equals(final Object obj) { 548 if (this == obj) { 549 return true; 550 } 551 if (!(obj instanceof Source)) { 552 return false; 553 } 554 final Source other = (Source) obj; 555 return Objects.equals(name, other.name) && data.equals(other.data); 556 } 557 558 @Override 559 public int hashCode() { 560 int h = hash; 561 if (h == 0) { 562 h = hash = data.hashCode() ^ Objects.hashCode(name); 563 } 564 return h; 565 } 566 567 /** 568 * Fetch source content. 569 * @return Source content. 570 */ 571 public String getString() { 572 return data.toString(); 573 } 574 575 /** 576 * Get the user supplied name of this script. 577 * @return User supplied source name. 578 */ 579 public String getName() { 580 return name; 581 } 582 583 /** 584 * Get the last modified time of this script. 585 * @return Last modified time. 586 */ 587 public long getLastModified() { 588 return data.lastModified(); 589 } 590 591 /** 592 * Get the "directory" part of the file or "base" of the URL. 593 * @return base of file or URL. 594 */ 595 public String getBase() { 596 return base; 597 } 598 599 /** 600 * Fetch a portion of source content. 601 * @param start start index in source 602 * @param len length of portion 603 * @return Source content portion. 604 */ 605 public String getString(final int start, final int len) { 606 return new String(data(), start, len); 607 } 608 609 /** 610 * Fetch a portion of source content associated with a token. 611 * @param token Token descriptor. 612 * @return Source content portion. 613 */ 614 public String getString(final long token) { 615 final int start = Token.descPosition(token); 616 final int len = Token.descLength(token); 617 return new String(data(), start, len); 618 } 619 620 /** 621 * Returns the source URL of this script Source. Can be null if Source 622 * was created from a String or a char[]. 623 * 624 * @return URL source or null 625 */ 626 public URL getURL() { 627 return data.url(); 628 } 629 630 /** 631 * Get explicit source URL. 632 * @return URL set via sourceURL directive 633 */ 634 public String getExplicitURL() { 635 return explicitURL; 636 } 637 638 /** 639 * Set explicit source URL. 640 * @param explicitURL URL set via sourceURL directive 641 */ 642 public void setExplicitURL(final String explicitURL) { 643 this.explicitURL = explicitURL; 644 } 645 646 /** 647 * Returns whether this source was submitted via 'eval' call or not. 648 * 649 * @return true if this source represents code submitted via 'eval' 650 */ 651 public boolean isEvalCode() { 652 return data.isEvalCode(); 653 } 654 655 /** 656 * Find the beginning of the line containing position. 657 * @param position Index to offending token. 658 * @return Index of first character of line. 659 */ 660 private int findBOLN(final int position) { 661 final char[] d = data(); 662 for (int i = position - 1; i > 0; i--) { 663 final char ch = d[i]; 664 665 if (ch == '\n' || ch == '\r') { 666 return i + 1; 667 } 668 } 669 670 return 0; 671 } 672 673 /** 674 * Find the end of the line containing position. 675 * @param position Index to offending token. 676 * @return Index of last character of line. 677 */ 678 private int findEOLN(final int position) { 679 final char[] d = data(); 680 final int length = d.length; 681 for (int i = position; i < length; i++) { 682 final char ch = d[i]; 683 684 if (ch == '\n' || ch == '\r') { 685 return i - 1; 686 } 687 } 688 689 return length - 1; 690 } 691 692 /** 693 * Return line number of character position. 694 * 695 * <p>This method can be expensive for large sources as it iterates through 696 * all characters up to {@code position}.</p> 697 * 698 * @param position Position of character in source content. 699 * @return Line number. 700 */ 701 public int getLine(final int position) { 702 final char[] d = data(); 703 // Line count starts at 1. 704 int line = 1; 705 706 for (int i = 0; i < position; i++) { 707 final char ch = d[i]; 708 // Works for both \n and \r\n. 709 if (ch == '\n') { 710 line++; 711 } 712 } 713 714 return line; 715 } 716 717 /** 718 * Return column number of character position. 719 * @param position Position of character in source content. 720 * @return Column number. 721 */ 722 public int getColumn(final int position) { 723 // TODO - column needs to account for tabs. 724 return position - findBOLN(position); 725 } 726 727 /** 728 * Return line text including character position. 729 * @param position Position of character in source content. 730 * @return Line text. 731 */ 732 public String getSourceLine(final int position) { 733 // Find end of previous line. 734 final int first = findBOLN(position); 735 // Find end of this line. 736 final int last = findEOLN(position); 737 738 return new String(data(), first, last - first + 1); 739 } 740 741 /** 742 * Get the content of this source as a char array. Note that the underlying array is returned instead of a 743 * clone; modifying the char array will cause modification to the source; this should not be done. While 744 * there is an apparent danger that we allow unfettered access to an underlying mutable array, the 745 * {@code Source} class is in a restricted {@code jdk.nashorn.internal.*} package and as such it is 746 * inaccessible by external actors in an environment with a security manager. Returning a clone would be 747 * detrimental to performance. 748 * @return content the content of this source as a char array 749 */ 750 public char[] getContent() { 751 return data(); 752 } 753 754 /** 755 * Get the length in chars for this source 756 * @return length 757 */ 758 public int getLength() { 759 return data.length(); 760 } 761 762 /** 763 * Read all of the source until end of file. Return it as char array 764 * 765 * @param reader reader opened to source stream 766 * @return source as content 767 * @throws IOException if source could not be read 768 */ 769 public static char[] readFully(final Reader reader) throws IOException { 770 final char[] arr = new char[BUF_SIZE]; 771 final StringBuilder sb = new StringBuilder(); 772 773 try { 774 int numChars; 775 while ((numChars = reader.read(arr, 0, arr.length)) > 0) { 776 sb.append(arr, 0, numChars); 777 } 778 } finally { 779 reader.close(); 780 } 781 782 return sb.toString().toCharArray(); 783 } 784 785 /** 786 * Read all of the source until end of file. Return it as char array 787 * 788 * @param file source file 789 * @return source as content 790 * @throws IOException if source could not be read 791 */ 792 public static char[] readFully(final File file) throws IOException { 793 if (!file.isFile()) { 794 throw new IOException(file + " is not a file"); //TODO localize? 795 } 796 return byteToCharArray(Files.readAllBytes(file.toPath())); 797 } 798 799 /** 800 * Read all of the source until end of file. Return it as char array 801 * 802 * @param file source file 803 * @param cs Charset used to convert bytes to chars 804 * @return source as content 805 * @throws IOException if source could not be read 806 */ 807 public static char[] readFully(final File file, final Charset cs) throws IOException { 808 if (!file.isFile()) { 809 throw new IOException(file + " is not a file"); //TODO localize? 810 } 811 812 final byte[] buf = Files.readAllBytes(file.toPath()); 813 return (cs != null) ? new String(buf, cs).toCharArray() : byteToCharArray(buf); 814 } 815 816 /** 817 * Read all of the source until end of stream from the given URL. Return it as char array 818 * 819 * @param url URL to read content from 820 * @return source as content 821 * @throws IOException if source could not be read 822 */ 823 public static char[] readFully(final URL url) throws IOException { 824 return readFully(url.openStream()); 825 } 826 827 /** 828 * Read all of the source until end of file. Return it as char array 829 * 830 * @param url URL to read content from 831 * @param cs Charset used to convert bytes to chars 832 * @return source as content 833 * @throws IOException if source could not be read 834 */ 835 public static char[] readFully(final URL url, final Charset cs) throws IOException { 836 return readFully(url.openStream(), cs); 837 } 838 839 /** 840 * Get a Base64-encoded SHA1 digest for this source. 841 * 842 * @return a Base64-encoded SHA1 digest for this source 843 */ 844 public String getDigest() { 845 return new String(getDigestBytes(), StandardCharsets.US_ASCII); 846 } 847 848 private byte[] getDigestBytes() { 849 byte[] ldigest = digest; 850 if (ldigest == null) { 851 final char[] content = data(); 852 final byte[] bytes = new byte[content.length * 2]; 853 854 for (int i = 0; i < content.length; i++) { 855 bytes[i * 2] = (byte) (content[i] & 0x00ff); 856 bytes[i * 2 + 1] = (byte) ((content[i] & 0xff00) >> 8); 857 } 858 859 try { 860 final MessageDigest md = MessageDigest.getInstance("SHA-1"); 861 if (name != null) { 862 md.update(name.getBytes(StandardCharsets.UTF_8)); 863 } 864 if (base != null) { 865 md.update(base.getBytes(StandardCharsets.UTF_8)); 866 } 867 if (getURL() != null) { 868 md.update(getURL().toString().getBytes(StandardCharsets.UTF_8)); 869 } 870 digest = ldigest = BASE64.encode(md.digest(bytes)); 871 } catch (final NoSuchAlgorithmException e) { 872 throw new RuntimeException(e); 873 } 874 } 875 return ldigest; 876 } 877 878 /** 879 * Returns the base directory or URL for the given URL. Used to implement __DIR__. 880 * @param url a URL 881 * @return base path or URL, or null if argument is not a hierarchical URL 882 */ 883 public static String baseURL(final URL url) { 884 try { 885 final URI uri = url.toURI(); 886 887 if (uri.getScheme().equals("file")) { 888 final Path path = Paths.get(uri); 889 final Path parent = path.getParent(); 890 return (parent != null) ? (parent + File.separator) : null; 891 } 892 if (uri.isOpaque() || uri.getPath() == null || uri.getPath().isEmpty()) { 893 return null; 894 } 895 return uri.resolve("").toString(); 896 897 } catch (final SecurityException | URISyntaxException | IOError e) { 898 return null; 899 } 900 } 901 902 private static String dirName(final File file, final String DEFAULT_BASE_NAME) { 903 final String res = file.getParent(); 904 return (res != null) ? (res + File.separator) : DEFAULT_BASE_NAME; 905 } 906 907 // fake directory like name 908 private static String baseName(final String name) { 909 int idx = name.lastIndexOf('/'); 910 if (idx == -1) { 911 idx = name.lastIndexOf('\\'); 912 } 913 return (idx != -1) ? name.substring(0, idx + 1) : null; 914 } 915 916 private static char[] readFully(final InputStream is, final Charset cs) throws IOException { 917 return (cs != null) ? new String(readBytes(is), cs).toCharArray() : readFully(is); 918 } 919 920 public static char[] readFully(final InputStream is) throws IOException { 921 return byteToCharArray(readBytes(is)); 922 } 923 924 private static char[] byteToCharArray(final byte[] bytes) { 925 Charset cs = StandardCharsets.UTF_8; 926 int start = 0; 927 // BOM detection. 928 if (bytes.length > 1 && bytes[0] == (byte) 0xFE && bytes[1] == (byte) 0xFF) { 929 start = 2; 930 cs = StandardCharsets.UTF_16BE; 931 } else if (bytes.length > 1 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE) { 932 if (bytes.length > 3 && bytes[2] == 0 && bytes[3] == 0) { 933 start = 4; 934 cs = Charset.forName("UTF-32LE"); 935 } else { 936 start = 2; 937 cs = StandardCharsets.UTF_16LE; 938 } 939 } else if (bytes.length > 2 && bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) { 940 start = 3; 941 cs = StandardCharsets.UTF_8; 942 } else if (bytes.length > 3 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == (byte) 0xFE && bytes[3] == (byte) 0xFF) { 943 start = 4; 944 cs = Charset.forName("UTF-32BE"); 945 } 946 947 return new String(bytes, start, bytes.length - start, cs).toCharArray(); 948 } 949 950 static byte[] readBytes(final InputStream is) throws IOException { 951 final byte[] arr = new byte[BUF_SIZE]; 952 try { 953 try (ByteArrayOutputStream buf = new ByteArrayOutputStream()) { 954 int numBytes; 955 while ((numBytes = is.read(arr, 0, arr.length)) > 0) { 956 buf.write(arr, 0, numBytes); 957 } 958 return buf.toByteArray(); 959 } 960 } finally { 961 is.close(); 962 } 963 } 964 965 @Override 966 public String toString() { 967 return getName(); 968 } 969 970 private static URL getURLFromFile(final File file) { 971 try { 972 return file.toURI().toURL(); 973 } catch (final SecurityException | MalformedURLException ignored) { 974 return null; 975 } 976 } 977 978 private static DebugLogger getLoggerStatic() { 979 final Context context = Context.getContextTrustedOrNull(); 980 return context == null ? null : context.getLogger(Source.class); 981 } 982 983 @Override 984 public DebugLogger initLogger(final Context context) { 985 return context.getLogger(this.getClass()); 986 } 987 988 @Override 989 public DebugLogger getLogger() { 990 return initLogger(Context.getContextTrusted()); 991 } 992 993 private File dumpFile(final File dirFile) { 994 final URL u = getURL(); 995 final StringBuilder buf = new StringBuilder(); 996 // make it unique by prefixing current date & time 997 buf.append(LocalDateTime.now().toString()); 998 buf.append('_'); 999 if (u != null) { 1000 // make it a safe file name 1001 buf.append(u.toString() 1002 .replace('/', '_') 1003 .replace('\\', '_')); 1004 } else { 1005 buf.append(getName()); 1006 } 1007 1008 return new File(dirFile, buf.toString()); 1009 } 1010 1011 void dump(final String dir) { 1012 final File dirFile = new File(dir); 1013 final File file = dumpFile(dirFile); 1014 if (!dirFile.exists() && !dirFile.mkdirs()) { 1015 debug("Skipping source dump for " + name); 1016 return; 1017 } 1018 1019 try (final FileOutputStream fos = new FileOutputStream(file)) { 1020 final PrintWriter pw = new PrintWriter(fos); 1021 pw.print(data.toString()); 1022 pw.flush(); 1023 } catch (final IOException ioExp) { 1024 debug("Skipping source dump for " + 1025 name + 1026 ": " + 1027 ECMAErrors.getMessage( 1028 "io.error.cant.write", 1029 dir + 1030 " : " + ioExp.toString())); 1031 } 1032 } 1033} 1034