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 }