const Long = require('long'); const { ArrayIndexExpression, BinaryOpExpression, ExpressionText, MemberExpression, MethodCallExpression, parse_expression, ParsedExpression, QualifierExpression, RootExpression, TypeCastExpression, UnaryOpExpression, } = require('./parse'); const { DebuggerValue, JavaTaggedValue, JavaType, LiteralValue } = require('../debugger-types'); const { Debugger } = require('../debugger'); const { AndroidThread } = require('../threads'); const { D } = require('../utils/print'); const { decodeJavaCharLiteral } = require('../utils/char-decode'); /** * @param {Long.Long} long */ function hex_long(long) { return long.toUnsigned().toString(16).padStart(64/4, '0'); } /** * Determine what type of primitive a decimal value will require * @param {string} decimal_value * @returns {'int'|'long'|'float'|'double'} */ function get_decimal_number_type(decimal_value) { if (/^-?0*\d{0,15}(\.0*)?$/.test(decimal_value)) { const n = parseInt(decimal_value, 10); if (n >= -2147483648 && n <= 2147483647) { return 'int'; } return 'long'; } // int64: 9223,372036854775807 let m = decimal_value.match(/^(-?)0*(\d*?)(\d{1,4})(\d{15})(\.0+)?$/); if (m) { const sign = m[1]; if (!m[2]) { const x = [parseInt(m[3],10), parseInt(m[4],10)]; if (x[0] < 9223) { return 'long'; } if (x[0] > 9223) { return 'float'; } let limit = 372036854775807 + (sign ? 1 : 0); if (x[1] <= limit) { return 'long'; } return 'float' } // single precision floats allow integers up to +/- 2^127: // 34028,236692093846346,3374,607431768211455 // but rounded to a power of 2 (not checked here) let q = m[2].match(/^(\d*?)(\d{0,5}?)(\d{1,15})$/); if (q[1]) { return 'double'; } const x = [parseInt(q[2],10), parseInt(q[3],10), parseInt(m[3],10), parseInt(m[4],10)] if (x[0] > 34028) { return 'double'; } if (x[0] < 34028) { return 'float'; } if (x[1] > 236692093846346) { return 'double'; } if (x[1] < 236692093846346) { return 'float'; } if (x[2] > 3374) { return 'double'; } if (x[2] < 3374) { return 'float'; } let limit = 607431768211455 + (sign ? 1 : 0); if (x[3] <= limit) { return 'float'; } return 'double'; } if (/^-?\d{0,38}\./.test(decimal_value)) return 'float'; return 'double' } /** * Convert an exponent-formatted number into a normalised decimal equivilent. * e.g '1.2345e3' -> '1234.5' * * If the number does not include an exponent, it is returned unchanged. * @param {string} n */ function decimalise_exponent_number(n) { const exp = n.match(/^(\D*)0*(\d+)(?:\.(\d+?)0*)?[eE]([+-]?)0*(\d+)(.*)/); if (!exp) { return n; } let i = exp[2], frac = (exp[3]||''), sign = exp[4]||'+', pow10 = parseInt(exp[5],10); if (pow10 > 0) { if (sign === '+') { let shifted_digits = Math.min(frac.length, pow10); i += frac.slice(0, shifted_digits); frac = frac.slice(shifted_digits); pow10 -= shifted_digits; i += '0'.repeat(pow10); } else { let shifted_digits = Math.min(i.length, pow10); frac = i.slice(-shifted_digits) + frac; // move up to pow10 digits from i to frac i = i.slice(0, -shifted_digits); pow10 -= shifted_digits; frac = '0'.repeat(pow10) + frac; } } i = (i || '0').match(/^0*(.+)/)[1]; if (/[1-9]/.test(frac)) i += `.${frac}`; return `${exp[1]}${i}${exp[6]}` } /** * @param {number|string} number */ function evaluate_number(number) { let n = number.toString(); // normalise exponents into decimal form n = decimalise_exponent_number(n); let number_type, base = 10; const m = n.match(/^([+-]?)0([bBxX0-7])(.+)/); if (m) { switch (m[2]) { case 'b': base = 2; n = m[1] + m[3]; break; case 'x': base = 16; n = m[1] + m[3]; break; default: base = 8; break; } } if (base !== 16 && /[fFdD]$/.test(n)) { number_type = /[fF]$/.test(n) ? 'float' : 'double'; n = n.slice(0, -1); } else if (/[lL]$/.test(n)) { number_type = 'long' n = n.slice(0, -1); } else { number_type = get_decimal_number_type(n); } let result; if (number_type === 'long') { result = hex_long(Long.fromString(n, false, base)); } else if (/^[fd]/.test(number_type)) { result = (base === 10) ? parseFloat(n) : parseInt(n, base); } else { result = parseInt(n, base) | 0; } const iszero = /^[+-]?0+(\.0*)?$/.test(result.toString()); return new LiteralValue(JavaType[number_type], result, iszero); } /** * @param {string} char */ function evaluate_char(char) { // JDWP returns char values as uint16's, so we need to set the value as a number return new LiteralValue(JavaType.char, char.charCodeAt(0)); } /** * Convert a value to a number * @param {DebuggerValue} local */ function numberify(local) { if (JavaType.isFloat(local.type)) { return parseFloat(local.value); } const radix = JavaType.isLong(local.type) ? 16 : 10; return parseInt(local.value, radix); } /** * Convert a value to a string * @param {Debugger} dbgr * @param {DebuggerValue} local */ async function stringify(dbgr, local) { let s = ''; switch(true) { case JavaType.isString(local.type): s = local.string; break; case JavaType.isPrimitive(local.type): s = local.value.toString(); break; case local.hasnullvalue: s = '(null)'; break; case JavaType.isReference(local.type): // call toString() on the object const str_literal = await dbgr.invokeToString(local.value, local.data.frame.threadid, local.type.signature); s = str_literal.string; break; } return s; } /** * @param {string} operator * @param {boolean} [is_unary] */ function invalid_operator(operator, is_unary = false) { return new Error(`Invalid ${is_unary ? 'type' : 'types'} for operator '${operator}'`); } /** * */ function divide_by_zero() { return new Error('ArithmeticException: divide by zero'); } /** * * @param {*} lhs_local * @param {*} rhs_local * @param {string} operator */ function evaluate_binary_boolean_expression(lhs_local, rhs_local, operator) { let a = lhs_local.value, b = rhs_local.value; switch (operator) { case '&': case '&&': a = a && b; break; case '|': case '||': a = a || b; break; case '^': a = !!(a ^ b); break; case '==': a = a === b; break; case '!=': a = a !== b; break; default: throw invalid_operator(operator); } return new LiteralValue(JavaType.boolean, a); } /** * * @param {*} lhs_local * @param {*} rhs_local * @param {string} operator */ function evaluate_binary_float_expression(lhs_local, rhs_local, operator) { /** @type {number|boolean} */ let a = numberify(lhs_local), b = numberify(rhs_local); switch (operator) { case '+': a += b; break; case '-': a -= b; break; case '*': a *= b; break; case '/': a /= b; break; case '==': a = a === b; break; case '!=': a = a !== b; break; case '<': a = a < b; break; case '<=': a = a <= b; break; case '>': a = a > b; break; case '>=': a = a >= b; break; default: throw invalid_operator(operator); } /** @type {number|boolean|string} */ let value = a, result_type = 'boolean' if (typeof a !== 'boolean') { result_type = (lhs_local.type.signature === 'D' || rhs_local.type.signature === 'D') ? 'double' : 'float'; } return new LiteralValue(JavaType[result_type], value); } /** * * @param {DebuggerValue} lhs * @param {DebuggerValue} rhs * @param {string} operator */ function evaluate_binary_int_expression(lhs, rhs, operator) { /** @type {number|boolean} */ let a = numberify(lhs), b = numberify(rhs); // dividend cannot be zero for / and % if (/[\/%]/.test(operator) && b === 0) { throw divide_by_zero(); } switch (operator) { case '+': a += b; break; case '-': a -= b; break; case '*': a *= b; break; case '/': a = Math.trunc(a / b); break; case '%': a %= b; break; case '<<': a <<= b; break; case '>>': a >>= b; break; case '>>>': a >>>= b; break; case '&': a &= b; break; case '|': a |= b; break; case '^': a ^= b; break; case '==': a = a === b; break; case '!=': a = a !== b; break; case '<': a = a < b; break; case '<=': a = a <= b; break; case '>': a = a > b; break; case '>=': a = a >= b; break; default: throw invalid_operator(operator); } /** @type {number|boolean|string} */ let value = a, result_type = 'boolean' if (typeof a !== 'boolean') { result_type = 'int'; } return new LiteralValue(JavaType[result_type], value); } /** * @param {DebuggerValue} lhs * @param {DebuggerValue} rhs * @param {string} operator */ function evaluate_binary_long_expression(lhs, rhs, operator) { function longify(local) { const radix = JavaType.isLong(local.type) ? 16 : 10; return Long.fromString(`${local.value}`, false, radix); } /** @type {Long.Long|boolean} */ let a = longify(lhs), b = longify(rhs); // dividend cannot be zero for / and % if (/[\/%]/.test(operator) && b.isZero()) { throw divide_by_zero(); } switch (operator) { case '+': a = a.add(b); break; case '-': a = a.subtract(b); break; case '*': a = a.multiply(b); break; case '/': a = a.divide(b); break; case '%': a = a.mod(b); break; case '<<': a = a.shl(b); break; case '>>': a = a.shr(b); break; case '>>>': a = a.shru(b); break; case '&': a = a.and(b); break; case '|': a = a.or(b); break; case '^': a = a.xor(b); break; case '==': a = a.eq(b); break; case '!=': a = !a.eq(b); break; case '<': a = a.lt(b); break; case '<=': a = a.lte(b); break; case '>': a = a.gt(b); break; case '>=': a = a.gte(b); break; default: throw invalid_operator(operator); } /** @type {boolean|Long.Long|string} */ let value = a, result_type = 'boolean'; if (typeof a !== 'boolean') { value = hex_long(a); result_type = 'long'; } return new LiteralValue(JavaType[result_type], value); } /** * @param {Debugger} dbgr * @param {DebuggerValue[]} locals * @param {AndroidThread} thread * @param {ParsedExpression} lhs * @param {ParsedExpression} rhs */ async function evaluate_assignment_expression(dbgr, locals, thread, lhs, rhs) { if (!(lhs instanceof RootExpression)) { throw new Error('Cannot assign value: left-hand-side is not a variable'); } // if there are any qualifiers, the last qualifier must not be a method call const qualified_terms = lhs.qualified_terms.slice(); const last_qualifier = qualified_terms.pop(); if ((lhs.root_term_type !== 'ident') || (last_qualifier instanceof MethodCallExpression)) { throw new Error('Cannot assign value: left-hand-side is not a variable'); } let lhs_value = locals.find(local => local.name === lhs.root_term); if (!lhs_value) { throw new Error(`Cannot assign value: variable '${lhs.root_term}' not found`); } // evaluate the qualified terms, until the last qualifier lhs_value = await evaluate_qualifiers(dbgr, locals, thread, lhs_value, qualified_terms); // evaluate the rhs const value = await evaluate_expression(dbgr, locals, thread, rhs); // assign the value if (last_qualifier instanceof ArrayIndexExpression) { const array_index = await evaluate_expression(dbgr, locals, thread, last_qualifier); await dbgr.setArrayElements(lhs_value, numberify(array_index), 1, JavaTaggedValue.from(value)); } else if (last_qualifier instanceof MemberExpression) { const field = (await dbgr.findNamedFields(lhs_value.type.signature, last_qualifier.name, true))[0] await dbgr.setFieldValue(lhs_value, field, JavaTaggedValue.from(value)); } else { //await dbgr.setLocalVariableValue(lhs_value, JavaTaggedValue.from(value)); } return value; } /** * * @param {Debugger} dbgr * @param {DebuggerValue[]} locals * @param {AndroidThread} thread * @param {ParsedExpression} lhs * @param {ParsedExpression} rhs * @param {string} operator */ async function evaluate_binary_expression(dbgr, locals, thread, lhs, rhs, operator) { if (operator === '=') { return evaluate_assignment_expression(dbgr, locals, thread, lhs, rhs); } const [lhs_value, rhs_value] = await Promise.all([ evaluate_expression(dbgr, locals, thread, lhs), evaluate_expression(dbgr, locals, thread, rhs) ]); const types_key = `${lhs_value.type.signature}#${rhs_value.type.signature}` if (/[BCIJS]#[BCIJS]/.test(types_key) && /J/.test(types_key)) { // both expressions are integers - one is a long return evaluate_binary_long_expression(lhs_value, rhs_value, operator); } if (/[BCIS]#[BCIS]/.test(types_key)) { // both expressions are (non-long) integer types return evaluate_binary_int_expression(lhs_value, rhs_value, operator); } if (/[BCIJSFD]#[BCIJSFD]/.test(types_key)) { // both expressions are number types - one is a float or double return evaluate_binary_float_expression(lhs_value, rhs_value, operator); } if (/Z#Z/.test(types_key)) { // both expressions are boolean types return evaluate_binary_boolean_expression(lhs_value, rhs_value, operator); } // any + operator with a lhs of type String is coerced into a string append if (JavaType.isString(lhs_value.type) && operator === '+') { const rhs_str = await stringify(dbgr, rhs_value); return dbgr.createJavaStringLiteral(lhs_value.string + rhs_str, { israw: true }); } // anything else is an invalid combination throw invalid_operator(operator); } /** * @param {Debugger} dbgr * @param {DebuggerValue[]} locals * @param {AndroidThread} thread * @param {string} operator * @param {ParsedExpression} expr */ async function evaluate_unary_expression(dbgr, locals, thread, operator, expr) { /** @type {DebuggerValue} */ let local = await evaluate_expression(dbgr, locals, thread, expr); const key = `${operator}${local.type.signature}`; switch(true) { case /!Z/.test(key): return new LiteralValue(JavaType.boolean, !local.value); case /~C/.test(key): return evaluate_number(~local.value.charCodeAt(0)); case /~[BIS]/.test(key): return evaluate_number(~local.value); case /~J/.test(key): return new LiteralValue(JavaType.long, hex_long(Long.fromString(local.value, false, 16).not())); case /-C/.test(key): return evaluate_number(-local.value.charCodeAt(0)); case /-[BCIS]/.test(key): return evaluate_number(-local.value); case /-J/.test(key): return new LiteralValue(JavaType.long, hex_long(Long.fromString(local.value, false, 16).neg())); case /\+[BCIJS]/.test(key): return local; default: throw invalid_operator(operator, true); } } /** * * @param {Debugger} dbgr * @param {DebuggerValue[]} locals * @param {string} identifier * @returns {Promise} */ async function evaluate_identifier(dbgr, locals, identifier) { const local = locals.find(l => l.name === identifier); if (local) { return local; } // check if the identifier is an unqualified member of the current 'this' context const this_context = locals.find(l => l.name === 'this'); if (this_context) { try { const member = await evaluate_member(dbgr, new MemberExpression(identifier), this_context); return member; } catch { // not a member of this - just continue } } // if it's not a local, it could be the start of a package name or a type const classes = Array.from(dbgr.session.loadedClasses); return evaluate_qualified_type_name(dbgr, identifier, classes); } /** * * @param {Debugger} dbgr * @param {string} dotted_name * @param {string[]} classes */ async function evaluate_qualified_type_name(dbgr, dotted_name, classes) { const exact_class_matcher = new RegExp(`^L(java/lang/)?${dotted_name.replace(/\./g,'[$/]')};$`); const exact_class = classes.find(signature => exact_class_matcher.test(signature)); if (exact_class) { return dbgr.getTypeValue(exact_class); } const class_matcher = new RegExp(`^L(java/lang/)?${dotted_name.replace('.','[$/]')}/`); const matching_classes = classes.filter(signature => class_matcher.test(signature)); if (matching_classes.length === 0) { // the dotted name doesn't match any packages throw new Error(`'${dotted_name}' is not a package, type or variable name`); } return new DebuggerValue('package', null, dotted_name, true, false, 'package', {matching_classes}); } /** * * @param {Debugger} dbgr * @param {DebuggerValue[]} locals * @param {RootExpression} expr * @returns {Promise} */ async function evaluate_root_term(dbgr, locals, expr) { switch (expr.root_term_type) { case 'boolean': return new LiteralValue(JavaType.boolean, expr.root_term === 'true'); case 'null': return LiteralValue.Null; case 'ident': return evaluate_identifier(dbgr, locals, expr.root_term); case 'hexint': case 'octint': case 'decint': case 'decfloat': return evaluate_number(expr.root_term); case 'char': case 'echar': case 'uchar': return evaluate_char(decodeJavaCharLiteral(expr.root_term)) case 'string': // we must get the runtime to create string instances return await dbgr.createJavaStringLiteral(expr.root_term); default: return null; } } /** * * @param {Debugger} dbgr * @param {DebuggerValue} value * @param {QualifierExpression[]} qualified_terms * @returns {Promise<[number, DebuggerValue]>} */ async function evaluate_package_qualifiers(dbgr, value, qualified_terms) { let i = 0; for (;;) { // while the value is a package identifier... if (value.vtype !== 'package') { break; } // ... and the next term is a member expression... const term = qualified_terms[i]; if (term instanceof MemberExpression) { // search for a valid type value = await evaluate_qualified_type_name(dbgr, `${value.value}.${term.name}`, value.data.matching_classes); i++; continue; } break; } if (value.vtype === 'package') { throw new Error('not available'); } // return the number of qualified terms we used and the resulting value return [i, value]; } /** * @param {Debugger} dbgr * @param {DebuggerValue[]} locals * @param {AndroidThread} thread * @param {DebuggerValue} value * @param {QualifierExpression[]} qualified_terms */ async function evaluate_qualifiers(dbgr, locals, thread, value, qualified_terms) { let pkg_members; [pkg_members, value] = await evaluate_package_qualifiers(dbgr, value, qualified_terms); for (let i = pkg_members; i < qualified_terms.length; i++) { const term = qualified_terms[i]; if (term instanceof MemberExpression) { // if this term is a member name, check if it's really a method call const next_term = qualified_terms[i + 1]; if (next_term instanceof MethodCallExpression) { value = await evaluate_methodcall(dbgr, locals, thread, term.name, next_term, value); i++; continue; } value = await evaluate_member(dbgr, term, value); continue; } if (term instanceof ArrayIndexExpression) { value = await evaluate_array_element(dbgr, locals, thread, term.indexExpression, value); continue; } throw new Error('not available'); } return value; } /** * @param {Debugger} dbgr * @param {DebuggerValue[]} locals * @param {AndroidThread} thread * @param {RootExpression} expr */ async function evaluate_root_expression(dbgr, locals, thread, expr) { let value = await evaluate_root_term(dbgr, locals, expr); if (!value || !value.valid) { throw new Error('not available'); } // we've evaluated the root term variable - work out the rest value = await evaluate_qualifiers(dbgr, locals, thread, value, expr.qualified_terms); return value; } /** * @param {Debugger} dbgr * @param {DebuggerValue[]} locals * @param {AndroidThread} thread * @param {ParsedExpression} expr * @returns {Promise} */ function evaluate_expression(dbgr, locals, thread, expr) { if (expr instanceof RootExpression) { return evaluate_root_expression(dbgr, locals, thread, expr); } if (expr instanceof BinaryOpExpression) { return evaluate_binary_expression(dbgr, locals, thread, expr.lhs, expr.rhs, expr.operator); } if (expr instanceof UnaryOpExpression) { return evaluate_unary_expression(dbgr, locals, thread, expr.operator, expr.rhs); } if (expr instanceof TypeCastExpression) { return evaluate_cast(dbgr, locals, thread, expr.cast_type, expr.rhs); } throw new Error('not available'); } /** * * @param {Debugger} dbgr * @param {DebuggerValue[]} locals * @param {AndroidThread} thread * @param {ParsedExpression} index_expr * @param {DebuggerValue} arr_local */ async function evaluate_array_element(dbgr, locals, thread, index_expr, arr_local) { if (arr_local.type.signature[0] !== '[') { throw new Error(`TypeError: cannot apply array index to non-array type '${arr_local.type.typename}'`); } if (arr_local.hasnullvalue) { throw new Error('NullPointerException'); } const idx_local = await evaluate_expression(dbgr, locals, thread, index_expr); if (!JavaType.isArrayIndex(idx_local.type)) { throw new Error('TypeError: array index is not an integer value'); } const idx = numberify(idx_local); if (idx < 0 || idx >= arr_local.arraylen) { throw new Error(`BoundsError: array index (${idx}) out of bounds. Array length = ${arr_local.arraylen}`); } const element_values = await dbgr.getArrayElementValues(arr_local, idx, 1); return element_values[0]; } /** * Build a regular expression which matches the possible parameter types for a value * @param {Debugger} dbgr * @param {DebuggerValue} argument */ async function getParameterSignatureRegex(dbgr, argument) { if (argument.type.signature == 'Lnull;') { return /^[LT[]/; // null matches any reference type } if (/^L/.test(argument.type.signature)) { // for class reference types, retrieve a list of inherited classes // since subclass instances can be passed as arguments const sigs = await dbgr.getClassInheritanceList(argument.type.signature); const re_sigs = sigs.map(signature => signature.replace(/[$]/g, '\\$')); return new RegExp(`(^${re_sigs.join('$)|(^')}$)`); } if (/^\[/.test(argument.type.signature)) { // for array types, only an exact array match or Object is allowed return new RegExp(`^(${argument.type.signature})|(${JavaType.Object.signature})$`); } switch(argument.type.signature) { case 'I': // match bytes/shorts/ints/longs/floats/doubles literals within range if (argument.value >= -128 && argument.value <= 127) return /^[BSIJFD]$/ if (argument.value >= -32768 && argument.value <= 32767) return /^[SIJFD]$/ return /^[IJFD]$/; case 'F': return /^[FD]$/; // floats can be assigned to floats or doubles default: // anything else must be an exact match (no implicit cast is valid) return new RegExp(`^${argument.type.signature}$`); } } /** * @param {Debugger} dbgr * @param {*} type * @param {string} method_name * @param {DebuggerValue[]} args */ async function findCompatibleMethod(dbgr, type, method_name, args) { // find any methods matching the member name with any parameters in the signature const methods = await dbgr.findNamedMethods(type.signature, method_name, /^/, false); if (!methods[0]) { throw new Error(`Error: method '${type.name}.${method_name}' not found`); } // filter the method based upon the types of parameters const arg_type_matchers = []; for (let arg of args) { arg_type_matchers.push(await getParameterSignatureRegex(dbgr, arg)); } // find the first method where the argument types match the parameter types const matching_method = methods.find(method => { // extract a list of parameter types from the method signature const param_type_re = /\[*([BSIJFDCZ]|([LT][^;]+;))/g; const parameter_types = []; for (let x; x = param_type_re.exec(method.sig); ) { parameter_types.push(x[0]); } // the last type is always the return value parameter_types.pop(); // check if the arguments and parameters match if (parameter_types.length !== arg_type_matchers.length) { return false; } // are there any argument types that don't match the corresponding parameter type? if (arg_type_matchers.find((m, idx) => !m.test(parameter_types[idx]))) { return false; } // we found a match return true; }); if (!matching_method) { throw new Error(`Error: incompatible parameters for method '${method_name}'`); } return matching_method; } /** * @param {Debugger} dbgr * @param {DebuggerValue[]} locals * @param {AndroidThread} thread * @param {string} method_name * @param {MethodCallExpression} m * @param {DebuggerValue} obj_local */ async function evaluate_methodcall(dbgr, locals, thread, method_name, m, obj_local) { if (obj_local.hasnullvalue) { throw new Error('NullPointerException'); } // evaluate any parameters const param_values = await Promise.all(m.arguments.map(arg => evaluate_expression(dbgr, locals, thread, arg))); // find a method in the object type matching the name and argument types const method = await findCompatibleMethod(dbgr, obj_local.type, method_name, param_values); return dbgr.invokeMethod( obj_local.value, thread.threadid, method, param_values ); } /** * @param {Debugger} dbgr * @param {MemberExpression} member * @param {DebuggerValue} value */ async function evaluate_member(dbgr, member, value) { if (!JavaType.isReference(value.type)) { throw new Error('TypeError: value is not a reference type'); } if (value.hasnullvalue) { throw new Error('NullPointerException'); } if (JavaType.isArray(value.type)) { // length is a 'fake' field of arrays, so special-case it if (member.name === 'length') { return evaluate_number(value.arraylen); } } // we also special-case :super (for object instances) if (member.name === ':super' && JavaType.isClass(value.type)) { return dbgr.getSuperInstance(value); } // check if the value is an enclosed type const enclosed_type = await dbgr.getTypeValue(`${value.type.signature.replace(/;$/,'')}$${member.name};`); if (enclosed_type.valid) { return enclosed_type; } // anything else must be a real field return dbgr.getFieldValue(value, member.name, true) } /** * @param {*} type * @param {*} local */ function incompatible_cast(type, local) { return new Error(`Incompatible cast from ${local.type.typename} to ${type}`); } /** * @param {Long.Long} value * @param {8|16|32} bits */ function signed_from_long(value, bits) { return (parseInt(value.toString(16).slice(-bits >> 3),16) << (32-bits)) >> (32-bits); } /** * @param {string} type * @param {DebuggerValue} local */ function cast_from_long(type, local) { const value = Long.fromString(local.value, true, 16); switch (true) { case (type === 'byte'): return evaluate_number(signed_from_long(value, 8)); case (type === 'short'): return evaluate_number(signed_from_long(value, 16)); case (type === 'int'): return evaluate_number(signed_from_long(value, 32)); case (type === 'char'): return evaluate_char(String.fromCharCode(signed_from_long(value, 16) & 0xffff)); case (type === 'float'): return evaluate_number(value.toSigned().toNumber() + 'F'); case (type === 'double'): return evaluate_number(value.toSigned().toNumber() + 'D'); default: throw incompatible_cast(type, local); } } /** * @param {Debugger} dbgr * @param {DebuggerValue[]} locals * @param {AndroidThread} thread * @param {string} cast_type * @param {ParsedExpression} rhs */ async function evaluate_cast(dbgr, locals, thread, cast_type, rhs) { let local = await evaluate_expression(dbgr, locals, thread, rhs); // check if a conversion is unnecessary if (cast_type === local.type.typename) { return local; } // boolean cannot be converted from anything else if (cast_type === 'boolean' || local.type.typename === 'boolean') { throw incompatible_cast(cast_type, local); } switch (true) { case local.type.typename === 'long': // conversion from long to something else local = cast_from_long(cast_type, local); break; case (cast_type === 'byte'): local = evaluate_number((local.value << 24) >> 24); break; case (cast_type === 'short'): local = evaluate_number((local.value << 16) >> 16); break; case (cast_type === 'int'): local = evaluate_number((local.value | 0)); break; case (cast_type === 'long'): local = evaluate_number(local.value + 'L'); break; case (cast_type === 'char'): local = evaluate_char(String.fromCharCode(local.value | 0)); break; case (cast_type === 'float'): case (cast_type === 'double'): break; default: throw incompatible_cast(cast_type, local); } local.type = JavaType[cast_type]; return local; } /** * @param {string} expression * @param {AndroidThread} thread * @param {DebuggerValue[]} locals * @param {Debugger} dbgr * @param {{allowFormatSpecifier:boolean}} [options] */ async function evaluate_one_expression(expression, thread, locals, dbgr, options) { D('evaluate: ' + expression); await dbgr.ensureConnected(); // the thread must be in the paused state if (thread && !thread.paused) { throw new Error('not available'); } // parse the expression const e = new ExpressionText(expression.trim()) if (!e.expr) { return null; } const parsed_expression = parse_expression(e); let display_format = null; if (options && options.allowFormatSpecifier) { // look for formatting specifiers in the form of ',' // ref: https://docs.microsoft.com/en-us/visualstudio/debugger/format-specifiers-in-cpp const df_match = e.expr.match(/^,([doc!]|[xX]b?|bb?|sb?)/); if (df_match) { display_format = df_match[1]; e.expr = e.expr.slice(df_match[0].length) } } // if there's anything left, it's an error if (!parsed_expression || e.expr) { // the expression is not well-formed throw new Error(`Invalid expression: ${expression.trim()}`); } // the expression is well-formed - start the (asynchronous) evaluation const value = await evaluate_expression(dbgr, locals, thread, parsed_expression); return { value, display_format, } } /** * */ const queuedExpressions = []; /** * @param {string} expression * @param {AndroidThread} thread * @param {DebuggerValue[]} locals * @param {Debugger} dbgr * @param {{allowFormatSpecifier:boolean}} [options] */ async function evaluate(expression, thread, locals, dbgr, options) { return new Promise(async (resolve, reject) => { const queue_length = queuedExpressions.push({ expression, thread, locals, dbgr, options, resolve, reject }); if (queue_length > 1) { return; } // run the queue while (queuedExpressions.length) { const { expression, thread, locals, dbgr, options, resolve, reject } = queuedExpressions[0]; try { const res = await evaluate_one_expression(expression, thread, locals, dbgr, options); resolve(res); } catch (err) { reject(err); } queuedExpressions.shift(); } }); } module.exports = { evaluate, }