blob: b2ffbe61838a143813b06f585927d05264e9dc90 [file] [log] [blame]
/*
* 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()));
}
}
}