mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-23 01:48:18 +00:00
* 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
1049 lines
34 KiB
JavaScript
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,
|
|
}
|