mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-25 10:58:42 +00:00
Version 1 (#83)
* replace jq-promises with native Promises * updates to use native promises and async await * Fix variable errors, remove extra parameters and correct export declaratons * refactor launch request to use async/await * fix running debugger on custom ADB port * remove unused files * move socket_ended check to ensure we don't loop reading 0 bytes * refactor logcat code and ensure disconnect status is passed on to webview * Fix warnings * Clean up util and remove unused functions * convert Debugger into a class * update jsconfig target to es2018 and enable checkJS * more updates to use async/await and more readable refactoring. - added type definitions and debugger classes - improved expression evaluation - refactored expressions into parsing, evaluation and variable assignment - fixed invoking methods with parameters - added support for static method invokes - improved exception display reliability - refactored launch into smaller functions - refactored utils into smaller modules - removed redundant code - converted JDWP functions to classes * set version 1.0.0 and update dependencies * add changelog notes
This commit is contained in:
109
src/expression/assign.js
Normal file
109
src/expression/assign.js
Normal file
@@ -0,0 +1,109 @@
|
||||
const { Debugger } = require('../debugger');
|
||||
const { DebuggerValue, JavaTaggedValue, JavaType } = require('../debugger-types');
|
||||
const { NumberBaseConverter } = require('../utils/nbc');
|
||||
|
||||
const validmap = {
|
||||
B: 'BC', // char might not fit into a byte - we special-case this
|
||||
S: 'BSC',
|
||||
I: 'BSIC',
|
||||
J: 'BSIJC',
|
||||
F: 'BSIJCF',
|
||||
D: 'BSIJCFD',
|
||||
C: 'BSC',
|
||||
Z: 'Z',
|
||||
isCharInRangeForByte: c => c.charCodeAt(0) < 256,
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the value will fit into a variable with given type
|
||||
* @param {JavaType} variable_type
|
||||
* @param {DebuggerValue} value
|
||||
*/
|
||||
function checkPrimitiveSize(variable_type, value) {
|
||||
// variable_type_signature must be a primitive
|
||||
if (!Object.prototype.hasOwnProperty.call(validmap, variable_type.signature)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let value_type_signature = value.type.signature;
|
||||
if (value.vtype === 'literal' && /[BSI]/.test(value_type_signature)) {
|
||||
// for integer literals, find the minimum type the value will fit into
|
||||
if (value.value >= -128 && value.value <= 127) value_type_signature = 'B';
|
||||
else if (value.value >= -32768 && value.value <= 32767) value_type_signature = 'S';
|
||||
else if (value.value >= -2147483648 && value.value <= 2147483647) value_type_signature = 'I';
|
||||
}
|
||||
|
||||
let is_in_range = validmap[variable_type.signature].indexOf(value_type_signature) >= 0;
|
||||
|
||||
// special check to see if a char value fits into a single byte
|
||||
if (JavaType.isByte(variable_type) && JavaType.isChar(value.type)) {
|
||||
is_in_range = validmap.isCharInRangeForByte(value.value);
|
||||
}
|
||||
|
||||
return is_in_range;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Debugger} dbgr
|
||||
* @param {DebuggerValue} destvar
|
||||
* @param {string} name
|
||||
* @param {DebuggerValue} result
|
||||
*/
|
||||
async function assignVariable(dbgr, destvar, name, result) {
|
||||
if (!destvar || !/^(field|local|arrelem)$/.test(destvar.vtype)) {
|
||||
throw new Error(`The value is read-only and cannot be updated.`);
|
||||
}
|
||||
|
||||
// non-string reference types can only set to null
|
||||
if (JavaType.isReference(destvar.type) && !JavaType.isString(destvar.type)) {
|
||||
if (!result.hasnullvalue) {
|
||||
throw new Error('Object references can only be set to null');
|
||||
}
|
||||
}
|
||||
|
||||
// as a nicety, if the destination is a string, stringify any primitive value
|
||||
if (JavaType.isPrimitive(result.type) && JavaType.isString(destvar.type)) {
|
||||
result = await dbgr.createJavaStringLiteral(result.value.toString(), { israw:true });
|
||||
}
|
||||
|
||||
if (JavaType.isPrimitive(destvar.type)) {
|
||||
// if the destination is a primitive, we need to range-check it here
|
||||
// Neither our debugger nor the JDWP endpoint validates primitives, so we end up with
|
||||
// weirdness if we allow primitives to be set with out-of-range values
|
||||
const is_in_range = checkPrimitiveSize(destvar.type, result);
|
||||
if (!is_in_range) {
|
||||
throw new Error(`'${result.value}' is not compatible with variable type: ${destvar.type.typename}`);
|
||||
}
|
||||
}
|
||||
|
||||
const data = JavaTaggedValue.from(result, destvar.type.signature);
|
||||
|
||||
if (JavaType.isLong(destvar.type) && typeof data.value === 'number') {
|
||||
// convert ints to hex-string longs
|
||||
data.value = NumberBaseConverter.decToHex(data.value.toString(),16);
|
||||
}
|
||||
|
||||
// convert the debugger value to a JavaTaggedValue
|
||||
let newlocalvar;
|
||||
// setxxxvalue sets the new value and then returns a new local for the variable
|
||||
switch(destvar.vtype) {
|
||||
case 'field':
|
||||
newlocalvar = await dbgr.setFieldValue(destvar.data.objvar, destvar.data.field, data);
|
||||
break;
|
||||
case 'local':
|
||||
newlocalvar = await dbgr.setLocalVariableValue(destvar.data.frame, destvar.data.slotinfo, data);
|
||||
break;
|
||||
case 'arrelem':
|
||||
newlocalvar = await dbgr.setArrayElements(destvar.data.array, parseInt(name, 10), 1, data);
|
||||
newlocalvar = newlocalvar[0];
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported variable type');
|
||||
}
|
||||
|
||||
return newlocalvar;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
assignVariable,
|
||||
}
|
||||
983
src/expression/evaluate.js
Normal file
983
src/expression/evaluate.js
Normal file
@@ -0,0 +1,983 @@
|
||||
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 {*} 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;
|
||||
}
|
||||
// if it's not a local, it could be the start of a package name or a type
|
||||
const classes = await dbgr.getAllClasses();
|
||||
return evaluate_qualified_type_name(dbgr, identifier, classes);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Debugger} dbgr
|
||||
* @param {string} dotted_name
|
||||
* @param {*[]} 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(c => exact_class_matcher.test(c.type.signature));
|
||||
if (exact_class) {
|
||||
return dbgr.getTypeValue(exact_class.type.signature);
|
||||
}
|
||||
|
||||
const class_matcher = new RegExp(`^L(java/lang/)?${dotted_name.replace('.','[$/]')}/`);
|
||||
const matching_classes = classes.filter(c => class_matcher.test(c.type.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, locals, thread, 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 {string} 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} v
|
||||
*/
|
||||
async function getParameterSignatureRegex(dbgr, v) {
|
||||
if (v.type.signature == 'Lnull;') {
|
||||
return /^[LT[]/; // null matches any reference type
|
||||
}
|
||||
if (/^L/.test(v.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(v.type.signature);
|
||||
const re_sigs = sigs.map(signature => signature.replace(/[$]/g, '\\$'));
|
||||
return new RegExp(`(^${re_sigs.join('$)|(^')}$)`);
|
||||
}
|
||||
if (/^\[/.test(v.type.signature)) {
|
||||
// for array types, only an exact array match or Object is allowed
|
||||
return new RegExp(`^(${v.type.signature})|(${JavaType.Object.signature})$`);
|
||||
}
|
||||
switch(v.type.signature) {
|
||||
case 'I':
|
||||
// match bytes/shorts/ints/longs/floats/doubles literals within range
|
||||
if (v.value >= -128 && v.value <= 127)
|
||||
return /^[BSIJFD]$/
|
||||
if (v.value >= -32768 && v.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(`^${v.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 {DebuggerValue[]} locals
|
||||
* @param {AndroidThread} thread
|
||||
* @param {MemberExpression} member
|
||||
* @param {DebuggerValue} value
|
||||
*/
|
||||
async function evaluate_member(dbgr, locals, thread, 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
|
||||
*/
|
||||
async function evaluate(expression, thread, locals, dbgr) {
|
||||
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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
evaluate,
|
||||
}
|
||||
323
src/expression/parse.js
Normal file
323
src/expression/parse.js
Normal file
@@ -0,0 +1,323 @@
|
||||
/**
|
||||
* Operator precedence levels.
|
||||
* Lower number = higher precedence.
|
||||
* Operators with equal precedence are evaluated left-to-right.
|
||||
*/
|
||||
const operator_precedences = {
|
||||
'*': 1, '%': 1, '/': 1,
|
||||
'+': 2, '-': 2,
|
||||
'<<': 3, '>>': 3, '>>>': 3,
|
||||
'<': 4, '>': 4, '<=': 4, '>=': 4, 'instanceof': 4,
|
||||
'==': 5, '!=': 5,
|
||||
'&': 6, '^': 7, '|': 8,
|
||||
'&&': 9, '||': 10,
|
||||
'?': 11,
|
||||
'=': 12,
|
||||
}
|
||||
|
||||
const lowest_precedence = 13;
|
||||
|
||||
class ExpressionText {
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
constructor(text) {
|
||||
this.expr = text;
|
||||
this.precedence_stack = [lowest_precedence];
|
||||
}
|
||||
|
||||
get current_precedence() {
|
||||
return this.precedence_stack[0];
|
||||
}
|
||||
}
|
||||
|
||||
class ParsedExpression {
|
||||
}
|
||||
|
||||
class RootExpression extends ParsedExpression {
|
||||
/**
|
||||
* @param {string} root_term
|
||||
* @param {string} root_term_type
|
||||
* @param {QualifierExpression[]} qualified_terms
|
||||
*/
|
||||
constructor(root_term, root_term_type, qualified_terms) {
|
||||
super();
|
||||
this.root_term = root_term;
|
||||
this.root_term_type = root_term_type;
|
||||
this.qualified_terms = qualified_terms;
|
||||
}
|
||||
}
|
||||
|
||||
class TypeCastExpression extends ParsedExpression {
|
||||
/**
|
||||
*
|
||||
* @param {string} cast_type
|
||||
* @param {ParsedExpression} rhs
|
||||
*/
|
||||
constructor(cast_type, rhs) {
|
||||
super();
|
||||
this.cast_type = cast_type;
|
||||
this.rhs = rhs;
|
||||
}
|
||||
}
|
||||
|
||||
class BinaryOpExpression extends ParsedExpression {
|
||||
/**
|
||||
* @param {ParsedExpression} lhs
|
||||
* @param {string} operator
|
||||
* @param {ParsedExpression} rhs
|
||||
*/
|
||||
constructor(lhs, operator, rhs) {
|
||||
super();
|
||||
this.lhs = lhs;
|
||||
this.operator = operator;
|
||||
this.rhs = rhs;
|
||||
}
|
||||
}
|
||||
|
||||
class UnaryOpExpression extends ParsedExpression {
|
||||
/**
|
||||
* @param {string} operator
|
||||
* @param {ParsedExpression} rhs
|
||||
*/
|
||||
constructor(operator, rhs) {
|
||||
super();
|
||||
this.operator = operator;
|
||||
this.rhs = rhs;
|
||||
}
|
||||
}
|
||||
|
||||
class TernaryExpression extends ParsedExpression {
|
||||
constructor(condition) {
|
||||
super();
|
||||
this.condition = condition;
|
||||
this.ternary_true = null;
|
||||
this.ternary_false = null;
|
||||
}
|
||||
}
|
||||
|
||||
class QualifierExpression extends ParsedExpression {
|
||||
|
||||
}
|
||||
|
||||
class ArrayIndexExpression extends QualifierExpression {
|
||||
constructor(e) {
|
||||
super();
|
||||
this.indexExpression = e;
|
||||
}
|
||||
}
|
||||
|
||||
class MethodCallExpression extends QualifierExpression {
|
||||
arguments = [];
|
||||
}
|
||||
|
||||
class MemberExpression extends QualifierExpression {
|
||||
constructor(name) {
|
||||
super();
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove characters from the expression followed by any leading whitespace/comments
|
||||
* @param {ExpressionText} e
|
||||
* @param {number|string} length_or_text
|
||||
*/
|
||||
function strip(e, length_or_text) {
|
||||
if (typeof length_or_text === 'string') {
|
||||
if (!e.expr.startsWith(length_or_text)) {
|
||||
return false;
|
||||
}
|
||||
length_or_text = length_or_text.length;
|
||||
}
|
||||
e.expr = e.expr.slice(length_or_text).trimLeft();
|
||||
for (;;) {
|
||||
const comment = e.expr.match(/(^\/\/.+)|(^\/\*[\d\D]*?\*\/)/);
|
||||
if (!comment) break;
|
||||
e.expr = e.expr.slice(comment[0].length).trimLeft();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ExpressionText} e
|
||||
* @returns {(MemberExpression|ArrayIndexExpression|MethodCallExpression)[]}
|
||||
*/
|
||||
function parse_qualified_terms(e) {
|
||||
const res = [];
|
||||
while (/^[([.]/.test(e.expr)) {
|
||||
if (strip(e, '.')) {
|
||||
// member access
|
||||
const name_match = e.expr.match(/^:?[a-zA-Z_$][a-zA-Z0-9_$]*/); // allow : at start for :super and :msg
|
||||
if (!name_match) {
|
||||
return null;
|
||||
}
|
||||
const member = new MemberExpression(name_match[0]);
|
||||
strip(e, member.name.length)
|
||||
res.push(member);
|
||||
}
|
||||
else if (strip(e, '(')) {
|
||||
// method call
|
||||
const call = new MethodCallExpression();
|
||||
if (!strip(e, ')')) {
|
||||
for (let arg; ;) {
|
||||
if ((arg = parse_expression(e)) === null) {
|
||||
return null;
|
||||
}
|
||||
call.arguments.push(arg);
|
||||
if (strip(e, ',')) continue;
|
||||
if (strip(e, ')')) break;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
res.push(call);
|
||||
}
|
||||
else if (strip(e, '[')) {
|
||||
// array index
|
||||
const index_expr = parse_expression(e);
|
||||
if (index_expr === null) {
|
||||
return null;
|
||||
}
|
||||
if (!strip(e, ']')) {
|
||||
return null;
|
||||
}
|
||||
res.push(new ArrayIndexExpression(index_expr));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ExpressionText} e
|
||||
*/
|
||||
function parseBracketOrCastExpression(e) {
|
||||
if (!strip(e, '(')) {
|
||||
return null;
|
||||
}
|
||||
let res = parse_expression(e);
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
if (!strip(e, ')')) {
|
||||
return null;
|
||||
}
|
||||
if (res instanceof RootExpression) {
|
||||
if (/^(int|long|byte|short|double|float|char|boolean)$/.test(res.root_term) && !res.qualified_terms.length) {
|
||||
// primitive typecast
|
||||
const castexpr = parse_expression_term(e);
|
||||
if (!castexpr) {
|
||||
return null;
|
||||
}
|
||||
res = new TypeCastExpression(res.root_term, castexpr);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ExpressionText} e
|
||||
* @param {string} unop
|
||||
*/
|
||||
function parseUnaryExpression(e, unop) {
|
||||
strip(e, unop.length);
|
||||
let res = parse_expression_term(e);
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
const op = unop.replace(/\s+/g, '');
|
||||
for (let i = op.length - 1; i >= 0; --i) {
|
||||
res = new UnaryOpExpression(op[i], res);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ExpressionText} e
|
||||
*/
|
||||
function parse_expression_term(e) {
|
||||
if (e.expr[0] === '(') {
|
||||
return parseBracketOrCastExpression(new ExpressionText(e.expr));
|
||||
}
|
||||
const unop = e.expr.match(/^(?:(!\s?)+|(~\s?)+|(?:([+-]\s?)+(?![\d.])))/);
|
||||
if (unop) {
|
||||
return parseUnaryExpression(e, unop[0]);
|
||||
}
|
||||
const root_term_types = ['boolean', 'boolean', 'null', 'ident', 'hexint', 'octint', 'decfloat', 'decint', 'char', 'echar', 'uchar', 'string'];
|
||||
const root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?\d+\.?\d*(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+(?:[eE]\+?\d+)?[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
|
||||
if (!root_term) {
|
||||
return null;
|
||||
}
|
||||
strip(e, root_term[0].length);
|
||||
const root_term_type = root_term_types[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].find(x => root_term[x]) - 1];
|
||||
const qualified_terms = parse_qualified_terms(e);
|
||||
if (qualified_terms === null) {
|
||||
return null;
|
||||
}
|
||||
// the root term is not allowed to be a method call
|
||||
if (qualified_terms[0] instanceof MethodCallExpression) {
|
||||
return null;
|
||||
}
|
||||
return new RootExpression(root_term[0], root_term_type, qualified_terms);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} s
|
||||
*/
|
||||
function getBinaryOperator(s) {
|
||||
const binary_op_match = s.match(/^([/%*&|^+-]=|<<=|>>>?=|[><!=]=|<<|>>>?|[><]|&&|\|\||[/%*&|^]|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/);
|
||||
return binary_op_match ? binary_op_match[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ExpressionText} e
|
||||
* @returns {ParsedExpression}
|
||||
*/
|
||||
function parse_expression(e) {
|
||||
let res = parse_expression_term(e);
|
||||
|
||||
for (; ;) {
|
||||
const binary_operator = getBinaryOperator(e.expr);
|
||||
if (!binary_operator) {
|
||||
break;
|
||||
}
|
||||
const prec_diff = operator_precedences[binary_operator] - e.current_precedence;
|
||||
if (prec_diff > 0) {
|
||||
// bigger number -> lower precendence -> end of (sub)expression
|
||||
break;
|
||||
}
|
||||
if (prec_diff === 0 && binary_operator !== '?') {
|
||||
// equal precedence, ltr evaluation
|
||||
break;
|
||||
}
|
||||
// higher or equal precendence
|
||||
e.precedence_stack.unshift(e.current_precedence + prec_diff);
|
||||
strip(e, binary_operator.length);
|
||||
if (binary_operator === '?') {
|
||||
res = new TernaryExpression(res);
|
||||
res.ternary_true = parse_expression(e);
|
||||
if (!strip(e, ':')) {
|
||||
return null;
|
||||
}
|
||||
res.ternary_false = parse_expression(e);
|
||||
} else {
|
||||
res = new BinaryOpExpression(res, binary_operator, parse_expression(e));
|
||||
}
|
||||
e.precedence_stack.shift();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ArrayIndexExpression,
|
||||
BinaryOpExpression,
|
||||
ExpressionText,
|
||||
MemberExpression,
|
||||
MethodCallExpression,
|
||||
parse_expression,
|
||||
ParsedExpression,
|
||||
QualifierExpression,
|
||||
RootExpression,
|
||||
TypeCastExpression,
|
||||
UnaryOpExpression,
|
||||
}
|
||||
Reference in New Issue
Block a user