diff --git a/pyrit/converter/math_obfuscation_converter.py b/pyrit/converter/math_obfuscation_converter.py index 881b1a200b..ca96e6a6ac 100644 --- a/pyrit/converter/math_obfuscation_converter.py +++ b/pyrit/converter/math_obfuscation_converter.py @@ -23,6 +23,9 @@ class MathObfuscationConverter(Converter): The transformation follows these rules: * Each non-space character becomes one algebraic line. + * Numeric characters, such as digits, superscripts and fractions, write the + multiplication explicitly, as in `5 = 8*5 - 7*5`, because `85 - 75` would + read as two-digit numbers. * Space characters are represented as blank output lines. * Newline characters are preserved as blank output lines. @@ -150,7 +153,8 @@ async def convert_async( continue n = rng.randint(self._min_n, self._max_n) - line = f"{ch} = {n}{ch} - {n - 1}{ch}" + multiply = "*" if ch.isnumeric() else "" + line = f"{ch} = {n}{multiply}{ch} - {n - 1}{multiply}{ch}" # Add hint inline after the first equation if not first_equation_added and self._hint: diff --git a/tests/unit/converter/test_math_obfuscation_converter.py b/tests/unit/converter/test_math_obfuscation_converter.py index 48095f9dd7..304edcf6f4 100644 --- a/tests/unit/converter/test_math_obfuscation_converter.py +++ b/tests/unit/converter/test_math_obfuscation_converter.py @@ -3,6 +3,7 @@ import asyncio import random +import re import pytest @@ -32,6 +33,40 @@ def test_math_obfuscation_deterministic_small(input_text, expected_output): assert result.output_text == expected_output +@pytest.mark.parametrize("digit", list("0123456789") + ["\u0665"]) +def test_math_obfuscation_digit_lines_are_true_identities(digit): + converter = MathObfuscationConverter(min_n=2, max_n=9, rng=random.Random(3), hint="", suffix="") + + result = asyncio.run(converter.convert_async(prompt=digit * 20)) + + for line in result.output_text.splitlines(): + match = re.fullmatch(rf"{digit} = (\d)\*{digit} - (\d)\*{digit}", line) + assert match is not None, line + value = int(digit) + assert int(match.group(1)) * value - int(match.group(2)) * value == value + assert int(match.group(1)) - int(match.group(2)) == 1 + + +@pytest.mark.parametrize("character", ["\u00b2", "\u2083", "\u00bd", "\u2167", "\u4e94"]) +def test_math_obfuscation_other_numeric_characters_use_explicit_multiplication(character): + converter = MathObfuscationConverter(min_n=2, max_n=9, rng=random.Random(3), hint="", suffix="") + + result = asyncio.run(converter.convert_async(prompt=character * 20)) + + for line in result.output_text.splitlines(): + match = re.fullmatch(rf"{character} = (\d)\*{character} - (\d)\*{character}", line) + assert match is not None, line + assert int(match.group(1)) - int(match.group(2)) == 1 + + +def test_math_obfuscation_digits_and_letters_in_one_prompt(): + converter = MathObfuscationConverter(rng=random.Random(1), hint="", suffix="") + + result = asyncio.run(converter.convert_async(prompt="A1")) + + assert result.output_text == "A = 4A - 3A\n1 = 3*1 - 2*1" + + def test_math_obfuscation_spaces_and_newlines(): """Spaces -> blank lines, newlines -> blank lines.""" rng = random.Random(1)