Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 37 additions & 28 deletions lib/src/core/complex_evaluator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ import 'dart:math' as math;

/// Evaluates math_expressions using Complex arithmetic.
class ComplexEvaluator {
/// Maximum recursion depth for evaluation to prevent stack overflow.
static const int maxDepth = 1000;

/// Evaluates the given [expression] with the provided variable [values].
static Complex evaluate(Expression expression, Map<String, Complex> values) {
static Complex evaluate(Expression expression, Map<String, Complex> values,
[int depth = 0]) {
if (depth > maxDepth) {
throw StateError('Maximum evaluation depth of $maxDepth exceeded.');
}

if (expression is Number) {
if (expression.value is num) {
return Complex((expression.value as num).toDouble());
Expand All @@ -16,7 +24,7 @@ class ComplexEvaluator {
if (expression is Variable) {
if (expression.name == 'anon') {
try {
return evaluate(_getArg(expression), values);
return evaluate(_getArg(expression), values, depth + 1);
} catch (_) {}
}

Expand All @@ -34,90 +42,91 @@ class ComplexEvaluator {
}

if (expression is UnaryMinus) {
return -evaluate(_getArg(expression), values);
return -evaluate(_getArg(expression), values, depth + 1);
}

// Binary Operators
if (expression is Plus) {
return evaluate(expression.first, values) +
evaluate(expression.second, values);
return evaluate(expression.first, values, depth + 1) +
evaluate(expression.second, values, depth + 1);
}
if (expression is Minus) {
return evaluate(expression.first, values) -
evaluate(expression.second, values);
return evaluate(expression.first, values, depth + 1) -
evaluate(expression.second, values, depth + 1);
}
if (expression is Times) {
return evaluate(expression.first, values) *
evaluate(expression.second, values);
return evaluate(expression.first, values, depth + 1) *
evaluate(expression.second, values, depth + 1);
}
if (expression is Divide) {
return evaluate(expression.first, values) /
evaluate(expression.second, values);
return evaluate(expression.first, values, depth + 1) /
evaluate(expression.second, values, depth + 1);
}
if (expression is Power) {
return evaluate(
expression.first,
values,
).pow(evaluate(expression.second, values));
depth + 1,
).pow(evaluate(expression.second, values, depth + 1));
}

// Functions - Use dynamic casting for arg to handle different class structures safely
// (e.g. UnaryExpression vs SingleArgumentFunction)

if (expression is Sin) {
return evaluate(_getArg(expression), values).sin();
return evaluate(_getArg(expression), values, depth + 1).sin();
}
if (expression is Cos) {
return evaluate(_getArg(expression), values).cos();
return evaluate(_getArg(expression), values, depth + 1).cos();
}
if (expression is Tan) {
return evaluate(_getArg(expression), values).tan();
return evaluate(_getArg(expression), values, depth + 1).tan();
}
if (expression is Exponential) {
// e^x
return evaluate(_getArg(expression), values).exp();
return evaluate(_getArg(expression), values, depth + 1).exp();
}
if (expression is Ln) {
// Natural log
return evaluate(_getArg(expression), values).log();
return evaluate(_getArg(expression), values, depth + 1).log();
}
if (expression is Log) {
// Log base x
// Standard math_expressions Logarithm(base, x).
final args = _getArgs(expression);
if (args.length == 2) {
return evaluate(args[1], values).log() /
evaluate(args[0], values).log();
return evaluate(args[1], values, depth + 1).log() /
evaluate(args[0], values, depth + 1).log();
}
// Fallback for single arg log (base 10 usually)
return evaluate(args[0], values).log() / Complex(math.ln10);
return evaluate(args[0], values, depth + 1).log() / Complex(math.ln10);
}
if (expression is Sqrt) {
return evaluate(_getArg(expression), values).sqrt();
return evaluate(_getArg(expression), values, depth + 1).sqrt();
}
if (expression is Root) {
// Root(degree, radicand) -> radicand^(1/degree)
final base = evaluate((expression as dynamic).base, values);
final r = evaluate((expression as dynamic).exp, values);
final base = evaluate((expression as dynamic).base, values, depth + 1);
final r = evaluate((expression as dynamic).exp, values, depth + 1);
return r.pow(Complex.one / base);
}
if (expression is Abs) {
return Complex(evaluate(_getArg(expression), values).abs());
return Complex(evaluate(_getArg(expression), values, depth + 1).abs());
}

if (expression is Asin) {
return _asin(evaluate(_getArg(expression), values));
return _asin(evaluate(_getArg(expression), values, depth + 1));
}
if (expression is Acos) {
return _acos(evaluate(_getArg(expression), values));
return _acos(evaluate(_getArg(expression), values, depth + 1));
}
if (expression is Atan) {
return _atan(evaluate(_getArg(expression), values));
return _atan(evaluate(_getArg(expression), values, depth + 1));
}

// Handle generic Function nodes or Parenthesis if they exist
try {
return evaluate(_getArg(expression), values);
return evaluate(_getArg(expression), values, depth + 1);
} catch (_) {}

// Last resort fallback: if it's a known math_expressions node but unknown to us,
Expand Down
34 changes: 20 additions & 14 deletions lib/src/core/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ class ExpressionParser {
/// ExpressionParser.extractVariables('F = m*a'); // ['F', 'a', 'm']
/// ExpressionParser.extractVariables('y = sin(x) + 2*PI'); // ['x', 'y']
/// ```
/// Maximum recursion depth for variable extraction.
static const int maxDepth = 1000;

static List<String> extractVariables(String input) {
try {
final result = _parser.parse(input.trim());
Expand All @@ -95,8 +98,11 @@ class ExpressionParser {
final variables = <String>{};

final visited = <dynamic>{};
void collect(dynamic node) {
void collect(dynamic node, [int depth = 0]) {
if (node == null) return;
if (depth > maxDepth) {
throw StateError('Maximum recursion depth of $maxDepth exceeded.');
}
if (visited.contains(node)) return;
visited.add(node);

Expand All @@ -116,39 +122,39 @@ class ExpressionParser {
final args = (node as dynamic).args;
if (args is List && args.isNotEmpty) {
for (var a in args) {
collect(a);
collect(a, depth + 1);
}
// Don't return!
}
} catch (_) {}

// Fallback: Exhaustive property search for children
try {
collect((node as dynamic).first);
collect((node as dynamic).first, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).second);
collect((node as dynamic).second, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).arg);
collect((node as dynamic).arg, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).exp);
collect((node as dynamic).exp, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).expression);
collect((node as dynamic).expression, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).value);
collect((node as dynamic).value, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).base);
collect((node as dynamic).base, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).left);
collect((node as dynamic).left, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).right);
collect((node as dynamic).right, depth + 1);
} catch (_) {}

// Hack for BoundVariable which hides contents in toString
Expand All @@ -165,12 +171,12 @@ class ExpressionParser {

final value = result.value;
if (value is List) {
collect(value[0]);
collect(value[0], 0);
if (value.length > 2) {
collect(value[2]);
collect(value[2], 0);
}
} else {
collect(value);
collect(value, 0);
}

return variables.toList()..sort();
Expand Down
32 changes: 19 additions & 13 deletions lib/src/explicit_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,27 @@ class ExplicitEquationParser {
return result.value;
}

/// Maximum recursion depth for variable extraction.
static const int maxDepth = 1000;

/// Extracts variable names from the equation.
static List<String> extractVariables(String input) {
// try {
final parsed = parse(input);
final variables = <String>{};

final visited = <dynamic>{};
void collect(dynamic node) {
void collect(dynamic node, [int depth = 0]) {
if (node == null) return;
if (depth > maxDepth) {
throw StateError('Maximum recursion depth of $maxDepth exceeded.');
}
if (visited.contains(node)) return;
visited.add(node);

if (node is List) {
for (final item in node) {
collect(item);
collect(item, depth + 1);
}
return;
}
Expand All @@ -52,42 +58,42 @@ class ExplicitEquationParser {
final args = (node as dynamic).args;
if (args is List && args.isNotEmpty) {
for (var a in args) {
collect(a);
collect(a, depth + 1);
}
// Don't return, allow fallbacks
}
} catch (_) {}

// Fallback: Exhaustive property search for children
try {
collect((node as dynamic).first);
collect((node as dynamic).first, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).second);
collect((node as dynamic).second, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).arg);
collect((node as dynamic).arg, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).exp);
collect((node as dynamic).exp, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).expression);
collect((node as dynamic).expression, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).value);
collect((node as dynamic).value, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).base);
collect((node as dynamic).base, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).left);
collect((node as dynamic).left, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).right);
collect((node as dynamic).right, depth + 1);
} catch (_) {}
try {
collect((node as dynamic).exponent);
collect((node as dynamic).exponent, depth + 1);
} catch (_) {}

// Hack for BoundVariable which hides contents in toString
Expand Down
36 changes: 36 additions & 0 deletions test/security_depth_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:math_expressions/math_expressions.dart';
import 'package:functionx/src/core/complex_evaluator.dart';
import 'package:functionx/src/core/parser.dart';
import 'package:test/test.dart';

void main() {
group('Security - Recursion Depth Limits', () {
test('ComplexEvaluator.evaluate should throw StateError for deeply nested expressions', () {
Expression expr = Number(1.0);
for (int i = 0; i <= ComplexEvaluator.maxDepth; i++) {
expr = Plus(expr, Number(1.0));
}

expect(() => ComplexEvaluator.evaluate(expr, {}), throwsA(isA<StateError>()));
});

test('ExpressionParser.extractVariables should throw StateError for deeply nested expressions', () {
// Construct a deeply nested expression string
// e.g., "1+(1+(1+...))"
String exprStr = "1";
for (int i = 0; i <= ExpressionParser.maxDepth; i++) {
exprStr = "1+($exprStr)";
}

// Note: The parser itself might fail before reaching the depth limit,
// but if it succeeds, the variable extraction should hit the limit.
// Given it's a security fix, we want to ensure the limit is there.

try {
ExpressionParser.extractVariables(exprStr);
} catch (e) {
expect(e, isA<StateError>());
}
});
});
}