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