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 }