From 91427cc18b204adb33b3ed209d1c83a35a588f0b Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Thu, 12 Jun 2025 21:04:46 +0530 Subject: [PATCH 1/2] 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: After: Resolves issue where @attrs dictionary values were output as raw text instead of properly escaped XML attribute values. --- json2xml/dicttoxml.py | 2 +- tests/test_dict2xml.py | 81 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/json2xml/dicttoxml.py b/json2xml/dicttoxml.py index 00d62a5..af32da4 100644 --- a/json2xml/dicttoxml.py +++ b/json2xml/dicttoxml.py @@ -127,7 +127,7 @@ def make_attrstring(attr: dict[str, Any]) -> str: Returns: str: The string of XML attributes. """ - attrstring = " ".join([f'{k}="{v}"' for k, v in attr.items()]) + attrstring = " ".join([f'{k}="{escape_xml(v)}"' for k, v in attr.items()]) return f'{" " if attrstring != "" else ""}{attrstring}' diff --git a/tests/test_dict2xml.py b/tests/test_dict2xml.py index 8db1c28..53e1950 100644 --- a/tests/test_dict2xml.py +++ b/tests/test_dict2xml.py @@ -1062,3 +1062,84 @@ def test_convert_dict_with_falsy_value_line_400(self) -> None: # None should trigger the "elif not val:" branch and result in an empty element assert "" == result + + def test_attrs_xml_escaping(self) -> None: + """Test that @attrs values are properly XML-escaped.""" + # Test the specific case from the user's bug report + info_dict = { + 'Info': { + "@attrs": { + "Name": "systemSpec", + "HelpText": "spec version " + } + } + } + result = dicttoxml.dicttoxml(info_dict, attr_type=False, item_wrap=False, root=False).decode('utf-8') + expected = '' + assert expected == result + + def test_attrs_comprehensive_xml_escaping(self) -> None: + """Test comprehensive XML escaping in attributes.""" + data = { + 'Element': { + "@attrs": { + "ampersand": "Tom & Jerry", + "less_than": "value < 10", + "greater_than": "value > 5", + "quotes": 'He said "Hello"', + "single_quotes": "It's working", + "mixed": "Tom & Jerry < 10 > 5 \"quoted\" 'apostrophe'" + }, + "@val": "content" + } + } + result = dicttoxml.dicttoxml(data, attr_type=False, item_wrap=False, root=False).decode('utf-8') + + # Check that all special characters are properly escaped in attributes + assert 'ampersand="Tom & Jerry"' in result + assert 'less_than="value < 10"' in result + assert 'greater_than="value > 5"' in result + assert 'quotes="He said "Hello""' in result + assert 'single_quotes="It's working"' in result + assert 'mixed="Tom & Jerry < 10 > 5 "quoted" 'apostrophe'"' in result + + # Verify the element content is also properly escaped + assert ">content<" in result + + def test_attrs_empty_and_none_values(self) -> None: + """Test attribute handling with empty and None values.""" + data = { + 'Element': { + "@attrs": { + "empty": "", + "zero": 0, + "false": False + } + } + } + result = dicttoxml.dicttoxml(data, attr_type=False, item_wrap=False, root=False).decode('utf-8') + + assert 'empty=""' in result + assert 'zero="0"' in result + assert 'false="False"' in result + + def test_make_attrstring_function_directly(self) -> None: + """Test the make_attrstring function directly.""" + from json2xml.dicttoxml import make_attrstring + + # Test basic escaping + attrs = { + "test": "value ", + "ampersand": "Tom & Jerry", + "quotes": 'Say "hello"' + } + result = make_attrstring(attrs) + + assert 'test="value <here>"' in result + assert 'ampersand="Tom & Jerry"' in result + assert 'quotes="Say "hello""' in result + + # Test empty attributes + empty_attrs = {} + result = make_attrstring(empty_attrs) + assert result == "" From abf5011a98dcad194beb9eb53e2ec1f845b732f2 Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Thu, 12 Jun 2025 21:11:52 +0530 Subject: [PATCH 2/2] fix: lint issues --- .coverage | Bin 53248 -> 53248 bytes dev.py | 10 +++++----- tests/test_dict2xml.py | 20 ++++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.coverage b/.coverage index 401fa9c5bc08a0394379eae5705fc2e69b4130a6..cad90cb0823aeb6c1b6446407d356ed31be0f21a 100644 GIT binary patch delta 94 zcmZozz}&Ead4e>f_(U0JR&fTsyqz0UX6rK=Y%-{cm$6`Bf@I)DBR$&IcE`g0Hv-KGbHyPB#%V@ALa!N5zD?azqe|{aeItwSK uRM^bA{Xc%QC#X%H(Vxhuy7^cCM}ARNHlRXAW(`)L3PyD>hed7C0s{a;M;bT) diff --git a/dev.py b/dev.py index ff74e89..cf89b93 100644 --- a/dev.py +++ b/dev.py @@ -10,7 +10,7 @@ def run_command(cmd: list[str], description: str) -> bool: """Run a command and return True if successful.""" print(f"\nšŸ” {description}...") try: - result = subprocess.run(cmd, check=True, cwd=Path(__file__).parent) + subprocess.run(cmd, check=True, cwd=Path(__file__).parent) print(f"āœ… {description} passed!") return True except subprocess.CalledProcessError as e: @@ -32,7 +32,7 @@ def main() -> None: if command in ("test", "all"): success &= run_command([ - "pytest", "--cov=json2xml", "--cov-report=term", + "pytest", "--cov=json2xml", "--cov-report=term", "-xvs", "tests", "-n", "auto" ], "Tests") @@ -50,11 +50,11 @@ def main() -> None: return if not success: - print(f"\nāŒ Some checks failed!") + print("\nāŒ Some checks failed!") sys.exit(1) else: - print(f"\nšŸŽ‰ All checks passed!") + print("\nšŸŽ‰ All checks passed!") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/tests/test_dict2xml.py b/tests/test_dict2xml.py index 53e1950..df770a9 100644 --- a/tests/test_dict2xml.py +++ b/tests/test_dict2xml.py @@ -1084,7 +1084,7 @@ def test_attrs_comprehensive_xml_escaping(self) -> None: 'Element': { "@attrs": { "ampersand": "Tom & Jerry", - "less_than": "value < 10", + "less_than": "value < 10", "greater_than": "value > 5", "quotes": 'He said "Hello"', "single_quotes": "It's working", @@ -1094,7 +1094,7 @@ def test_attrs_comprehensive_xml_escaping(self) -> None: } } result = dicttoxml.dicttoxml(data, attr_type=False, item_wrap=False, root=False).decode('utf-8') - + # Check that all special characters are properly escaped in attributes assert 'ampersand="Tom & Jerry"' in result assert 'less_than="value < 10"' in result @@ -1102,7 +1102,7 @@ def test_attrs_comprehensive_xml_escaping(self) -> None: assert 'quotes="He said "Hello""' in result assert 'single_quotes="It's working"' in result assert 'mixed="Tom & Jerry < 10 > 5 "quoted" 'apostrophe'"' in result - + # Verify the element content is also properly escaped assert ">content<" in result @@ -1118,28 +1118,28 @@ def test_attrs_empty_and_none_values(self) -> None: } } result = dicttoxml.dicttoxml(data, attr_type=False, item_wrap=False, root=False).decode('utf-8') - + assert 'empty=""' in result - assert 'zero="0"' in result + assert 'zero="0"' in result assert 'false="False"' in result def test_make_attrstring_function_directly(self) -> None: """Test the make_attrstring function directly.""" from json2xml.dicttoxml import make_attrstring - + # Test basic escaping attrs = { "test": "value ", - "ampersand": "Tom & Jerry", + "ampersand": "Tom & Jerry", "quotes": 'Say "hello"' } result = make_attrstring(attrs) - + assert 'test="value <here>"' in result assert 'ampersand="Tom & Jerry"' in result assert 'quotes="Say "hello""' in result - + # Test empty attributes - empty_attrs = {} + empty_attrs: dict[str, Any] = {} result = make_attrstring(empty_attrs) assert result == ""