diff --git a/osc/_private/api.py b/osc/_private/api.py index 20825c8c..9fc2d731 100644 --- a/osc/_private/api.py +++ b/osc/_private/api.py @@ -4,6 +4,7 @@ and work with related XML data. """ +import xml.sax.saxutils from xml.etree import ElementTree as ET @@ -120,6 +121,19 @@ def write_xml_node_to_file(node, path, indent=True): ET.ElementTree(node).write(path) +def xml_escape(string): + """ + Escape the string so it's safe to use in XML and xpath. + """ + entities = { + "\"": """, + "'": "'", + } + if isinstance(string, bytes): + return xml.sax.saxutils.escape(string.decode("utf-8"), entities=entities).encode("utf-8") + return xml.sax.saxutils.escape(string, entities=entities) + + def xml_indent(root): """ Indent XML so it looks pretty after printing or saving to file. diff --git a/tests/test__private_api.py b/tests/test__private_api.py new file mode 100644 index 00000000..84f2d134 --- /dev/null +++ b/tests/test__private_api.py @@ -0,0 +1,34 @@ +import unittest + +from osc._private.api import xml_escape + + +class TestXmlEscape(unittest.TestCase): + def test_lt(self): + actual = xml_escape("<") + expected = "<" + self.assertEqual(actual, expected) + + def test_gt(self): + actual = xml_escape(">") + expected = ">" + self.assertEqual(actual, expected) + + def test_apos(self): + actual = xml_escape("'") + expected = "'" + self.assertEqual(actual, expected) + + def test_quot(self): + actual = xml_escape("\"") + expected = """ + self.assertEqual(actual, expected) + + def test_amp(self): + actual = xml_escape("&") + expected = "&" + self.assertEqual(actual, expected) + + +if __name__ == "__main__": + unittest.main()