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