Source.java revision 971:c93b6091b11e
1139825Simp/*
298542Smckusick * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
398542Smckusick * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
498542Smckusick *
598542Smckusick * This code is free software; you can redistribute it and/or modify it
698542Smckusick * under the terms of the GNU General Public License version 2 only, as
798542Smckusick * published by the Free Software Foundation.  Oracle designates this
898542Smckusick * particular file as subject to the "Classpath" exception as provided
998542Smckusick * by Oracle in the LICENSE file that accompanied this code.
1098542Smckusick *
11136721Srwatson * This code is distributed in the hope that it will be useful, but WITHOUT
12136721Srwatson * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13136721Srwatson * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14136721Srwatson * version 2 for more details (a copy is included in the LICENSE file that
15136721Srwatson * accompanied this code).
16136721Srwatson *
17136721Srwatson * You should have received a copy of the GNU General Public License version
18136721Srwatson * 2 along with this work; if not, write to the Free Software Foundation,
19136721Srwatson * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20136721Srwatson *
21136721Srwatson * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22136721Srwatson * or visit www.oracle.com if you need additional information or have any
23136721Srwatson * questions.
24136721Srwatson */
25136721Srwatson
26136721Srwatsonpackage jdk.nashorn.internal.runtime;
27136721Srwatson
28136721Srwatsonimport java.io.ByteArrayOutputStream;
29136721Srwatsonimport java.io.File;
30136721Srwatsonimport java.io.FileNotFoundException;
31136721Srwatsonimport java.io.IOError;
321541Srgrimesimport java.io.IOException;
331541Srgrimesimport java.io.InputStream;
341541Srgrimesimport java.io.Reader;
351541Srgrimesimport java.lang.ref.WeakReference;
361541Srgrimesimport java.net.MalformedURLException;
371541Srgrimesimport java.net.URISyntaxException;
381541Srgrimesimport java.net.URL;
391541Srgrimesimport java.net.URLConnection;
401541Srgrimesimport java.nio.charset.Charset;
411541Srgrimesimport java.nio.charset.StandardCharsets;
421541Srgrimesimport java.nio.file.Files;
431541Srgrimesimport java.nio.file.Path;
441541Srgrimesimport java.nio.file.Paths;
451541Srgrimesimport java.security.MessageDigest;
461541Srgrimesimport java.security.NoSuchAlgorithmException;
471541Srgrimesimport java.util.Arrays;
481541Srgrimesimport java.util.Base64;
491541Srgrimesimport java.util.Objects;
501541Srgrimesimport java.util.WeakHashMap;
511541Srgrimesimport jdk.nashorn.api.scripting.URLReader;
521541Srgrimesimport jdk.nashorn.internal.parser.Token;
531541Srgrimesimport jdk.nashorn.internal.runtime.logging.DebugLogger;
541541Srgrimesimport jdk.nashorn.internal.runtime.logging.Loggable;
551541Srgrimesimport jdk.nashorn.internal.runtime.logging.Logger;
561541Srgrimes/**
571541Srgrimes * Source objects track the origin of JavaScript entities.
581541Srgrimes */
5922521Sdyson@Logger(name="source")
601541Srgrimespublic final class Source implements Loggable {
611541Srgrimes    private static final int BUF_SIZE = 8 * 1024;
62116192Sobrien    private static final Cache CACHE = new Cache();
63116192Sobrien
64116192Sobrien    // Message digest to file name encoder
6513260Swollman    private final static Base64.Encoder BASE64 = Base64.getUrlEncoder().withoutPadding();
6613260Swollman
671541Srgrimes    /**
68224778Srwatson     * Descriptive name of the source as supplied by the user. Used for error
691541Srgrimes     * reporting to the user. For example, SyntaxError will use this to print message.
7060041Sphk     * Used to implement __FILE__. Also used for SourceFile in .class for debugger usage.
711541Srgrimes     */
7250253Sbde    private final String name;
73202113Smckusick
7474548Smckusick    /**
75108524Salfred     * Base directory the File or base part of the URL. Used to implement __DIR__.
76164033Srwatson     * Used to load scripts relative to the 'directory' or 'base' URL of current script.
771541Srgrimes     * This will be null when it can't be computed.
781541Srgrimes     */
791541Srgrimes    private final String base;
8041124Sdg
81202113Smckusick    /** Source content */
8212911Sphk    private final Data data;
831541Srgrimes
84216796Skib    /** Cached hash code */
851541Srgrimes    private int hash;
86202113Smckusick
87202113Smckusick    /** Base64-encoded SHA1 digest of this source object */
88216796Skib    private volatile byte[] digest;
89216796Skib
90202113Smckusick    /** source URL set via //@ sourceURL or //# sourceURL directive */
9159241Srwatson    private String explicitURL;
921541Srgrimes
931541Srgrimes    // Do *not* make this public, ever! Trusts the URL and content.
9441124Sdg    private Source(final String name, final String base, final Data data) {
9530474Sphk        this.name = name;
961541Srgrimes        this.base = base;
971541Srgrimes        this.data = data;
981541Srgrimes    }
99216796Skib
1001541Srgrimes    private static synchronized Source sourceFor(final String name, final String base, final URLData data) throws IOException {
101203763Smckusick        try {
102207141Sjeff            final Source newSource = new Source(name, base, data);
10312590Sbde            final Source existingSource = CACHE.get(newSource);
104207141Sjeff            if (existingSource != null) {
10598542Smckusick                // Force any access errors
106207141Sjeff                data.checkPermissionAndClose();
107216796Skib                return existingSource;
108216796Skib            }
109216796Skib
110216796Skib            // All sources in cache must be fully loaded
111216796Skib            data.load();
112173464Sobrien            CACHE.put(newSource, newSource);
11398542Smckusick
11431352Sbde            return newSource;
115207141Sjeff        } catch (final RuntimeException e) {
116207141Sjeff            final Throwable cause = e.getCause();
11792728Salfred            if (cause instanceof IOException) {
118203763Smckusick                throw (IOException) cause;
119203763Smckusick            }
12098542Smckusick            throw e;
121207141Sjeff        }
122207141Sjeff    }
123207141Sjeff
12498542Smckusick    private static class Cache extends WeakHashMap<Source, WeakReference<Source>> {
12598542Smckusick        public Source get(final Source key) {
12698542Smckusick            final WeakReference<Source> ref = super.get(key);
1271541Srgrimes            return ref == null ? null : ref.get();
1281541Srgrimes        }
12996755Strhodes
1308876Srgrimes        public void put(final Source key, final Source value) {
1311541Srgrimes            assert !(value.data instanceof RawData);
1321541Srgrimes            put(key, new WeakReference<>(value));
1331541Srgrimes        }
1341541Srgrimes    }
1351541Srgrimes
1361541Srgrimes    /* package-private */
1371541Srgrimes    DebuggerSupport.SourceInfo getSourceInfo() {
1381541Srgrimes        return new DebuggerSupport.SourceInfo(getName(), data.hashCode(),  data.url(), data.array());
1391541Srgrimes    }
140166051Smpp
1411541Srgrimes    // Wrapper to manage lazy loading
1421541Srgrimes    private static interface Data {
1431541Srgrimes
1441541Srgrimes        URL url();
1451541Srgrimes
1461541Srgrimes        int length();
1471549Srgrimes
148187790Srwatson        long lastModified();
14996506Sphk
15098542Smckusick        char[] array();
151187790Srwatson
1521541Srgrimes        boolean isEvalCode();
15398542Smckusick    }
1541541Srgrimes
15596506Sphk    private static class RawData implements Data {
156140704Sjeff        private final char[] array;
15798542Smckusick        private final boolean evalCode;
158203763Smckusick        private int hash;
159151906Sps
160151906Sps        private RawData(final char[] array, final boolean evalCode) {
161166924Sbrian            this.array = Objects.requireNonNull(array);
1626357Sphk            this.evalCode = evalCode;
1636357Sphk        }
1646357Sphk
1658876Srgrimes        private RawData(final String source, final boolean evalCode) {
1661541Srgrimes            this.array = Objects.requireNonNull(source).toCharArray();
1671541Srgrimes            this.evalCode = evalCode;
168140704Sjeff        }
169140704Sjeff
170173464Sobrien        private RawData(final Reader reader) throws IOException {
1711541Srgrimes            this(readFully(reader), false);
17250253Sbde        }
17350253Sbde
17450253Sbde        @Override
1751541Srgrimes        public int hashCode() {
1761541Srgrimes            int h = hash;
1771541Srgrimes            if (h == 0) {
1787170Sdg                h = hash = Arrays.hashCode(array) ^ (evalCode? 1 : 0);
179173464Sobrien            }
18089637Smckusick            return h;
18189637Smckusick        }
182140704Sjeff
183140704Sjeff        @Override
184140704Sjeff        public boolean equals(final Object obj) {
185140704Sjeff            if (this == obj) {
186140704Sjeff                return true;
187140704Sjeff            }
188140704Sjeff            if (obj instanceof RawData) {
1891541Srgrimes                final RawData other = (RawData)obj;
1901541Srgrimes                return Arrays.equals(array, other.array) && evalCode == other.evalCode;
191170587Srwatson            }
19229609Sphk            return false;
1931541Srgrimes        }
1941541Srgrimes
1951541Srgrimes        @Override
1961541Srgrimes        public String toString() {
1971541Srgrimes            return new String(array());
1981541Srgrimes        }
1991541Srgrimes
200207141Sjeff        @Override
2011541Srgrimes        public URL url() {
202166924Sbrian            return null;
203166924Sbrian        }
204187790Srwatson
205187790Srwatson        @Override
206187790Srwatson        public int length() {
207187790Srwatson            return array.length;
2081541Srgrimes        }
2091541Srgrimes
2101541Srgrimes        @Override
211166142Smpp        public long lastModified() {
2121541Srgrimes            return 0;
213140704Sjeff        }
2141541Srgrimes
2151541Srgrimes        @Override
2161541Srgrimes        public char[] array() {
21798542Smckusick            return array;
218140704Sjeff        }
2191541Srgrimes
220222958Sjeff
22189637Smckusick        @Override
222220374Smckusick        public boolean isEvalCode() {
22389637Smckusick            return evalCode;
22489637Smckusick        }
225140704Sjeff    }
226223114Smckusick
227151906Sps    private static class URLData implements Data {
228151906Sps        private final URL url;
229151906Sps        protected final Charset cs;
230151906Sps        private int hash;
2311541Srgrimes        protected char[] array;
2321541Srgrimes        protected int length;
2331541Srgrimes        protected long lastModified;
2341541Srgrimes
2351541Srgrimes        private URLData(final URL url, final Charset cs) {
2361541Srgrimes            this.url = Objects.requireNonNull(url);
2371541Srgrimes            this.cs = cs;
2381541Srgrimes        }
2391541Srgrimes
2401541Srgrimes        @Override
2411541Srgrimes        public int hashCode() {
2421549Srgrimes            int h = hash;
243187790Srwatson            if (h == 0) {
24496506Sphk                h = hash = url.hashCode();
24598542Smckusick            }
246100344Smckusick            return h;
24798542Smckusick        }
248187790Srwatson
2491541Srgrimes        @Override
2501541Srgrimes        public boolean equals(final Object other) {
2511541Srgrimes            if (this == other) {
25289637Smckusick                return true;
25389637Smckusick            }
2541541Srgrimes            if (!(other instanceof URLData)) {
255140704Sjeff                return false;
256203763Smckusick            }
257248521Skib
258100344Smckusick            final URLData otherData = (URLData) other;
259151906Sps
260151906Sps            if (url.equals(otherData.url)) {
261166924Sbrian                // Make sure both have meta data loaded
2628876Srgrimes                try {
2631541Srgrimes                    if (isDeferred()) {
26489637Smckusick                        // Data in cache is always loaded, and we only compare to cached data.
2651541Srgrimes                        assert !otherData.isDeferred();
266140704Sjeff                        loadMeta();
267140704Sjeff                    } else if (otherData.isDeferred()) {
268248521Skib                        otherData.loadMeta();
269248521Skib                    }
270140704Sjeff                } catch (final IOException e) {
271173464Sobrien                    throw new RuntimeException(e);
27289637Smckusick                }
27362976Smckusick
2741541Srgrimes                // Compare meta data
2751541Srgrimes                return this.length == otherData.length && this.lastModified == otherData.lastModified;
2761541Srgrimes            }
27750253Sbde            return false;
27850253Sbde        }
2798456Srgrimes
2801541Srgrimes        @Override
2811541Srgrimes        public String toString() {
2821541Srgrimes            return new String(array());
2837170Sdg        }
284173464Sobrien
28589637Smckusick        @Override
28689637Smckusick        public URL url() {
287170587Srwatson            return url;
288140704Sjeff        }
2891541Srgrimes
290140704Sjeff        @Override
291100344Smckusick        public int length() {
29298687Smux            return length;
29398542Smckusick        }
29437555Sbde
2951541Srgrimes        @Override
2961541Srgrimes        public long lastModified() {
297140704Sjeff            return lastModified;
2981541Srgrimes        }
2991541Srgrimes
3001541Srgrimes        @Override
301248521Skib        public char[] array() {
3023487Sphk            assert !isDeferred();
3031541Srgrimes            return array;
3041541Srgrimes        }
3051541Srgrimes
3066864Sdg        @Override
30798542Smckusick        public boolean isEvalCode() {
30898542Smckusick            return false;
3096864Sdg        }
3106864Sdg
3116864Sdg        boolean isDeferred() {
3128876Srgrimes            return array == null;
3131541Srgrimes        }
31498542Smckusick
3153487Sphk        @SuppressWarnings("try")
3161541Srgrimes        protected void checkPermissionAndClose() throws IOException {
3171541Srgrimes            try (InputStream in = url.openStream()) {
3181541Srgrimes                // empty
3191541Srgrimes            }
3201541Srgrimes            debug("permission checked for ", url);
3211541Srgrimes        }
3221541Srgrimes
3231541Srgrimes        protected void load() throws IOException {
324140704Sjeff            if (array == null) {
32598542Smckusick                final URLConnection c = url.openConnection();
3263487Sphk                try (InputStream in = c.getInputStream()) {
3271541Srgrimes                    array = cs == null ? readFully(in) : readFully(in, cs);
32823560Smpp                    length = array.length;
329166924Sbrian                    lastModified = c.getLastModified();
330166924Sbrian                    debug("loaded content for ", url);
331187790Srwatson                }
332187790Srwatson            }
333187790Srwatson        }
334187790Srwatson
3357399Sdg        protected void loadMeta() throws IOException {
3361541Srgrimes            if (length == 0 && lastModified == 0) {
337248521Skib                final URLConnection c = url.openConnection();
338192260Salc                length = c.getContentLength();
339192260Salc                lastModified = c.getLastModified();
3401541Srgrimes                debug("loaded metadata for ", url);
3411541Srgrimes            }
3421541Srgrimes        }
3431541Srgrimes    }
3441541Srgrimes
3451541Srgrimes    private static class FileData extends URLData {
3461541Srgrimes        private final File file;
3471541Srgrimes
3481541Srgrimes        private FileData(final File file, final Charset cs) {
3491541Srgrimes            super(getURLFromFile(file), cs);
3501541Srgrimes            this.file = file;
3518876Srgrimes
3528876Srgrimes        }
3531541Srgrimes
3541541Srgrimes        @Override
3551541Srgrimes        protected void checkPermissionAndClose() throws IOException {
3561541Srgrimes            if (!file.canRead()) {
3571541Srgrimes                throw new FileNotFoundException(file + " (Permission Denied)");
3586993Sdg            }
3591541Srgrimes            debug("permission checked for ", file);
36058087Smckusick        }
3611541Srgrimes
3621541Srgrimes        @Override
3631541Srgrimes        protected void loadMeta() {
3641541Srgrimes            if (length == 0 && lastModified == 0) {
3651541Srgrimes                length = (int) file.length();
3661541Srgrimes                lastModified = file.lastModified();
3671541Srgrimes                debug("loaded metadata for ", file);
3681541Srgrimes            }
3691541Srgrimes        }
3701541Srgrimes
3711541Srgrimes        @Override
3721541Srgrimes        protected void load() throws IOException {
3731541Srgrimes            if (array == null) {
3741541Srgrimes                array = cs == null ? readFully(file) : readFully(file, cs);
3751541Srgrimes                length = array.length;
3761541Srgrimes                lastModified = file.lastModified();
3771541Srgrimes                debug("loaded content for ", file);
3781541Srgrimes            }
37958087Smckusick        }
3801541Srgrimes    }
3811541Srgrimes
3821541Srgrimes    private static void debug(final Object... msg) {
3831541Srgrimes        final DebugLogger logger = getLoggerStatic();
3841541Srgrimes        if (logger != null) {
3851541Srgrimes            logger.info(msg);
38650253Sbde        }
38750253Sbde    }
3881541Srgrimes
3891541Srgrimes    private char[] data() {
3901541Srgrimes        return data.array();
391207141Sjeff    }
3921541Srgrimes
3931541Srgrimes    /**
39489637Smckusick     * Returns a Source instance
395140704Sjeff     *
396223127Smckusick     * @param name    source name
397166924Sbrian     * @param content contents as char array
398166924Sbrian     * @param isEval does this represent code from 'eval' call?
399187790Srwatson     * @return source instance
400187790Srwatson     */
401187790Srwatson    public static Source sourceFor(final String name, final char[] content, final boolean isEval) {
402187790Srwatson        return new Source(name, baseName(name), new RawData(content, isEval));
4037399Sdg    }
4041541Srgrimes
405248521Skib    /**
406192260Salc     * Returns a Source instance
407192260Salc     *
4081541Srgrimes     * @param name    source name
4091541Srgrimes     * @param content contents as char array
4101541Srgrimes     *
4111541Srgrimes     * @return source instance
412140704Sjeff     */
4131541Srgrimes    public static Source sourceFor(final String name, final char[] content) {
4141541Srgrimes        return sourceFor(name, content, false);
4151541Srgrimes    }
41698542Smckusick
417140704Sjeff    /**
4181541Srgrimes     * Returns a Source instance
4191541Srgrimes     *
4201541Srgrimes     * @param name    source name
4211541Srgrimes     * @param content contents as string
4221541Srgrimes     * @param isEval does this represent code from 'eval' call?
423222958Sjeff     * @return source instance
42489637Smckusick     */
425140704Sjeff    public static Source sourceFor(final String name, final String content, final boolean isEval) {
426203818Skib        return new Source(name, baseName(name), new RawData(content, isEval));
427140704Sjeff    }
428203818Skib
429203818Skib    /**
430140704Sjeff     * Returns a Source instance
431222724Smckusick     *
43289637Smckusick     * @param name    source name
43389637Smckusick     * @param content contents as string
434140704Sjeff     * @return source instance
435140704Sjeff     */
436140704Sjeff    public static Source sourceFor(final String name, final String content) {
437223114Smckusick        return sourceFor(name, content, false);
438151906Sps    }
439151906Sps
440151906Sps    /**
441151906Sps     * Constructor
4421541Srgrimes     *
4431541Srgrimes     * @param name  source name
4441541Srgrimes     * @param url   url from which source can be loaded
4451541Srgrimes     *
4461541Srgrimes     * @return source instance
4471541Srgrimes     *
4481541Srgrimes     * @throws IOException if source cannot be loaded
4491541Srgrimes     */
45098542Smckusick    public static Source sourceFor(final String name, final URL url) throws IOException {
45198542Smckusick        return sourceFor(name, url, null);
45298542Smckusick    }
45398542Smckusick
45498542Smckusick    /**
45598542Smckusick     * Constructor
45698542Smckusick     *
45798542Smckusick     * @param name  source name
4581541Srgrimes     * @param url   url from which source can be loaded
45974548Smckusick     * @param cs    Charset used to convert bytes to chars
46074548Smckusick     *
46174548Smckusick     * @return source instance
46212911Sphk     *
46374548Smckusick     * @throws IOException if source cannot be loaded
46422521Sdyson     */
46531352Sbde    public static Source sourceFor(final String name, final URL url, final Charset cs) throws IOException {
46674548Smckusick        return sourceFor(name, baseURL(url), new URLData(url, cs));
46722521Sdyson    }
46842351Sbde
46942351Sbde    /**
47042351Sbde     * Constructor
47131351Sbde     *
4721541Srgrimes     * @param name  source name
4731541Srgrimes     * @param file  file from which source can be loaded
4741541Srgrimes     *
4751541Srgrimes     * @return source instance
4761541Srgrimes     *
4771541Srgrimes     * @throws IOException if source cannot be loaded
4781541Srgrimes     */
47998542Smckusick    public static Source sourceFor(final String name, final File file) throws IOException {
48098542Smckusick        return sourceFor(name, file, null);
48198542Smckusick    }
482207141Sjeff
483207141Sjeff    /**
484207141Sjeff     * Constructor
485207141Sjeff     *
486207141Sjeff     * @param name  source name
487207141Sjeff     * @param file  file from which source can be loaded
488207141Sjeff     * @param cs    Charset used to convert bytes to chars
489207141Sjeff     *
49098542Smckusick     * @return source instance
49198542Smckusick     *
49298542Smckusick     * @throws IOException if source cannot be loaded
49398542Smckusick     */
49498542Smckusick    public static Source sourceFor(final String name, final File file, final Charset cs) throws IOException {
49598542Smckusick        final File absFile = file.getAbsoluteFile();
49698542Smckusick        return sourceFor(name, dirName(absFile, null), new FileData(file, cs));
49798542Smckusick    }
49898542Smckusick
49998542Smckusick    /**
50098542Smckusick     * Returns an instance
50198542Smckusick     *
5021541Srgrimes     * @param name source name
5031541Srgrimes     * @param reader reader from which source can be loaded
5041541Srgrimes     *
5051541Srgrimes     * @return source instance
50698542Smckusick     *
5071541Srgrimes     * @throws IOException if source cannot be loaded
508140704Sjeff     */
50998542Smckusick    public static Source sourceFor(final String name, final Reader reader) throws IOException {
51098542Smckusick        // Extract URL from URLReader to defer loading and reuse cached data if available.
51198542Smckusick        if (reader instanceof URLReader) {
5121541Srgrimes            final URLReader urlReader = (URLReader) reader;
51398542Smckusick            return sourceFor(name, urlReader.getURL(), urlReader.getCharset());
5141541Srgrimes        }
5151541Srgrimes        return new Source(name, baseName(name), new RawData(reader));
5161541Srgrimes    }
5171541Srgrimes
518140704Sjeff    @Override
519254995Smckusick    public boolean equals(final Object obj) {
520260828Smckusick        if (this == obj) {
521254995Smckusick            return true;
522254995Smckusick        }
523254995Smckusick        if (!(obj instanceof Source)) {
524254995Smckusick            return false;
525260828Smckusick        }
5261541Srgrimes        final Source other = (Source) obj;
5271541Srgrimes        return Objects.equals(name, other.name) && data.equals(other.data);
5281541Srgrimes    }
5291541Srgrimes
5301541Srgrimes    @Override
531173464Sobrien    public int hashCode() {
53222521Sdyson        int h = hash;
53322521Sdyson        if (h == 0) {
53422521Sdyson            h = hash = data.hashCode() ^ Objects.hashCode(name);
53522521Sdyson        }
5361541Srgrimes        return h;
5371541Srgrimes    }
53822521Sdyson
53922521Sdyson    /**
54022521Sdyson     * Fetch source content.
54122521Sdyson     * @return Source content.
54222521Sdyson     */
54322521Sdyson    public String getString() {
5441541Srgrimes        return data.toString();
5451541Srgrimes    }
546242520Smckusick
547242520Smckusick    /**
548242520Smckusick     * Get the user supplied name of this script.
549242520Smckusick     * @return User supplied source name.
550242520Smckusick     */
551242520Smckusick    public String getName() {
552242520Smckusick        return name;
553242520Smckusick    }
554242520Smckusick
555242520Smckusick    /**
556242520Smckusick     * Get the last modified time of this script.
557242520Smckusick     * @return Last modified time.
5581541Srgrimes     */
5591541Srgrimes    public long getLastModified() {
5601541Srgrimes        return data.lastModified();
5611541Srgrimes    }
5621541Srgrimes
5631541Srgrimes    /**
5641541Srgrimes     * Get the "directory" part of the file or "base" of the URL.
5651541Srgrimes     * @return base of file or URL.
5661541Srgrimes     */
5671541Srgrimes    public String getBase() {
5681541Srgrimes        return base;
5691541Srgrimes    }
5701541Srgrimes
5711541Srgrimes    /**
57298542Smckusick     * Fetch a portion of source content.
5731541Srgrimes     * @param start start index in source
5741541Srgrimes     * @param len length of portion
5751541Srgrimes     * @return Source content portion.
5761541Srgrimes     */
5771541Srgrimes    public String getString(final int start, final int len) {
5781541Srgrimes        return new String(data(), start, len);
5791541Srgrimes    }
58098542Smckusick
5811541Srgrimes    /**
5821541Srgrimes     * Fetch a portion of source content associated with a token.
5831541Srgrimes     * @param token Token descriptor.
5841541Srgrimes     * @return Source content portion.
5851541Srgrimes     */
5861541Srgrimes    public String getString(final long token) {
5871541Srgrimes        final int start = Token.descPosition(token);
5881541Srgrimes        final int len = Token.descLength(token);
589173464Sobrien        return new String(data(), start, len);
590174126Skensmith    }
591174126Skensmith
5921541Srgrimes    /**
5931541Srgrimes     * Returns the source URL of this script Source. Can be null if Source
5941541Srgrimes     * was created from a String or a char[].
5951541Srgrimes     *
5961541Srgrimes     * @return URL source or null
59798542Smckusick     */
5981541Srgrimes    public URL getURL() {
5991541Srgrimes        return data.url();
600140704Sjeff    }
601140704Sjeff
602140704Sjeff    /**
603140704Sjeff     * Get explicit source URL.
604140704Sjeff     * @return URL set vial sourceURL directive
6051541Srgrimes     */
6061541Srgrimes    public String getExplicitURL() {
60798542Smckusick        return explicitURL;
608207141Sjeff    }
609140704Sjeff
6101541Srgrimes    /**
611140704Sjeff     * Set explicit source URL.
6121541Srgrimes     * @param explicitURL URL set via sourceURL directive
6131541Srgrimes     */
6141541Srgrimes    public void setExplicitURL(final String explicitURL) {
6151541Srgrimes        this.explicitURL = explicitURL;
6161541Srgrimes    }
6171541Srgrimes
6181541Srgrimes    /**
61922521Sdyson     * Returns whether this source was submitted via 'eval' call or not.
62022521Sdyson     *
621241011Smdf     * @return true if this source represents code submitted via 'eval'
622241011Smdf     */
62398542Smckusick    public boolean isEvalCode() {
62422521Sdyson        return data.isEvalCode();
6251541Srgrimes    }
6261541Srgrimes
62734266Sjulian    /**
6281541Srgrimes     * Find the beginning of the line containing position.
62934266Sjulian     * @param position Index to offending token.
63034266Sjulian     * @return Index of first character of line.
631173464Sobrien     */
63222521Sdyson    private int findBOLN(final int position) {
63322521Sdyson        final char[] d = data();
63422521Sdyson        for (int i = position - 1; i > 0; i--) {
63522521Sdyson            final char ch = d[i];
6361541Srgrimes
6371541Srgrimes            if (ch == '\n' || ch == '\r') {
63822521Sdyson                return i + 1;
63922521Sdyson            }
64022521Sdyson        }
64122521Sdyson
64234266Sjulian        return 0;
64398542Smckusick    }
64434266Sjulian
64534266Sjulian    /**
64634266Sjulian     * Find the end of the line containing position.
64734266Sjulian     * @param position Index to offending token.
64834266Sjulian     * @return Index of last character of line.
64934266Sjulian     */
65034266Sjulian    private int findEOLN(final int position) {
65134266Sjulian        final char[] d = data();
6521541Srgrimes        final int length = d.length;
6531541Srgrimes        for (int i = position; i < length; i++) {
6541541Srgrimes            final char ch = d[i];
6551541Srgrimes
6561541Srgrimes            if (ch == '\n' || ch == '\r') {
6571541Srgrimes                return i - 1;
6588876Srgrimes            }
6591541Srgrimes        }
6601541Srgrimes
6611541Srgrimes        return length - 1;
6621541Srgrimes    }
6631541Srgrimes
6641541Srgrimes    /**
6651541Srgrimes     * Return line number of character position.
6661541Srgrimes     *
6671541Srgrimes     * <p>This method can be expensive for large sources as it iterates through
66898542Smckusick     * all characters up to {@code position}.</p>
6691541Srgrimes     *
6701541Srgrimes     * @param position Position of character in source content.
6711541Srgrimes     * @return Line number.
6721541Srgrimes     */
6731541Srgrimes    public int getLine(final int position) {
6741541Srgrimes        final char[] d = data();
67542374Sbde        // Line count starts at 1.
676141526Sphk        int line = 1;
6771541Srgrimes
67846568Speter        for (int i = 0; i < position; i++) {
6791541Srgrimes            final char ch = d[i];
6801541Srgrimes            // Works for both \n and \r\n.
6811541Srgrimes            if (ch == '\n') {
6821541Srgrimes                line++;
68346568Speter            }
6841541Srgrimes        }
6851541Srgrimes
6861541Srgrimes        return line;
68722521Sdyson    }
68822521Sdyson
68922521Sdyson    /**
69022521Sdyson     * Return column number of character position.
6911541Srgrimes     * @param position Position of character in source content.
69234266Sjulian     * @return Column number.
693140704Sjeff     */
69434266Sjulian    public int getColumn(final int position) {
695223127Smckusick        // TODO - column needs to account for tabs.
6961541Srgrimes        return position - findBOLN(position);
697173464Sobrien    }
69822521Sdyson
69922521Sdyson    /**
70022521Sdyson     * Return line text including character position.
70150305Ssheldonh     * @param position Position of character in source content.
70250305Ssheldonh     * @return Line text.
70322521Sdyson     */
70422521Sdyson    public String getSourceLine(final int position) {
70522521Sdyson        // Find end of previous line.
7061541Srgrimes        final int first = findBOLN(position);
70722521Sdyson        // Find end of this line.
70822521Sdyson        final int last = findEOLN(position);
70922521Sdyson
71022521Sdyson        return new String(data(), first, last - first + 1);
71122521Sdyson    }
71222521Sdyson
7131541Srgrimes    /**
7141541Srgrimes     * Get the content of this source as a char array
7151541Srgrimes     * @return content
7161541Srgrimes     */
7171541Srgrimes    public char[] getContent() {
71898542Smckusick        return data().clone();
7191541Srgrimes    }
7201541Srgrimes
7211541Srgrimes    /**
7221541Srgrimes     * Get the length in chars for this source
72398542Smckusick     * @return length
72498542Smckusick     */
72598542Smckusick    public int getLength() {
72698542Smckusick        return data.length();
72798542Smckusick    }
72898542Smckusick
72998542Smckusick    /**
73098542Smckusick     * Read all of the source until end of file. Return it as char array
73198542Smckusick     *
73298542Smckusick     * @param reader reader opened to source stream
73398542Smckusick     * @return source as content
73498542Smckusick     * @throws IOException if source could not be read
73598542Smckusick     */
736140704Sjeff    public static char[] readFully(final Reader reader) throws IOException {
73798542Smckusick        final char[]        arr = new char[BUF_SIZE];
73898542Smckusick        final StringBuilder sb  = new StringBuilder();
73998542Smckusick
74098542Smckusick        try {
74198542Smckusick            int numChars;
74298542Smckusick            while ((numChars = reader.read(arr, 0, arr.length)) > 0) {
74398542Smckusick                sb.append(arr, 0, numChars);
74498542Smckusick            }
745140704Sjeff        } finally {
746254995Smckusick            reader.close();
747260828Smckusick        }
748254995Smckusick
749254995Smckusick        return sb.toString().toCharArray();
750254995Smckusick    }
751254995Smckusick
752260828Smckusick    /**
75398542Smckusick     * Read all of the source until end of file. Return it as char array
75498542Smckusick     *
75598542Smckusick     * @param file source file
75698542Smckusick     * @return source as content
75798542Smckusick     * @throws IOException if source could not be read
758173464Sobrien     */
75998542Smckusick    public static char[] readFully(final File file) throws IOException {
76098542Smckusick        if (!file.isFile()) {
76198542Smckusick            throw new IOException(file + " is not a file"); //TODO localize?
76298542Smckusick        }
76398542Smckusick        return byteToCharArray(Files.readAllBytes(file.toPath()));
76498542Smckusick    }
76598542Smckusick
76698542Smckusick    /**
76798542Smckusick     * Read all of the source until end of file. Return it as char array
76898542Smckusick     *
76998542Smckusick     * @param file source file
77098542Smckusick     * @param cs Charset used to convert bytes to chars
77198542Smckusick     * @return source as content
77298542Smckusick     * @throws IOException if source could not be read
773242520Smckusick     */
774242520Smckusick    public static char[] readFully(final File file, final Charset cs) throws IOException {
775242520Smckusick        if (!file.isFile()) {
776242520Smckusick            throw new IOException(file + " is not a file"); //TODO localize?
777242520Smckusick        }
778242520Smckusick
779242520Smckusick        final byte[] buf = Files.readAllBytes(file.toPath());
780242520Smckusick        return (cs != null) ? new String(buf, cs).toCharArray() : byteToCharArray(buf);
781242520Smckusick    }
782242520Smckusick
783242520Smckusick    /**
784242520Smckusick     * Read all of the source until end of stream from the given URL. Return it as char array
78598542Smckusick     *
78698542Smckusick     * @param url URL to read content from
78798542Smckusick     * @return source as content
78898542Smckusick     * @throws IOException if source could not be read
78998542Smckusick     */
79098542Smckusick    public static char[] readFully(final URL url) throws IOException {
79198542Smckusick        return readFully(url.openStream());
79298542Smckusick    }
79398542Smckusick
79498542Smckusick    /**
79598542Smckusick     * Read all of the source until end of file. Return it as char array
79698542Smckusick     *
79798542Smckusick     * @param url URL to read content from
79898542Smckusick     * @param cs Charset used to convert bytes to chars
79998542Smckusick     * @return source as content
80098542Smckusick     * @throws IOException if source could not be read
80198542Smckusick     */
80298542Smckusick    public static char[] readFully(final URL url, final Charset cs) throws IOException {
80398542Smckusick        return readFully(url.openStream(), cs);
80498542Smckusick    }
80598542Smckusick
80698542Smckusick    /**
80798542Smckusick     * Get a Base64-encoded SHA1 digest for this source.
80898542Smckusick     *
80998542Smckusick     * @return a Base64-encoded SHA1 digest for this source
81098542Smckusick     */
81198542Smckusick    public String getDigest() {
81298542Smckusick        return new String(getDigestBytes(), StandardCharsets.US_ASCII);
81398542Smckusick    }
81498542Smckusick
81598542Smckusick    private byte[] getDigestBytes() {
816173464Sobrien        byte[] ldigest = digest;
817174126Skensmith        if (ldigest == null) {
818174126Skensmith            final char[] content = data();
81998542Smckusick            final byte[] bytes = new byte[content.length * 2];
82098542Smckusick
82198542Smckusick            for (int i = 0; i < content.length; i++) {
82298542Smckusick                bytes[i * 2]     = (byte)  (content[i] & 0x00ff);
82398542Smckusick                bytes[i * 2 + 1] = (byte) ((content[i] & 0xff00) >> 8);
82498542Smckusick            }
82598542Smckusick
82698542Smckusick            try {
827140704Sjeff                final MessageDigest md = MessageDigest.getInstance("SHA-1");
828140704Sjeff                if (name != null) {
829140704Sjeff                    md.update(name.getBytes(StandardCharsets.UTF_8));
830140704Sjeff                }
831140704Sjeff                if (base != null) {
83298542Smckusick                    md.update(base.getBytes(StandardCharsets.UTF_8));
83398542Smckusick                }
83498542Smckusick                if (getURL() != null) {
835207141Sjeff                    md.update(getURL().toString().getBytes(StandardCharsets.UTF_8));
836140704Sjeff                }
83798542Smckusick                digest = ldigest = BASE64.encode(md.digest(bytes));
838140704Sjeff            } catch (final NoSuchAlgorithmException e) {
83998542Smckusick                throw new RuntimeException(e);
84098542Smckusick            }
84198542Smckusick        }
84298542Smckusick        return ldigest;
84398542Smckusick    }
84498542Smckusick
84598542Smckusick    /**
84698542Smckusick     * Get the base url. This is currently used for testing only
84798542Smckusick     * @param url a URL
848103594Sobrien     * @return base URL for url
84998542Smckusick     */
85098542Smckusick    public static String baseURL(final URL url) {
85198542Smckusick        if (url.getProtocol().equals("file")) {
85298542Smckusick            try {
85398542Smckusick                final Path path = Paths.get(url.toURI());
85498542Smckusick                final Path parent = path.getParent();
85598542Smckusick                return (parent != null) ? (parent + File.separator) : null;
85698542Smckusick            } catch (final SecurityException | URISyntaxException | IOError e) {
857173464Sobrien                return null;
85898542Smckusick            }
85998542Smckusick        }
86098542Smckusick
86198542Smckusick        // FIXME: is there a better way to find 'base' URL of a given URL?
86298542Smckusick        String path = url.getPath();
86398542Smckusick        if (path.isEmpty()) {
86498542Smckusick            return null;
86598542Smckusick        }
866103594Sobrien        path = path.substring(0, path.lastIndexOf('/') + 1);
86798542Smckusick        final int port = url.getPort();
86898542Smckusick        try {
86998542Smckusick            return new URL(url.getProtocol(), url.getHost(), port, path).toString();
87098542Smckusick        } catch (final MalformedURLException e) {
87198542Smckusick            return null;
87298542Smckusick        }
87398542Smckusick    }
87498542Smckusick
87598542Smckusick    private static String dirName(final File file, final String DEFAULT_BASE_NAME) {
87698542Smckusick        final String res = file.getParent();
87798542Smckusick        return (res != null) ? (res + File.separator) : DEFAULT_BASE_NAME;
87898542Smckusick    }
87998542Smckusick
88098542Smckusick    // fake directory like name
88198542Smckusick    private static String baseName(final String name) {
88298542Smckusick        int idx = name.lastIndexOf('/');
88398542Smckusick        if (idx == -1) {
88498542Smckusick            idx = name.lastIndexOf('\\');
88598542Smckusick        }
88698542Smckusick        return (idx != -1) ? name.substring(0, idx + 1) : null;
88798542Smckusick    }
88898542Smckusick
88998542Smckusick    private static char[] readFully(final InputStream is, final Charset cs) throws IOException {
89098542Smckusick        return (cs != null) ? new String(readBytes(is), cs).toCharArray() : readFully(is);
89198542Smckusick    }
89298542Smckusick
89398542Smckusick    private static char[] readFully(final InputStream is) throws IOException {
89498542Smckusick        return byteToCharArray(readBytes(is));
89598542Smckusick    }
89698542Smckusick
89798542Smckusick    private static char[] byteToCharArray(final byte[] bytes) {
89898542Smckusick        Charset cs = StandardCharsets.UTF_8;
89998542Smckusick        int start = 0;
90098542Smckusick        // BOM detection.
90198542Smckusick        if (bytes.length > 1 && bytes[0] == (byte) 0xFE && bytes[1] == (byte) 0xFF) {
902141526Sphk            start = 2;
90398542Smckusick            cs = StandardCharsets.UTF_16BE;
90498542Smckusick        } else if (bytes.length > 1 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE) {
90598542Smckusick            start = 2;
90698542Smckusick            cs = StandardCharsets.UTF_16LE;
90798542Smckusick        } else if (bytes.length > 2 && bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) {
90898542Smckusick            start = 3;
90998542Smckusick            cs = StandardCharsets.UTF_8;
91098542Smckusick        } else if (bytes.length > 3 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE && bytes[2] == 0 && bytes[3] == 0) {
91198542Smckusick            start = 4;
91298542Smckusick            cs = Charset.forName("UTF-32LE");
91398542Smckusick        } else if (bytes.length > 3 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == (byte) 0xFE && bytes[3] == (byte) 0xFF) {
91498542Smckusick            start = 4;
91598542Smckusick            cs = Charset.forName("UTF-32BE");
91698542Smckusick        }
91798542Smckusick
91898542Smckusick        return new String(bytes, start, bytes.length - start, cs).toCharArray();
919140704Sjeff    }
92098542Smckusick
921223127Smckusick    static byte[] readBytes(final InputStream is) throws IOException {
92298542Smckusick        final byte[] arr = new byte[BUF_SIZE];
923173464Sobrien        try {
92498542Smckusick            try (ByteArrayOutputStream buf = new ByteArrayOutputStream()) {
92598542Smckusick                int numBytes;
92698542Smckusick                while ((numBytes = is.read(arr, 0, arr.length)) > 0) {
92798542Smckusick                    buf.write(arr, 0, numBytes);
92898542Smckusick                }
92998542Smckusick                return buf.toByteArray();
93099590Sbde            }
93198542Smckusick        } finally {
93298542Smckusick            is.close();
93398542Smckusick        }
93498542Smckusick    }
93598542Smckusick
93698542Smckusick    @Override
93798542Smckusick    public String toString() {
93898542Smckusick        return getName();
93998542Smckusick    }
94098542Smckusick
94198542Smckusick    private static URL getURLFromFile(final File file) {
94298542Smckusick        try {
94398542Smckusick            return file.toURI().toURL();
94498542Smckusick        } catch (final SecurityException | MalformedURLException ignored) {
94598542Smckusick            return null;
94698542Smckusick        }
94798542Smckusick    }
94898542Smckusick
9491541Srgrimes    private static DebugLogger getLoggerStatic() {
95096755Strhodes        final Context context = Context.getContextTrustedOrNull();
9518876Srgrimes        return context == null ? null : context.getLogger(Source.class);
9521541Srgrimes    }
9531541Srgrimes
9541541Srgrimes    @Override
9551541Srgrimes    public DebugLogger initLogger(final Context context) {
9561541Srgrimes        return context.getLogger(this.getClass());
9571541Srgrimes    }
958166051Smpp
9591541Srgrimes    @Override
9601541Srgrimes    public DebugLogger getLogger() {
9611541Srgrimes        return initLogger(Context.getContextTrusted());
9621541Srgrimes    }
9631541Srgrimes}
9641549Srgrimes