Skip to content

Commit 446e47b

Browse files
feiiiiii5Copilot
andauthored
FIX: preserve unknown characters in BrailleConverter (#2309)
Signed-off-by: fei <204683769+feiiiiii5@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 46d2071a-7f67-4855-84da-d0f19efb8a53
1 parent 60d8abc commit 446e47b

2 files changed

Lines changed: 107 additions & 13 deletions

File tree

pyrit/converter/braille_converter.py

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,17 @@ class BrailleConverter(Converter):
1111
Converts text into Braille Unicode representation.
1212
1313
This converter transforms standard text into Braille patterns using Unicode
14-
Braille characters (U+2800 to U+28FF). It supports lowercase and uppercase
15-
letters, numbers, common punctuation, and spaces. Uppercase letters are
16-
prefixed with the Braille capitalization indicator.
17-
18-
The Braille mapping is based on the implementation from Garak:
14+
Braille characters (U+2800 to U+28FF). Every printable ASCII character is
15+
mapped: letters, digits, punctuation, and symbols. Uppercase letters are
16+
prefixed with the Braille capitalization indicator and digit runs with the
17+
number indicator. Characters with no Braille mapping (accented and CJK
18+
letters, emoji, control characters) pass through unchanged rather than being
19+
dropped, so the encoded prompt keeps its meaning.
20+
21+
The letter, digit, and core punctuation mappings are based on the
22+
implementation from Garak:
1923
https://github.com/NVIDIA/garak/blob/main/garak/probes/encoding.py
24+
The ASCII symbol cells follow Unified English Braille (UEB).
2025
2126
Note: This converter is useful for testing how AI systems handle Braille-encoded
2227
text, which can be used to obfuscate potentially harmful content.
@@ -99,6 +104,27 @@ def _get_braile(self, text: str) -> str:
99104
";": "\u2806",
100105
"(": "\u2836",
101106
")": "\u2836",
107+
# Remaining printable ASCII, as two-cell UEB symbol sequences.
108+
"@": "\u2808\u2801",
109+
"#": "\u2838\u2839",
110+
"%": "\u2828\u2834",
111+
"&": "\u2808\u282f",
112+
"*": "\u2810\u2814",
113+
"+": "\u2810\u2816",
114+
"<": "\u2808\u2823",
115+
"=": "\u2810\u2836",
116+
">": "\u2808\u281c",
117+
'"': "\u2820\u2836",
118+
"[": "\u2828\u2823",
119+
"]": "\u2828\u281c",
120+
"\\": "\u2838\u2821",
121+
"^": "\u2808\u2822",
122+
"_": "\u2828\u2824",
123+
"`": "\u2828\u2821",
124+
"{": "\u2838\u2823",
125+
"}": "\u2838\u281c",
126+
"|": "\u2838\u2833",
127+
"~": "\u2808\u2814",
102128
"1": "\u2801",
103129
"2": "\u2803",
104130
"3": "\u2809",
@@ -112,23 +138,21 @@ def _get_braile(self, text: str) -> str:
112138
" ": " ",
113139
}
114140
number_punctuations = [".", ",", "-", "/", "$"]
115-
escape_characters = ["\n", "\r", "\t"]
116141

117142
output = ""
118143

119144
is_number = False
120145
for char in text:
121-
if char in escape_characters:
122-
output += char
123-
elif char.isupper():
124-
if char.lower() in character_unicodes:
125-
output += character_unicodes["caps"]
126-
output += character_unicodes[char.lower()]
127-
elif char in character_unicodes:
146+
if char in character_unicodes:
128147
if char.isdigit() and not is_number:
129148
is_number = True
130149
output += character_unicodes["num"]
131150
output += character_unicodes[char]
151+
elif char.isupper() and char.lower() in character_unicodes:
152+
output += character_unicodes["caps"]
153+
output += character_unicodes[char.lower()]
154+
else:
155+
output += char
132156
if is_number and not char.isdigit() and char not in number_punctuations:
133157
is_number = False
134158

tests/unit/converter/test_braille_converter.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@
55

66
from pyrit.converter import BrailleConverter, ConverterResult
77

8+
# Printable ASCII symbols and their two-cell Unified English Braille sequences.
9+
UEB_SYMBOL_CELLS = {
10+
"@": "\u2808\u2801", # dot 4, dot 1
11+
"#": "\u2838\u2839", # dots 456, dots 1456
12+
"%": "\u2828\u2834", # dots 46, dots 356
13+
"&": "\u2808\u282f", # dot 4, dots 12346
14+
"*": "\u2810\u2814", # dot 5, dots 35
15+
"+": "\u2810\u2816", # dot 5, dots 235
16+
"<": "\u2808\u2823", # dot 4, dots 126
17+
"=": "\u2810\u2836", # dot 5, dots 2356
18+
">": "\u2808\u281c", # dot 4, dots 345
19+
'"': "\u2820\u2836", # dot 6, dots 2356
20+
"[": "\u2828\u2823", # dots 46, dots 126
21+
"]": "\u2828\u281c", # dots 46, dots 345
22+
"\\": "\u2838\u2821", # dots 456, dots 16
23+
"^": "\u2808\u2822", # dot 4, dots 26
24+
"_": "\u2828\u2824", # dots 46, dots 36
25+
"`": "\u2828\u2821", # dots 46, dots 16
26+
"{": "\u2838\u2823", # dots 456, dots 126
27+
"}": "\u2838\u281c", # dots 456, dots 345
28+
"|": "\u2838\u2833", # dots 456, dots 1256
29+
"~": "\u2808\u2814", # dot 4, dots 35
30+
}
31+
832

933
async def test_braille_converter_simple_text():
1034
"""Test basic Braille conversion."""
@@ -131,3 +155,49 @@ async def test_braille_converter_punctuation_cells():
131155
f"{char!r} -> {result.output_text!r} (U+{ord(result.output_text):04X}), "
132156
f"expected {cell!r} (U+{ord(cell):04X})"
133157
)
158+
159+
160+
@pytest.mark.parametrize("char, expected", sorted(UEB_SYMBOL_CELLS.items()))
161+
async def test_braille_converter_ascii_symbol_cells(char, expected):
162+
"""Printable ASCII symbols map to their UEB cells rather than being dropped.
163+
164+
Regression: '@', '%', '+', '<' and the other unmapped symbols were silently
165+
dropped, corrupting the encoded prompt (e.g. "a@b.com" became "ab.com").
166+
Cells are pinned against the Unified English Braille symbol definitions.
167+
"""
168+
converter = BrailleConverter()
169+
170+
result = await converter.convert_async(prompt=char, input_type="text")
171+
assert result.output_text == expected
172+
173+
174+
@pytest.mark.parametrize(
175+
"char",
176+
["\u00e9", "\u4e2d", "\U0001f600", "\n", "\t", "\r"],
177+
ids=["e-acute", "cjk", "emoji", "newline", "tab", "carriage-return"],
178+
)
179+
async def test_braille_converter_unmapped_characters_pass_through(char):
180+
"""Characters with no Braille cell survive conversion unchanged."""
181+
converter = BrailleConverter()
182+
183+
result = await converter.convert_async(prompt=f"a{char}b", input_type="text")
184+
assert result.output_text == f"\u2801{char}\u2803"
185+
186+
187+
@pytest.mark.parametrize(
188+
"prompt, expected",
189+
[
190+
("hello", "\u2813\u2811\u2807\u2807\u2815"),
191+
("a@b.com", "\u2801\u2808\u2801\u2803\u2832\u2809\u2815\u280d"),
192+
("1+2", "\u283c\u2801\u2810\u2816\u283c\u2803"),
193+
("100%", "\u283c\u2801\u281a\u281a\u2828\u2834"),
194+
("caf\u00e9", "\u2809\u2801\u280b\u00e9"),
195+
],
196+
ids=["letters", "email", "digits-around-symbol", "percent", "accented-letter"],
197+
)
198+
async def test_braille_converter_exact_output(prompt, expected):
199+
"""Mapped characters are still encoded, and pass-through does not corrupt number mode."""
200+
converter = BrailleConverter()
201+
202+
result = await converter.convert_async(prompt=prompt, input_type="text")
203+
assert result.output_text == expected

0 commit comments

Comments
 (0)