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  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  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  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.util.HashSet;
  29 import java.util.Set;
  30 
  31 /**
  32  * This class provides support for external debuggers.  Its primary purpose is
  33  * is to simplify the debugger tasks and provide better performance.
  34  */
  35 final class DebuggerSupport {
  36     /**
  37      * Hook to force the loading of the DebuggerSupport class so that it is
  38      * available to external debuggers.
  39      */
  40     static boolean FORCELOAD = true;
  41 
  42     static {
  43         /**
  44          * Hook to force the loading of the DebuggerValueDesc class so that it is
  45          * available to external debuggers.
  46          */
  47         @SuppressWarnings("unused")
  48         DebuggerValueDesc forceLoad = new DebuggerValueDesc(null, false, null, null);
  49     }
  50 
  51     /** This class is used to send a bulk description of a value. */
  52     static class DebuggerValueDesc {
  53         /** Property key (or index) or field name. */
  54         final String key;
  55 
  56         /** If the value is expandable. */
  57         final boolean expandable;
  58 
  59         /** Property or field value as object. */
  60         final Object valueAsObject;
  61 
  62         /** Property or field value as string. */
  63         final String valueAsString;
  64 
  65         DebuggerValueDesc(final String key, final boolean expandable, final Object valueAsObject, final String valueAsString) {
  66             this.key           = key;
  67             this.expandable    = expandable;
  68             this.valueAsObject = valueAsObject;
  69             this.valueAsString = valueAsString;
  70         }
  71     }
  72 
  73     /**
  74      * Return the current context global.
  75      * @return context global.
  76      */
  77     static Object getGlobal() {
  78         return Context.getGlobalTrusted();
  79     }
  80 
  81     /**
  82      * Call eval on the current global.
  83      * @param scope           Scope to use.
  84      * @param self            Receiver to use.
  85      * @param string          String to evaluate.
  86      * @param returnException true if exceptions are to be returned.
  87      * @return Result of eval as string, or, an exception or null depending on returnException.
  88      */
  89     static Object eval(final ScriptObject scope, final Object self, final String string, final boolean returnException) {
  90         final ScriptObject global = Context.getGlobalTrusted();
  91         final ScriptObject initialScope = scope != null ? scope : global;
  92         final Object callThis = self != null ? self : global;
  93         final Context context = global.getContext();
  94 
  95         try {
  96             return context.eval(initialScope, string, callThis, ScriptRuntime.UNDEFINED, false);
  97         } catch (Throwable ex) {
  98             return returnException ? ex : null;
  99         }
 100     }
 101 
 102     /**
 103      * This method returns a bulk description of an object's properties.
 104      * @param object Script object to be displayed by the debugger.
 105      * @param all    true if to include non-enumerable values.
 106      * @return An array of DebuggerValueDesc.
 107      */
 108     static DebuggerValueDesc[] valueInfos(final Object object, final boolean all) {
 109         assert object instanceof ScriptObject;
 110         return getDebuggerValueDescs((ScriptObject)object, all, new HashSet<>());
 111     }
 112 
 113     /**
 114      * This method returns a debugger description of the value.
 115      * @param name  Name of value (property name).
 116      * @param value Data value.
 117      * @param all   true if to include non-enumerable values.
 118      * @return A DebuggerValueDesc.
 119      */
 120     static DebuggerValueDesc valueInfo(final String name, final Object value, final boolean all) {
 121         return valueInfo(name, value, all, new HashSet<>());
 122     }
 123 
 124    /**
 125      * This method returns a debugger description of the value.
 126      * @param name       Name of value (property name).
 127      * @param value      Data value.
 128      * @param all        true if to include non-enumerable values.
 129      * @param duplicates Duplication set to avoid cycles.
 130      * @return A DebuggerValueDesc.
 131      */
 132     private static DebuggerValueDesc valueInfo(final String name, final Object value, final boolean all, final Set<Object> duplicates) {
 133         if (value instanceof ScriptObject && !(value instanceof ScriptFunction)) {
 134             final ScriptObject object = (ScriptObject)value;
 135             return new DebuggerValueDesc(name, !object.isEmpty(), value, objectAsString(object, all, duplicates));
 136         }
 137         return new DebuggerValueDesc(name, false, value, valueAsString(value));
 138     }
 139 
 140     /**
 141      * Generate the descriptions for an object's properties.
 142      * @param object     Object to introspect.
 143      * @param all        true if to include non-enumerable values.
 144      * @param duplicates Duplication set to avoid cycles.
 145      * @return An array of DebuggerValueDesc.
 146      */
 147     private static DebuggerValueDesc[] getDebuggerValueDescs(final ScriptObject object, final boolean all, final Set<Object> duplicates) {
 148         if (duplicates.contains(object)) {
 149             return null;
 150         }
 151 
 152         duplicates.add(object);
 153 
 154         final String[] keys = object.getOwnKeys(all);
 155         final DebuggerValueDesc[] descs = new DebuggerValueDesc[keys.length];
 156 
 157         for (int i = 0; i < keys.length; i++) {
 158             final String key = keys[i];
 159             descs[i] = valueInfo(key, object.get(key), all, duplicates);
 160         }
 161 
 162         duplicates.remove(object);
 163 
 164         return descs;
 165     }
 166 
 167     /**
 168      * Generate a string representation of a Script object.
 169      * @param object     Script object to represent.
 170      * @param all        true if to include non-enumerable values.
 171      * @param duplicates Duplication set to avoid cycles.
 172      * @return String representation.
 173      */
 174     private static String objectAsString(final ScriptObject object, final boolean all, final Set<Object> duplicates) {
 175         final StringBuilder sb = new StringBuilder();
 176 
 177         if (ScriptObject.isArray(object)) {
 178             sb.append('[');
 179             final long length = object.getLong("length");
 180 
 181             for (long i = 0; i < length; i++) {
 182                 if (object.has(i)) {
 183                     final Object valueAsObject = object.get(i);
 184                     final boolean isUndefined = JSType.of(valueAsObject) == JSType.UNDEFINED;
 185 
 186                     if (isUndefined) {
 187                         if (i != 0) {
 188                             sb.append(",");
 189                         }
 190                     } else {
 191                         if (i != 0) {
 192                             sb.append(", ");
 193                         }
 194 
 195                         if (valueAsObject instanceof ScriptObject && !(valueAsObject instanceof ScriptFunction)) {
 196                             final String objectString = objectAsString((ScriptObject)valueAsObject, all, duplicates);
 197                             sb.append(objectString != null ? objectString : "{...}");
 198                         } else {
 199                             sb.append(valueAsString(valueAsObject));
 200                         }
 201                     }
 202                 } else {
 203                     if (i != 0) {
 204                         sb.append(',');
 205                     }
 206                 }
 207             }
 208 
 209             sb.append(']');
 210         } else {
 211             sb.append('{');
 212             final DebuggerValueDesc[] descs = getDebuggerValueDescs(object, all, duplicates);
 213 
 214             if (descs != null) {
 215                 for (int i = 0; i < descs.length; i++) {
 216                     if (i != 0) {
 217                         sb.append(", ");
 218                     }
 219 
 220                     final String valueAsString = descs[i].valueAsString;
 221                     sb.append(descs[i].key);
 222                     sb.append(": ");
 223                     sb.append(valueAsString);
 224                 }
 225             }
 226 
 227             sb.append('}');
 228         }
 229 
 230         return sb.toString();
 231     }
 232 
 233     /**
 234      * This method returns a string representation of a value.
 235      * @param value Arbitrary value to be displayed by the debugger.
 236      * @return A string representation of the value or an array of DebuggerValueDesc.
 237      */
 238     private static String valueAsString(final Object value) {
 239         final JSType type = JSType.of(value);
 240 
 241         switch (type) {
 242         case BOOLEAN:
 243             return value.toString();
 244 
 245         case STRING:
 246             return escape(value.toString());
 247 
 248         case NUMBER:
 249             return JSType.toString(((Number)value).doubleValue());
 250 
 251         case NULL:
 252             return "null";
 253 
 254         case UNDEFINED:
 255             return "undefined";
 256 
 257         case OBJECT:
 258             return ScriptRuntime.safeToString(value);
 259 
 260         case FUNCTION:
 261             if (value instanceof ScriptFunction) {
 262                 return ((ScriptFunction)value).toSource();
 263             }
 264             return value.toString();
 265 
 266         default:
 267             return value.toString();
 268         }
 269     }
 270 
 271     /**
 272      * Escape a string into a form that can be parsed by JavaScript.
 273      * @param value String to be escaped.
 274      * @return Escaped string.
 275      */
 276     private static String escape(final String value) {
 277         final StringBuilder sb = new StringBuilder();
 278 
 279         sb.append("\"");
 280 
 281         for (final char ch : value.toCharArray()) {
 282             switch (ch) {
 283             case '\\':
 284                 sb.append("\\\\");
 285                 break;
 286             case '"':
 287                 sb.append("\\\"");
 288                 break;
 289             case '\'':
 290                 sb.append("\\\'");
 291                 break;
 292             case '\b':
 293                 sb.append("\\b");
 294                 break;
 295             case '\f':
 296                 sb.append("\\f");
 297                 break;
 298             case '\n':
 299                 sb.append("\\n");
 300                 break;
 301             case '\r':
 302                 sb.append("\\r");
 303                 break;
 304             case '\t':
 305                 sb.append("\\t");
 306                 break;
 307             default:
 308                 if (ch < ' ' || ch >= 0xFF) {
 309                     sb.append("\\u");
 310 
 311                     final String hex = Integer.toHexString(ch);
 312                     for (int i = hex.length(); i < 4; i++) {
 313                         sb.append('0');
 314                     }
 315                     sb.append(hex);
 316                 } else {
 317                     sb.append(ch);
 318                 }
 319 
 320                 break;
 321             }
 322         }
 323 
 324         sb.append("\"");
 325 
 326         return sb.toString();
 327     }
 328 }
 329 
 330