Files
android-dev-ext/src/expression/evaluate.js
Dave Holoway 6439e1b8b7 Version 1.1 improvements (#88)
* fix 0 alignment in binary xml decoding

* output reason for APK manifest read failure

* try and match package name against process name
when determining which pid to attach

* make post launch pause user-configurable

* code tidy, jsdocs and types

* more types in expression parse classes

* fix issue with expandable objects not evaluating

* update build task example

* fix package/type evaluation

* improve handling of targetDevice and processID combinations

* show full call stack by default

* implement a queue for evaluations

* improve performance of retrieving single fields

* check root term identifiers against this fields
2020-04-24 19:03:39 +01:00

1049 lines
34 KiB
JavaScript

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<DebuggerValue>}
*/
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<DebuggerValue>}
*/
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<DebuggerValue>}
*/
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 ',<x>'
// 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,
}