diff --git a/graphify/security.py b/graphify/security.py index 2dbe5bd77..491d9e09f 100644 --- a/graphify/security.py +++ b/graphify/security.py @@ -405,6 +405,14 @@ def sanitize_label(text: str | None) -> str: return text +def escape_graphml_text(text: str | None) -> str: + """Escape XML special characters and control characters for GraphML serialization.""" + if text is None: + return "" + text = _CONTROL_CHAR_RE.sub("", str(text)) + return html.escape(text, quote=True) + + # --------------------------------------------------------------------------- # Metadata sanitisation (recursive, bounded, HTML-safe) # --------------------------------------------------------------------------- diff --git a/tests/test_export_escaping.py b/tests/test_export_escaping.py new file mode 100644 index 000000000..376b7ed5c --- /dev/null +++ b/tests/test_export_escaping.py @@ -0,0 +1,16 @@ +import unittest +from graphify.security import escape_graphml_text + + +class TestExportEscaping(unittest.TestCase): + def test_escape_graphml_text_special_chars(self): + self.assertEqual(escape_graphml_text(" & "), "<a> & <b>") + self.assertEqual(escape_graphml_text('quote "test"'), "quote "test"") + self.assertEqual(escape_graphml_text(None), "") + + def test_escape_graphml_text_control_chars(self): + self.assertEqual(escape_graphml_text("hello\x00world\x1f!"), "helloworld!") + + +if __name__ == '__main__': + unittest.main()