Skip to content

Commit 91427cc

Browse files
committed
fix: escape XML special characters in @attrs values
XML attribute values containing special characters (<, >, &, ", ') were not being properly escaped, resulting in invalid XML output. Changes: - Update make_attrstring() to call escape_xml() on attribute values - Add comprehensive tests for attribute escaping scenarios - Ensure backward compatibility with existing functionality Before: <Info HelpText="spec version <here>" /> After: <Info HelpText="spec version &lt;here&gt;" /> Resolves issue where @attrs dictionary values were output as raw text instead of properly escaped XML attribute values.
1 parent 25c2284 commit 91427cc

File tree

2 files changed

+82
-1
lines changed

2 files changed

+82
-1
lines changed

json2xml/dicttoxml.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def make_attrstring(attr: dict[str, Any]) -> str:
127127
Returns:
128128
str: The string of XML attributes.
129129
"""
130-
attrstring = " ".join([f'{k}="{v}"' for k, v in attr.items()])
130+
attrstring = " ".join([f'{k}="{escape_xml(v)}"' for k, v in attr.items()])
131131
return f'{" " if attrstring != "" else ""}{attrstring}'
132132

133133

tests/test_dict2xml.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,3 +1062,84 @@ def test_convert_dict_with_falsy_value_line_400(self) -> None:
10621062

10631063
# None should trigger the "elif not val:" branch and result in an empty element
10641064
assert "<none_key></none_key>" == result
1065+
1066+
def test_attrs_xml_escaping(self) -> None:
1067+
"""Test that @attrs values are properly XML-escaped."""
1068+
# Test the specific case from the user's bug report
1069+
info_dict = {
1070+
'Info': {
1071+
"@attrs": {
1072+
"Name": "systemSpec",
1073+
"HelpText": "spec version <here>"
1074+
}
1075+
}
1076+
}
1077+
result = dicttoxml.dicttoxml(info_dict, attr_type=False, item_wrap=False, root=False).decode('utf-8')
1078+
expected = '<Info Name="systemSpec" HelpText="spec version &lt;here&gt;"></Info>'
1079+
assert expected == result
1080+
1081+
def test_attrs_comprehensive_xml_escaping(self) -> None:
1082+
"""Test comprehensive XML escaping in attributes."""
1083+
data = {
1084+
'Element': {
1085+
"@attrs": {
1086+
"ampersand": "Tom & Jerry",
1087+
"less_than": "value < 10",
1088+
"greater_than": "value > 5",
1089+
"quotes": 'He said "Hello"',
1090+
"single_quotes": "It's working",
1091+
"mixed": "Tom & Jerry < 10 > 5 \"quoted\" 'apostrophe'"
1092+
},
1093+
"@val": "content"
1094+
}
1095+
}
1096+
result = dicttoxml.dicttoxml(data, attr_type=False, item_wrap=False, root=False).decode('utf-8')
1097+
1098+
# Check that all special characters are properly escaped in attributes
1099+
assert 'ampersand="Tom &amp; Jerry"' in result
1100+
assert 'less_than="value &lt; 10"' in result
1101+
assert 'greater_than="value &gt; 5"' in result
1102+
assert 'quotes="He said &quot;Hello&quot;"' in result
1103+
assert 'single_quotes="It&apos;s working"' in result
1104+
assert 'mixed="Tom &amp; Jerry &lt; 10 &gt; 5 &quot;quoted&quot; &apos;apostrophe&apos;"' in result
1105+
1106+
# Verify the element content is also properly escaped
1107+
assert ">content<" in result
1108+
1109+
def test_attrs_empty_and_none_values(self) -> None:
1110+
"""Test attribute handling with empty and None values."""
1111+
data = {
1112+
'Element': {
1113+
"@attrs": {
1114+
"empty": "",
1115+
"zero": 0,
1116+
"false": False
1117+
}
1118+
}
1119+
}
1120+
result = dicttoxml.dicttoxml(data, attr_type=False, item_wrap=False, root=False).decode('utf-8')
1121+
1122+
assert 'empty=""' in result
1123+
assert 'zero="0"' in result
1124+
assert 'false="False"' in result
1125+
1126+
def test_make_attrstring_function_directly(self) -> None:
1127+
"""Test the make_attrstring function directly."""
1128+
from json2xml.dicttoxml import make_attrstring
1129+
1130+
# Test basic escaping
1131+
attrs = {
1132+
"test": "value <here>",
1133+
"ampersand": "Tom & Jerry",
1134+
"quotes": 'Say "hello"'
1135+
}
1136+
result = make_attrstring(attrs)
1137+
1138+
assert 'test="value &lt;here&gt;"' in result
1139+
assert 'ampersand="Tom &amp; Jerry"' in result
1140+
assert 'quotes="Say &quot;hello&quot;"' in result
1141+
1142+
# Test empty attributes
1143+
empty_attrs = {}
1144+
result = make_attrstring(empty_attrs)
1145+
assert result == ""

0 commit comments

Comments
 (0)