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 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.URISyntaxException; 40 import java.net.URL; 41 import java.net.URLConnection; 42 import java.nio.charset.Charset; 43 import java.nio.charset.StandardCharsets; 44 import java.nio.file.Files; 45 import java.nio.file.Path; 46 import java.nio.file.Paths; 47 import java.security.MessageDigest; 48 import java.security.NoSuchAlgorithmException; 49 import java.time.LocalDateTime; 50 import java.util.Arrays; 51 import java.util.Base64; 52 import java.util.Objects; 53 import java.util.WeakHashMap; 54 import jdk.nashorn.api.scripting.URLReader; 55 import jdk.nashorn.internal.parser.Token; 56 import jdk.nashorn.internal.runtime.logging.DebugLogger; 57 import jdk.nashorn.internal.runtime.logging.Loggable; 58 import jdk.nashorn.internal.runtime.logging.Logger; 59 /** 60 * Source objects track the origin of JavaScript entities. 61 */ 62 @Logger(name="source") 63 public 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. Source(final String name, final String base, final Data data)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 sourceFor(final String name, final String base, final URLData data)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>> { get(final Source key)128 public Source get(final Source key) { 129 final WeakReference<Source> ref = super.get(key); 130 return ref == null ? null : ref.get(); 131 } 132 put(final Source key, final Source value)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 */ getSourceInfo()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 url()147 URL url(); 148 length()149 int length(); 150 lastModified()151 long lastModified(); 152 array()153 char[] array(); 154 isEvalCode()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 RawData(final char[] array, final boolean evalCode)163 private RawData(final char[] array, final boolean evalCode) { 164 this.array = Objects.requireNonNull(array); 165 this.evalCode = evalCode; 166 } 167 RawData(final String source, final boolean evalCode)168 private RawData(final String source, final boolean evalCode) { 169 this.array = Objects.requireNonNull(source).toCharArray(); 170 this.evalCode = evalCode; 171 } 172 RawData(final Reader reader)173 private RawData(final Reader reader) throws IOException { 174 this(readFully(reader), false); 175 } 176 177 @Override hashCode()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 equals(final Object obj)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 toString()199 public String toString() { 200 return new String(array()); 201 } 202 203 @Override url()204 public URL url() { 205 return null; 206 } 207 208 @Override length()209 public int length() { 210 return array.length; 211 } 212 213 @Override lastModified()214 public long lastModified() { 215 return 0; 216 } 217 218 @Override array()219 public char[] array() { 220 return array; 221 } 222 223 224 @Override isEvalCode()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 URLData(final URL url, final Charset cs)238 private URLData(final URL url, final Charset cs) { 239 this.url = Objects.requireNonNull(url); 240 this.cs = cs; 241 } 242 243 @Override hashCode()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 equals(final Object other)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 toString()284 public String toString() { 285 return new String(array()); 286 } 287 288 @Override url()289 public URL url() { 290 return url; 291 } 292 293 @Override length()294 public int length() { 295 return length; 296 } 297 298 @Override lastModified()299 public long lastModified() { 300 return lastModified; 301 } 302 303 @Override array()304 public char[] array() { 305 assert !isDeferred(); 306 return array; 307 } 308 309 @Override isEvalCode()310 public boolean isEvalCode() { 311 return false; 312 } 313 isDeferred()314 boolean isDeferred() { 315 return array == null; 316 } 317 318 @SuppressWarnings("try") checkPermissionAndClose()319 protected void checkPermissionAndClose() throws IOException { 320 try (InputStream in = url.openStream()) { 321 // empty 322 } 323 debug("permission checked for ", url); 324 } 325 load()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 loadMeta()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 FileData(final File file, final Charset cs)351 private FileData(final File file, final Charset cs) { 352 super(getURLFromFile(file), cs); 353 this.file = file; 354 355 } 356 357 @Override checkPermissionAndClose()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 loadMeta()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 load()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 debug(final Object... msg)385 private static void debug(final Object... msg) { 386 final DebugLogger logger = getLoggerStatic(); 387 if (logger != null) { 388 logger.info(msg); 389 } 390 } 391 data()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 */ sourceFor(final String name, final char[] content, final boolean isEval)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 */ sourceFor(final String name, final char[] content)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 */ sourceFor(final String name, final String content, final boolean isEval)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 */ sourceFor(final String name, final String content)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 */ sourceFor(final String name, final URL url)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 */ sourceFor(final String name, final URL url, final Charset cs)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 */ sourceFor(final String name, final File file)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 file file from which source can be loaded 491 * @param cs Charset used to convert bytes to chars 492 * 493 * @return source instance 494 * 495 * @throws IOException if source cannot be loaded 496 */ sourceFor(final String name, final File file, final Charset cs)497 public static Source sourceFor(final String name, final File file, final Charset cs) throws IOException { 498 final File absFile = file.getAbsoluteFile(); 499 return sourceFor(name, dirName(absFile, null), new FileData(file, cs)); 500 } 501 502 /** 503 * Returns an instance 504 * 505 * @param name source name 506 * @param reader reader from which source can be loaded 507 * 508 * @return source instance 509 * 510 * @throws IOException if source cannot be loaded 511 */ sourceFor(final String name, final Reader reader)512 public static Source sourceFor(final String name, final Reader reader) throws IOException { 513 // Extract URL from URLReader to defer loading and reuse cached data if available. 514 if (reader instanceof URLReader) { 515 final URLReader urlReader = (URLReader) reader; 516 return sourceFor(name, urlReader.getURL(), urlReader.getCharset()); 517 } 518 return new Source(name, baseName(name), new RawData(reader)); 519 } 520 521 @Override equals(final Object obj)522 public boolean equals(final Object obj) { 523 if (this == obj) { 524 return true; 525 } 526 if (!(obj instanceof Source)) { 527 return false; 528 } 529 final Source other = (Source) obj; 530 return Objects.equals(name, other.name) && data.equals(other.data); 531 } 532 533 @Override hashCode()534 public int hashCode() { 535 int h = hash; 536 if (h == 0) { 537 h = hash = data.hashCode() ^ Objects.hashCode(name); 538 } 539 return h; 540 } 541 542 /** 543 * Fetch source content. 544 * @return Source content. 545 */ getString()546 public String getString() { 547 return data.toString(); 548 } 549 550 /** 551 * Get the user supplied name of this script. 552 * @return User supplied source name. 553 */ getName()554 public String getName() { 555 return name; 556 } 557 558 /** 559 * Get the last modified time of this script. 560 * @return Last modified time. 561 */ getLastModified()562 public long getLastModified() { 563 return data.lastModified(); 564 } 565 566 /** 567 * Get the "directory" part of the file or "base" of the URL. 568 * @return base of file or URL. 569 */ getBase()570 public String getBase() { 571 return base; 572 } 573 574 /** 575 * Fetch a portion of source content. 576 * @param start start index in source 577 * @param len length of portion 578 * @return Source content portion. 579 */ getString(final int start, final int len)580 public String getString(final int start, final int len) { 581 return new String(data(), start, len); 582 } 583 584 /** 585 * Fetch a portion of source content associated with a token. 586 * @param token Token descriptor. 587 * @return Source content portion. 588 */ getString(final long token)589 public String getString(final long token) { 590 final int start = Token.descPosition(token); 591 final int len = Token.descLength(token); 592 return new String(data(), start, len); 593 } 594 595 /** 596 * Returns the source URL of this script Source. Can be null if Source 597 * was created from a String or a char[]. 598 * 599 * @return URL source or null 600 */ getURL()601 public URL getURL() { 602 return data.url(); 603 } 604 605 /** 606 * Get explicit source URL. 607 * @return URL set vial sourceURL directive 608 */ getExplicitURL()609 public String getExplicitURL() { 610 return explicitURL; 611 } 612 613 /** 614 * Set explicit source URL. 615 * @param explicitURL URL set via sourceURL directive 616 */ setExplicitURL(final String explicitURL)617 public void setExplicitURL(final String explicitURL) { 618 this.explicitURL = explicitURL; 619 } 620 621 /** 622 * Returns whether this source was submitted via 'eval' call or not. 623 * 624 * @return true if this source represents code submitted via 'eval' 625 */ isEvalCode()626 public boolean isEvalCode() { 627 return data.isEvalCode(); 628 } 629 630 /** 631 * Find the beginning of the line containing position. 632 * @param position Index to offending token. 633 * @return Index of first character of line. 634 */ findBOLN(final int position)635 private int findBOLN(final int position) { 636 final char[] d = data(); 637 for (int i = position - 1; i > 0; i--) { 638 final char ch = d[i]; 639 640 if (ch == '\n' || ch == '\r') { 641 return i + 1; 642 } 643 } 644 645 return 0; 646 } 647 648 /** 649 * Find the end of the line containing position. 650 * @param position Index to offending token. 651 * @return Index of last character of line. 652 */ findEOLN(final int position)653 private int findEOLN(final int position) { 654 final char[] d = data(); 655 final int length = d.length; 656 for (int i = position; i < length; i++) { 657 final char ch = d[i]; 658 659 if (ch == '\n' || ch == '\r') { 660 return i - 1; 661 } 662 } 663 664 return length - 1; 665 } 666 667 /** 668 * Return line number of character position. 669 * 670 * <p>This method can be expensive for large sources as it iterates through 671 * all characters up to {@code position}.</p> 672 * 673 * @param position Position of character in source content. 674 * @return Line number. 675 */ getLine(final int position)676 public int getLine(final int position) { 677 final char[] d = data(); 678 // Line count starts at 1. 679 int line = 1; 680 681 for (int i = 0; i < position; i++) { 682 final char ch = d[i]; 683 // Works for both \n and \r\n. 684 if (ch == '\n') { 685 line++; 686 } 687 } 688 689 return line; 690 } 691 692 /** 693 * Return column number of character position. 694 * @param position Position of character in source content. 695 * @return Column number. 696 */ getColumn(final int position)697 public int getColumn(final int position) { 698 // TODO - column needs to account for tabs. 699 return position - findBOLN(position); 700 } 701 702 /** 703 * Return line text including character position. 704 * @param position Position of character in source content. 705 * @return Line text. 706 */ getSourceLine(final int position)707 public String getSourceLine(final int position) { 708 // Find end of previous line. 709 final int first = findBOLN(position); 710 // Find end of this line. 711 final int last = findEOLN(position); 712 713 return new String(data(), first, last - first + 1); 714 } 715 716 /** 717 * Get the content of this source as a char array. Note that the underlying array is returned instead of a 718 * clone; modifying the char array will cause modification to the source; this should not be done. While 719 * there is an apparent danger that we allow unfettered access to an underlying mutable array, the 720 * {@code Source} class is in a restricted {@code jdk.nashorn.internal.*} package and as such it is 721 * inaccessible by external actors in an environment with a security manager. Returning a clone would be 722 * detrimental to performance. 723 * @return content the content of this source as a char array 724 */ getContent()725 public char[] getContent() { 726 return data(); 727 } 728 729 /** 730 * Get the length in chars for this source 731 * @return length 732 */ getLength()733 public int getLength() { 734 return data.length(); 735 } 736 737 /** 738 * Read all of the source until end of file. Return it as char array 739 * 740 * @param reader reader opened to source stream 741 * @return source as content 742 * @throws IOException if source could not be read 743 */ readFully(final Reader reader)744 public static char[] readFully(final Reader reader) throws IOException { 745 final char[] arr = new char[BUF_SIZE]; 746 final StringBuilder sb = new StringBuilder(); 747 748 try { 749 int numChars; 750 while ((numChars = reader.read(arr, 0, arr.length)) > 0) { 751 sb.append(arr, 0, numChars); 752 } 753 } finally { 754 reader.close(); 755 } 756 757 return sb.toString().toCharArray(); 758 } 759 760 /** 761 * Read all of the source until end of file. Return it as char array 762 * 763 * @param file source file 764 * @return source as content 765 * @throws IOException if source could not be read 766 */ readFully(final File file)767 public static char[] readFully(final File file) throws IOException { 768 if (!file.isFile()) { 769 throw new IOException(file + " is not a file"); //TODO localize? 770 } 771 return byteToCharArray(Files.readAllBytes(file.toPath())); 772 } 773 774 /** 775 * Read all of the source until end of file. Return it as char array 776 * 777 * @param file source file 778 * @param cs Charset used to convert bytes to chars 779 * @return source as content 780 * @throws IOException if source could not be read 781 */ readFully(final File file, final Charset cs)782 public static char[] readFully(final File file, final Charset cs) throws IOException { 783 if (!file.isFile()) { 784 throw new IOException(file + " is not a file"); //TODO localize? 785 } 786 787 final byte[] buf = Files.readAllBytes(file.toPath()); 788 return (cs != null) ? new String(buf, cs).toCharArray() : byteToCharArray(buf); 789 } 790 791 /** 792 * Read all of the source until end of stream from the given URL. Return it as char array 793 * 794 * @param url URL to read content from 795 * @return source as content 796 * @throws IOException if source could not be read 797 */ readFully(final URL url)798 public static char[] readFully(final URL url) throws IOException { 799 return readFully(url.openStream()); 800 } 801 802 /** 803 * Read all of the source until end of file. Return it as char array 804 * 805 * @param url URL to read content from 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 */ readFully(final URL url, final Charset cs)810 public static char[] readFully(final URL url, final Charset cs) throws IOException { 811 return readFully(url.openStream(), cs); 812 } 813 814 /** 815 * Get a Base64-encoded SHA1 digest for this source. 816 * 817 * @return a Base64-encoded SHA1 digest for this source 818 */ getDigest()819 public String getDigest() { 820 return new String(getDigestBytes(), StandardCharsets.US_ASCII); 821 } 822 getDigestBytes()823 private byte[] getDigestBytes() { 824 byte[] ldigest = digest; 825 if (ldigest == null) { 826 final char[] content = data(); 827 final byte[] bytes = new byte[content.length * 2]; 828 829 for (int i = 0; i < content.length; i++) { 830 bytes[i * 2] = (byte) (content[i] & 0x00ff); 831 bytes[i * 2 + 1] = (byte) ((content[i] & 0xff00) >> 8); 832 } 833 834 try { 835 final MessageDigest md = MessageDigest.getInstance("SHA-1"); 836 if (name != null) { 837 md.update(name.getBytes(StandardCharsets.UTF_8)); 838 } 839 if (base != null) { 840 md.update(base.getBytes(StandardCharsets.UTF_8)); 841 } 842 if (getURL() != null) { 843 md.update(getURL().toString().getBytes(StandardCharsets.UTF_8)); 844 } 845 digest = ldigest = BASE64.encode(md.digest(bytes)); 846 } catch (final NoSuchAlgorithmException e) { 847 throw new RuntimeException(e); 848 } 849 } 850 return ldigest; 851 } 852 853 /** 854 * Get the base url. This is currently used for testing only 855 * @param url a URL 856 * @return base URL for url 857 */ baseURL(final URL url)858 public static String baseURL(final URL url) { 859 if (url.getProtocol().equals("file")) { 860 try { 861 final Path path = Paths.get(url.toURI()); 862 final Path parent = path.getParent(); 863 return (parent != null) ? (parent + File.separator) : null; 864 } catch (final SecurityException | URISyntaxException | IOError e) { 865 return null; 866 } 867 } 868 869 // FIXME: is there a better way to find 'base' URL of a given URL? 870 String path = url.getPath(); 871 if (path.isEmpty()) { 872 return null; 873 } 874 path = path.substring(0, path.lastIndexOf('/') + 1); 875 final int port = url.getPort(); 876 try { 877 return new URL(url.getProtocol(), url.getHost(), port, path).toString(); 878 } catch (final MalformedURLException e) { 879 return null; 880 } 881 } 882 dirName(final File file, final String DEFAULT_BASE_NAME)883 private static String dirName(final File file, final String DEFAULT_BASE_NAME) { 884 final String res = file.getParent(); 885 return (res != null) ? (res + File.separator) : DEFAULT_BASE_NAME; 886 } 887 888 // fake directory like name baseName(final String name)889 private static String baseName(final String name) { 890 int idx = name.lastIndexOf('/'); 891 if (idx == -1) { 892 idx = name.lastIndexOf('\\'); 893 } 894 return (idx != -1) ? name.substring(0, idx + 1) : null; 895 } 896 readFully(final InputStream is, final Charset cs)897 private static char[] readFully(final InputStream is, final Charset cs) throws IOException { 898 return (cs != null) ? new String(readBytes(is), cs).toCharArray() : readFully(is); 899 } 900 readFully(final InputStream is)901 private static char[] readFully(final InputStream is) throws IOException { 902 return byteToCharArray(readBytes(is)); 903 } 904 byteToCharArray(final byte[] bytes)905 private static char[] byteToCharArray(final byte[] bytes) { 906 Charset cs = StandardCharsets.UTF_8; 907 int start = 0; 908 // BOM detection. 909 if (bytes.length > 1 && bytes[0] == (byte) 0xFE && bytes[1] == (byte) 0xFF) { 910 start = 2; 911 cs = StandardCharsets.UTF_16BE; 912 } else if (bytes.length > 1 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE) { 913 if (bytes.length > 3 && bytes[2] == 0 && bytes[3] == 0) { 914 start = 4; 915 cs = Charset.forName("UTF-32LE"); 916 } else { 917 start = 2; 918 cs = StandardCharsets.UTF_16LE; 919 } 920 } else if (bytes.length > 2 && bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) { 921 start = 3; 922 cs = StandardCharsets.UTF_8; 923 } else if (bytes.length > 3 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == (byte) 0xFE && bytes[3] == (byte) 0xFF) { 924 start = 4; 925 cs = Charset.forName("UTF-32BE"); 926 } 927 928 return new String(bytes, start, bytes.length - start, cs).toCharArray(); 929 } 930 readBytes(final InputStream is)931 static byte[] readBytes(final InputStream is) throws IOException { 932 final byte[] arr = new byte[BUF_SIZE]; 933 try { 934 try (ByteArrayOutputStream buf = new ByteArrayOutputStream()) { 935 int numBytes; 936 while ((numBytes = is.read(arr, 0, arr.length)) > 0) { 937 buf.write(arr, 0, numBytes); 938 } 939 return buf.toByteArray(); 940 } 941 } finally { 942 is.close(); 943 } 944 } 945 946 @Override toString()947 public String toString() { 948 return getName(); 949 } 950 getURLFromFile(final File file)951 private static URL getURLFromFile(final File file) { 952 try { 953 return file.toURI().toURL(); 954 } catch (final SecurityException | MalformedURLException ignored) { 955 return null; 956 } 957 } 958 getLoggerStatic()959 private static DebugLogger getLoggerStatic() { 960 final Context context = Context.getContextTrustedOrNull(); 961 return context == null ? null : context.getLogger(Source.class); 962 } 963 964 @Override initLogger(final Context context)965 public DebugLogger initLogger(final Context context) { 966 return context.getLogger(this.getClass()); 967 } 968 969 @Override getLogger()970 public DebugLogger getLogger() { 971 return initLogger(Context.getContextTrusted()); 972 } 973 dumpFile(final File dirFile)974 private File dumpFile(final File dirFile) { 975 final URL u = getURL(); 976 final StringBuilder buf = new StringBuilder(); 977 // make it unique by prefixing current date & time 978 buf.append(LocalDateTime.now().toString()); 979 buf.append('_'); 980 if (u != null) { 981 // make it a safe file name 982 buf.append(u.toString() 983 .replace('/', '_') 984 .replace('\\', '_')); 985 } else { 986 buf.append(getName()); 987 } 988 989 return new File(dirFile, buf.toString()); 990 } 991 dump(final String dir)992 void dump(final String dir) { 993 final File dirFile = new File(dir); 994 final File file = dumpFile(dirFile); 995 if (!dirFile.exists() && !dirFile.mkdirs()) { 996 debug("Skipping source dump for " + name); 997 return; 998 } 999 1000 try (final FileOutputStream fos = new FileOutputStream(file)) { 1001 final PrintWriter pw = new PrintWriter(fos); 1002 pw.print(data.toString()); 1003 pw.flush(); 1004 } catch (final IOException ioExp) { 1005 debug("Skipping source dump for " + 1006 name + 1007 ": " + 1008 ECMAErrors.getMessage( 1009 "io.error.cant.write", 1010 dir.toString() + 1011 " : " + ioExp.toString())); 1012 } 1013 } 1014 } 1015