1 /*
2  * Copyright (c) 2010, 2015, 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 static jdk.nashorn.internal.lookup.Lookup.MH;
29 import static jdk.nashorn.internal.runtime.ECMAErrors.rangeError;
30 import static jdk.nashorn.internal.runtime.ECMAErrors.typeError;
31 import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;
32 
33 import java.io.BufferedReader;
34 import java.io.File;
35 import java.io.IOException;
36 import java.io.InputStreamReader;
37 import java.io.OutputStreamWriter;
38 import java.io.StreamTokenizer;
39 import java.io.StringReader;
40 import java.lang.invoke.MethodHandle;
41 import java.lang.invoke.MethodHandles;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.List;
45 import java.util.Map;
46 import jdk.nashorn.internal.objects.NativeArray;
47 
48 /**
49  * Global functions supported only in scripting mode.
50  */
51 public final class ScriptingFunctions {
52 
53     /** Handle to implementation of {@link ScriptingFunctions#readLine} - Nashorn extension */
54     public static final MethodHandle READLINE = findOwnMH("readLine", Object.class, Object.class, Object.class);
55 
56     /** Handle to implementation of {@link ScriptingFunctions#readFully} - Nashorn extension */
57     public static final MethodHandle READFULLY = findOwnMH("readFully",     Object.class, Object.class, Object.class);
58 
59     /** Handle to implementation of {@link ScriptingFunctions#exec} - Nashorn extension */
60     public static final MethodHandle EXEC = findOwnMH("exec",     Object.class, Object.class, Object[].class);
61 
62     /** EXEC name - special property used by $EXEC API. */
63     public static final String EXEC_NAME = "$EXEC";
64 
65     /** OUT name - special property used by $EXEC API. */
66     public static final String OUT_NAME  = "$OUT";
67 
68     /** ERR name - special property used by $EXEC API. */
69     public static final String ERR_NAME  = "$ERR";
70 
71     /** EXIT name - special property used by $EXEC API. */
72     public static final String EXIT_NAME = "$EXIT";
73 
74     /** THROW_ON_ERROR name - special property of the $EXEC function used by $EXEC API. */
75     public static final String THROW_ON_ERROR_NAME = "throwOnError";
76 
77     /** Names of special properties used by $ENV API. */
78     public  static final String ENV_NAME  = "$ENV";
79 
80     /** Name of the environment variable for the current working directory. */
81     public static final String PWD_NAME  = "PWD";
82 
ScriptingFunctions()83     private ScriptingFunctions() {
84     }
85 
86     /**
87      * Nashorn extension: global.readLine (scripting-mode-only)
88      * Read one line of input from the standard input.
89      *
90      * @param self   self reference
91      * @param prompt String used as input prompt
92      *
93      * @return line that was read
94      *
95      * @throws IOException if an exception occurs
96      */
readLine(final Object self, final Object prompt)97     public static Object readLine(final Object self, final Object prompt) throws IOException {
98         if (prompt != UNDEFINED) {
99             System.out.print(JSType.toString(prompt));
100         }
101         final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
102         return reader.readLine();
103     }
104 
105     /**
106      * Nashorn extension: Read the entire contents of a text file and return as String.
107      *
108      * @param self self reference
109      * @param file The input file whose content is read.
110      *
111      * @return String content of the input file.
112      *
113      * @throws IOException if an exception occurs
114      */
readFully(final Object self, final Object file)115     public static Object readFully(final Object self, final Object file) throws IOException {
116         File f = null;
117 
118         if (file instanceof File) {
119             f = (File)file;
120         } else if (JSType.isString(file)) {
121             f = new java.io.File(((CharSequence)file).toString());
122         }
123 
124         if (f == null || !f.isFile()) {
125             throw typeError("not.a.file", ScriptRuntime.safeToString(file));
126         }
127 
128         return new String(Source.readFully(f));
129     }
130 
131     /**
132      * Nashorn extension: exec a string in a separate process.
133      *
134      * @param self   self reference
135      * @param args   string to execute, input and additional arguments, to be appended to {@code string}. Additional
136      *               arguments can be passed as either one JavaScript array, whose elements will be converted to
137      *               strings; or as a sequence of varargs, each of which will be converted to a string.
138      *
139      * @return output string from the request
140      *
141      * @throws IOException           if any stream access fails
142      * @throws InterruptedException  if execution is interrupted
143      */
exec(final Object self, final Object... args)144     public static Object exec(final Object self, final Object... args) throws IOException, InterruptedException {
145         // Current global is need to fetch additional inputs and for additional results.
146         final ScriptObject global = Context.getGlobal();
147         final Object string = args.length > 0? args[0] : UNDEFINED;
148         final Object input = args.length > 1? args[1] : UNDEFINED;
149         final Object[] argv = (args.length > 2)? Arrays.copyOfRange(args, 2, args.length) : ScriptRuntime.EMPTY_ARRAY;
150         // Assemble command line, process additional arguments.
151         final List<String> cmdLine = tokenizeString(JSType.toString(string));
152         final Object[] additionalArgs = argv.length == 1 && argv[0] instanceof NativeArray ?
153                 ((NativeArray) argv[0]).asObjectArray() :
154                 argv;
155         for (Object arg : additionalArgs) {
156             cmdLine.add(JSType.toString(arg));
157         }
158 
159         // Set up initial process.
160         final ProcessBuilder processBuilder = new ProcessBuilder(cmdLine);
161 
162         // Current ENV property state.
163         final Object env = global.get(ENV_NAME);
164         if (env instanceof ScriptObject) {
165             final ScriptObject envProperties = (ScriptObject)env;
166 
167             // If a working directory is present, use it.
168             final Object pwd = envProperties.get(PWD_NAME);
169             if (pwd != UNDEFINED) {
170                 final File pwdFile = new File(JSType.toString(pwd));
171                 if (pwdFile.exists()) {
172                     processBuilder.directory(pwdFile);
173                 }
174             }
175 
176             // Set up ENV variables.
177             final Map<String, String> environment = processBuilder.environment();
178             environment.clear();
179             for (final Map.Entry<Object, Object> entry : envProperties.entrySet()) {
180                 environment.put(JSType.toString(entry.getKey()), JSType.toString(entry.getValue()));
181             }
182         }
183 
184         // Start the process.
185         final Process process = processBuilder.start();
186         final IOException exception[] = new IOException[2];
187 
188         // Collect output.
189         final StringBuilder outBuffer = new StringBuilder();
190         final Thread outThread = new Thread(new Runnable() {
191             @Override
192             public void run() {
193                 final char buffer[] = new char[1024];
194                 try (final InputStreamReader inputStream = new InputStreamReader(process.getInputStream())) {
195                     for (int length; (length = inputStream.read(buffer, 0, buffer.length)) != -1; ) {
196                         outBuffer.append(buffer, 0, length);
197                     }
198                 } catch (final IOException ex) {
199                     exception[0] = ex;
200                 }
201             }
202         }, "$EXEC output");
203 
204         // Collect errors.
205         final StringBuilder errBuffer = new StringBuilder();
206         final Thread errThread = new Thread(new Runnable() {
207             @Override
208             public void run() {
209                 final char buffer[] = new char[1024];
210                 try (final InputStreamReader inputStream = new InputStreamReader(process.getErrorStream())) {
211                     for (int length; (length = inputStream.read(buffer, 0, buffer.length)) != -1; ) {
212                         errBuffer.append(buffer, 0, length);
213                     }
214                 } catch (final IOException ex) {
215                     exception[1] = ex;
216                 }
217             }
218         }, "$EXEC error");
219 
220         // Start gathering output.
221         outThread.start();
222         errThread.start();
223 
224         // If input is present, pass on to process.
225         if (!JSType.nullOrUndefined(input)) {
226             try (OutputStreamWriter outputStream = new OutputStreamWriter(process.getOutputStream())) {
227                 final String in = JSType.toString(input);
228                 outputStream.write(in, 0, in.length());
229             } catch (final IOException ex) {
230                 // Process was not expecting input.  May be normal state of affairs.
231             }
232         }
233 
234         // Wait for the process to complete.
235         final int exit = process.waitFor();
236         outThread.join();
237         errThread.join();
238 
239         final String out = outBuffer.toString();
240         final String err = errBuffer.toString();
241 
242         // Set globals for secondary results.
243         global.set(OUT_NAME, out, 0);
244         global.set(ERR_NAME, err, 0);
245         global.set(EXIT_NAME, exit, 0);
246 
247         // Propagate exception if present.
248         for (final IOException element : exception) {
249             if (element != null) {
250                 throw element;
251             }
252         }
253 
254         // if we got a non-zero exit code ("failure"), then we have to decide to throw error or not
255         if (exit != 0) {
256             // get the $EXEC function object from the global object
257             final Object exec = global.get(EXEC_NAME);
258             assert exec instanceof ScriptObject : EXEC_NAME + " is not a script object!";
259 
260             // Check if the user has set $EXEC.throwOnError property to true. If so, throw RangeError
261             // If that property is not set or set to false, then silently proceed with the rest.
262             if (JSType.toBoolean(((ScriptObject)exec).get(THROW_ON_ERROR_NAME))) {
263                 throw rangeError("exec.returned.non.zero", ScriptRuntime.safeToString(exit));
264             }
265         }
266 
267         // Return the result from stdout.
268         return out;
269     }
270 
findOwnMH(final String name, final Class<?> rtype, final Class<?>... types)271     private static MethodHandle findOwnMH(final String name, final Class<?> rtype, final Class<?>... types) {
272         return MH.findStatic(MethodHandles.lookup(), ScriptingFunctions.class, name, MH.type(rtype, types));
273     }
274 
275     /**
276      * Break a string into tokens, honoring quoted arguments and escaped spaces.
277      *
278      * @param str a {@link String} to tokenize.
279      * @return a {@link List} of {@link String}s representing the tokens that
280      * constitute the string.
281      */
tokenizeString(final String str)282     public static List<String> tokenizeString(final String str) {
283         final StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(str));
284         tokenizer.resetSyntax();
285         tokenizer.wordChars(0, 255);
286         tokenizer.whitespaceChars(0, ' ');
287         tokenizer.commentChar('#');
288         tokenizer.quoteChar('"');
289         tokenizer.quoteChar('\'');
290         final List<String> tokenList = new ArrayList<>();
291         final StringBuilder toAppend = new StringBuilder();
292         while (nextToken(tokenizer) != StreamTokenizer.TT_EOF) {
293             final String s = tokenizer.sval;
294             // The tokenizer understands about honoring quoted strings and recognizes
295             // them as one token that possibly contains multiple space-separated words.
296             // It does not recognize quoted spaces, though, and will split after the
297             // escaping \ character. This is handled here.
298             if (s.endsWith("\\")) {
299                 // omit trailing \, append space instead
300                 toAppend.append(s.substring(0, s.length() - 1)).append(' ');
301             } else {
302                 tokenList.add(toAppend.append(s).toString());
303                 toAppend.setLength(0);
304             }
305         }
306         if (toAppend.length() != 0) {
307             tokenList.add(toAppend.toString());
308         }
309         return tokenList;
310     }
311 
nextToken(final StreamTokenizer tokenizer)312     private static int nextToken(final StreamTokenizer tokenizer) {
313         try {
314             return tokenizer.nextToken();
315         } catch (final IOException ioe) {
316             return StreamTokenizer.TT_EOF;
317         }
318     }
319 }
320