From fcbc2fbb7bf828cbe7b077619abd9a6375a0f5e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9rome=20Perrin?= <jerome@nexedi.com>
Date: Fri, 27 Dec 2024 14:17:03 +0100
Subject: [PATCH] XMLExportImport.OrderedPickle: prevent TypeError on dicts
 with non orderable keys

---
 .../test.erp5.testXMLPickle.py                | 24 ++++++++++++++++++-
 product/ERP5Type/XMLExportImport/__init__.py  |  7 +++---
 2 files changed, 27 insertions(+), 4 deletions(-)

diff --git a/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testXMLPickle.py b/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testXMLPickle.py
index cebc840af3..6a38e7e68f 100644
--- a/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testXMLPickle.py
+++ b/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testXMLPickle.py
@@ -33,7 +33,7 @@ import zodbpickle.fastpickle as pickle
 import re
 from io import BytesIO
 from six import StringIO
-from Products.ERP5Type.XMLExportImport import importXML, ppml
+from Products.ERP5Type.XMLExportImport import importXML, OrderedPickler, ppml
 import six
 import lxml.etree
 
@@ -194,6 +194,7 @@ class TestXMLPickle(XMLPickleTestCase):
     self.check_and_load({'a': 1, 'b': 2})
     self.check_and_load({'hé': 'ho'})
     self.check_and_load(dict.fromkeys(range(3000)))
+    self.check_and_load({1: 'one', 'two': 2})
 
   def test_tuple(self):
     self.check_and_load((1, ))
@@ -346,3 +347,24 @@ class TestXMLPickleStringHeuristics(XMLPickleTestCase):
     self.assertEqual(
       persistent_ids,
       [b'\x00\x00\x00\x00\x00\x00\x00\x01'])
+
+
+class TestOrderedPickler(unittest.TestCase):
+  def test_ordered_pickler(self):
+    def check(obj, check_items_order=True):
+      f = BytesIO()
+      pickler = OrderedPickler(f)
+      pickler.dump(obj)
+      f.seek(0)
+      reconstructed = pickle.load(f)
+      self.assertEqual(reconstructed, obj)
+      self.assertIs(type(reconstructed), type(obj))
+      if check_items_order:
+        self.assertEqual(list(reconstructed.items()), list(obj.items()))
+
+    check({"one": 1, "two": 2})
+    check({1: "one", "two": 2})
+    check({b"one": 1, b"two": 2})
+    check({})
+    check(1, check_items_order=False)
+    check("one", check_items_order=False)
diff --git a/product/ERP5Type/XMLExportImport/__init__.py b/product/ERP5Type/XMLExportImport/__init__.py
index e64e8ec1e2..2fb2a58f81 100644
--- a/product/ERP5Type/XMLExportImport/__init__.py
+++ b/product/ERP5Type/XMLExportImport/__init__.py
@@ -74,9 +74,10 @@ class OrderedPickler(Pickler):
     dispatch = Pickler.dispatch.copy()
 
     def save_dict(self, obj):
-        return Pickler.save_dict(
-            self,
-            OrderedDict(sorted(obj.items())))
+        items = sorted(
+            obj.items(),
+            key=lambda item:str(item[0]))
+        return Pickler.save_dict(self, OrderedDict(items))
 
     dispatch[dict] = save_dict
 
-- 
2.30.9