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.security.MessageDigest; 43 import java.security.NoSuchAlgorithmException; 44 import java.util.Arrays; 45 import java.util.Objects; 46 import jdk.nashorn.internal.parser.Token; 47 48 /** 49 * Source objects track the origin of JavaScript entities. 50 * 51 */ 52 public final class Source { 53 /** 54 * Descriptive name of the source as supplied by the user. Used for error 55 * reporting to the user. For example, SyntaxError will use this to print message. 56 * Used to implement __FILE__. Also used for SourceFile in .class for debugger usage. 57 */ 58 private final String name; 59 60 /** 61 * Base directory the File or base part of the URL. Used to implement __DIR__. 62 * Used to load scripts relative to the 'directory' or 'base' URL of current script. 63 * This will be null when it can't be computed. 64 */ 65 private final String base; 66 67 /** Cached source content. */ 68 private final char[] content; 69 70 /** Length of source content. */ 71 private final int length; 72 73 /** Cached hash code */ 74 private int hash; 75 76 /** Message digest */ 77 private byte[] digest; 78 79 /** Source URL if available */ 80 private final URL url; 81 82 private static final int BUFSIZE = 8 * 1024; 83 84 // Do *not* make this public ever! Trusts the URL and content. So has to be called 85 // from other public constructors. Note that this can not be some init method as 86 // we initialize final fields from here. 87 private Source(final String name, final String base, final char[] content, final URL url) { 88 this.name = name; 89 this.base = base; 90 this.content = content; 91 this.length = content.length; 92 this.url = url; 93 } 94 95 /** 96 * Constructor 97 * 98 * @param name source name 99 * @param content contents as char array 100 */ 101 public Source(final String name, final char[] content) { 102 this(name, baseName(name, null), content, null); 103 } 104 105 /** 106 * Constructor 107 * 108 * @param name source name 109 * @param content contents as string 110 */ 111 public Source(final String name, final String content) { 112 this(name, content.toCharArray()); 113 } 114 115 /** 116 * Constructor 117 * 118 * @param name source name 119 * @param url url from which source can be loaded 120 * 121 * @throws IOException if source cannot be loaded 122 */ 123 public Source(final String name, final URL url) throws IOException { 124 this(name, baseURL(url, null), readFully(url), url); 125 } 126 127 /** 128 * Constructor 129 * 130 * @param name source name 131 * @param url url from which source can be loaded 132 * @param cs Charset used to convert bytes to chars 133 * 134 * @throws IOException if source cannot be loaded 135 */ 136 public Source(final String name, final URL url, final Charset cs) throws IOException { 137 this(name, baseURL(url, null), readFully(url, cs), url); 138 } 139 140 /** 141 * Constructor 142 * 143 * @param name source name 144 * @param file file from which source can be loaded 145 * 146 * @throws IOException if source cannot be loaded 147 */ 148 public Source(final String name, final File file) throws IOException { 149 this(name, dirName(file, null), readFully(file), getURLFromFile(file)); 150 } 151 152 /** 153 * Constructor 154 * 155 * @param name source name 156 * @param file file from which source can be loaded 157 * @param cs Charset used to convert bytes to chars 158 * 159 * @throws IOException if source cannot be loaded 160 */ 161 public Source(final String name, final File file, final Charset cs) throws IOException { 162 this(name, dirName(file, null), readFully(file, cs), getURLFromFile(file)); 163 } 164 165 @Override 166 public boolean equals(final Object obj) { 167 if (this == obj) { 168 return true; 169 } 170 171 if (!(obj instanceof Source)) { 172 return false; 173 } 174 175 final Source src = (Source)obj; 176 // Only compare content as a last resort measure 177 return length == src.length && Objects.equals(url, src.url) && Objects.equals(name, src.name) && Arrays.equals(content, src.content); 178 } 179 180 @Override 181 public int hashCode() { 182 int h = hash; 183 if (h == 0) { 184 h = hash = Arrays.hashCode(content) ^ Objects.hashCode(name); 185 } 186 return h; 187 } 188 189 /** 190 * Fetch source content. 191 * @return Source content. 192 */ 193 public String getString() { 194 return new String(content, 0, length); 195 } 196 197 /** 198 * Get the user supplied name of this script. 199 * @return User supplied source name. 200 */ 201 public String getName() { 202 return name; 203 } 204 205 /** 206 * Get the "directory" part of the file or "base" of the URL. 207 * @return base of file or URL. 208 */ 209 public String getBase() { 210 return base; 211 } 212 213 /** 214 * Fetch a portion of source content. 215 * @param start start index in source 216 * @param len length of portion 217 * @return Source content portion. 218 */ 219 public String getString(final int start, final int len) { 220 return new String(content, start, len); 221 } 222 223 /** 224 * Fetch a portion of source content associated with a token. 225 * @param token Token descriptor. 226 * @return Source content portion. 227 */ 228 public String getString(final long token) { 229 final int start = Token.descPosition(token); 230 final int len = Token.descLength(token); 231 return new String(content, start, len); 232 } 233 234 /** 235 * Returns the source URL of this script Source. Can be null if Source 236 * was created from a String or a char[]. 237 * 238 * @return URL source or null 239 */ 240 public URL getURL() { 241 return url; 242 } 243 244 /** 245 * Find the beginning of the line containing position. 246 * @param position Index to offending token. 247 * @return Index of first character of line. 248 */ 249 private int findBOLN(final int position) { 250 for (int i = position - 1; i > 0; i--) { 251 final char ch = content[i]; 252 253 if (ch == '\n' || ch == '\r') { 254 return i + 1; 255 } 256 } 257 258 return 0; 259 } 260 261 /** 262 * Find the end of the line containing position. 263 * @param position Index to offending token. 264 * @return Index of last character of line. 265 */ 266 private int findEOLN(final int position) { 267 for (int i = position; i < length; i++) { 268 final char ch = content[i]; 269 270 if (ch == '\n' || ch == '\r') { 271 return i - 1; 272 } 273 } 274 275 return length - 1; 276 } 277 278 /** 279 * Return line number of character position. 280 * 281 * <p>This method can be expensive for large sources as it iterates through 282 * all characters up to {@code position}.</p> 283 * 284 * @param position Position of character in source content. 285 * @return Line number. 286 */ 287 public int getLine(final int position) { 288 // Line count starts at 1. 289 int line = 1; 290 291 for (int i = 0; i < position; i++) { 292 final char ch = content[i]; 293 // Works for both \n and \r\n. 294 if (ch == '\n') { 295 line++; 296 } 297 } 298 299 return line; 300 } 301 302 /** 303 * Return column number of character position. 304 * @param position Position of character in source content. 305 * @return Column number. 306 */ 307 public int getColumn(final int position) { 308 // TODO - column needs to account for tabs. 309 return position - findBOLN(position); 310 } 311 312 /** 313 * Return line text including character position. 314 * @param position Position of character in source content. 315 * @return Line text. 316 */ 317 public String getSourceLine(final int position) { 318 // Find end of previous line. 319 final int first = findBOLN(position); 320 // Find end of this line. 321 final int last = findEOLN(position); 322 323 return new String(content, first, last - first + 1); 324 } 325 326 /** 327 * Get the content of this source as a char array 328 * @return content 329 */ 330 public char[] getContent() { 331 return content.clone(); 332 } 333 334 /** 335 * Get the length in chars for this source 336 * @return length 337 */ 338 public int getLength() { 339 return length; 340 } 341 342 /** 343 * Read all of the source until end of file. Return it as char array 344 * 345 * @param reader reader opened to source stream 346 * @return source as content 347 * 348 * @throws IOException if source could not be read 349 */ 350 public static char[] readFully(final Reader reader) throws IOException { 351 final char[] arr = new char[BUFSIZE]; 352 final StringBuilder sb = new StringBuilder(); 353 354 try { 355 int numChars; 356 while ((numChars = reader.read(arr, 0, arr.length)) > 0) { 357 sb.append(arr, 0, numChars); 358 } 359 } finally { 360 reader.close(); 361 } 362 363 return sb.toString().toCharArray(); 364 } 365 366 /** 367 * Read all of the source until end of file. Return it as char array 368 * 369 * @param file source file 370 * @return source as content 371 * 372 * @throws IOException if source could not be read 373 */ 374 public static char[] readFully(final File file) throws IOException { 375 if (!file.isFile()) { 376 throw new IOException(file + " is not a file"); //TODO localize? 377 } 378 return byteToCharArray(Files.readAllBytes(file.toPath())); 379 } 380 381 /** 382 * Read all of the source until end of file. Return it as char array 383 * 384 * @param file source file 385 * @param cs Charset used to convert bytes to chars 386 * @return source as content 387 * 388 * @throws IOException if source could not be read 389 */ 390 public static char[] readFully(final File file, final Charset cs) throws IOException { 391 if (!file.isFile()) { 392 throw new IOException(file + " is not a file"); //TODO localize? 393 } 394 395 final byte[] buf = Files.readAllBytes(file.toPath()); 396 return (cs != null)? new String(buf, cs).toCharArray() : byteToCharArray(buf); 397 } 398 399 /** 400 * Read all of the source until end of stream from the given URL. Return it as char array 401 * 402 * @param url URL to read content from 403 * @return source as content 404 * 405 * @throws IOException if source could not be read 406 */ 407 public static char[] readFully(final URL url) throws IOException { 408 return readFully(url.openStream()); 409 } 410 411 /** 412 * Read all of the source until end of file. Return it as char array 413 * 414 * @param url URL to read content from 415 * @param cs Charset used to convert bytes to chars 416 * @return source as content 417 * 418 * @throws IOException if source could not be read 419 */ 420 public static char[] readFully(final URL url, final Charset cs) throws IOException { 421 return readFully(url.openStream(), cs); 422 } 423 424 /** 425 * Get a message digest for this source. 426 * 427 * @return a message digest for this source 428 */ 429 public byte[] getDigest() { 430 if (digest == null) { 431 432 final byte[] bytes = new byte[content.length * 2]; 433 434 for (int i = 0; i < content.length; i++) { 435 bytes[i * 2] = (byte) (content[i] & 0x00ff); 436 bytes[i * 2 + 1] = (byte) ((content[i] & 0xff00) >> 8); 437 } 438 439 try { 440 final MessageDigest md = MessageDigest.getInstance("SHA-1"); 441 if (name != null) { 442 md.update(name.getBytes(StandardCharsets.UTF_8)); 443 } 444 if (base != null) { 445 md.update(base.getBytes(StandardCharsets.UTF_8)); 446 } 447 if (url != null) { 448 md.update(url.toString().getBytes(StandardCharsets.UTF_8)); 449 } 450 digest = md.digest(bytes); 451 } catch (NoSuchAlgorithmException e) { 452 throw new RuntimeException(e); 453 } 454 } 455 return digest; 456 } 457 458 /** 459 * Get the base url. This is currently used for testing only 460 * @param url a URL 461 * @return base URL for url 462 */ 463 public static String baseURL(final URL url) { 464 return baseURL(url, null); 465 } 466 467 private static String baseURL(final URL url, final String defaultValue) { 468 if (url.getProtocol().equals("file")) { 469 try { 470 final Path path = Paths.get(url.toURI()); 471 final Path parent = path.getParent(); 472 return (parent != null) ? (parent + File.separator) : defaultValue; 473 } catch (final SecurityException | URISyntaxException | IOError e) { 474 return defaultValue; 475 } 476 } 477 478 // FIXME: is there a better way to find 'base' URL of a given URL? 479 String path = url.getPath(); 480 if (path.isEmpty()) { 481 return defaultValue; 482 } 483 path = path.substring(0, path.lastIndexOf('/') + 1); 484 final int port = url.getPort(); 485 try { 486 return new URL(url.getProtocol(), url.getHost(), port, path).toString(); 487 } catch (final MalformedURLException e) { 488 return defaultValue; 489 } 490 } 491 492 private static String dirName(final File file, final String defaultValue) { 493 final String res = file.getParent(); 494 return (res != null)? (res + File.separator) : defaultValue; 495 } 496 497 // fake directory like name 498 private static String baseName(final String name, final String defaultValue) { 499 int idx = name.lastIndexOf('/'); 500 if (idx == -1) { 501 idx = name.lastIndexOf('\\'); 502 } 503 return (idx != -1)? name.substring(0, idx + 1) : defaultValue; 504 } 505 506 private static char[] readFully(final InputStream is, final Charset cs) throws IOException { 507 return (cs != null)? new String(readBytes(is), cs).toCharArray() : readFully(is); 508 } 509 510 private static char[] readFully(final InputStream is) throws IOException { 511 return byteToCharArray(readBytes(is)); 512 } 513 514 private static char[] byteToCharArray(final byte[] bytes) { 515 Charset cs = StandardCharsets.UTF_8; 516 int start = 0; 517 // BOM detection. 518 if (bytes.length > 1 && bytes[0] == (byte)0xFE && bytes[1] == (byte)0xFF) { 519 start = 2; 520 cs = StandardCharsets.UTF_16BE; 521 } else if (bytes.length > 1 && bytes[0] == (byte)0xFF && bytes[1] == (byte)0xFE) { 522 start = 2; 523 cs = StandardCharsets.UTF_16LE; 524 } else if (bytes.length > 2 && bytes[0] == (byte)0xEF && bytes[1] == (byte)0xBB && bytes[2] == (byte)0xBF) { 525 start = 3; 526 cs = StandardCharsets.UTF_8; 527 } else if (bytes.length > 3 && bytes[0] == (byte)0xFF && bytes[1] == (byte)0xFE && bytes[2] == 0 && bytes[3] == 0) { 528 start = 4; 529 cs = Charset.forName("UTF-32LE"); 530 } else if (bytes.length > 3 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == (byte)0xFE && bytes[3] == (byte)0xFF) { 531 start = 4; 532 cs = Charset.forName("UTF-32BE"); 533 } 534 535 return new String(bytes, start, bytes.length - start, cs).toCharArray(); 536 } 537 538 static byte[] readBytes(final InputStream is) throws IOException { 539 final byte[] arr = new byte[BUFSIZE]; 540 try { 541 try (ByteArrayOutputStream buf = new ByteArrayOutputStream()) { 542 int numBytes; 543 while ((numBytes = is.read(arr, 0, arr.length)) > 0) { 544 buf.write(arr, 0, numBytes); 545 } 546 return buf.toByteArray(); 547 } 548 } finally { 549 is.close(); 550 } 551 } 552 553 @Override 554 public String toString() { 555 return getName(); 556 } 557 558 private static URL getURLFromFile(final File file) { 559 try { 560 return file.toURI().toURL(); 561 } catch (final SecurityException | MalformedURLException ignored) { 562 return null; 563 } 564 } 565 }