| /* |
| * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package jdk.nashorn.internal.runtime; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOError; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.PrintWriter; |
| import java.io.Reader; |
| import java.lang.ref.WeakReference; |
| import java.net.MalformedURLException; |
| import java.net.URISyntaxException; |
| import java.net.URL; |
| import java.net.URLConnection; |
| import java.nio.charset.Charset; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.time.LocalDateTime; |
| import java.util.Arrays; |
| import java.util.Base64; |
| import java.util.Objects; |
| import java.util.WeakHashMap; |
| import jdk.nashorn.api.scripting.URLReader; |
| import jdk.nashorn.internal.parser.Token; |
| import jdk.nashorn.internal.runtime.logging.DebugLogger; |
| import jdk.nashorn.internal.runtime.logging.Loggable; |
| import jdk.nashorn.internal.runtime.logging.Logger; |
| /** |
| * Source objects track the origin of JavaScript entities. |
| */ |
| @Logger(name="source") |
| public final class Source implements Loggable { |
| private static final int BUF_SIZE = 8 * 1024; |
| private static final Cache CACHE = new Cache(); |
| |
| // Message digest to file name encoder |
| private final static Base64.Encoder BASE64 = Base64.getUrlEncoder().withoutPadding(); |
| |
| /** |
| * Descriptive name of the source as supplied by the user. Used for error |
| * reporting to the user. For example, SyntaxError will use this to print message. |
| * Used to implement __FILE__. Also used for SourceFile in .class for debugger usage. |
| */ |
| private final String name; |
| |
| /** |
| * Base directory the File or base part of the URL. Used to implement __DIR__. |
| * Used to load scripts relative to the 'directory' or 'base' URL of current script. |
| * This will be null when it can't be computed. |
| */ |
| private final String base; |
| |
| /** Source content */ |
| private final Data data; |
| |
| /** Cached hash code */ |
| private int hash; |
| |
| /** Base64-encoded SHA1 digest of this source object */ |
| private volatile byte[] digest; |
| |
| /** source URL set via //@ sourceURL or //# sourceURL directive */ |
| private String explicitURL; |
| |
| // Do *not* make this public, ever! Trusts the URL and content. |
| private Source(final String name, final String base, final Data data) { |
| this.name = name; |
| this.base = base; |
| this.data = data; |
| } |
| |
| private static synchronized Source sourceFor(final String name, final String base, final URLData data) throws IOException { |
| try { |
| final Source newSource = new Source(name, base, data); |
| final Source existingSource = CACHE.get(newSource); |
| if (existingSource != null) { |
| // Force any access errors |
| data.checkPermissionAndClose(); |
| return existingSource; |
| } |
| |
| // All sources in cache must be fully loaded |
| data.load(); |
| CACHE.put(newSource, newSource); |
| |
| return newSource; |
| } catch (final RuntimeException e) { |
| final Throwable cause = e.getCause(); |
| if (cause instanceof IOException) { |
| throw (IOException) cause; |
| } |
| throw e; |
| } |
| } |
| |
| private static class Cache extends WeakHashMap<Source, WeakReference<Source>> { |
| public Source get(final Source key) { |
| final WeakReference<Source> ref = super.get(key); |
| return ref == null ? null : ref.get(); |
| } |
| |
| public void put(final Source key, final Source value) { |
| assert !(value.data instanceof RawData); |
| put(key, new WeakReference<>(value)); |
| } |
| } |
| |
| /* package-private */ |
| DebuggerSupport.SourceInfo getSourceInfo() { |
| return new DebuggerSupport.SourceInfo(getName(), data.hashCode(), data.url(), data.array()); |
| } |
| |
| // Wrapper to manage lazy loading |
| private static interface Data { |
| |
| URL url(); |
| |
| int length(); |
| |
| long lastModified(); |
| |
| char[] array(); |
| |
| boolean isEvalCode(); |
| } |
| |
| private static class RawData implements Data { |
| private final char[] array; |
| private final boolean evalCode; |
| private int hash; |
| |
| private RawData(final char[] array, final boolean evalCode) { |
| this.array = Objects.requireNonNull(array); |
| this.evalCode = evalCode; |
| } |
| |
| private RawData(final String source, final boolean evalCode) { |
| this.array = Objects.requireNonNull(source).toCharArray(); |
| this.evalCode = evalCode; |
| } |
| |
| private RawData(final Reader reader) throws IOException { |
| this(readFully(reader), false); |
| } |
| |
| @Override |
| public int hashCode() { |
| int h = hash; |
| if (h == 0) { |
| h = hash = Arrays.hashCode(array) ^ (evalCode? 1 : 0); |
| } |
| return h; |
| } |
| |
| @Override |
| public boolean equals(final Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (obj instanceof RawData) { |
| final RawData other = (RawData)obj; |
| return Arrays.equals(array, other.array) && evalCode == other.evalCode; |
| } |
| return false; |
| } |
| |
| @Override |
| public String toString() { |
| return new String(array()); |
| } |
| |
| @Override |
| public URL url() { |
| return null; |
| } |
| |
| @Override |
| public int length() { |
| return array.length; |
| } |
| |
| @Override |
| public long lastModified() { |
| return 0; |
| } |
| |
| @Override |
| public char[] array() { |
| return array; |
| } |
| |
| |
| @Override |
| public boolean isEvalCode() { |
| return evalCode; |
| } |
| } |
| |
| private static class URLData implements Data { |
| private final URL url; |
| protected final Charset cs; |
| private int hash; |
| protected char[] array; |
| protected int length; |
| protected long lastModified; |
| |
| private URLData(final URL url, final Charset cs) { |
| this.url = Objects.requireNonNull(url); |
| this.cs = cs; |
| } |
| |
| @Override |
| public int hashCode() { |
| int h = hash; |
| if (h == 0) { |
| h = hash = url.hashCode(); |
| } |
| return h; |
| } |
| |
| @Override |
| public boolean equals(final Object other) { |
| if (this == other) { |
| return true; |
| } |
| if (!(other instanceof URLData)) { |
| return false; |
| } |
| |
| final URLData otherData = (URLData) other; |
| |
| if (url.equals(otherData.url)) { |
| // Make sure both have meta data loaded |
| try { |
| if (isDeferred()) { |
| // Data in cache is always loaded, and we only compare to cached data. |
| assert !otherData.isDeferred(); |
| loadMeta(); |
| } else if (otherData.isDeferred()) { |
| otherData.loadMeta(); |
| } |
| } catch (final IOException e) { |
| throw new RuntimeException(e); |
| } |
| |
| // Compare meta data |
| return this.length == otherData.length && this.lastModified == otherData.lastModified; |
| } |
| return false; |
| } |
| |
| @Override |
| public String toString() { |
| return new String(array()); |
| } |
| |
| @Override |
| public URL url() { |
| return url; |
| } |
| |
| @Override |
| public int length() { |
| return length; |
| } |
| |
| @Override |
| public long lastModified() { |
| return lastModified; |
| } |
| |
| @Override |
| public char[] array() { |
| assert !isDeferred(); |
| return array; |
| } |
| |
| @Override |
| public boolean isEvalCode() { |
| return false; |
| } |
| |
| boolean isDeferred() { |
| return array == null; |
| } |
| |
| @SuppressWarnings("try") |
| protected void checkPermissionAndClose() throws IOException { |
| try (InputStream in = url.openStream()) { |
| // empty |
| } |
| debug("permission checked for ", url); |
| } |
| |
| protected void load() throws IOException { |
| if (array == null) { |
| final URLConnection c = url.openConnection(); |
| try (InputStream in = c.getInputStream()) { |
| array = cs == null ? readFully(in) : readFully(in, cs); |
| length = array.length; |
| lastModified = c.getLastModified(); |
| debug("loaded content for ", url); |
| } |
| } |
| } |
| |
| protected void loadMeta() throws IOException { |
| if (length == 0 && lastModified == 0) { |
| final URLConnection c = url.openConnection(); |
| length = c.getContentLength(); |
| lastModified = c.getLastModified(); |
| debug("loaded metadata for ", url); |
| } |
| } |
| } |
| |
| private static class FileData extends URLData { |
| private final File file; |
| |
| private FileData(final File file, final Charset cs) { |
| super(getURLFromFile(file), cs); |
| this.file = file; |
| |
| } |
| |
| @Override |
| protected void checkPermissionAndClose() throws IOException { |
| if (!file.canRead()) { |
| throw new FileNotFoundException(file + " (Permission Denied)"); |
| } |
| debug("permission checked for ", file); |
| } |
| |
| @Override |
| protected void loadMeta() { |
| if (length == 0 && lastModified == 0) { |
| length = (int) file.length(); |
| lastModified = file.lastModified(); |
| debug("loaded metadata for ", file); |
| } |
| } |
| |
| @Override |
| protected void load() throws IOException { |
| if (array == null) { |
| array = cs == null ? readFully(file) : readFully(file, cs); |
| length = array.length; |
| lastModified = file.lastModified(); |
| debug("loaded content for ", file); |
| } |
| } |
| } |
| |
| private static void debug(final Object... msg) { |
| final DebugLogger logger = getLoggerStatic(); |
| if (logger != null) { |
| logger.info(msg); |
| } |
| } |
| |
| private char[] data() { |
| return data.array(); |
| } |
| |
| /** |
| * Returns a Source instance |
| * |
| * @param name source name |
| * @param content contents as char array |
| * @param isEval does this represent code from 'eval' call? |
| * @return source instance |
| */ |
| public static Source sourceFor(final String name, final char[] content, final boolean isEval) { |
| return new Source(name, baseName(name), new RawData(content, isEval)); |
| } |
| |
| /** |
| * Returns a Source instance |
| * |
| * @param name source name |
| * @param content contents as char array |
| * |
| * @return source instance |
| */ |
| public static Source sourceFor(final String name, final char[] content) { |
| return sourceFor(name, content, false); |
| } |
| |
| /** |
| * Returns a Source instance |
| * |
| * @param name source name |
| * @param content contents as string |
| * @param isEval does this represent code from 'eval' call? |
| * @return source instance |
| */ |
| public static Source sourceFor(final String name, final String content, final boolean isEval) { |
| return new Source(name, baseName(name), new RawData(content, isEval)); |
| } |
| |
| /** |
| * Returns a Source instance |
| * |
| * @param name source name |
| * @param content contents as string |
| * @return source instance |
| */ |
| public static Source sourceFor(final String name, final String content) { |
| return sourceFor(name, content, false); |
| } |
| |
| /** |
| * Constructor |
| * |
| * @param name source name |
| * @param url url from which source can be loaded |
| * |
| * @return source instance |
| * |
| * @throws IOException if source cannot be loaded |
| */ |
| public static Source sourceFor(final String name, final URL url) throws IOException { |
| return sourceFor(name, url, null); |
| } |
| |
| /** |
| * Constructor |
| * |
| * @param name source name |
| * @param url url from which source can be loaded |
| * @param cs Charset used to convert bytes to chars |
| * |
| * @return source instance |
| * |
| * @throws IOException if source cannot be loaded |
| */ |
| public static Source sourceFor(final String name, final URL url, final Charset cs) throws IOException { |
| return sourceFor(name, baseURL(url), new URLData(url, cs)); |
| } |
| |
| /** |
| * Constructor |
| * |
| * @param name source name |
| * @param file file from which source can be loaded |
| * |
| * @return source instance |
| * |
| * @throws IOException if source cannot be loaded |
| */ |
| public static Source sourceFor(final String name, final File file) throws IOException { |
| return sourceFor(name, file, null); |
| } |
| |
| /** |
| * Constructor |
| * |
| * @param name source name |
| * @param file file from which source can be loaded |
| * @param cs Charset used to convert bytes to chars |
| * |
| * @return source instance |
| * |
| * @throws IOException if source cannot be loaded |
| */ |
| public static Source sourceFor(final String name, final File file, final Charset cs) throws IOException { |
| final File absFile = file.getAbsoluteFile(); |
| return sourceFor(name, dirName(absFile, null), new FileData(file, cs)); |
| } |
| |
| /** |
| * Returns an instance |
| * |
| * @param name source name |
| * @param reader reader from which source can be loaded |
| * |
| * @return source instance |
| * |
| * @throws IOException if source cannot be loaded |
| */ |
| public static Source sourceFor(final String name, final Reader reader) throws IOException { |
| // Extract URL from URLReader to defer loading and reuse cached data if available. |
| if (reader instanceof URLReader) { |
| final URLReader urlReader = (URLReader) reader; |
| return sourceFor(name, urlReader.getURL(), urlReader.getCharset()); |
| } |
| return new Source(name, baseName(name), new RawData(reader)); |
| } |
| |
| @Override |
| public boolean equals(final Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (!(obj instanceof Source)) { |
| return false; |
| } |
| final Source other = (Source) obj; |
| return Objects.equals(name, other.name) && data.equals(other.data); |
| } |
| |
| @Override |
| public int hashCode() { |
| int h = hash; |
| if (h == 0) { |
| h = hash = data.hashCode() ^ Objects.hashCode(name); |
| } |
| return h; |
| } |
| |
| /** |
| * Fetch source content. |
| * @return Source content. |
| */ |
| public String getString() { |
| return data.toString(); |
| } |
| |
| /** |
| * Get the user supplied name of this script. |
| * @return User supplied source name. |
| */ |
| public String getName() { |
| return name; |
| } |
| |
| /** |
| * Get the last modified time of this script. |
| * @return Last modified time. |
| */ |
| public long getLastModified() { |
| return data.lastModified(); |
| } |
| |
| /** |
| * Get the "directory" part of the file or "base" of the URL. |
| * @return base of file or URL. |
| */ |
| public String getBase() { |
| return base; |
| } |
| |
| /** |
| * Fetch a portion of source content. |
| * @param start start index in source |
| * @param len length of portion |
| * @return Source content portion. |
| */ |
| public String getString(final int start, final int len) { |
| return new String(data(), start, len); |
| } |
| |
| /** |
| * Fetch a portion of source content associated with a token. |
| * @param token Token descriptor. |
| * @return Source content portion. |
| */ |
| public String getString(final long token) { |
| final int start = Token.descPosition(token); |
| final int len = Token.descLength(token); |
| return new String(data(), start, len); |
| } |
| |
| /** |
| * Returns the source URL of this script Source. Can be null if Source |
| * was created from a String or a char[]. |
| * |
| * @return URL source or null |
| */ |
| public URL getURL() { |
| return data.url(); |
| } |
| |
| /** |
| * Get explicit source URL. |
| * @return URL set vial sourceURL directive |
| */ |
| public String getExplicitURL() { |
| return explicitURL; |
| } |
| |
| /** |
| * Set explicit source URL. |
| * @param explicitURL URL set via sourceURL directive |
| */ |
| public void setExplicitURL(final String explicitURL) { |
| this.explicitURL = explicitURL; |
| } |
| |
| /** |
| * Returns whether this source was submitted via 'eval' call or not. |
| * |
| * @return true if this source represents code submitted via 'eval' |
| */ |
| public boolean isEvalCode() { |
| return data.isEvalCode(); |
| } |
| |
| /** |
| * Find the beginning of the line containing position. |
| * @param position Index to offending token. |
| * @return Index of first character of line. |
| */ |
| private int findBOLN(final int position) { |
| final char[] d = data(); |
| for (int i = position - 1; i > 0; i--) { |
| final char ch = d[i]; |
| |
| if (ch == '\n' || ch == '\r') { |
| return i + 1; |
| } |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * Find the end of the line containing position. |
| * @param position Index to offending token. |
| * @return Index of last character of line. |
| */ |
| private int findEOLN(final int position) { |
| final char[] d = data(); |
| final int length = d.length; |
| for (int i = position; i < length; i++) { |
| final char ch = d[i]; |
| |
| if (ch == '\n' || ch == '\r') { |
| return i - 1; |
| } |
| } |
| |
| return length - 1; |
| } |
| |
| /** |
| * Return line number of character position. |
| * |
| * <p>This method can be expensive for large sources as it iterates through |
| * all characters up to {@code position}.</p> |
| * |
| * @param position Position of character in source content. |
| * @return Line number. |
| */ |
| public int getLine(final int position) { |
| final char[] d = data(); |
| // Line count starts at 1. |
| int line = 1; |
| |
| for (int i = 0; i < position; i++) { |
| final char ch = d[i]; |
| // Works for both \n and \r\n. |
| if (ch == '\n') { |
| line++; |
| } |
| } |
| |
| return line; |
| } |
| |
| /** |
| * Return column number of character position. |
| * @param position Position of character in source content. |
| * @return Column number. |
| */ |
| public int getColumn(final int position) { |
| // TODO - column needs to account for tabs. |
| return position - findBOLN(position); |
| } |
| |
| /** |
| * Return line text including character position. |
| * @param position Position of character in source content. |
| * @return Line text. |
| */ |
| public String getSourceLine(final int position) { |
| // Find end of previous line. |
| final int first = findBOLN(position); |
| // Find end of this line. |
| final int last = findEOLN(position); |
| |
| return new String(data(), first, last - first + 1); |
| } |
| |
| /** |
| * Get the content of this source as a char array. Note that the underlying array is returned instead of a |
| * clone; modifying the char array will cause modification to the source; this should not be done. While |
| * there is an apparent danger that we allow unfettered access to an underlying mutable array, the |
| * {@code Source} class is in a restricted {@code jdk.nashorn.internal.*} package and as such it is |
| * inaccessible by external actors in an environment with a security manager. Returning a clone would be |
| * detrimental to performance. |
| * @return content the content of this source as a char array |
| */ |
| public char[] getContent() { |
| return data(); |
| } |
| |
| /** |
| * Get the length in chars for this source |
| * @return length |
| */ |
| public int getLength() { |
| return data.length(); |
| } |
| |
| /** |
| * Read all of the source until end of file. Return it as char array |
| * |
| * @param reader reader opened to source stream |
| * @return source as content |
| * @throws IOException if source could not be read |
| */ |
| public static char[] readFully(final Reader reader) throws IOException { |
| final char[] arr = new char[BUF_SIZE]; |
| final StringBuilder sb = new StringBuilder(); |
| |
| try { |
| int numChars; |
| while ((numChars = reader.read(arr, 0, arr.length)) > 0) { |
| sb.append(arr, 0, numChars); |
| } |
| } finally { |
| reader.close(); |
| } |
| |
| return sb.toString().toCharArray(); |
| } |
| |
| /** |
| * Read all of the source until end of file. Return it as char array |
| * |
| * @param file source file |
| * @return source as content |
| * @throws IOException if source could not be read |
| */ |
| public static char[] readFully(final File file) throws IOException { |
| if (!file.isFile()) { |
| throw new IOException(file + " is not a file"); //TODO localize? |
| } |
| return byteToCharArray(Files.readAllBytes(file.toPath())); |
| } |
| |
| /** |
| * Read all of the source until end of file. Return it as char array |
| * |
| * @param file source file |
| * @param cs Charset used to convert bytes to chars |
| * @return source as content |
| * @throws IOException if source could not be read |
| */ |
| public static char[] readFully(final File file, final Charset cs) throws IOException { |
| if (!file.isFile()) { |
| throw new IOException(file + " is not a file"); //TODO localize? |
| } |
| |
| final byte[] buf = Files.readAllBytes(file.toPath()); |
| return (cs != null) ? new String(buf, cs).toCharArray() : byteToCharArray(buf); |
| } |
| |
| /** |
| * Read all of the source until end of stream from the given URL. Return it as char array |
| * |
| * @param url URL to read content from |
| * @return source as content |
| * @throws IOException if source could not be read |
| */ |
| public static char[] readFully(final URL url) throws IOException { |
| return readFully(url.openStream()); |
| } |
| |
| /** |
| * Read all of the source until end of file. Return it as char array |
| * |
| * @param url URL to read content from |
| * @param cs Charset used to convert bytes to chars |
| * @return source as content |
| * @throws IOException if source could not be read |
| */ |
| public static char[] readFully(final URL url, final Charset cs) throws IOException { |
| return readFully(url.openStream(), cs); |
| } |
| |
| /** |
| * Get a Base64-encoded SHA1 digest for this source. |
| * |
| * @return a Base64-encoded SHA1 digest for this source |
| */ |
| public String getDigest() { |
| return new String(getDigestBytes(), StandardCharsets.US_ASCII); |
| } |
| |
| private byte[] getDigestBytes() { |
| byte[] ldigest = digest; |
| if (ldigest == null) { |
| final char[] content = data(); |
| final byte[] bytes = new byte[content.length * 2]; |
| |
| for (int i = 0; i < content.length; i++) { |
| bytes[i * 2] = (byte) (content[i] & 0x00ff); |
| bytes[i * 2 + 1] = (byte) ((content[i] & 0xff00) >> 8); |
| } |
| |
| try { |
| final MessageDigest md = MessageDigest.getInstance("SHA-1"); |
| if (name != null) { |
| md.update(name.getBytes(StandardCharsets.UTF_8)); |
| } |
| if (base != null) { |
| md.update(base.getBytes(StandardCharsets.UTF_8)); |
| } |
| if (getURL() != null) { |
| md.update(getURL().toString().getBytes(StandardCharsets.UTF_8)); |
| } |
| digest = ldigest = BASE64.encode(md.digest(bytes)); |
| } catch (final NoSuchAlgorithmException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| return ldigest; |
| } |
| |
| /** |
| * Get the base url. This is currently used for testing only |
| * @param url a URL |
| * @return base URL for url |
| */ |
| public static String baseURL(final URL url) { |
| if (url.getProtocol().equals("file")) { |
| try { |
| final Path path = Paths.get(url.toURI()); |
| final Path parent = path.getParent(); |
| return (parent != null) ? (parent + File.separator) : null; |
| } catch (final SecurityException | URISyntaxException | IOError e) { |
| return null; |
| } |
| } |
| |
| // FIXME: is there a better way to find 'base' URL of a given URL? |
| String path = url.getPath(); |
| if (path.isEmpty()) { |
| return null; |
| } |
| path = path.substring(0, path.lastIndexOf('/') + 1); |
| final int port = url.getPort(); |
| try { |
| return new URL(url.getProtocol(), url.getHost(), port, path).toString(); |
| } catch (final MalformedURLException e) { |
| return null; |
| } |
| } |
| |
| private static String dirName(final File file, final String DEFAULT_BASE_NAME) { |
| final String res = file.getParent(); |
| return (res != null) ? (res + File.separator) : DEFAULT_BASE_NAME; |
| } |
| |
| // fake directory like name |
| private static String baseName(final String name) { |
| int idx = name.lastIndexOf('/'); |
| if (idx == -1) { |
| idx = name.lastIndexOf('\\'); |
| } |
| return (idx != -1) ? name.substring(0, idx + 1) : null; |
| } |
| |
| private static char[] readFully(final InputStream is, final Charset cs) throws IOException { |
| return (cs != null) ? new String(readBytes(is), cs).toCharArray() : readFully(is); |
| } |
| |
| private static char[] readFully(final InputStream is) throws IOException { |
| return byteToCharArray(readBytes(is)); |
| } |
| |
| private static char[] byteToCharArray(final byte[] bytes) { |
| Charset cs = StandardCharsets.UTF_8; |
| int start = 0; |
| // BOM detection. |
| if (bytes.length > 1 && bytes[0] == (byte) 0xFE && bytes[1] == (byte) 0xFF) { |
| start = 2; |
| cs = StandardCharsets.UTF_16BE; |
| } else if (bytes.length > 1 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE) { |
| if (bytes.length > 3 && bytes[2] == 0 && bytes[3] == 0) { |
| start = 4; |
| cs = Charset.forName("UTF-32LE"); |
| } else { |
| start = 2; |
| cs = StandardCharsets.UTF_16LE; |
| } |
| } else if (bytes.length > 2 && bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) { |
| start = 3; |
| cs = StandardCharsets.UTF_8; |
| } else if (bytes.length > 3 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == (byte) 0xFE && bytes[3] == (byte) 0xFF) { |
| start = 4; |
| cs = Charset.forName("UTF-32BE"); |
| } |
| |
| return new String(bytes, start, bytes.length - start, cs).toCharArray(); |
| } |
| |
| static byte[] readBytes(final InputStream is) throws IOException { |
| final byte[] arr = new byte[BUF_SIZE]; |
| try { |
| try (ByteArrayOutputStream buf = new ByteArrayOutputStream()) { |
| int numBytes; |
| while ((numBytes = is.read(arr, 0, arr.length)) > 0) { |
| buf.write(arr, 0, numBytes); |
| } |
| return buf.toByteArray(); |
| } |
| } finally { |
| is.close(); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return getName(); |
| } |
| |
| private static URL getURLFromFile(final File file) { |
| try { |
| return file.toURI().toURL(); |
| } catch (final SecurityException | MalformedURLException ignored) { |
| return null; |
| } |
| } |
| |
| private static DebugLogger getLoggerStatic() { |
| final Context context = Context.getContextTrustedOrNull(); |
| return context == null ? null : context.getLogger(Source.class); |
| } |
| |
| @Override |
| public DebugLogger initLogger(final Context context) { |
| return context.getLogger(this.getClass()); |
| } |
| |
| @Override |
| public DebugLogger getLogger() { |
| return initLogger(Context.getContextTrusted()); |
| } |
| |
| private File dumpFile(final File dirFile) { |
| final URL u = getURL(); |
| final StringBuilder buf = new StringBuilder(); |
| // make it unique by prefixing current date & time |
| buf.append(LocalDateTime.now().toString()); |
| buf.append('_'); |
| if (u != null) { |
| // make it a safe file name |
| buf.append(u.toString() |
| .replace('/', '_') |
| .replace('\\', '_')); |
| } else { |
| buf.append(getName()); |
| } |
| |
| return new File(dirFile, buf.toString()); |
| } |
| |
| void dump(final String dir) { |
| final File dirFile = new File(dir); |
| final File file = dumpFile(dirFile); |
| if (!dirFile.exists() && !dirFile.mkdirs()) { |
| debug("Skipping source dump for " + name); |
| return; |
| } |
| |
| try (final FileOutputStream fos = new FileOutputStream(file)) { |
| final PrintWriter pw = new PrintWriter(fos); |
| pw.print(data.toString()); |
| pw.flush(); |
| } catch (final IOException ioExp) { |
| debug("Skipping source dump for " + |
| name + |
| ": " + |
| ECMAErrors.getMessage( |
| "io.error.cant.write", |
| dir.toString() + |
| " : " + ioExp.toString())); |
| } |
| } |
| } |