Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions graphify/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
# ---------------------------------------------------------------------------
Expand Down
16 changes: 16 additions & 0 deletions tests/test_export_escaping.py
Original file line number Diff line number Diff line change
@@ -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>"), "&lt;a&gt; &amp; &lt;b&gt;")
self.assertEqual(escape_graphml_text('quote "test"'), "quote &quot;test&quot;")
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()