1 /* 2 * Copyright (c) 2010, 2017, 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 26 package jdk.nashorn.internal.runtime; 27 28 import java.io.ByteArrayOutputStream; 29 import java.io.File; 30 import java.io.FileNotFoundException; 31 import java.io.FileOutputStream; 32 import java.io.IOError; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.PrintWriter; 36 import java.io.Reader; 37 import java.lang.ref.WeakReference; 38 import java.net.MalformedURLException; 39 import java.net.URI; 40 import java.net.URISyntaxException; 41 import java.net.URL; 42 import java.net.URLConnection; 43 import java.nio.charset.Charset; 44 import java.nio.charset.StandardCharsets; 45 import java.nio.file.Files; 46 import java.nio.file.Path; 47 import java.nio.file.Paths; 48 import java.security.MessageDigest; 49 import java.security.NoSuchAlgorithmException; 50 import java.time.LocalDateTime; 51 import java.util.Arrays; 52 import java.util.Base64; 53 import java.util.Objects; 54 import java.util.WeakHashMap; 55 import jdk.nashorn.api.scripting.URLReader; 56 import jdk.nashorn.internal.parser.Token; 57 import jdk.nashorn.internal.runtime.logging.DebugLogger; 58 import jdk.nashorn.internal.runtime.logging.Loggable; 59 import jdk.nashorn.internal.runtime.logging.Logger; 60 /** 61 * Source objects track the origin of JavaScript entities. 62 */ 63 @Logger(name="source") 64 public 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 @SuppressWarnings("try") 340 protected void loadMeta() throws IOException { 341 if (length == 0 && lastModified == 0) { 342 final URLConnection c = url.openConnection(); 343 try (InputStream in = c.getInputStream()) { 344 length = c.getContentLength(); 345 lastModified = c.getLastModified(); 346 debug("loaded metadata for ", url); 347 } 348 } 349 } 350 } 351 352 private static class FileData extends URLData { 353 private final File file; 354 355 private FileData(final File file, final Charset cs) { 356 super(getURLFromFile(file), cs); 357 this.file = file; 358 359 } 360 361 @Override 362 protected void checkPermissionAndClose() throws IOException { 363 if (!file.canRead()) { 364 throw new FileNotFoundException(file + " (Permission Denied)"); 365 } 366 debug("permission checked for ", file); 367 } 368 369 @Override 370 protected void loadMeta() { 371 if (length == 0 && lastModified == 0) { 372 length = (int) file.length(); 373 lastModified = file.lastModified(); 374 debug("loaded metadata for ", file); 375 } 376 } 377 378 @Override 379 protected void load() throws IOException { 380 if (array == null) { 381 array = cs == null ? readFully(file) : readFully(file, cs); 382 length = array.length; 383 lastModified = file.lastModified(); 384 debug("loaded content for ", file); 385 } 386 } 387 } 388 389 private static void debug(final Object... msg) { 390 final DebugLogger logger = getLoggerStatic(); 391 if (logger != null) { 392 logger.info(msg); 393 } 394 } 395 396 private char[] data() { 397 return data.array(); 398 } 399 400 /** 401 * Returns a Source instance 402 * 403 * @param name source name 404 * @param content contents as char array 405 * @param isEval does this represent code from 'eval' call? 406 * @return source instance 407 */ 408 public static Source sourceFor(final String name, final char[] content, final boolean isEval) { 409 return new Source(name, baseName(name), new RawData(content, isEval)); 410 } 411 412 /** 413 * Returns a Source instance 414 * 415 * @param name source name 416 * @param content contents as char array 417 * 418 * @return source instance 419 */ 420 public static Source sourceFor(final String name, final char[] content) { 421 return sourceFor(name, content, false); 422 } 423 424 /** 425 * Returns a Source instance 426 * 427 * @param name source name 428 * @param content contents as string 429 * @param isEval does this represent code from 'eval' call? 430 * @return source instance 431 */ 432 public static Source sourceFor(final String name, final String content, final boolean isEval) { 433 return new Source(name, baseName(name), new RawData(content, isEval)); 434 } 435 436 /** 437 * Returns a Source instance 438 * 439 * @param name source name 440 * @param content contents as string 441 * @return source instance 442 */ 443 public static Source sourceFor(final String name, final String content) { 444 return sourceFor(name, content, false); 445 } 446 447 /** 448 * Constructor 449 * 450 * @param name source name 451 * @param url url from which source can be loaded 452 * 453 * @return source instance 454 * 455 * @throws IOException if source cannot be loaded 456 */ 457 public static Source sourceFor(final String name, final URL url) throws IOException { 458 return sourceFor(name, url, null); 459 } 460 461 /** 462 * Constructor 463 * 464 * @param name source name 465 * @param url url from which source can be loaded 466 * @param cs Charset used to convert bytes to chars 467 * 468 * @return source instance 469 * 470 * @throws IOException if source cannot be loaded 471 */ 472 public static Source sourceFor(final String name, final URL url, final Charset cs) throws IOException { 473 return sourceFor(name, baseURL(url), new URLData(url, cs)); 474 } 475 476 /** 477 * Constructor 478 * 479 * @param name source name 480 * @param file file from which source can be loaded 481 * 482 * @return source instance 483 * 484 * @throws IOException if source cannot be loaded 485 */ 486 public static Source sourceFor(final String name, final File file) throws IOException { 487 return sourceFor(name, file, null); 488 } 489 490 /** 491 * Constructor 492 * 493 * @param name source name 494 * @param path path from which source can be loaded 495 * 496 * @return source instance 497 * 498 * @throws IOException if source cannot be loaded 499 */ 500 public static Source sourceFor(final String name, final Path path) throws IOException { 501 File file = null; 502 try { 503 file = path.toFile(); 504 } catch (final UnsupportedOperationException uoe) { 505 } 506 507 if (file != null) { 508 return sourceFor(name, file); 509 } else { 510 return sourceFor(name, Files.newBufferedReader(path)); 511 } 512 } 513 514 /** 515 * Constructor 516 * 517 * @param name source name 518 * @param file file from which source can be loaded 519 * @param cs Charset used to convert bytes to chars 520 * 521 * @return source instance 522 * 523 * @throws IOException if source cannot be loaded 524 */ 525 public static Source sourceFor(final String name, final File file, final Charset cs) throws IOException { 526 final File absFile = file.getAbsoluteFile(); 527 return sourceFor(name, dirName(absFile, null), new FileData(file, cs)); 528 } 529 530 /** 531 * Returns an instance 532 * 533 * @param name source name 534 * @param reader reader from which source can be loaded 535 * 536 * @return source instance 537 * 538 * @throws IOException if source cannot be loaded 539 */ 540 public static Source sourceFor(final String name, final Reader reader) throws IOException { 541 // Extract URL from URLReader to defer loading and reuse cached data if available. 542 if (reader instanceof URLReader) { 543 final URLReader urlReader = (URLReader) reader; 544 return sourceFor(name, urlReader.getURL(), urlReader.getCharset()); 545 } 546 return new Source(name, baseName(name), new RawData(reader)); 547 } 548 549 @Override 550 public boolean equals(final Object obj) { 551 if (this == obj) { 552 return true; 553 } 554 if (!(obj instanceof Source)) { 555 return false; 556 } 557 final Source other = (Source) obj; 558 return Objects.equals(name, other.name) && data.equals(other.data); 559 } 560 561 @Override 562 public int hashCode() { 563 int h = hash; 564 if (h == 0) { 565 h = hash = data.hashCode() ^ Objects.hashCode(name); 566 } 567 return h; 568 } 569 570 /** 571 * Fetch source content. 572 * @return Source content. 573 */ 574 public String getString() { 575 return data.toString(); 576 } 577 578 /** 579 * Get the user supplied name of this script. 580 * @return User supplied source name. 581 */ 582 public String getName() { 583 return name; 584 } 585 586 /** 587 * Get the last modified time of this script. 588 * @return Last modified time. 589 */ 590 public long getLastModified() { 591 return data.lastModified(); 592 } 593 594 /** 595 * Get the "directory" part of the file or "base" of the URL. 596 * @return base of file or URL. 597 */ 598 public String getBase() { 599 return base; 600 } 601 602 /** 603 * Fetch a portion of source content. 604 * @param start start index in source 605 * @param len length of portion 606 * @return Source content portion. 607 */ 608 public String getString(final int start, final int len) { 609 return new String(data(), start, len); 610 } 611 612 /** 613 * Fetch a portion of source content associated with a token. 614 * @param token Token descriptor. 615 * @return Source content portion. 616 */ 617 public String getString(final long token) { 618 final int start = Token.descPosition(token); 619 final int len = Token.descLength(token); 620 return new String(data(), start, len); 621 } 622 623 /** 624 * Returns the source URL of this script Source. Can be null if Source 625 * was created from a String or a char[]. 626 * 627 * @return URL source or null 628 */ 629 public URL getURL() { 630 return data.url(); 631 } 632 633 /** 634 * Get explicit source URL. 635 * @return URL set via sourceURL directive 636 */ 637 public String getExplicitURL() { 638 return explicitURL; 639 } 640 641 /** 642 * Set explicit source URL. 643 * @param explicitURL URL set via sourceURL directive 644 */ 645 public void setExplicitURL(final String explicitURL) { 646 this.explicitURL = explicitURL; 647 } 648 649 /** 650 * Returns whether this source was submitted via 'eval' call or not. 651 * 652 * @return true if this source represents code submitted via 'eval' 653 */ 654 public boolean isEvalCode() { 655 return data.isEvalCode(); 656 } 657 658 /** 659 * Find the beginning of the line containing position. 660 * @param position Index to offending token. 661 * @return Index of first character of line. 662 */ 663 private int findBOLN(final int position) { 664 final char[] d = data(); 665 for (int i = position - 1; i > 0; i--) { 666 final char ch = d[i]; 667 668 if (ch == '\n' || ch == '\r') { 669 return i + 1; 670 } 671 } 672 673 return 0; 674 } 675 676 /** 677 * Find the end of the line containing position. 678 * @param position Index to offending token. 679 * @return Index of last character of line. 680 */ 681 private int findEOLN(final int position) { 682 final char[] d = data(); 683 final int length = d.length; 684 for (int i = position; i < length; i++) { 685 final char ch = d[i]; 686 687 if (ch == '\n' || ch == '\r') { 688 return i - 1; 689 } 690 } 691 692 return length - 1; 693 } 694 695 /** 696 * Return line number of character position. 697 * 698 * <p>This method can be expensive for large sources as it iterates through 699 * all characters up to {@code position}.</p> 700 * 701 * @param position Position of character in source content. 702 * @return Line number. 703 */ 704 public int getLine(final int position) { 705 final char[] d = data(); 706 // Line count starts at 1. 707 int line = 1; 708 709 for (int i = 0; i < position; i++) { 710 final char ch = d[i]; 711 // Works for both \n and \r\n. 712 if (ch == '\n') { 713 line++; 714 } 715 } 716 717 return line; 718 } 719 720 /** 721 * Return column number of character position. 722 * @param position Position of character in source content. 723 * @return Column number. 724 */ 725 public int getColumn(final int position) { 726 // TODO - column needs to account for tabs. 727 return position - findBOLN(position); 728 } 729 730 /** 731 * Return line text including character position. 732 * @param position Position of character in source content. 733 * @return Line text. 734 */ 735 public String getSourceLine(final int position) { 736 // Find end of previous line. 737 final int first = findBOLN(position); 738 // Find end of this line. 739 final int last = findEOLN(position); 740 741 return new String(data(), first, last - first + 1); 742 } 743 744 /** 745 * Get the content of this source as a char array. Note that the underlying array is returned instead of a 746 * clone; modifying the char array will cause modification to the source; this should not be done. While 747 * there is an apparent danger that we allow unfettered access to an underlying mutable array, the 748 * {@code Source} class is in a restricted {@code jdk.nashorn.internal.*} package and as such it is 749 * inaccessible by external actors in an environment with a security manager. Returning a clone would be 750 * detrimental to performance. 751 * @return content the content of this source as a char array 752 */ 753 public char[] getContent() { 754 return data(); 755 } 756 757 /** 758 * Get the length in chars for this source 759 * @return length 760 */ 761 public int getLength() { 762 return data.length(); 763 } 764 765 /** 766 * Read all of the source until end of file. Return it as char array 767 * 768 * @param reader reader opened to source stream 769 * @return source as content 770 * @throws IOException if source could not be read 771 */ 772 public static char[] readFully(final Reader reader) throws IOException { 773 final char[] arr = new char[BUF_SIZE]; 774 final StringBuilder sb = new StringBuilder(); 775 776 try { 777 int numChars; 778 while ((numChars = reader.read(arr, 0, arr.length)) > 0) { 779 sb.append(arr, 0, numChars); 780 } 781 } finally { 782 reader.close(); 783 } 784 785 return sb.toString().toCharArray(); 786 } 787 788 /** 789 * Read all of the source until end of file. Return it as char array 790 * 791 * @param file source file 792 * @return source as content 793 * @throws IOException if source could not be read 794 */ 795 public static char[] readFully(final File file) throws IOException { 796 if (!file.isFile()) { 797 throw new IOException(file + " is not a file"); //TODO localize? 798 } 799 return byteToCharArray(Files.readAllBytes(file.toPath())); 800 } 801 802 /** 803 * Read all of the source until end of file. Return it as char array 804 * 805 * @param file source file 806 * @param cs Charset used to convert bytes to chars 807 * @return source as content 808 * @throws IOException if source could not be read 809 */ 810 public static char[] readFully(final File file, final Charset cs) throws IOException { 811 if (!file.isFile()) { 812 throw new IOException(file + " is not a file"); //TODO localize? 813 } 814 815 final byte[] buf = Files.readAllBytes(file.toPath()); 816 return (cs != null) ? new String(buf, cs).toCharArray() : byteToCharArray(buf); 817 } 818 819 /** 820 * Read all of the source until end of stream from the given URL. Return it as char array 821 * 822 * @param url URL to read content from 823 * @return source as content 824 * @throws IOException if source could not be read 825 */ 826 public static char[] readFully(final URL url) throws IOException { 827 return readFully(url.openStream()); 828 } 829 830 /** 831 * Read all of the source until end of file. Return it as char array 832 * 833 * @param url URL to read content from 834 * @param cs Charset used to convert bytes to chars 835 * @return source as content 836 * @throws IOException if source could not be read 837 */ 838 public static char[] readFully(final URL url, final Charset cs) throws IOException { 839 return readFully(url.openStream(), cs); 840 } 841 842 /** 843 * Get a Base64-encoded SHA1 digest for this source. 844 * 845 * @return a Base64-encoded SHA1 digest for this source 846 */ 847 public String getDigest() { 848 return new String(getDigestBytes(), StandardCharsets.US_ASCII); 849 } 850 851 private byte[] getDigestBytes() { 852 byte[] ldigest = digest; 853 if (ldigest == null) { 854 final char[] content = data(); 855 final byte[] bytes = new byte[content.length * 2]; 856 857 for (int i = 0; i < content.length; i++) { 858 bytes[i * 2] = (byte) (content[i] & 0x00ff); 859 bytes[i * 2 + 1] = (byte) ((content[i] & 0xff00) >> 8); 860 } 861 862 try { 863 final MessageDigest md = MessageDigest.getInstance("SHA-1"); 864 if (name != null) { 865 md.update(name.getBytes(StandardCharsets.UTF_8)); 866 } 867 if (base != null) { 868 md.update(base.getBytes(StandardCharsets.UTF_8)); 869 } 870 if (getURL() != null) { 871 md.update(getURL().toString().getBytes(StandardCharsets.UTF_8)); 872 } 873 digest = ldigest = BASE64.encode(md.digest(bytes)); 874 } catch (final NoSuchAlgorithmException e) { 875 throw new RuntimeException(e); 876 } 877 } 878 return ldigest; 879 } 880 881 /** 882 * Returns the base directory or URL for the given URL. Used to implement __DIR__. 883 * @param url a URL 884 * @return base path or URL, or null if argument is not a hierarchical URL 885 */ 886 public static String baseURL(final URL url) { 887 try { 888 final URI uri = url.toURI(); 889 890 if (uri.getScheme().equals("file")) { 891 final Path path = Paths.get(uri); 892 final Path parent = path.getParent(); 893 return (parent != null) ? (parent + File.separator) : null; 894 } 895 if (uri.isOpaque() || uri.getPath() == null || uri.getPath().isEmpty()) { 896 return null; 897 } 898 return uri.resolve("").toString(); 899 900 } catch (final SecurityException | URISyntaxException | IOError e) { 901 return null; 902 } 903 } 904 905 private static String dirName(final File file, final String DEFAULT_BASE_NAME) { 906 final String res = file.getParent(); 907 return (res != null) ? (res + File.separator) : DEFAULT_BASE_NAME; 908 } 909 910 // fake directory like name 911 private static String baseName(final String name) { 912 int idx = name.lastIndexOf('/'); 913 if (idx == -1) { 914 idx = name.lastIndexOf('\\'); 915 } 916 return (idx != -1) ? name.substring(0, idx + 1) : null; 917 } 918 919 private static char[] readFully(final InputStream is, final Charset cs) throws IOException { 920 return (cs != null) ? new String(readBytes(is), cs).toCharArray() : readFully(is); 921 } 922 923 public static char[] readFully(final InputStream is) throws IOException { 924 return byteToCharArray(readBytes(is)); 925 } 926 927 private static char[] byteToCharArray(final byte[] bytes) { 928 Charset cs = StandardCharsets.UTF_8; 929 int start = 0; 930 // BOM detection. 931 if (bytes.length > 1 && bytes[0] == (byte) 0xFE && bytes[1] == (byte) 0xFF) { 932 start = 2; 933 cs = StandardCharsets.UTF_16BE; 934 } else if (bytes.length > 1 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE) { 935 if (bytes.length > 3 && bytes[2] == 0 && bytes[3] == 0) { 936 start = 4; 937 cs = Charset.forName("UTF-32LE"); 938 } else { 939 start = 2; 940 cs = StandardCharsets.UTF_16LE; 941 } 942 } else if (bytes.length > 2 && bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) { 943 start = 3; 944 cs = StandardCharsets.UTF_8; 945 } else if (bytes.length > 3 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == (byte) 0xFE && bytes[3] == (byte) 0xFF) { 946 start = 4; 947 cs = Charset.forName("UTF-32BE"); 948 } 949 950 return new String(bytes, start, bytes.length - start, cs).toCharArray(); 951 } 952 953 static byte[] readBytes(final InputStream is) throws IOException { 954 final byte[] arr = new byte[BUF_SIZE]; 955 try { 956 try (ByteArrayOutputStream buf = new ByteArrayOutputStream()) { 957 int numBytes; 958 while ((numBytes = is.read(arr, 0, arr.length)) > 0) { 959 buf.write(arr, 0, numBytes); 960 } 961 return buf.toByteArray(); 962 } 963 } finally { 964 is.close(); 965 } 966 } 967 968 @Override 969 public String toString() { 970 return getName(); 971 } 972 973 private static URL getURLFromFile(final File file) { 974 try { 975 return file.toURI().toURL(); 976 } catch (final SecurityException | MalformedURLException ignored) { 977 return null; 978 } 979 } 980 981 private static DebugLogger getLoggerStatic() { 982 final Context context = Context.getContextTrustedOrNull(); 983 return context == null ? null : context.getLogger(Source.class); 984 } 985 986 @Override 987 public DebugLogger initLogger(final Context context) { 988 return context.getLogger(this.getClass()); 989 } 990 991 @Override 992 public DebugLogger getLogger() { 993 return initLogger(Context.getContextTrusted()); 994 } 995 996 private File dumpFile(final File dirFile) { 997 final URL u = getURL(); 998 final StringBuilder buf = new StringBuilder(); 999 // make it unique by prefixing current date & time 1000 buf.append(LocalDateTime.now().toString()); 1001 buf.append('_'); 1002 if (u != null) { 1003 // make it a safe file name 1004 buf.append(u.toString() 1005 .replace('/', '_') 1006 .replace('\\', '_')); 1007 } else { 1008 buf.append(getName()); 1009 } 1010 1011 return new File(dirFile, buf.toString()); 1012 } 1013 1014 void dump(final String dir) { 1015 final File dirFile = new File(dir); 1016 final File file = dumpFile(dirFile); 1017 if (!dirFile.exists() && !dirFile.mkdirs()) { 1018 debug("Skipping source dump for " + name); 1019 return; 1020 } 1021 1022 try (final FileOutputStream fos = new FileOutputStream(file)) { 1023 final PrintWriter pw = new PrintWriter(fos); 1024 pw.print(data.toString()); 1025 pw.flush(); 1026 } catch (final IOException ioExp) { 1027 debug("Skipping source dump for " + 1028 name + 1029 ": " + 1030 ECMAErrors.getMessage( 1031 "io.error.cant.write", 1032 dir + 1033 " : " + ioExp.toString())); 1034 } 1035 } 1036 }