Source.java revision 1204:9597425b6b38
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
26package jdk.nashorn.internal.runtime;
27
28import java.io.ByteArrayOutputStream;
29import java.io.File;
30import java.io.FileNotFoundException;
31import java.io.IOError;
32import java.io.IOException;
33import java.io.InputStream;
34import java.io.Reader;
35import java.lang.ref.WeakReference;
36import java.net.MalformedURLException;
37import java.net.URISyntaxException;
38import java.net.URL;
39import java.net.URLConnection;
40import java.nio.charset.Charset;
41import java.nio.charset.StandardCharsets;
42import java.nio.file.Files;
43import java.nio.file.Path;
44import java.nio.file.Paths;
45import java.security.MessageDigest;
46import java.security.NoSuchAlgorithmException;
47import java.util.Arrays;
48import java.util.Base64;
49import java.util.Objects;
50import java.util.WeakHashMap;
51import jdk.nashorn.api.scripting.URLReader;
52import jdk.nashorn.internal.parser.Token;
53import jdk.nashorn.internal.runtime.logging.DebugLogger;
54import jdk.nashorn.internal.runtime.logging.Loggable;
55import jdk.nashorn.internal.runtime.logging.Logger;
56/**
57 * Source objects track the origin of JavaScript entities.
58 */
59@Logger(name="source")
60public 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}
993