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