rev 1336 : 8080090: -d option should dump script source as well
Reviewed-by: hannesw, lagergren

   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.
  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 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      */
 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      */
 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
 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
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 601     public URL getURL() {
 602         return data.url();
 603     }
 604 
 605     /**
 606      * Get explicit source URL.
 607      * @return URL set vial sourceURL directive
 608      */
 609     public String getExplicitURL() {
 610         return explicitURL;
 611     }
 612 
 613     /**
 614      * Set explicit source URL.
 615      * @param explicitURL URL set via sourceURL directive
 616      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 725     public char[] getContent() {
 726         return data();
 727     }
 728 
 729     /**
 730      * Get the length in chars for this source
 731      * @return length
 732      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 819     public String getDigest() {
 820         return new String(getDigestBytes(), StandardCharsets.US_ASCII);
 821     }
 822 
 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      */
 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 
 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
 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 
 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 
 901     private static char[] readFully(final InputStream is) throws IOException {
 902         return byteToCharArray(readBytes(is));
 903     }
 904 
 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             start = 2;
 914             cs = StandardCharsets.UTF_16LE;
 915         } else if (bytes.length > 2 && bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) {
 916             start = 3;
 917             cs = StandardCharsets.UTF_8;
 918         } else if (bytes.length > 3 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE && bytes[2] == 0 && bytes[3] == 0) {
 919             start = 4;
 920             cs = Charset.forName("UTF-32LE");
 921         } else if (bytes.length > 3 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == (byte) 0xFE && bytes[3] == (byte) 0xFF) {
 922             start = 4;
 923             cs = Charset.forName("UTF-32BE");
 924         }
 925 
 926         return new String(bytes, start, bytes.length - start, cs).toCharArray();
 927     }
 928 
 929     static byte[] readBytes(final InputStream is) throws IOException {
 930         final byte[] arr = new byte[BUF_SIZE];
 931         try {
 932             try (ByteArrayOutputStream buf = new ByteArrayOutputStream()) {
 933                 int numBytes;
 934                 while ((numBytes = is.read(arr, 0, arr.length)) > 0) {
 935                     buf.write(arr, 0, numBytes);
 936                 }
 937                 return buf.toByteArray();
 938             }
 939         } finally {
 940             is.close();
 941         }
 942     }
 943 
 944     @Override
 945     public String toString() {
 946         return getName();
 947     }
 948 
 949     private static URL getURLFromFile(final File file) {
 950         try {
 951             return file.toURI().toURL();
 952         } catch (final SecurityException | MalformedURLException ignored) {
 953             return null;
 954         }
 955     }
 956 
 957     private static DebugLogger getLoggerStatic() {
 958         final Context context = Context.getContextTrustedOrNull();
 959         return context == null ? null : context.getLogger(Source.class);
 960     }
 961 
 962     @Override
 963     public DebugLogger initLogger(final Context context) {
 964         return context.getLogger(this.getClass());
 965     }
 966 
 967     @Override
 968     public DebugLogger getLogger() {
 969         return initLogger(Context.getContextTrusted());
 970     }
 971 
 972     private File dumpFile(final String dir) {
 973         final URL u = getURL();
 974         final StringBuilder buf = new StringBuilder();
 975         // make it unique by prefixing current date & time
 976         buf.append(LocalDateTime.now().toString());
 977         buf.append('_');
 978         if (u != null) {
 979             // make it a safe file name
 980             buf.append(u.toString()
 981                     .replace('/', '_')
 982                     .replace('\\', '_'));
 983         } else {
 984             buf.append(getName());
 985         }
 986 
 987         return new File(dir, buf.toString());
 988     }
 989 
 990     void dump(final String dir) {
 991         final File file = dumpFile(dir);
 992         try (final FileOutputStream fos = new FileOutputStream(file)) {
 993             final PrintWriter pw = new PrintWriter(fos);
 994             pw.print(data.toString());
 995             pw.flush();
 996         } catch (final IOException ioExp) {
 997             debug("Skipping source dump for " +
 998                     name +
 999                     ": " +
1000                     ECMAErrors.getMessage(
1001                         "io.error.cant.write",
1002                         dir.toString() +
1003                         " : " + ioExp.toString()));
1004         }
1005     }
1006 }
--- EOF ---