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 file  file from which source can be loaded
 488      * @param cs    Charset used to convert bytes to chars
 489      *
 490      * @return source instance
 491      *
 492      * @throws IOException if source cannot be loaded
 493      */
 494     public static Source sourceFor(final String name, final File file, final Charset cs) throws IOException {
 495         final File absFile = file.getAbsoluteFile();
 496         return sourceFor(name, dirName(absFile, null), new FileData(file, cs));
 497     }
 498 
 499     /**
 500      * Returns an instance
 501      *
 502      * @param name source name
 503      * @param reader reader from which source can be loaded
 504      *
 505      * @return source instance
 506      *
 507      * @throws IOException if source cannot be loaded
 508      */
 509     public static Source sourceFor(final String name, final Reader reader) throws IOException {
 510         // Extract URL from URLReader to defer loading and reuse cached data if available.
 511         if (reader instanceof URLReader) {
 512             final URLReader urlReader = (URLReader) reader;
 513             return sourceFor(name, urlReader.getURL(), urlReader.getCharset());
 514         }
 515         return new Source(name, baseName(name), new RawData(reader));
 516     }
 517 
 518     @Override
 519     public boolean equals(final Object obj) {
 520         if (this == obj) {
 521             return true;
 522         }
 523         if (!(obj instanceof Source)) {
 524             return false;
 525         }
 526         final Source other = (Source) obj;
 527         return Objects.equals(name, other.name) && data.equals(other.data);
 528     }
 529 
 530     @Override
 531     public int hashCode() {
 532         int h = hash;
 533         if (h == 0) {
 534             h = hash = data.hashCode() ^ Objects.hashCode(name);
 535         }
 536         return h;
 537     }
 538 
 539     /**
 540      * Fetch source content.
 541      * @return Source content.
 542      */
 543     public String getString() {
 544         return data.toString();
 545     }
 546 
 547     /**
 548      * Get the user supplied name of this script.
 549      * @return User supplied source name.
 550      */
 551     public String getName() {
 552         return name;
 553     }
 554 
 555     /**
 556      * Get the last modified time of this script.
 557      * @return Last modified time.
 558      */
 559     public long getLastModified() {
 560         return data.lastModified();
 561     }
 562 
 563     /**
 564      * Get the "directory" part of the file or "base" of the URL.
 565      * @return base of file or URL.
 566      */
 567     public String getBase() {
 568         return base;
 569     }
 570 
 571     /**
 572      * Fetch a portion of source content.
 573      * @param start start index in source
 574      * @param len length of portion
 575      * @return Source content portion.
 576      */
 577     public String getString(final int start, final int len) {
 578         return new String(data(), start, len);
 579     }
 580 
 581     /**
 582      * Fetch a portion of source content associated with a token.
 583      * @param token Token descriptor.
 584      * @return Source content portion.
 585      */
 586     public String getString(final long token) {
 587         final int start = Token.descPosition(token);
 588         final int len = Token.descLength(token);
 589         return new String(data(), start, len);
 590     }
 591 
 592     /**
 593      * Returns the source URL of this script Source. Can be null if Source
 594      * was created from a String or a char[].
 595      *
 596      * @return URL source or null
 597      */
 598     public URL getURL() {
 599         return data.url();
 600     }
 601 
 602     /**
 603      * Get explicit source URL.
 604      * @return URL set vial sourceURL directive
 605      */
 606     public String getExplicitURL() {
 607         return explicitURL;
 608     }
 609 
 610     /**
 611      * Set explicit source URL.
 612      * @param explicitURL URL set via sourceURL directive
 613      */
 614     public void setExplicitURL(final String explicitURL) {
 615         this.explicitURL = explicitURL;
 616     }
 617 
 618     /**
 619      * Returns whether this source was submitted via 'eval' call or not.
 620      *
 621      * @return true if this source represents code submitted via 'eval'
 622      */
 623     public boolean isEvalCode() {
 624         return data.isEvalCode();
 625     }
 626 
 627     /**
 628      * Find the beginning of the line containing position.
 629      * @param position Index to offending token.
 630      * @return Index of first character of line.
 631      */
 632     private int findBOLN(final int position) {
 633         final char[] d = data();
 634         for (int i = position - 1; i > 0; i--) {
 635             final char ch = d[i];
 636 
 637             if (ch == '\n' || ch == '\r') {
 638                 return i + 1;
 639             }
 640         }
 641 
 642         return 0;
 643     }
 644 
 645     /**
 646      * Find the end of the line containing position.
 647      * @param position Index to offending token.
 648      * @return Index of last character of line.
 649      */
 650     private int findEOLN(final int position) {
 651         final char[] d = data();
 652         final int length = d.length;
 653         for (int i = position; i < length; i++) {
 654             final char ch = d[i];
 655 
 656             if (ch == '\n' || ch == '\r') {
 657                 return i - 1;
 658             }
 659         }
 660 
 661         return length - 1;
 662     }
 663 
 664     /**
 665      * Return line number of character position.
 666      *
 667      * <p>This method can be expensive for large sources as it iterates through
 668      * all characters up to {@code position}.</p>
 669      *
 670      * @param position Position of character in source content.
 671      * @return Line number.
 672      */
 673     public int getLine(final int position) {
 674         final char[] d = data();
 675         // Line count starts at 1.
 676         int line = 1;
 677 
 678         for (int i = 0; i < position; i++) {
 679             final char ch = d[i];
 680             // Works for both \n and \r\n.
 681             if (ch == '\n') {
 682                 line++;
 683             }
 684         }
 685 
 686         return line;
 687     }
 688 
 689     /**
 690      * Return column number of character position.
 691      * @param position Position of character in source content.
 692      * @return Column number.
 693      */
 694     public int getColumn(final int position) {
 695         // TODO - column needs to account for tabs.
 696         return position - findBOLN(position);
 697     }
 698 
 699     /**
 700      * Return line text including character position.
 701      * @param position Position of character in source content.
 702      * @return Line text.
 703      */
 704     public String getSourceLine(final int position) {
 705         // Find end of previous line.
 706         final int first = findBOLN(position);
 707         // Find end of this line.
 708         final int last = findEOLN(position);
 709 
 710         return new String(data(), first, last - first + 1);
 711     }
 712 
 713     /**
 714      * Get the content of this source as a char array. Note that the underlying array is returned instead of a
 715      * clone; modifying the char array will cause modification to the source; this should not be done. While
 716      * there is an apparent danger that we allow unfettered access to an underlying mutable array, the
 717      * {@code Source} class is in a restricted {@code jdk.nashorn.internal.*} package and as such it is
 718      * inaccessible by external actors in an environment with a security manager. Returning a clone would be
 719      * detrimental to performance.
 720      * @return content the content of this source as a char array
 721      */
 722     public char[] getContent() {
 723         return data();
 724     }
 725 
 726     /**
 727      * Get the length in chars for this source
 728      * @return length
 729      */
 730     public int getLength() {
 731         return data.length();
 732     }
 733 
 734     /**
 735      * Read all of the source until end of file. Return it as char array
 736      *
 737      * @param reader reader opened to source stream
 738      * @return source as content
 739      * @throws IOException if source could not be read
 740      */
 741     public static char[] readFully(final Reader reader) throws IOException {
 742         final char[]        arr = new char[BUF_SIZE];
 743         final StringBuilder sb  = new StringBuilder();
 744 
 745         try {
 746             int numChars;
 747             while ((numChars = reader.read(arr, 0, arr.length)) > 0) {
 748                 sb.append(arr, 0, numChars);
 749             }
 750         } finally {
 751             reader.close();
 752         }
 753 
 754         return sb.toString().toCharArray();
 755     }
 756 
 757     /**
 758      * Read all of the source until end of file. Return it as char array
 759      *
 760      * @param file source file
 761      * @return source as content
 762      * @throws IOException if source could not be read
 763      */
 764     public static char[] readFully(final File file) throws IOException {
 765         if (!file.isFile()) {
 766             throw new IOException(file + " is not a file"); //TODO localize?
 767         }
 768         return byteToCharArray(Files.readAllBytes(file.toPath()));
 769     }
 770 
 771     /**
 772      * Read all of the source until end of file. Return it as char array
 773      *
 774      * @param file source file
 775      * @param cs Charset used to convert bytes to chars
 776      * @return source as content
 777      * @throws IOException if source could not be read
 778      */
 779     public static char[] readFully(final File file, final Charset cs) throws IOException {
 780         if (!file.isFile()) {
 781             throw new IOException(file + " is not a file"); //TODO localize?
 782         }
 783 
 784         final byte[] buf = Files.readAllBytes(file.toPath());
 785         return (cs != null) ? new String(buf, cs).toCharArray() : byteToCharArray(buf);
 786     }
 787 
 788     /**
 789      * Read all of the source until end of stream from the given URL. Return it as char array
 790      *
 791      * @param url URL to read content from
 792      * @return source as content
 793      * @throws IOException if source could not be read
 794      */
 795     public static char[] readFully(final URL url) throws IOException {
 796         return readFully(url.openStream());
 797     }
 798 
 799     /**
 800      * Read all of the source until end of file. Return it as char array
 801      *
 802      * @param url URL to read content from
 803      * @param cs Charset used to convert bytes to chars
 804      * @return source as content
 805      * @throws IOException if source could not be read
 806      */
 807     public static char[] readFully(final URL url, final Charset cs) throws IOException {
 808         return readFully(url.openStream(), cs);
 809     }
 810 
 811     /**
 812      * Get a Base64-encoded SHA1 digest for this source.
 813      *
 814      * @return a Base64-encoded SHA1 digest for this source
 815      */
 816     public String getDigest() {
 817         return new String(getDigestBytes(), StandardCharsets.US_ASCII);
 818     }
 819 
 820     private byte[] getDigestBytes() {
 821         byte[] ldigest = digest;
 822         if (ldigest == null) {
 823             final char[] content = data();
 824             final byte[] bytes = new byte[content.length * 2];
 825 
 826             for (int i = 0; i < content.length; i++) {
 827                 bytes[i * 2]     = (byte)  (content[i] & 0x00ff);
 828                 bytes[i * 2 + 1] = (byte) ((content[i] & 0xff00) >> 8);
 829             }
 830 
 831             try {
 832                 final MessageDigest md = MessageDigest.getInstance("SHA-1");
 833                 if (name != null) {
 834                     md.update(name.getBytes(StandardCharsets.UTF_8));
 835                 }
 836                 if (base != null) {
 837                     md.update(base.getBytes(StandardCharsets.UTF_8));
 838                 }
 839                 if (getURL() != null) {
 840                     md.update(getURL().toString().getBytes(StandardCharsets.UTF_8));
 841                 }
 842                 digest = ldigest = BASE64.encode(md.digest(bytes));
 843             } catch (final NoSuchAlgorithmException e) {
 844                 throw new RuntimeException(e);
 845             }
 846         }
 847         return ldigest;
 848     }
 849 
 850     /**
 851      * Get the base url. This is currently used for testing only
 852      * @param url a URL
 853      * @return base URL for url
 854      */
 855     public static String baseURL(final URL url) {
 856         if (url.getProtocol().equals("file")) {
 857             try {
 858                 final Path path = Paths.get(url.toURI());
 859                 final Path parent = path.getParent();
 860                 return (parent != null) ? (parent + File.separator) : null;
 861             } catch (final SecurityException | URISyntaxException | IOError e) {
 862                 return null;
 863             }
 864         }
 865 
 866         // FIXME: is there a better way to find 'base' URL of a given URL?
 867         String path = url.getPath();
 868         if (path.isEmpty()) {
 869             return null;
 870         }
 871         path = path.substring(0, path.lastIndexOf('/') + 1);
 872         final int port = url.getPort();
 873         try {
 874             return new URL(url.getProtocol(), url.getHost(), port, path).toString();
 875         } catch (final MalformedURLException e) {
 876             return null;
 877         }
 878     }
 879 
 880     private static String dirName(final File file, final String DEFAULT_BASE_NAME) {
 881         final String res = file.getParent();
 882         return (res != null) ? (res + File.separator) : DEFAULT_BASE_NAME;
 883     }
 884 
 885     // fake directory like name
 886     private static String baseName(final String name) {
 887         int idx = name.lastIndexOf('/');
 888         if (idx == -1) {
 889             idx = name.lastIndexOf('\\');
 890         }
 891         return (idx != -1) ? name.substring(0, idx + 1) : null;
 892     }
 893 
 894     private static char[] readFully(final InputStream is, final Charset cs) throws IOException {
 895         return (cs != null) ? new String(readBytes(is), cs).toCharArray() : readFully(is);
 896     }
 897 
 898     private static char[] readFully(final InputStream is) throws IOException {
 899         return byteToCharArray(readBytes(is));
 900     }
 901 
 902     private static char[] byteToCharArray(final byte[] bytes) {
 903         Charset cs = StandardCharsets.UTF_8;
 904         int start = 0;
 905         // BOM detection.
 906         if (bytes.length > 1 && bytes[0] == (byte) 0xFE && bytes[1] == (byte) 0xFF) {
 907             start = 2;
 908             cs = StandardCharsets.UTF_16BE;
 909         } else if (bytes.length > 1 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE) {
 910             start = 2;
 911             cs = StandardCharsets.UTF_16LE;
 912         } else if (bytes.length > 2 && bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) {
 913             start = 3;
 914             cs = StandardCharsets.UTF_8;
 915         } else if (bytes.length > 3 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE && bytes[2] == 0 && bytes[3] == 0) {
 916             start = 4;
 917             cs = Charset.forName("UTF-32LE");
 918         } else if (bytes.length > 3 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == (byte) 0xFE && bytes[3] == (byte) 0xFF) {
 919             start = 4;
 920             cs = Charset.forName("UTF-32BE");
 921         }
 922 
 923         return new String(bytes, start, bytes.length - start, cs).toCharArray();
 924     }
 925 
 926     static byte[] readBytes(final InputStream is) throws IOException {
 927         final byte[] arr = new byte[BUF_SIZE];
 928         try {
 929             try (ByteArrayOutputStream buf = new ByteArrayOutputStream()) {
 930                 int numBytes;
 931                 while ((numBytes = is.read(arr, 0, arr.length)) > 0) {
 932                     buf.write(arr, 0, numBytes);
 933                 }
 934                 return buf.toByteArray();
 935             }
 936         } finally {
 937             is.close();
 938         }
 939     }
 940 
 941     @Override
 942     public String toString() {
 943         return getName();
 944     }
 945 
 946     private static URL getURLFromFile(final File file) {
 947         try {
 948             return file.toURI().toURL();
 949         } catch (final SecurityException | MalformedURLException ignored) {
 950             return null;
 951         }
 952     }
 953 
 954     private static DebugLogger getLoggerStatic() {
 955         final Context context = Context.getContextTrustedOrNull();
 956         return context == null ? null : context.getLogger(Source.class);
 957     }
 958 
 959     @Override
 960     public DebugLogger initLogger(final Context context) {
 961         return context.getLogger(this.getClass());
 962     }
 963 
 964     @Override
 965     public DebugLogger getLogger() {
 966         return initLogger(Context.getContextTrusted());
 967     }
 968 }