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