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 static jdk.nashorn.internal.codegen.CompilerConstants.SOURCE;
  29 import static jdk.nashorn.internal.runtime.UnwarrantedOptimismException.INVALID_PROGRAM_POINT;
  30 
  31 import java.lang.invoke.MethodHandle;
  32 import java.lang.reflect.Field;
  33 import java.net.URL;
  34 import java.util.HashSet;
  35 import java.util.Set;
  36 import jdk.nashorn.internal.scripts.JS;
  37 
  38 /**
  39  * This class provides support for external debuggers.  Its primary purpose is
  40  * is to simplify the debugger tasks and provide better performance.
  41  * Even though the methods are not public, there are still part of the
  42  * external debugger interface.
  43  */
  44 final class DebuggerSupport {
  45     /**
  46      * Hook to force the loading of the DebuggerSupport class so that it is
  47      * available to external debuggers.
  48      */
  49     static boolean FORCELOAD = true;
  50 
  51     static {
  52         /**
  53          * Hook to force the loading of the DebuggerValueDesc class so that it is
  54          * available to external debuggers.
  55          */
  56         @SuppressWarnings("unused")
  57         final
  58         DebuggerValueDesc forceLoad = new DebuggerValueDesc(null, false, null, null);
  59 
  60         // Hook to force the loading of the SourceInfo class
  61         @SuppressWarnings("unused")
  62         final
  63         SourceInfo srcInfo = new SourceInfo(null, 0, null, null);
  64     }
  65 
  66     /** This class is used to send a bulk description of a value. */
  67     static class DebuggerValueDesc {
  68         /** Property key (or index) or field name. */
  69         final String key;
  70 
  71         /** If the value is expandable. */
  72         final boolean expandable;
  73 
  74         /** Property or field value as object. */
  75         final Object valueAsObject;
  76 
  77         /** Property or field value as string. */
  78         final String valueAsString;
  79 
  80         DebuggerValueDesc(final String key, final boolean expandable, final Object valueAsObject, final String valueAsString) {
  81             this.key           = key;
  82             this.expandable    = expandable;
  83             this.valueAsObject = valueAsObject;
  84             this.valueAsString = valueAsString;
  85         }
  86     }
  87 
  88     static class SourceInfo {
  89         final String name;
  90         final URL    url;
  91         final int    hash;
  92         final char[] content;
  93 
  94         SourceInfo(final String name, final int hash, final URL url, final char[] content) {
  95             this.name    = name;
  96             this.hash    = hash;
  97             this.url     = url;
  98             this.content = content;
  99         }
 100     }
 101 
 102     /**
 103      * Hook that is called just before invoking method handle
 104      * from ScriptFunctionData via invoke, constructor method calls.
 105      *
 106      * @param mh script class method about to be invoked.
 107      */
 108     static void notifyInvoke(final MethodHandle mh) {
 109         // Do nothing here. This is placeholder method on which a
 110         // debugger can place a breakpoint so that it can access the
 111         // (script class) method handle that is about to be invoked.
 112         // See ScriptFunctionData.invoke and ScriptFunctionData.construct.
 113     }
 114 
 115     /**
 116      * Return the script source info for the given script class.
 117      *
 118      * @param clazz compiled script class
 119      * @return SourceInfo
 120      */
 121     static SourceInfo getSourceInfo(final Class<?> clazz) {
 122         if (JS.class.isAssignableFrom(clazz)) {
 123             try {
 124                 final Field sourceField = clazz.getDeclaredField(SOURCE.symbolName());
 125                 sourceField.setAccessible(true);
 126                 final Source src = (Source) sourceField.get(null);
 127                 return src.getSourceInfo();
 128             } catch (final IllegalAccessException | NoSuchFieldException ignored) {
 129                 return null;
 130             }
 131         }
 132 
 133         return null;
 134     }
 135 
 136     /**
 137      * Return the current context global.
 138      * @return context global.
 139      */
 140     static Object getGlobal() {
 141         return Context.getGlobal();
 142     }
 143 
 144     /**
 145      * Call eval on the current global.
 146      * @param scope           Scope to use.
 147      * @param self            Receiver to use.
 148      * @param string          String to evaluate.
 149      * @param returnException true if exceptions are to be returned.
 150      * @return Result of eval, or, an exception or null depending on returnException.
 151      */
 152     static Object eval(final ScriptObject scope, final Object self, final String string, final boolean returnException) {
 153         final ScriptObject global = Context.getGlobal();
 154         final ScriptObject initialScope = scope != null ? scope : global;
 155         final Object callThis = self != null ? self : global;
 156         final Context context = global.getContext();
 157 
 158         try {
 159             return context.eval(initialScope, string, callThis, ScriptRuntime.UNDEFINED);
 160         } catch (final Throwable ex) {
 161             return returnException ? ex : null;
 162         }
 163     }
 164 
 165     /**
 166      * This method returns a bulk description of an object's properties.
 167      * @param object Script object to be displayed by the debugger.
 168      * @param all    true if to include non-enumerable values.
 169      * @return An array of DebuggerValueDesc.
 170      */
 171     static DebuggerValueDesc[] valueInfos(final Object object, final boolean all) {
 172         assert object instanceof ScriptObject;
 173         return getDebuggerValueDescs((ScriptObject)object, all, new HashSet<>());
 174     }
 175 
 176     /**
 177      * This method returns a debugger description of the value.
 178      * @param name  Name of value (property name).
 179      * @param value Data value.
 180      * @param all   true if to include non-enumerable values.
 181      * @return A DebuggerValueDesc.
 182      */
 183     static DebuggerValueDesc valueInfo(final String name, final Object value, final boolean all) {
 184         return valueInfo(name, value, all, new HashSet<>());
 185     }
 186 
 187    /**
 188      * This method returns a debugger description of the value.
 189      * @param name       Name of value (property name).
 190      * @param value      Data value.
 191      * @param all        true if to include non-enumerable values.
 192      * @param duplicates Duplication set to avoid cycles.
 193      * @return A DebuggerValueDesc.
 194      */
 195     private static DebuggerValueDesc valueInfo(final String name, final Object value, final boolean all, final Set<Object> duplicates) {
 196         if (value instanceof ScriptObject && !(value instanceof ScriptFunction)) {
 197             final ScriptObject object = (ScriptObject)value;
 198             return new DebuggerValueDesc(name, !object.isEmpty(), value, objectAsString(object, all, duplicates));
 199         }
 200         return new DebuggerValueDesc(name, false, value, valueAsString(value));
 201     }
 202 
 203     /**
 204      * Generate the descriptions for an object's properties.
 205      * @param object     Object to introspect.
 206      * @param all        true if to include non-enumerable values.
 207      * @param duplicates Duplication set to avoid cycles.
 208      * @return An array of DebuggerValueDesc.
 209      */
 210     private static DebuggerValueDesc[] getDebuggerValueDescs(final ScriptObject object, final boolean all, final Set<Object> duplicates) {
 211         if (duplicates.contains(object)) {
 212             return null;
 213         }
 214 
 215         duplicates.add(object);
 216 
 217         final String[] keys = object.getOwnKeys(all);
 218         final DebuggerValueDesc[] descs = new DebuggerValueDesc[keys.length];
 219 
 220         for (int i = 0; i < keys.length; i++) {
 221             final String key = keys[i];
 222             descs[i] = valueInfo(key, object.get(key), all, duplicates);
 223         }
 224 
 225         duplicates.remove(object);
 226 
 227         return descs;
 228     }
 229 
 230     /**
 231      * Generate a string representation of a Script object.
 232      * @param object     Script object to represent.
 233      * @param all        true if to include non-enumerable values.
 234      * @param duplicates Duplication set to avoid cycles.
 235      * @return String representation.
 236      */
 237     private static String objectAsString(final ScriptObject object, final boolean all, final Set<Object> duplicates) {
 238         final StringBuilder sb = new StringBuilder();
 239 
 240         if (ScriptObject.isArray(object)) {
 241             sb.append('[');
 242             final long length = (long) object.getDouble("length", INVALID_PROGRAM_POINT);
 243 
 244             for (long i = 0; i < length; i++) {
 245                 if (object.has(i)) {
 246                     final Object valueAsObject = object.get(i);
 247                     final boolean isUndefined = valueAsObject == ScriptRuntime.UNDEFINED;
 248 
 249                     if (isUndefined) {
 250                         if (i != 0) {
 251                             sb.append(",");
 252                         }
 253                     } else {
 254                         if (i != 0) {
 255                             sb.append(", ");
 256                         }
 257 
 258                         if (valueAsObject instanceof ScriptObject && !(valueAsObject instanceof ScriptFunction)) {
 259                             final String objectString = objectAsString((ScriptObject)valueAsObject, all, duplicates);
 260                             sb.append(objectString != null ? objectString : "{...}");
 261                         } else {
 262                             sb.append(valueAsString(valueAsObject));
 263                         }
 264                     }
 265                 } else {
 266                     if (i != 0) {
 267                         sb.append(',');
 268                     }
 269                 }
 270             }
 271 
 272             sb.append(']');
 273         } else {
 274             sb.append('{');
 275             final DebuggerValueDesc[] descs = getDebuggerValueDescs(object, all, duplicates);
 276 
 277             if (descs != null) {
 278                 for (int i = 0; i < descs.length; i++) {
 279                     if (i != 0) {
 280                         sb.append(", ");
 281                     }
 282 
 283                     final String valueAsString = descs[i].valueAsString;
 284                     sb.append(descs[i].key);
 285                     sb.append(": ");
 286                     sb.append(valueAsString);
 287                 }
 288             }
 289 
 290             sb.append('}');
 291         }
 292 
 293         return sb.toString();
 294     }
 295 
 296     /**
 297      * This method returns a string representation of a value.
 298      * @param value Arbitrary value to be displayed by the debugger.
 299      * @return A string representation of the value or an array of DebuggerValueDesc.
 300      */
 301     static String valueAsString(final Object value) {
 302         final JSType type = JSType.of(value);
 303 
 304         switch (type) {
 305         case BOOLEAN:
 306             return value.toString();
 307 
 308         case STRING:
 309             return escape(value.toString());
 310 
 311         case NUMBER:
 312             return JSType.toString(((Number)value).doubleValue());
 313 
 314         case NULL:
 315             return "null";
 316 
 317         case UNDEFINED:
 318             return "undefined";
 319 
 320         case OBJECT:
 321             return ScriptRuntime.safeToString(value);
 322 
 323         case FUNCTION:
 324             if (value instanceof ScriptFunction) {
 325                 return ((ScriptFunction)value).toSource();
 326             }
 327             return value.toString();
 328 
 329         default:
 330             return value.toString();
 331         }
 332     }
 333 
 334     /**
 335      * Escape a string into a form that can be parsed by JavaScript.
 336      * @param value String to be escaped.
 337      * @return Escaped string.
 338      */
 339     private static String escape(final String value) {
 340         final StringBuilder sb = new StringBuilder();
 341 
 342         sb.append("\"");
 343 
 344         for (final char ch : value.toCharArray()) {
 345             switch (ch) {
 346             case '\\':
 347                 sb.append("\\\\");
 348                 break;
 349             case '"':
 350                 sb.append("\\\"");
 351                 break;
 352             case '\'':
 353                 sb.append("\\\'");
 354                 break;
 355             case '\b':
 356                 sb.append("\\b");
 357                 break;
 358             case '\f':
 359                 sb.append("\\f");
 360                 break;
 361             case '\n':
 362                 sb.append("\\n");
 363                 break;
 364             case '\r':
 365                 sb.append("\\r");
 366                 break;
 367             case '\t':
 368                 sb.append("\\t");
 369                 break;
 370             default:
 371                 if (ch < ' ' || ch >= 0xFF) {
 372                     sb.append("\\u");
 373 
 374                     final String hex = Integer.toHexString(ch);
 375                     for (int i = hex.length(); i < 4; i++) {
 376                         sb.append('0');
 377                     }
 378                     sb.append(hex);
 379                 } else {
 380                     sb.append(ch);
 381                 }
 382 
 383                 break;
 384             }
 385         }
 386 
 387         sb.append("\"");
 388 
 389         return sb.toString();
 390     }
 391 }
 392 
 393