1 /*
   2  * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.nashorn.internal.runtime;
  27 
  28 import java.io.ByteArrayOutputStream;
  29 import java.io.File;
  30 import java.io.IOError;
  31 import java.io.IOException;
  32 import java.io.InputStream;
  33 import java.io.Reader;
  34 import java.net.MalformedURLException;
  35 import java.net.URISyntaxException;
  36 import java.net.URL;
  37 import java.nio.charset.Charset;
  38 import java.nio.charset.StandardCharsets;
  39 import java.nio.file.Files;
  40 import java.nio.file.Path;
  41 import java.nio.file.Paths;
  42 import java.util.Arrays;
  43 import java.util.Objects;
  44 import jdk.nashorn.internal.parser.Token;
  45 
  46 /**
  47  * Source objects track the origin of JavaScript entities.
  48  *
  49  */
  50 public final class Source {
  51     /**
  52      * Descriptive name of the source as supplied by the user. Used for error
  53      * reporting to the user. For example, SyntaxError will use this to print message.
  54      * Used to implement __FILE__. Also used for SourceFile in .class for debugger usage.
  55      */
  56     private final String name;
  57 
  58     /**
  59      * Base directory the File or base part of the URL. Used to implement __DIR__.
  60      * Used to load scripts relative to the 'directory' or 'base' URL of current script.
  61      * This will be null when it can't be computed.
  62      */
  63     private final String base;
  64 
  65     /** Cached source content. */
  66     private final char[] content;
  67 
  68     /** Length of source content. */
  69     private final int length;
  70 
  71     /** Cached hash code */
  72     private int hash;
  73 
  74     /** Source URL if available */
  75     private final URL url;
  76 
  77     private static final int BUFSIZE = 8 * 1024;
  78 
  79     // Do *not* make this public ever! Trusts the URL and content. So has to be called
  80     // from other public constructors. Note that this can not be some init method as
  81     // we initialize final fields from here.
  82     private Source(final String name, final String base, final char[] content, final URL url) {
  83         this.name    = name;
  84         this.base    = base;
  85         this.content = content;
  86         this.length  = content.length;
  87         this.url     = url;
  88     }
  89 
  90     /**
  91      * Constructor
  92      *
  93      * @param name    source name
  94      * @param content contents as char array
  95      */
  96     public Source(final String name, final char[] content) {
  97         this(name, baseName(name, null), content, null);
  98     }
  99 
 100     /**
 101      * Constructor
 102      *
 103      * @param name    source name
 104      * @param content contents as string
 105      */
 106     public Source(final String name, final String content) {
 107         this(name, content.toCharArray());
 108     }
 109 
 110     /**
 111      * Constructor
 112      *
 113      * @param name  source name
 114      * @param url   url from which source can be loaded
 115      *
 116      * @throws IOException if source cannot be loaded
 117      */
 118     public Source(final String name, final URL url) throws IOException {
 119         this(name, baseURL(url, null), readFully(url), url);
 120     }
 121 
 122     /**
 123      * Constructor
 124      *
 125      * @param name  source name
 126      * @param url   url from which source can be loaded
 127      * @param cs    Charset used to convert bytes to chars
 128      *
 129      * @throws IOException if source cannot be loaded
 130      */
 131     public Source(final String name, final URL url, final Charset cs) throws IOException {
 132         this(name, baseURL(url, null), readFully(url, cs), url);
 133     }
 134 
 135     /**
 136      * Constructor
 137      *
 138      * @param name  source name
 139      * @param file  file from which source can be loaded
 140      *
 141      * @throws IOException if source cannot be loaded
 142      */
 143     public Source(final String name, final File file) throws IOException {
 144         this(name, dirName(file, null), readFully(file), getURLFromFile(file));
 145     }
 146 
 147     /**
 148      * Constructor
 149      *
 150      * @param name  source name
 151      * @param file  file from which source can be loaded
 152      * @param cs    Charset used to convert bytes to chars
 153      *
 154      * @throws IOException if source cannot be loaded
 155      */
 156     public Source(final String name, final File file, final Charset cs) throws IOException {
 157         this(name, dirName(file, null), readFully(file, cs), getURLFromFile(file));
 158     }
 159 
 160     @Override
 161     public boolean equals(final Object obj) {
 162         if (this == obj) {
 163             return true;
 164         }
 165 
 166         if (!(obj instanceof Source)) {
 167             return false;
 168         }
 169 
 170         final Source src = (Source)obj;
 171         // Only compare content as a last resort measure
 172         return length == src.length && Objects.equals(url, src.url) && Objects.equals(name, src.name) && Arrays.equals(content, src.content);
 173     }
 174 
 175     @Override
 176     public int hashCode() {
 177         int h = hash;
 178         if (h == 0) {
 179             h = hash = Arrays.hashCode(content) ^ Objects.hashCode(name);
 180         }
 181         return h;
 182     }
 183 
 184     /**
 185      * Fetch source content.
 186      * @return Source content.
 187      */
 188     public String getString() {
 189         return new String(content, 0, length);
 190     }
 191 
 192     /**
 193      * Get the user supplied name of this script.
 194      * @return User supplied source name.
 195      */
 196     public String getName() {
 197         return name;
 198     }
 199 
 200     /**
 201      * Get the "directory" part of the file or "base" of the URL.
 202      * @return base of file or URL.
 203      */
 204     public String getBase() {
 205         return base;
 206     }
 207 
 208     /**
 209      * Fetch a portion of source content.
 210      * @param start start index in source
 211      * @param len length of portion
 212      * @return Source content portion.
 213      */
 214     public String getString(final int start, final int len) {
 215         return new String(content, start, len);
 216     }
 217 
 218     /**
 219      * Fetch a portion of source content associated with a token.
 220      * @param token Token descriptor.
 221      * @return Source content portion.
 222      */
 223     public String getString(final long token) {
 224         final int start = Token.descPosition(token);
 225         final int len = Token.descLength(token);
 226         return new String(content, start, len);
 227     }
 228 
 229     /**
 230      * Returns the source URL of this script Source. Can be null if Source
 231      * was created from a String or a char[].
 232      *
 233      * @return URL source or null
 234      */
 235     public URL getURL() {
 236         return url;
 237     }
 238 
 239     /**
 240      * Find the beginning of the line containing position.
 241      * @param position Index to offending token.
 242      * @return Index of first character of line.
 243      */
 244     private int findBOLN(final int position) {
 245         for (int i = position - 1; i > 0; i--) {
 246             final char ch = content[i];
 247 
 248             if (ch == '\n' || ch == '\r') {
 249                 return i + 1;
 250             }
 251         }
 252 
 253         return 0;
 254     }
 255 
 256     /**
 257      * Find the end of the line containing position.
 258      * @param position Index to offending token.
 259      * @return Index of last character of line.
 260      */
 261     private int findEOLN(final int position) {
 262          for (int i = position; i < length; i++) {
 263             final char ch = content[i];
 264 
 265             if (ch == '\n' || ch == '\r') {
 266                 return i - 1;
 267             }
 268         }
 269 
 270         return length - 1;
 271     }
 272 
 273     /**
 274      * Return line number of character position.
 275      * @param position Position of character in source content.
 276      * @return Line number.
 277      */
 278     public int getLine(final int position) {
 279         // Line count starts at 1.
 280         int line = 1;
 281 
 282         for (int i = 0; i < position; i++) {
 283             final char ch = content[i];
 284             // Works for both \n and \r\n.
 285             if (ch == '\n') {
 286                 line++;
 287             }
 288         }
 289 
 290         return line;
 291     }
 292 
 293     /**
 294      * Return column number of character position.
 295      * @param position Position of character in source content.
 296      * @return Column number.
 297      */
 298     public int getColumn(final int position) {
 299         // TODO - column needs to account for tabs.
 300         return position - findBOLN(position);
 301     }
 302 
 303     /**
 304      * Return line text including character position.
 305      * @param position Position of character in source content.
 306      * @return Line text.
 307      */
 308     public String getSourceLine(final int position) {
 309         // Find end of previous line.
 310         final int first = findBOLN(position);
 311         // Find end of this line.
 312         final int last = findEOLN(position);
 313 
 314         return new String(content, first, last - first + 1);
 315     }
 316 
 317     /**
 318      * Get the content of this source as a char array
 319      * @return content
 320      */
 321     public char[] getContent() {
 322         return content.clone();
 323     }
 324 
 325     /**
 326      * Get the length in chars for this source
 327      * @return length
 328      */
 329     public int getLength() {
 330         return length;
 331     }
 332 
 333     /**
 334      * Read all of the source until end of file. Return it as char array
 335      *
 336      * @param reader  reader opened to source stream
 337      * @return source as content
 338      *
 339      * @throws IOException if source could not be read
 340      */
 341     public static char[] readFully(final Reader reader) throws IOException {
 342         final char[]        arr = new char[BUFSIZE];
 343         final StringBuilder sb  = new StringBuilder();
 344 
 345         try {
 346             int numChars;
 347             while ((numChars = reader.read(arr, 0, arr.length)) > 0) {
 348                 sb.append(arr, 0, numChars);
 349             }
 350         } finally {
 351             reader.close();
 352         }
 353 
 354         return sb.toString().toCharArray();
 355     }
 356 
 357     /**
 358      * Read all of the source until end of file. Return it as char array
 359      *
 360      * @param file  source file
 361      * @return source as content
 362      *
 363      * @throws IOException if source could not be read
 364      */
 365     public static char[] readFully(final File file) throws IOException {
 366         if (!file.isFile()) {
 367             throw new IOException(file + " is not a file"); //TODO localize?
 368         }
 369         return byteToCharArray(Files.readAllBytes(file.toPath()));
 370     }
 371 
 372     /**
 373      * Read all of the source until end of file. Return it as char array
 374      *
 375      * @param file  source file
 376      * @param cs Charset used to convert bytes to chars
 377      * @return source as content
 378      *
 379      * @throws IOException if source could not be read
 380      */
 381     public static char[] readFully(final File file, final Charset cs) throws IOException {
 382         if (!file.isFile()) {
 383             throw new IOException(file + " is not a file"); //TODO localize?
 384         }
 385 
 386         final byte[] buf = Files.readAllBytes(file.toPath());
 387         return (cs != null)? new String(buf, cs).toCharArray() : byteToCharArray(buf);
 388     }
 389 
 390     /**
 391      * Read all of the source until end of stream from the given URL. Return it as char array
 392      *
 393      * @param url URL to read content from
 394      * @return source as content
 395      *
 396      * @throws IOException if source could not be read
 397      */
 398     public static char[] readFully(final URL url) throws IOException {
 399         return readFully(url.openStream());
 400     }
 401 
 402     /**
 403      * Read all of the source until end of file. Return it as char array
 404      *
 405      * @param url URL to read content from
 406      * @param cs Charset used to convert bytes to chars
 407      * @return source as content
 408      *
 409      * @throws IOException if source could not be read
 410      */
 411     public static char[] readFully(final URL url, final Charset cs) throws IOException {
 412         return readFully(url.openStream(), cs);
 413     }
 414 
 415     /**
 416      * Get the base url. This is currently used for testing only
 417      * @param url a URL
 418      * @return base URL for url
 419      */
 420     public static String baseURL(final URL url) {
 421         return baseURL(url, null);
 422     }
 423 
 424     private static String baseURL(final URL url, final String defaultValue) {
 425         if (url.getProtocol().equals("file")) {
 426             try {
 427                 final Path path = Paths.get(url.toURI());
 428                 final Path parent = path.getParent();
 429                 return (parent != null) ? (parent + File.separator) : defaultValue;
 430             } catch (final SecurityException | URISyntaxException | IOError e) {
 431                 return defaultValue;
 432             }
 433         }
 434 
 435         // FIXME: is there a better way to find 'base' URL of a given URL?
 436         String path = url.getPath();
 437         if (path.isEmpty()) {
 438             return defaultValue;
 439         }
 440         path = path.substring(0, path.lastIndexOf('/') + 1);
 441         final int port = url.getPort();
 442         try {
 443             return new URL(url.getProtocol(), url.getHost(), port, path).toString();
 444         } catch (final MalformedURLException e) {
 445             return defaultValue;
 446         }
 447     }
 448 
 449     private static String dirName(final File file, final String defaultValue) {
 450         final String res = file.getParent();
 451         return (res != null)? (res + File.separator) : defaultValue;
 452     }
 453 
 454     // fake directory like name
 455     private static String baseName(final String name, final String defaultValue) {
 456         int idx = name.lastIndexOf('/');
 457         if (idx == -1) {
 458             idx = name.lastIndexOf('\\');
 459         }
 460         return (idx != -1)? name.substring(0, idx + 1) : defaultValue;
 461     }
 462 
 463     private static char[] readFully(final InputStream is, final Charset cs) throws IOException {
 464         return (cs != null)? new String(readBytes(is), cs).toCharArray() : readFully(is);
 465     }
 466 
 467     private static char[] readFully(final InputStream is) throws IOException {
 468         return byteToCharArray(readBytes(is));
 469     }
 470 
 471     private static char[] byteToCharArray(final byte[] bytes) {
 472         Charset cs = StandardCharsets.UTF_8;
 473         int start = 0;
 474         // BOM detection.
 475         if (bytes.length > 1 && bytes[0] == (byte)0xFE && bytes[1] == (byte)0xFF) {
 476             start = 2;
 477             cs = StandardCharsets.UTF_16BE;
 478         } else if (bytes.length > 1 && bytes[0] == (byte)0xFF && bytes[1] == (byte)0xFE) {
 479             start = 2;
 480             cs = StandardCharsets.UTF_16LE;
 481         } else if (bytes.length > 2 && bytes[0] == (byte)0xEF && bytes[1] == (byte)0xBB && bytes[2] == (byte)0xBF) {
 482             start = 3;
 483             cs = StandardCharsets.UTF_8;
 484         } else if (bytes.length > 3 && bytes[0] == (byte)0xFF && bytes[1] == (byte)0xFE && bytes[2] == 0 && bytes[3] == 0) {
 485             start = 4;
 486             cs = Charset.forName("UTF-32LE");
 487         } else if (bytes.length > 3 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == (byte)0xFE && bytes[3] == (byte)0xFF) {
 488             start = 4;
 489             cs = Charset.forName("UTF-32BE");
 490         }
 491 
 492         return new String(bytes, start, bytes.length - start, cs).toCharArray();
 493     }
 494 
 495     static byte[] readBytes(final InputStream is) throws IOException {
 496         final byte[] arr = new byte[BUFSIZE];
 497         try {
 498             try (ByteArrayOutputStream buf = new ByteArrayOutputStream()) {
 499                 int numBytes;
 500                 while ((numBytes = is.read(arr, 0, arr.length)) > 0) {
 501                     buf.write(arr, 0, numBytes);
 502                 }
 503                 return buf.toByteArray();
 504             }
 505         } finally {
 506             is.close();
 507         }
 508     }
 509 
 510     @Override
 511     public String toString() {
 512         return getName();
 513     }
 514 
 515     private static URL getURLFromFile(final File file) {
 516         try {
 517             return file.toURI().toURL();
 518         } catch (final SecurityException | MalformedURLException ignored) {
 519             return null;
 520         }
 521     }
 522 }