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.FileNotFoundException;
31 import java.io.FileOutputStream;
32 import java.io.IOError;
33 import java.io.IOException;
34 import java.io.InputStream;
35 import java.io.PrintWriter;
36 import java.io.Reader;
37 import java.lang.ref.WeakReference;
38 import java.net.MalformedURLException;
39 import java.net.URISyntaxException;
40 import java.net.URL;
41 import java.net.URLConnection;
42 import java.nio.charset.Charset;
43 import java.nio.charset.StandardCharsets;
44 import java.nio.file.Files;
45 import java.nio.file.Path;
46 import java.nio.file.Paths;
47 import java.security.MessageDigest;
48 import java.security.NoSuchAlgorithmException;
49 import java.time.LocalDateTime;
50 import java.util.Arrays;
51 import java.util.Base64;
52 import java.util.Objects;
53 import java.util.WeakHashMap;
54 import jdk.nashorn.api.scripting.URLReader;
55 import jdk.nashorn.internal.parser.Token;
56 import jdk.nashorn.internal.runtime.logging.DebugLogger;
57 import jdk.nashorn.internal.runtime.logging.Loggable;
58 import jdk.nashorn.internal.runtime.logging.Logger;
59 /**
60  * Source objects track the origin of JavaScript entities.
61  */
62 @Logger(name="source")
63 public final class Source implements Loggable {
64     private static final int BUF_SIZE = 8 * 1024;
65     private static final Cache CACHE = new Cache();
66 
67     // Message digest to file name encoder
68     private final static Base64.Encoder BASE64 = Base64.getUrlEncoder().withoutPadding();
69 
70     /**
71      * Descriptive name of the source as supplied by the user. Used for error
72      * reporting to the user. For example, SyntaxError will use this to print message.
73      * Used to implement __FILE__. Also used for SourceFile in .class for debugger usage.
74      */
75     private final String name;
76 
77     /**
78      * Base directory the File or base part of the URL. Used to implement __DIR__.
79      * Used to load scripts relative to the 'directory' or 'base' URL of current script.
80      * This will be null when it can't be computed.
81      */
82     private final String base;
83 
84     /** Source content */
85     private final Data data;
86 
87     /** Cached hash code */
88     private int hash;
89 
90     /** Base64-encoded SHA1 digest of this source object */
91     private volatile byte[] digest;
92 
93     /** source URL set via //@ sourceURL or //# sourceURL directive */
94     private String explicitURL;
95 
96     // Do *not* make this public, ever! Trusts the URL and content.
Source(final String name, final String base, final Data data)97     private Source(final String name, final String base, final Data data) {
98         this.name = name;
99         this.base = base;
100         this.data = data;
101     }
102 
sourceFor(final String name, final String base, final URLData data)103     private static synchronized Source sourceFor(final String name, final String base, final URLData data) throws IOException {
104         try {
105             final Source newSource = new Source(name, base, data);
106             final Source existingSource = CACHE.get(newSource);
107             if (existingSource != null) {
108                 // Force any access errors
109                 data.checkPermissionAndClose();
110                 return existingSource;
111             }
112 
113             // All sources in cache must be fully loaded
114             data.load();
115             CACHE.put(newSource, newSource);
116 
117             return newSource;
118         } catch (final RuntimeException e) {
119             final Throwable cause = e.getCause();
120             if (cause instanceof IOException) {
121                 throw (IOException) cause;
122             }
123             throw e;
124         }
125     }
126 
127     private static class Cache extends WeakHashMap<Source, WeakReference<Source>> {
get(final Source key)128         public Source get(final Source key) {
129             final WeakReference<Source> ref = super.get(key);
130             return ref == null ? null : ref.get();
131         }
132 
put(final Source key, final Source value)133         public void put(final Source key, final Source value) {
134             assert !(value.data instanceof RawData);
135             put(key, new WeakReference<>(value));
136         }
137     }
138 
139     /* package-private */
getSourceInfo()140     DebuggerSupport.SourceInfo getSourceInfo() {
141         return new DebuggerSupport.SourceInfo(getName(), data.hashCode(),  data.url(), data.array());
142     }
143 
144     // Wrapper to manage lazy loading
145     private static interface Data {
146 
url()147         URL url();
148 
length()149         int length();
150 
lastModified()151         long lastModified();
152 
array()153         char[] array();
154 
isEvalCode()155         boolean isEvalCode();
156     }
157 
158     private static class RawData implements Data {
159         private final char[] array;
160         private final boolean evalCode;
161         private int hash;
162 
RawData(final char[] array, final boolean evalCode)163         private RawData(final char[] array, final boolean evalCode) {
164             this.array = Objects.requireNonNull(array);
165             this.evalCode = evalCode;
166         }
167 
RawData(final String source, final boolean evalCode)168         private RawData(final String source, final boolean evalCode) {
169             this.array = Objects.requireNonNull(source).toCharArray();
170             this.evalCode = evalCode;
171         }
172 
RawData(final Reader reader)173         private RawData(final Reader reader) throws IOException {
174             this(readFully(reader), false);
175         }
176 
177         @Override
hashCode()178         public int hashCode() {
179             int h = hash;
180             if (h == 0) {
181                 h = hash = Arrays.hashCode(array) ^ (evalCode? 1 : 0);
182             }
183             return h;
184         }
185 
186         @Override
equals(final Object obj)187         public boolean equals(final Object obj) {
188             if (this == obj) {
189                 return true;
190             }
191             if (obj instanceof RawData) {
192                 final RawData other = (RawData)obj;
193                 return Arrays.equals(array, other.array) && evalCode == other.evalCode;
194             }
195             return false;
196         }
197 
198         @Override
toString()199         public String toString() {
200             return new String(array());
201         }
202 
203         @Override
url()204         public URL url() {
205             return null;
206         }
207 
208         @Override
length()209         public int length() {
210             return array.length;
211         }
212 
213         @Override
lastModified()214         public long lastModified() {
215             return 0;
216         }
217 
218         @Override
array()219         public char[] array() {
220             return array;
221         }
222 
223 
224         @Override
isEvalCode()225         public boolean isEvalCode() {
226             return evalCode;
227         }
228     }
229 
230     private static class URLData implements Data {
231         private final URL url;
232         protected final Charset cs;
233         private int hash;
234         protected char[] array;
235         protected int length;
236         protected long lastModified;
237 
URLData(final URL url, final Charset cs)238         private URLData(final URL url, final Charset cs) {
239             this.url = Objects.requireNonNull(url);
240             this.cs = cs;
241         }
242 
243         @Override
hashCode()244         public int hashCode() {
245             int h = hash;
246             if (h == 0) {
247                 h = hash = url.hashCode();
248             }
249             return h;
250         }
251 
252         @Override
equals(final Object other)253         public boolean equals(final Object other) {
254             if (this == other) {
255                 return true;
256             }
257             if (!(other instanceof URLData)) {
258                 return false;
259             }
260 
261             final URLData otherData = (URLData) other;
262 
263             if (url.equals(otherData.url)) {
264                 // Make sure both have meta data loaded
265                 try {
266                     if (isDeferred()) {
267                         // Data in cache is always loaded, and we only compare to cached data.
268                         assert !otherData.isDeferred();
269                         loadMeta();
270                     } else if (otherData.isDeferred()) {
271                         otherData.loadMeta();
272                     }
273                 } catch (final IOException e) {
274                     throw new RuntimeException(e);
275                 }
276 
277                 // Compare meta data
278                 return this.length == otherData.length && this.lastModified == otherData.lastModified;
279             }
280             return false;
281         }
282 
283         @Override
toString()284         public String toString() {
285             return new String(array());
286         }
287 
288         @Override
url()289         public URL url() {
290             return url;
291         }
292 
293         @Override
length()294         public int length() {
295             return length;
296         }
297 
298         @Override
lastModified()299         public long lastModified() {
300             return lastModified;
301         }
302 
303         @Override
array()304         public char[] array() {
305             assert !isDeferred();
306             return array;
307         }
308 
309         @Override
isEvalCode()310         public boolean isEvalCode() {
311             return false;
312         }
313 
isDeferred()314         boolean isDeferred() {
315             return array == null;
316         }
317 
318         @SuppressWarnings("try")
checkPermissionAndClose()319         protected void checkPermissionAndClose() throws IOException {
320             try (InputStream in = url.openStream()) {
321                 // empty
322             }
323             debug("permission checked for ", url);
324         }
325 
load()326         protected void load() throws IOException {
327             if (array == null) {
328                 final URLConnection c = url.openConnection();
329                 try (InputStream in = c.getInputStream()) {
330                     array = cs == null ? readFully(in) : readFully(in, cs);
331                     length = array.length;
332                     lastModified = c.getLastModified();
333                     debug("loaded content for ", url);
334                 }
335             }
336         }
337 
loadMeta()338         protected void loadMeta() throws IOException {
339             if (length == 0 && lastModified == 0) {
340                 final URLConnection c = url.openConnection();
341                 length = c.getContentLength();
342                 lastModified = c.getLastModified();
343                 debug("loaded metadata for ", url);
344             }
345         }
346     }
347 
348     private static class FileData extends URLData {
349         private final File file;
350 
FileData(final File file, final Charset cs)351         private FileData(final File file, final Charset cs) {
352             super(getURLFromFile(file), cs);
353             this.file = file;
354 
355         }
356 
357         @Override
checkPermissionAndClose()358         protected void checkPermissionAndClose() throws IOException {
359             if (!file.canRead()) {
360                 throw new FileNotFoundException(file + " (Permission Denied)");
361             }
362             debug("permission checked for ", file);
363         }
364 
365         @Override
loadMeta()366         protected void loadMeta() {
367             if (length == 0 && lastModified == 0) {
368                 length = (int) file.length();
369                 lastModified = file.lastModified();
370                 debug("loaded metadata for ", file);
371             }
372         }
373 
374         @Override
load()375         protected void load() throws IOException {
376             if (array == null) {
377                 array = cs == null ? readFully(file) : readFully(file, cs);
378                 length = array.length;
379                 lastModified = file.lastModified();
380                 debug("loaded content for ", file);
381             }
382         }
383     }
384 
debug(final Object... msg)385     private static void debug(final Object... msg) {
386         final DebugLogger logger = getLoggerStatic();
387         if (logger != null) {
388             logger.info(msg);
389         }
390     }
391 
data()392     private char[] data() {
393         return data.array();
394     }
395 
396     /**
397      * Returns a Source instance
398      *
399      * @param name    source name
400      * @param content contents as char array
401      * @param isEval does this represent code from 'eval' call?
402      * @return source instance
403      */
sourceFor(final String name, final char[] content, final boolean isEval)404     public static Source sourceFor(final String name, final char[] content, final boolean isEval) {
405         return new Source(name, baseName(name), new RawData(content, isEval));
406     }
407 
408     /**
409      * Returns a Source instance
410      *
411      * @param name    source name
412      * @param content contents as char array
413      *
414      * @return source instance
415      */
sourceFor(final String name, final char[] content)416     public static Source sourceFor(final String name, final char[] content) {
417         return sourceFor(name, content, false);
418     }
419 
420     /**
421      * Returns a Source instance
422      *
423      * @param name    source name
424      * @param content contents as string
425      * @param isEval does this represent code from 'eval' call?
426      * @return source instance
427      */
sourceFor(final String name, final String content, final boolean isEval)428     public static Source sourceFor(final String name, final String content, final boolean isEval) {
429         return new Source(name, baseName(name), new RawData(content, isEval));
430     }
431 
432     /**
433      * Returns a Source instance
434      *
435      * @param name    source name
436      * @param content contents as string
437      * @return source instance
438      */
sourceFor(final String name, final String content)439     public static Source sourceFor(final String name, final String content) {
440         return sourceFor(name, content, false);
441     }
442 
443     /**
444      * Constructor
445      *
446      * @param name  source name
447      * @param url   url from which source can be loaded
448      *
449      * @return source instance
450      *
451      * @throws IOException if source cannot be loaded
452      */
sourceFor(final String name, final URL url)453     public static Source sourceFor(final String name, final URL url) throws IOException {
454         return sourceFor(name, url, null);
455     }
456 
457     /**
458      * Constructor
459      *
460      * @param name  source name
461      * @param url   url from which source can be loaded
462      * @param cs    Charset used to convert bytes to chars
463      *
464      * @return source instance
465      *
466      * @throws IOException if source cannot be loaded
467      */
sourceFor(final String name, final URL url, final Charset cs)468     public static Source sourceFor(final String name, final URL url, final Charset cs) throws IOException {
469         return sourceFor(name, baseURL(url), new URLData(url, cs));
470     }
471 
472     /**
473      * Constructor
474      *
475      * @param name  source name
476      * @param file  file from which source can be loaded
477      *
478      * @return source instance
479      *
480      * @throws IOException if source cannot be loaded
481      */
sourceFor(final String name, final File file)482     public static Source sourceFor(final String name, final File file) throws IOException {
483         return sourceFor(name, file, null);
484     }
485 
486     /**
487      * Constructor
488      *
489      * @param name  source name
490      * @param file  file from which source can be loaded
491      * @param cs    Charset used to convert bytes to chars
492      *
493      * @return source instance
494      *
495      * @throws IOException if source cannot be loaded
496      */
sourceFor(final String name, final File file, final Charset cs)497     public static Source sourceFor(final String name, final File file, final Charset cs) throws IOException {
498         final File absFile = file.getAbsoluteFile();
499         return sourceFor(name, dirName(absFile, null), new FileData(file, cs));
500     }
501 
502     /**
503      * Returns an instance
504      *
505      * @param name source name
506      * @param reader reader from which source can be loaded
507      *
508      * @return source instance
509      *
510      * @throws IOException if source cannot be loaded
511      */
sourceFor(final String name, final Reader reader)512     public static Source sourceFor(final String name, final Reader reader) throws IOException {
513         // Extract URL from URLReader to defer loading and reuse cached data if available.
514         if (reader instanceof URLReader) {
515             final URLReader urlReader = (URLReader) reader;
516             return sourceFor(name, urlReader.getURL(), urlReader.getCharset());
517         }
518         return new Source(name, baseName(name), new RawData(reader));
519     }
520 
521     @Override
equals(final Object obj)522     public boolean equals(final Object obj) {
523         if (this == obj) {
524             return true;
525         }
526         if (!(obj instanceof Source)) {
527             return false;
528         }
529         final Source other = (Source) obj;
530         return Objects.equals(name, other.name) && data.equals(other.data);
531     }
532 
533     @Override
hashCode()534     public int hashCode() {
535         int h = hash;
536         if (h == 0) {
537             h = hash = data.hashCode() ^ Objects.hashCode(name);
538         }
539         return h;
540     }
541 
542     /**
543      * Fetch source content.
544      * @return Source content.
545      */
getString()546     public String getString() {
547         return data.toString();
548     }
549 
550     /**
551      * Get the user supplied name of this script.
552      * @return User supplied source name.
553      */
getName()554     public String getName() {
555         return name;
556     }
557 
558     /**
559      * Get the last modified time of this script.
560      * @return Last modified time.
561      */
getLastModified()562     public long getLastModified() {
563         return data.lastModified();
564     }
565 
566     /**
567      * Get the "directory" part of the file or "base" of the URL.
568      * @return base of file or URL.
569      */
getBase()570     public String getBase() {
571         return base;
572     }
573 
574     /**
575      * Fetch a portion of source content.
576      * @param start start index in source
577      * @param len length of portion
578      * @return Source content portion.
579      */
getString(final int start, final int len)580     public String getString(final int start, final int len) {
581         return new String(data(), start, len);
582     }
583 
584     /**
585      * Fetch a portion of source content associated with a token.
586      * @param token Token descriptor.
587      * @return Source content portion.
588      */
getString(final long token)589     public String getString(final long token) {
590         final int start = Token.descPosition(token);
591         final int len = Token.descLength(token);
592         return new String(data(), start, len);
593     }
594 
595     /**
596      * Returns the source URL of this script Source. Can be null if Source
597      * was created from a String or a char[].
598      *
599      * @return URL source or null
600      */
getURL()601     public URL getURL() {
602         return data.url();
603     }
604 
605     /**
606      * Get explicit source URL.
607      * @return URL set vial sourceURL directive
608      */
getExplicitURL()609     public String getExplicitURL() {
610         return explicitURL;
611     }
612 
613     /**
614      * Set explicit source URL.
615      * @param explicitURL URL set via sourceURL directive
616      */
setExplicitURL(final String explicitURL)617     public void setExplicitURL(final String explicitURL) {
618         this.explicitURL = explicitURL;
619     }
620 
621     /**
622      * Returns whether this source was submitted via 'eval' call or not.
623      *
624      * @return true if this source represents code submitted via 'eval'
625      */
isEvalCode()626     public boolean isEvalCode() {
627         return data.isEvalCode();
628     }
629 
630     /**
631      * Find the beginning of the line containing position.
632      * @param position Index to offending token.
633      * @return Index of first character of line.
634      */
findBOLN(final int position)635     private int findBOLN(final int position) {
636         final char[] d = data();
637         for (int i = position - 1; i > 0; i--) {
638             final char ch = d[i];
639 
640             if (ch == '\n' || ch == '\r') {
641                 return i + 1;
642             }
643         }
644 
645         return 0;
646     }
647 
648     /**
649      * Find the end of the line containing position.
650      * @param position Index to offending token.
651      * @return Index of last character of line.
652      */
findEOLN(final int position)653     private int findEOLN(final int position) {
654         final char[] d = data();
655         final int length = d.length;
656         for (int i = position; i < length; i++) {
657             final char ch = d[i];
658 
659             if (ch == '\n' || ch == '\r') {
660                 return i - 1;
661             }
662         }
663 
664         return length - 1;
665     }
666 
667     /**
668      * Return line number of character position.
669      *
670      * <p>This method can be expensive for large sources as it iterates through
671      * all characters up to {@code position}.</p>
672      *
673      * @param position Position of character in source content.
674      * @return Line number.
675      */
getLine(final int position)676     public int getLine(final int position) {
677         final char[] d = data();
678         // Line count starts at 1.
679         int line = 1;
680 
681         for (int i = 0; i < position; i++) {
682             final char ch = d[i];
683             // Works for both \n and \r\n.
684             if (ch == '\n') {
685                 line++;
686             }
687         }
688 
689         return line;
690     }
691 
692     /**
693      * Return column number of character position.
694      * @param position Position of character in source content.
695      * @return Column number.
696      */
getColumn(final int position)697     public int getColumn(final int position) {
698         // TODO - column needs to account for tabs.
699         return position - findBOLN(position);
700     }
701 
702     /**
703      * Return line text including character position.
704      * @param position Position of character in source content.
705      * @return Line text.
706      */
getSourceLine(final int position)707     public String getSourceLine(final int position) {
708         // Find end of previous line.
709         final int first = findBOLN(position);
710         // Find end of this line.
711         final int last = findEOLN(position);
712 
713         return new String(data(), first, last - first + 1);
714     }
715 
716     /**
717      * Get the content of this source as a char array. Note that the underlying array is returned instead of a
718      * clone; modifying the char array will cause modification to the source; this should not be done. While
719      * there is an apparent danger that we allow unfettered access to an underlying mutable array, the
720      * {@code Source} class is in a restricted {@code jdk.nashorn.internal.*} package and as such it is
721      * inaccessible by external actors in an environment with a security manager. Returning a clone would be
722      * detrimental to performance.
723      * @return content the content of this source as a char array
724      */
getContent()725     public char[] getContent() {
726         return data();
727     }
728 
729     /**
730      * Get the length in chars for this source
731      * @return length
732      */
getLength()733     public int getLength() {
734         return data.length();
735     }
736 
737     /**
738      * Read all of the source until end of file. Return it as char array
739      *
740      * @param reader reader opened to source stream
741      * @return source as content
742      * @throws IOException if source could not be read
743      */
readFully(final Reader reader)744     public static char[] readFully(final Reader reader) throws IOException {
745         final char[]        arr = new char[BUF_SIZE];
746         final StringBuilder sb  = new StringBuilder();
747 
748         try {
749             int numChars;
750             while ((numChars = reader.read(arr, 0, arr.length)) > 0) {
751                 sb.append(arr, 0, numChars);
752             }
753         } finally {
754             reader.close();
755         }
756 
757         return sb.toString().toCharArray();
758     }
759 
760     /**
761      * Read all of the source until end of file. Return it as char array
762      *
763      * @param file source file
764      * @return source as content
765      * @throws IOException if source could not be read
766      */
readFully(final File file)767     public static char[] readFully(final File file) throws IOException {
768         if (!file.isFile()) {
769             throw new IOException(file + " is not a file"); //TODO localize?
770         }
771         return byteToCharArray(Files.readAllBytes(file.toPath()));
772     }
773 
774     /**
775      * Read all of the source until end of file. Return it as char array
776      *
777      * @param file source file
778      * @param cs Charset used to convert bytes to chars
779      * @return source as content
780      * @throws IOException if source could not be read
781      */
readFully(final File file, final Charset cs)782     public static char[] readFully(final File file, final Charset cs) throws IOException {
783         if (!file.isFile()) {
784             throw new IOException(file + " is not a file"); //TODO localize?
785         }
786 
787         final byte[] buf = Files.readAllBytes(file.toPath());
788         return (cs != null) ? new String(buf, cs).toCharArray() : byteToCharArray(buf);
789     }
790 
791     /**
792      * Read all of the source until end of stream from the given URL. Return it as char array
793      *
794      * @param url URL to read content from
795      * @return source as content
796      * @throws IOException if source could not be read
797      */
readFully(final URL url)798     public static char[] readFully(final URL url) throws IOException {
799         return readFully(url.openStream());
800     }
801 
802     /**
803      * Read all of the source until end of file. Return it as char array
804      *
805      * @param url URL to read content from
806      * @param cs Charset used to convert bytes to chars
807      * @return source as content
808      * @throws IOException if source could not be read
809      */
readFully(final URL url, final Charset cs)810     public static char[] readFully(final URL url, final Charset cs) throws IOException {
811         return readFully(url.openStream(), cs);
812     }
813 
814     /**
815      * Get a Base64-encoded SHA1 digest for this source.
816      *
817      * @return a Base64-encoded SHA1 digest for this source
818      */
getDigest()819     public String getDigest() {
820         return new String(getDigestBytes(), StandardCharsets.US_ASCII);
821     }
822 
getDigestBytes()823     private byte[] getDigestBytes() {
824         byte[] ldigest = digest;
825         if (ldigest == null) {
826             final char[] content = data();
827             final byte[] bytes = new byte[content.length * 2];
828 
829             for (int i = 0; i < content.length; i++) {
830                 bytes[i * 2]     = (byte)  (content[i] & 0x00ff);
831                 bytes[i * 2 + 1] = (byte) ((content[i] & 0xff00) >> 8);
832             }
833 
834             try {
835                 final MessageDigest md = MessageDigest.getInstance("SHA-1");
836                 if (name != null) {
837                     md.update(name.getBytes(StandardCharsets.UTF_8));
838                 }
839                 if (base != null) {
840                     md.update(base.getBytes(StandardCharsets.UTF_8));
841                 }
842                 if (getURL() != null) {
843                     md.update(getURL().toString().getBytes(StandardCharsets.UTF_8));
844                 }
845                 digest = ldigest = BASE64.encode(md.digest(bytes));
846             } catch (final NoSuchAlgorithmException e) {
847                 throw new RuntimeException(e);
848             }
849         }
850         return ldigest;
851     }
852 
853     /**
854      * Get the base url. This is currently used for testing only
855      * @param url a URL
856      * @return base URL for url
857      */
baseURL(final URL url)858     public static String baseURL(final URL url) {
859         if (url.getProtocol().equals("file")) {
860             try {
861                 final Path path = Paths.get(url.toURI());
862                 final Path parent = path.getParent();
863                 return (parent != null) ? (parent + File.separator) : null;
864             } catch (final SecurityException | URISyntaxException | IOError e) {
865                 return null;
866             }
867         }
868 
869         // FIXME: is there a better way to find 'base' URL of a given URL?
870         String path = url.getPath();
871         if (path.isEmpty()) {
872             return null;
873         }
874         path = path.substring(0, path.lastIndexOf('/') + 1);
875         final int port = url.getPort();
876         try {
877             return new URL(url.getProtocol(), url.getHost(), port, path).toString();
878         } catch (final MalformedURLException e) {
879             return null;
880         }
881     }
882 
dirName(final File file, final String DEFAULT_BASE_NAME)883     private static String dirName(final File file, final String DEFAULT_BASE_NAME) {
884         final String res = file.getParent();
885         return (res != null) ? (res + File.separator) : DEFAULT_BASE_NAME;
886     }
887 
888     // fake directory like name
baseName(final String name)889     private static String baseName(final String name) {
890         int idx = name.lastIndexOf('/');
891         if (idx == -1) {
892             idx = name.lastIndexOf('\\');
893         }
894         return (idx != -1) ? name.substring(0, idx + 1) : null;
895     }
896 
readFully(final InputStream is, final Charset cs)897     private static char[] readFully(final InputStream is, final Charset cs) throws IOException {
898         return (cs != null) ? new String(readBytes(is), cs).toCharArray() : readFully(is);
899     }
900 
readFully(final InputStream is)901     private static char[] readFully(final InputStream is) throws IOException {
902         return byteToCharArray(readBytes(is));
903     }
904 
byteToCharArray(final byte[] bytes)905     private static char[] byteToCharArray(final byte[] bytes) {
906         Charset cs = StandardCharsets.UTF_8;
907         int start = 0;
908         // BOM detection.
909         if (bytes.length > 1 && bytes[0] == (byte) 0xFE && bytes[1] == (byte) 0xFF) {
910             start = 2;
911             cs = StandardCharsets.UTF_16BE;
912         } else if (bytes.length > 1 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE) {
913             if (bytes.length > 3 && bytes[2] == 0 && bytes[3] == 0) {
914                 start = 4;
915                 cs = Charset.forName("UTF-32LE");
916             } else {
917                 start = 2;
918                 cs = StandardCharsets.UTF_16LE;
919             }
920         } else if (bytes.length > 2 && bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) {
921             start = 3;
922             cs = StandardCharsets.UTF_8;
923         } else if (bytes.length > 3 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == (byte) 0xFE && bytes[3] == (byte) 0xFF) {
924             start = 4;
925             cs = Charset.forName("UTF-32BE");
926         }
927 
928         return new String(bytes, start, bytes.length - start, cs).toCharArray();
929     }
930 
readBytes(final InputStream is)931     static byte[] readBytes(final InputStream is) throws IOException {
932         final byte[] arr = new byte[BUF_SIZE];
933         try {
934             try (ByteArrayOutputStream buf = new ByteArrayOutputStream()) {
935                 int numBytes;
936                 while ((numBytes = is.read(arr, 0, arr.length)) > 0) {
937                     buf.write(arr, 0, numBytes);
938                 }
939                 return buf.toByteArray();
940             }
941         } finally {
942             is.close();
943         }
944     }
945 
946     @Override
toString()947     public String toString() {
948         return getName();
949     }
950 
getURLFromFile(final File file)951     private static URL getURLFromFile(final File file) {
952         try {
953             return file.toURI().toURL();
954         } catch (final SecurityException | MalformedURLException ignored) {
955             return null;
956         }
957     }
958 
getLoggerStatic()959     private static DebugLogger getLoggerStatic() {
960         final Context context = Context.getContextTrustedOrNull();
961         return context == null ? null : context.getLogger(Source.class);
962     }
963 
964     @Override
initLogger(final Context context)965     public DebugLogger initLogger(final Context context) {
966         return context.getLogger(this.getClass());
967     }
968 
969     @Override
getLogger()970     public DebugLogger getLogger() {
971         return initLogger(Context.getContextTrusted());
972     }
973 
dumpFile(final File dirFile)974     private File dumpFile(final File dirFile) {
975         final URL u = getURL();
976         final StringBuilder buf = new StringBuilder();
977         // make it unique by prefixing current date & time
978         buf.append(LocalDateTime.now().toString());
979         buf.append('_');
980         if (u != null) {
981             // make it a safe file name
982             buf.append(u.toString()
983                     .replace('/', '_')
984                     .replace('\\', '_'));
985         } else {
986             buf.append(getName());
987         }
988 
989         return new File(dirFile, buf.toString());
990     }
991 
dump(final String dir)992     void dump(final String dir) {
993         final File dirFile = new File(dir);
994         final File file = dumpFile(dirFile);
995         if (!dirFile.exists() && !dirFile.mkdirs()) {
996             debug("Skipping source dump for " + name);
997             return;
998         }
999 
1000         try (final FileOutputStream fos = new FileOutputStream(file)) {
1001             final PrintWriter pw = new PrintWriter(fos);
1002             pw.print(data.toString());
1003             pw.flush();
1004         } catch (final IOException ioExp) {
1005             debug("Skipping source dump for " +
1006                     name +
1007                     ": " +
1008                     ECMAErrors.getMessage(
1009                         "io.error.cant.write",
1010                         dir.toString() +
1011                         " : " + ioExp.toString()));
1012         }
1013     }
1014 }
1015