exonware-xwsystem 0.0.1.408__py3-none-any.whl → 0.0.1.410__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. exonware/__init__.py +2 -2
  2. exonware/conf.py +10 -20
  3. exonware/xwsystem/__init__.py +5 -15
  4. exonware/xwsystem/caching/__init__.py +1 -1
  5. exonware/xwsystem/caching/base.py +15 -15
  6. exonware/xwsystem/caching/bloom_cache.py +4 -4
  7. exonware/xwsystem/caching/cache_manager.py +2 -2
  8. exonware/xwsystem/caching/conditional.py +3 -3
  9. exonware/xwsystem/caching/contracts.py +12 -12
  10. exonware/xwsystem/caching/decorators.py +1 -1
  11. exonware/xwsystem/caching/defs.py +1 -1
  12. exonware/xwsystem/caching/disk_cache.py +4 -4
  13. exonware/xwsystem/caching/distributed.py +1 -1
  14. exonware/xwsystem/caching/errors.py +1 -1
  15. exonware/xwsystem/caching/events.py +7 -7
  16. exonware/xwsystem/caching/eviction_strategies.py +9 -9
  17. exonware/xwsystem/caching/fluent.py +1 -1
  18. exonware/xwsystem/caching/integrity.py +1 -1
  19. exonware/xwsystem/caching/lfu_cache.py +23 -8
  20. exonware/xwsystem/caching/lfu_optimized.py +20 -20
  21. exonware/xwsystem/caching/lru_cache.py +13 -6
  22. exonware/xwsystem/caching/memory_bounded.py +7 -7
  23. exonware/xwsystem/caching/metrics_exporter.py +5 -5
  24. exonware/xwsystem/caching/observable_cache.py +1 -1
  25. exonware/xwsystem/caching/pluggable_cache.py +8 -8
  26. exonware/xwsystem/caching/rate_limiter.py +1 -1
  27. exonware/xwsystem/caching/read_through.py +5 -5
  28. exonware/xwsystem/caching/secure_cache.py +1 -1
  29. exonware/xwsystem/caching/serializable.py +2 -2
  30. exonware/xwsystem/caching/stats.py +7 -7
  31. exonware/xwsystem/caching/tagging.py +10 -10
  32. exonware/xwsystem/caching/ttl_cache.py +21 -6
  33. exonware/xwsystem/caching/two_tier_cache.py +5 -5
  34. exonware/xwsystem/caching/utils.py +3 -3
  35. exonware/xwsystem/caching/validation.py +1 -1
  36. exonware/xwsystem/caching/warming.py +8 -8
  37. exonware/xwsystem/caching/write_behind.py +4 -4
  38. exonware/xwsystem/cli/__init__.py +1 -1
  39. exonware/xwsystem/cli/args.py +10 -10
  40. exonware/xwsystem/cli/base.py +15 -15
  41. exonware/xwsystem/cli/colors.py +1 -1
  42. exonware/xwsystem/cli/console.py +1 -1
  43. exonware/xwsystem/cli/contracts.py +5 -5
  44. exonware/xwsystem/cli/defs.py +1 -1
  45. exonware/xwsystem/cli/errors.py +1 -1
  46. exonware/xwsystem/cli/progress.py +1 -1
  47. exonware/xwsystem/cli/prompts.py +1 -1
  48. exonware/xwsystem/cli/tables.py +7 -7
  49. exonware/xwsystem/config/__init__.py +1 -1
  50. exonware/xwsystem/config/base.py +13 -13
  51. exonware/xwsystem/config/contracts.py +22 -22
  52. exonware/xwsystem/config/defaults.py +2 -2
  53. exonware/xwsystem/config/defs.py +1 -1
  54. exonware/xwsystem/config/errors.py +1 -1
  55. exonware/xwsystem/config/logging.py +1 -1
  56. exonware/xwsystem/config/logging_setup.py +1 -1
  57. exonware/xwsystem/config/performance.py +7 -7
  58. exonware/xwsystem/config/performance_modes.py +20 -20
  59. exonware/xwsystem/config/version_manager.py +4 -4
  60. exonware/xwsystem/{http → http_client}/__init__.py +1 -1
  61. exonware/xwsystem/{http → http_client}/advanced_client.py +20 -20
  62. exonware/xwsystem/{http → http_client}/base.py +12 -12
  63. exonware/xwsystem/{http → http_client}/client.py +43 -43
  64. exonware/xwsystem/{http → http_client}/contracts.py +5 -5
  65. exonware/xwsystem/{http → http_client}/defs.py +2 -2
  66. exonware/xwsystem/{http → http_client}/errors.py +1 -1
  67. exonware/xwsystem/io/__init__.py +1 -1
  68. exonware/xwsystem/io/archive/__init__.py +1 -1
  69. exonware/xwsystem/io/archive/archive.py +5 -5
  70. exonware/xwsystem/io/archive/archive_files.py +8 -8
  71. exonware/xwsystem/io/archive/archivers.py +2 -2
  72. exonware/xwsystem/io/archive/base.py +15 -15
  73. exonware/xwsystem/io/archive/codec_integration.py +1 -1
  74. exonware/xwsystem/io/archive/compression.py +1 -1
  75. exonware/xwsystem/io/archive/formats/__init__.py +1 -1
  76. exonware/xwsystem/io/archive/formats/brotli_format.py +7 -7
  77. exonware/xwsystem/io/archive/formats/lz4_format.py +7 -7
  78. exonware/xwsystem/io/archive/formats/rar.py +7 -7
  79. exonware/xwsystem/io/archive/formats/sevenzip.py +7 -7
  80. exonware/xwsystem/io/archive/formats/squashfs_format.py +7 -7
  81. exonware/xwsystem/io/archive/formats/tar.py +8 -8
  82. exonware/xwsystem/io/archive/formats/wim_format.py +7 -7
  83. exonware/xwsystem/io/archive/formats/zip.py +8 -8
  84. exonware/xwsystem/io/archive/formats/zpaq_format.py +7 -7
  85. exonware/xwsystem/io/archive/formats/zstandard.py +7 -7
  86. exonware/xwsystem/io/base.py +17 -17
  87. exonware/xwsystem/io/codec/__init__.py +1 -1
  88. exonware/xwsystem/io/codec/base.py +260 -13
  89. exonware/xwsystem/io/codec/contracts.py +3 -6
  90. exonware/xwsystem/io/codec/registry.py +28 -28
  91. exonware/xwsystem/io/common/__init__.py +1 -1
  92. exonware/xwsystem/io/common/atomic.py +2 -2
  93. exonware/xwsystem/io/common/base.py +1 -1
  94. exonware/xwsystem/io/common/lock.py +1 -1
  95. exonware/xwsystem/io/common/watcher.py +4 -4
  96. exonware/xwsystem/io/contracts.py +34 -39
  97. exonware/xwsystem/io/defs.py +2 -2
  98. exonware/xwsystem/io/errors.py +32 -3
  99. exonware/xwsystem/io/facade.py +3 -3
  100. exonware/xwsystem/io/file/__init__.py +1 -1
  101. exonware/xwsystem/io/file/base.py +2 -2
  102. exonware/xwsystem/io/file/conversion.py +1 -1
  103. exonware/xwsystem/io/file/file.py +3 -3
  104. exonware/xwsystem/io/file/paged_source.py +1 -1
  105. exonware/xwsystem/io/file/paging/__init__.py +1 -1
  106. exonware/xwsystem/io/file/paging/byte_paging.py +1 -1
  107. exonware/xwsystem/io/file/paging/line_paging.py +1 -1
  108. exonware/xwsystem/io/file/paging/record_paging.py +1 -1
  109. exonware/xwsystem/io/file/paging/registry.py +4 -4
  110. exonware/xwsystem/io/file/source.py +3 -3
  111. exonware/xwsystem/io/filesystem/__init__.py +1 -1
  112. exonware/xwsystem/io/filesystem/base.py +1 -1
  113. exonware/xwsystem/io/filesystem/local.py +1 -1
  114. exonware/xwsystem/io/folder/__init__.py +1 -1
  115. exonware/xwsystem/io/folder/base.py +2 -2
  116. exonware/xwsystem/io/folder/folder.py +5 -5
  117. exonware/xwsystem/io/serialization/__init__.py +1 -1
  118. exonware/xwsystem/io/serialization/auto_serializer.py +3 -3
  119. exonware/xwsystem/io/serialization/base.py +84 -35
  120. exonware/xwsystem/io/serialization/contracts.py +6 -4
  121. exonware/xwsystem/io/serialization/defs.py +1 -1
  122. exonware/xwsystem/io/serialization/errors.py +1 -1
  123. exonware/xwsystem/io/serialization/flyweight.py +18 -18
  124. exonware/xwsystem/io/serialization/format_detector.py +11 -11
  125. exonware/xwsystem/io/serialization/formats/__init__.py +1 -1
  126. exonware/xwsystem/io/serialization/formats/binary/bson.py +1 -1
  127. exonware/xwsystem/io/serialization/formats/binary/cbor.py +1 -1
  128. exonware/xwsystem/io/serialization/formats/binary/marshal.py +1 -1
  129. exonware/xwsystem/io/serialization/formats/binary/msgpack.py +1 -1
  130. exonware/xwsystem/io/serialization/formats/binary/pickle.py +1 -1
  131. exonware/xwsystem/io/serialization/formats/binary/plistlib.py +1 -1
  132. exonware/xwsystem/io/serialization/formats/database/dbm.py +1 -1
  133. exonware/xwsystem/io/serialization/formats/database/shelve.py +1 -1
  134. exonware/xwsystem/io/serialization/formats/database/sqlite3.py +1 -1
  135. exonware/xwsystem/io/serialization/formats/text/configparser.py +2 -2
  136. exonware/xwsystem/io/serialization/formats/text/csv.py +2 -2
  137. exonware/xwsystem/io/serialization/formats/text/formdata.py +2 -2
  138. exonware/xwsystem/io/serialization/formats/text/json.py +23 -5
  139. exonware/xwsystem/io/serialization/formats/text/json5.py +93 -10
  140. exonware/xwsystem/io/serialization/formats/text/jsonlines.py +4 -4
  141. exonware/xwsystem/io/serialization/formats/text/multipart.py +2 -2
  142. exonware/xwsystem/io/serialization/formats/text/toml.py +47 -2
  143. exonware/xwsystem/io/serialization/formats/text/xml.py +444 -69
  144. exonware/xwsystem/io/serialization/formats/text/yaml.py +1 -1
  145. exonware/xwsystem/io/serialization/registry.py +5 -5
  146. exonware/xwsystem/io/serialization/serializer.py +11 -11
  147. exonware/xwsystem/io/serialization/utils/__init__.py +1 -1
  148. exonware/xwsystem/io/serialization/utils/path_ops.py +3 -3
  149. exonware/xwsystem/io/stream/__init__.py +1 -1
  150. exonware/xwsystem/io/stream/async_operations.py +3 -3
  151. exonware/xwsystem/io/stream/base.py +3 -7
  152. exonware/xwsystem/io/stream/codec_io.py +4 -7
  153. exonware/xwsystem/ipc/async_fabric.py +7 -7
  154. exonware/xwsystem/ipc/base.py +8 -8
  155. exonware/xwsystem/ipc/contracts.py +4 -4
  156. exonware/xwsystem/ipc/defs.py +1 -1
  157. exonware/xwsystem/ipc/errors.py +1 -1
  158. exonware/xwsystem/ipc/message_queue.py +4 -6
  159. exonware/xwsystem/ipc/process_manager.py +7 -7
  160. exonware/xwsystem/ipc/process_pool.py +8 -8
  161. exonware/xwsystem/ipc/shared_memory.py +5 -5
  162. exonware/xwsystem/monitoring/base.py +32 -32
  163. exonware/xwsystem/monitoring/contracts.py +27 -27
  164. exonware/xwsystem/monitoring/defs.py +1 -1
  165. exonware/xwsystem/monitoring/error_recovery.py +15 -15
  166. exonware/xwsystem/monitoring/errors.py +1 -1
  167. exonware/xwsystem/monitoring/memory_monitor.py +11 -11
  168. exonware/xwsystem/monitoring/metrics.py +8 -8
  169. exonware/xwsystem/monitoring/performance_manager_generic.py +19 -19
  170. exonware/xwsystem/monitoring/performance_monitor.py +11 -11
  171. exonware/xwsystem/monitoring/performance_validator.py +20 -20
  172. exonware/xwsystem/monitoring/system_monitor.py +16 -16
  173. exonware/xwsystem/monitoring/tracing.py +19 -19
  174. exonware/xwsystem/monitoring/tracker.py +7 -7
  175. exonware/xwsystem/operations/__init__.py +5 -5
  176. exonware/xwsystem/operations/base.py +3 -3
  177. exonware/xwsystem/operations/contracts.py +3 -3
  178. exonware/xwsystem/operations/defs.py +5 -5
  179. exonware/xwsystem/operations/diff.py +5 -5
  180. exonware/xwsystem/operations/merge.py +2 -2
  181. exonware/xwsystem/operations/patch.py +5 -5
  182. exonware/xwsystem/patterns/base.py +3 -3
  183. exonware/xwsystem/patterns/context_manager.py +6 -6
  184. exonware/xwsystem/patterns/contracts.py +22 -24
  185. exonware/xwsystem/patterns/defs.py +1 -1
  186. exonware/xwsystem/patterns/dynamic_facade.py +5 -5
  187. exonware/xwsystem/patterns/errors.py +7 -7
  188. exonware/xwsystem/patterns/handler_factory.py +11 -10
  189. exonware/xwsystem/patterns/import_registry.py +22 -22
  190. exonware/xwsystem/patterns/object_pool.py +11 -10
  191. exonware/xwsystem/patterns/registry.py +44 -31
  192. exonware/xwsystem/plugins/__init__.py +1 -1
  193. exonware/xwsystem/plugins/base.py +23 -23
  194. exonware/xwsystem/plugins/contracts.py +26 -26
  195. exonware/xwsystem/plugins/defs.py +1 -1
  196. exonware/xwsystem/plugins/errors.py +7 -7
  197. exonware/xwsystem/runtime/__init__.py +1 -1
  198. exonware/xwsystem/runtime/base.py +40 -40
  199. exonware/xwsystem/runtime/contracts.py +8 -8
  200. exonware/xwsystem/runtime/defs.py +1 -1
  201. exonware/xwsystem/runtime/env.py +8 -8
  202. exonware/xwsystem/runtime/errors.py +1 -1
  203. exonware/xwsystem/runtime/reflection.py +13 -13
  204. exonware/xwsystem/security/auth.py +47 -15
  205. exonware/xwsystem/security/base.py +16 -16
  206. exonware/xwsystem/security/contracts.py +30 -30
  207. exonware/xwsystem/security/crypto.py +7 -7
  208. exonware/xwsystem/security/defs.py +1 -1
  209. exonware/xwsystem/security/errors.py +1 -1
  210. exonware/xwsystem/security/hazmat.py +6 -6
  211. exonware/xwsystem/security/path_validator.py +1 -1
  212. exonware/xwsystem/shared/__init__.py +1 -1
  213. exonware/xwsystem/shared/base.py +14 -14
  214. exonware/xwsystem/shared/contracts.py +6 -6
  215. exonware/xwsystem/shared/defs.py +1 -1
  216. exonware/xwsystem/shared/errors.py +1 -1
  217. exonware/xwsystem/structures/base.py +28 -28
  218. exonware/xwsystem/structures/circular_detector.py +15 -15
  219. exonware/xwsystem/structures/contracts.py +9 -9
  220. exonware/xwsystem/structures/defs.py +1 -1
  221. exonware/xwsystem/structures/errors.py +1 -1
  222. exonware/xwsystem/structures/tree_walker.py +8 -8
  223. exonware/xwsystem/threading/async_primitives.py +6 -6
  224. exonware/xwsystem/threading/base.py +18 -18
  225. exonware/xwsystem/threading/contracts.py +13 -13
  226. exonware/xwsystem/threading/defs.py +1 -1
  227. exonware/xwsystem/threading/errors.py +1 -1
  228. exonware/xwsystem/threading/safe_factory.py +10 -9
  229. exonware/xwsystem/utils/base.py +33 -33
  230. exonware/xwsystem/utils/contracts.py +9 -9
  231. exonware/xwsystem/utils/dt/__init__.py +1 -1
  232. exonware/xwsystem/utils/dt/base.py +5 -5
  233. exonware/xwsystem/utils/dt/contracts.py +2 -2
  234. exonware/xwsystem/utils/dt/defs.py +1 -1
  235. exonware/xwsystem/utils/dt/errors.py +1 -1
  236. exonware/xwsystem/utils/dt/formatting.py +3 -3
  237. exonware/xwsystem/utils/dt/humanize.py +1 -1
  238. exonware/xwsystem/utils/dt/parsing.py +2 -2
  239. exonware/xwsystem/utils/dt/timezone_utils.py +5 -5
  240. exonware/xwsystem/utils/errors.py +1 -1
  241. exonware/xwsystem/utils/test_runner.py +6 -6
  242. exonware/xwsystem/utils/utils_contracts.py +1 -1
  243. exonware/xwsystem/validation/__init__.py +1 -1
  244. exonware/xwsystem/validation/base.py +39 -39
  245. exonware/xwsystem/validation/contracts.py +8 -8
  246. exonware/xwsystem/validation/declarative.py +9 -9
  247. exonware/xwsystem/validation/defs.py +1 -1
  248. exonware/xwsystem/validation/errors.py +1 -1
  249. exonware/xwsystem/validation/fluent_validator.py +8 -8
  250. exonware/xwsystem/version.py +2 -2
  251. {exonware_xwsystem-0.0.1.408.dist-info → exonware_xwsystem-0.0.1.410.dist-info}/METADATA +9 -11
  252. exonware_xwsystem-0.0.1.410.dist-info/RECORD +273 -0
  253. {exonware_xwsystem-0.0.1.408.dist-info → exonware_xwsystem-0.0.1.410.dist-info}/WHEEL +1 -1
  254. exonware/xwsystem/lazy_bootstrap.py +0 -79
  255. exonware_xwsystem-0.0.1.408.dist-info/RECORD +0 -274
  256. {exonware_xwsystem-0.0.1.408.dist-info → exonware_xwsystem-0.0.1.410.dist-info}/licenses/LICENSE +0 -0
@@ -2,7 +2,7 @@
2
2
  Company: eXonware.com
3
3
  Author: Eng. Muhammad AlShehri
4
4
  Email: connect@exonware.com
5
- Version: 0.0.1.408
5
+ Version: 0.0.1.410
6
6
  Generation Date: November 2, 2025
7
7
 
8
8
  XML serialization - Extensible Markup Language.
@@ -11,6 +11,12 @@ Following I→A pattern:
11
11
  - I: ISerialization (interface)
12
12
  - A: ASerialization (abstract base)
13
13
  - Concrete: XmlSerializer
14
+
15
+ Improved implementation:
16
+ - Uses xmltodict for both encoding and decoding (better round-trip compatibility)
17
+ - Requires xmltodict >= 0.13.0 for security features
18
+ - Preserves types using XML attributes
19
+ - Minimal try/catch blocks with proper error handling
14
20
  """
15
21
 
16
22
  from typing import Any, Optional, Union
@@ -21,20 +27,18 @@ from ....contracts import EncodeOptions, DecodeOptions
21
27
  from ....defs import CodecCapability
22
28
  from ....errors import SerializationError
23
29
 
24
- # Use defusedxml for security
25
- # Try defusedxml first (more secure), fallback to standard library if not available
26
- # The lazy hook will handle defusedxml installation if missing
30
+ # Primary: xmltodict for both encoding and decoding (better round-trip)
31
+ # xmltodict has built-in security features (disable_entities=True by default)
32
+ # No need for defusedxml - xmltodict handles XML security internally
33
+ import xmltodict
34
+
35
+ # Optional: dicttoxml as fallback (not recommended for round-trip)
27
36
  try:
28
- import defusedxml.ElementTree as ET
29
- from defusedxml import defuse_stdlib
30
- defuse_stdlib()
37
+ import dicttoxml
38
+ DICTTOXML_AVAILABLE = True
31
39
  except ImportError:
32
- # Fallback to standard library (always available)
33
- import xml.etree.ElementTree as ET
34
-
35
- # Lazy import for dicttoxml and xmltodict - the lazy hook will automatically handle ImportError
36
- import dicttoxml
37
- import xmltodict
40
+ DICTTOXML_AVAILABLE = False
41
+ dicttoxml = None
38
42
 
39
43
 
40
44
  class XmlSerializer(ASerialization):
@@ -45,7 +49,8 @@ class XmlSerializer(ASerialization):
45
49
  A: ASerialization (abstract base)
46
50
  Concrete: XmlSerializer
47
51
 
48
- Uses defusedxml, dicttoxml, and xmltodict for secure XML handling.
52
+ Uses xmltodict for both encoding and decoding to ensure perfect round-trip compatibility.
53
+ Requires xmltodict >= 0.13.0 for security features.
49
54
 
50
55
  Examples:
51
56
  >>> serializer = XmlSerializer()
@@ -53,8 +58,9 @@ class XmlSerializer(ASerialization):
53
58
  >>> # Encode data
54
59
  >>> xml_str = serializer.encode({"user": {"name": "John", "age": 30}})
55
60
  >>>
56
- >>> # Decode data
57
- >>> data = serializer.decode("<user><name>John</name></user>")
61
+ >>> # Decode data (perfect round-trip)
62
+ >>> data = serializer.decode(xml_str)
63
+ >>> assert data == {"user": {"name": "John", "age": 30}}
58
64
  >>>
59
65
  >>> # Save to file
60
66
  >>> serializer.save_file({"config": {"debug": True}}, "config.xml")
@@ -66,10 +72,25 @@ class XmlSerializer(ASerialization):
66
72
  def __init__(self):
67
73
  """Initialize XML serializer."""
68
74
  super().__init__()
69
- if dicttoxml is None or xmltodict is None:
75
+ if xmltodict is None:
76
+ raise ImportError(
77
+ "xmltodict >= 0.13.0 is required for XML serialization. "
78
+ "Install with: pip install xmltodict>=0.13.0"
79
+ )
80
+
81
+ # Verify security features are available
82
+ # Root cause fixed: Check for security features at initialization.
83
+ # Priority #1: Security - Use available security features, recommend upgrade for full security.
84
+ import inspect
85
+ parse_sig = inspect.signature(xmltodict.parse)
86
+ self._has_disable_entities = 'disable_entities' in parse_sig.parameters
87
+ self._has_forbid_dtd = 'forbid_dtd' in parse_sig.parameters
88
+ self._has_forbid_entities = 'forbid_entities' in parse_sig.parameters
89
+
90
+ if not self._has_disable_entities:
70
91
  raise ImportError(
71
- "dicttoxml and xmltodict are required for XML serialization. "
72
- "Install with: pip install dicttoxml xmltodict defusedxml"
92
+ "xmltodict with disable_entities support is required for XML serialization. "
93
+ "Install with: pip install 'xmltodict>=0.12.0'"
73
94
  )
74
95
 
75
96
  # ========================================================================
@@ -118,18 +139,212 @@ class XmlSerializer(ASerialization):
118
139
  return ["serialization", "markup"]
119
140
 
120
141
  # ========================================================================
121
- # CORE ENCODE/DECODE (Using dicttoxml + xmltodict)
142
+ # XML SANITIZATION HELPERS
143
+ # ========================================================================
144
+
145
+ def _sanitize_xml_name(self, name: str) -> str:
146
+ """
147
+ Sanitize a string to be a valid XML element/attribute name.
148
+
149
+ XML 1.0 element name rules:
150
+ - Must start with letter, underscore, or colon (colon for namespaces)
151
+ - Can contain letters, digits, hyphens, underscores, periods, colons
152
+ - Cannot start with "xml" (case-insensitive)
153
+ - Cannot contain spaces or other special characters
154
+
155
+ Root cause fixed: Dictionary keys used as XML element names must be valid XML names.
156
+ Solution: Prefix invalid names with underscore, replace invalid chars with underscore.
157
+ Priority #2: Usability - Ensure all data can be serialized to XML.
158
+
159
+ Args:
160
+ name: String to sanitize as XML name
161
+
162
+ Returns:
163
+ Valid XML element/attribute name
164
+ """
165
+ import re
166
+
167
+ # Convert to string if not already
168
+ name_str = str(name)
169
+
170
+ # Replace invalid characters with underscore
171
+ # Valid chars: letters, digits, hyphens, underscores, periods, colons
172
+ sanitized = re.sub(r'[^a-zA-Z0-9_\-.:]', '_', name_str)
173
+
174
+ # If starts with digit, hyphen, period, or colon, prefix with underscore
175
+ if sanitized and sanitized[0] in '0123456789-.:':
176
+ sanitized = '_' + sanitized
177
+
178
+ # If starts with "xml" (case-insensitive), prefix with underscore
179
+ if sanitized.lower().startswith('xml'):
180
+ sanitized = '_' + sanitized
181
+
182
+ # If empty after sanitization, use default name
183
+ if not sanitized:
184
+ sanitized = '_item'
185
+
186
+ return sanitized
187
+
188
+ def _sanitize_for_xml(self, data: Any, preserve_keys: bool = True) -> Any:
189
+ """
190
+ Sanitize data for XML encoding by removing/replacing invalid XML characters.
191
+
192
+ XML 1.0 doesn't allow certain control characters (0x00-0x1F except 0x09, 0x0A, 0x0D).
193
+ Also sanitizes dictionary keys to be valid XML element names.
194
+
195
+ Root cause fixed: Dictionary keys must be valid XML element names (can't start with digits).
196
+ Solution: Sanitize keys and store original keys as XML attributes for round-trip preservation.
197
+ Priority #2: Usability - Ensure all data structures can be serialized to XML with key preservation.
198
+
199
+ Args:
200
+ data: Python data structure
201
+ preserve_keys: If True, store original keys as @_original_key attributes
202
+
203
+ Returns:
204
+ Sanitized data structure safe for XML encoding
205
+ """
206
+ if isinstance(data, dict):
207
+ # Root cause fixed: Dictionary keys must be valid XML element names.
208
+ # Keys that start with digits (like UUIDs) are invalid XML element names.
209
+ # Solution: Sanitize keys and preserve originals as attributes for round-trip.
210
+ sanitized_dict = {}
211
+ for key, value in data.items():
212
+ # Sanitize key to be valid XML element name
213
+ sanitized_key = self._sanitize_xml_name(key)
214
+ # Handle key collisions (if sanitization produces duplicate keys)
215
+ original_key = sanitized_key
216
+ counter = 1
217
+ while sanitized_key in sanitized_dict:
218
+ sanitized_key = f"{original_key}_{counter}"
219
+ counter += 1
220
+
221
+ # Sanitize value recursively
222
+ sanitized_value = self._sanitize_for_xml(value, preserve_keys=preserve_keys)
223
+
224
+ # If key was changed and we want to preserve it, store original as attribute
225
+ if preserve_keys and sanitized_key != str(key):
226
+ # Wrap value in dict with original key as attribute
227
+ # xmltodict uses @ prefix for attributes
228
+ if isinstance(sanitized_value, dict):
229
+ # Add original key as attribute to existing dict
230
+ sanitized_value['@_original_key'] = str(key)
231
+ sanitized_dict[sanitized_key] = sanitized_value
232
+ else:
233
+ # Wrap non-dict value to add attribute
234
+ sanitized_dict[sanitized_key] = {
235
+ '@_original_key': str(key),
236
+ '#text': sanitized_value
237
+ }
238
+ else:
239
+ sanitized_dict[sanitized_key] = sanitized_value
240
+ return sanitized_dict
241
+ elif isinstance(data, list):
242
+ return [self._sanitize_for_xml(item, preserve_keys=preserve_keys) for item in data]
243
+ elif isinstance(data, str):
244
+ # Remove invalid XML 1.0 control characters (except tab, newline, carriage return)
245
+ # XML 1.0 allows: #x9 (tab), #xA (newline), #xD (carriage return)
246
+ # All other control chars (0x00-0x1F) are invalid
247
+ import re
248
+ # Remove control characters except tab, newline, carriage return
249
+ sanitized = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', data)
250
+ return sanitized
251
+ elif isinstance(data, (int, float, bool, type(None))):
252
+ return data
253
+ else:
254
+ # Convert other types to string and sanitize
255
+ return self._sanitize_for_xml(str(data))
256
+
257
+ # ========================================================================
258
+ # TYPE PRESERVATION HELPERS
259
+ # ========================================================================
260
+
261
+ def _preserve_types(self, data: Any) -> Any:
262
+ """
263
+ Preserve Python types in XML structure using type hints.
264
+
265
+ Adds '@type' attributes to preserve type information for round-trip.
266
+ This allows us to restore int, float, bool, None from string representations.
267
+
268
+ Args:
269
+ data: Python data structure
270
+
271
+ Returns:
272
+ Data structure with type hints embedded
273
+ """
274
+ if isinstance(data, dict):
275
+ result = {}
276
+ for key, value in data.items():
277
+ if isinstance(value, (int, float, bool, type(None))):
278
+ # Store type information
279
+ result[key] = {
280
+ '@type': type(value).__name__,
281
+ '#text': str(value) if value is not None else ''
282
+ }
283
+ elif isinstance(value, dict):
284
+ result[key] = self._preserve_types(value)
285
+ elif isinstance(value, list):
286
+ result[key] = [self._preserve_types(item) for item in value]
287
+ else:
288
+ result[key] = value
289
+ return result
290
+ elif isinstance(data, list):
291
+ return [self._preserve_types(item) for item in data]
292
+ else:
293
+ return data
294
+
295
+ def _restore_types(self, data: Any) -> Any:
296
+ """
297
+ Restore Python types from XML structure with type hints.
298
+
299
+ Converts '@type' attributes back to proper Python types.
300
+
301
+ Args:
302
+ data: XML data structure with type hints
303
+
304
+ Returns:
305
+ Data structure with restored types
306
+ """
307
+ if isinstance(data, dict):
308
+ # Check if this is a type-hinted value
309
+ if '@type' in data and '#text' in data and len(data) == 2:
310
+ type_name = data['@type']
311
+ text_value = data['#text']
312
+
313
+ if type_name == 'int':
314
+ return int(text_value) if text_value else 0
315
+ elif type_name == 'float':
316
+ return float(text_value) if text_value else 0.0
317
+ elif type_name == 'bool':
318
+ return text_value.lower() in ('true', '1', 'yes')
319
+ elif type_name == 'NoneType':
320
+ return None
321
+ else:
322
+ return text_value
323
+
324
+ # Recursively process dict
325
+ result = {}
326
+ for key, value in data.items():
327
+ if key not in ('@type', '#text'):
328
+ result[key] = self._restore_types(value)
329
+ return result
330
+ elif isinstance(data, list):
331
+ return [self._restore_types(item) for item in data]
332
+ else:
333
+ return data
334
+
335
+ # ========================================================================
336
+ # CORE ENCODE/DECODE (Using xmltodict for both - perfect round-trip)
122
337
  # ========================================================================
123
338
 
124
339
  def encode(self, value: Any, *, options: Optional[EncodeOptions] = None) -> Union[bytes, str]:
125
340
  """
126
341
  Encode data to XML string.
127
342
 
128
- Uses dicttoxml.dicttoxml().
343
+ Uses xmltodict.unparse() for encoding (better round-trip compatibility than dicttoxml).
129
344
 
130
345
  Args:
131
346
  value: Data to serialize
132
- options: XML options (root, attr_type, etc.)
347
+ options: XML options (root, pretty, preserve_types, etc.)
133
348
 
134
349
  Returns:
135
350
  XML string
@@ -137,44 +352,70 @@ class XmlSerializer(ASerialization):
137
352
  Raises:
138
353
  SerializationError: If encoding fails
139
354
  """
355
+ opts = options or {}
356
+
357
+ # Determine root element name
358
+ root_name = opts.get('root', 'root')
359
+
360
+ # Root cause fixed: Sanitize data to remove invalid XML characters.
361
+ # Priority #1: Security - Prevent XML injection and malformed XML.
362
+ # Priority #2: Usability - Ensure data can be encoded without errors.
363
+ value = self._sanitize_for_xml(value)
364
+
365
+ # Root cause fixed: Type preservation disabled by default - XML is text-based.
366
+ # Priority #2: Usability - Focus on structure preservation first, types are secondary.
367
+ # Note: Numbers will be strings in XML (this is expected XML behavior).
368
+ preserve_types = opts.get('preserve_types', False)
369
+ if preserve_types:
370
+ value = self._preserve_types(value)
371
+
372
+ # Wrap in root element if needed (xmltodict requires single root)
373
+ # Root cause fixed: Always wrap in root element for xmltodict compatibility.
374
+ if not isinstance(value, dict):
375
+ # Non-dict value - wrap it
376
+ wrapped_value = {root_name: value}
377
+ elif len(value) != 1:
378
+ # Multiple keys - wrap in root
379
+ wrapped_value = {root_name: value}
380
+ else:
381
+ # Single key dict - check if we should use it as root or wrap it
382
+ single_key = list(value.keys())[0]
383
+ if single_key == root_name:
384
+ # Already has correct root name
385
+ wrapped_value = value
386
+ else:
387
+ # Different root name - wrap it
388
+ wrapped_value = {root_name: value}
389
+
390
+ # Encode to XML string using xmltodict.unparse()
391
+ # Root cause fixed: Use xmltodict for both encode and decode for perfect round-trip.
392
+ # Priority #2: Usability - Round-trip serialization should preserve data structure.
140
393
  try:
141
- opts = options or {}
142
-
143
- # Encode to XML bytes
144
- xml_bytes = dicttoxml.dicttoxml(
145
- value,
146
- custom_root=opts.get('root', 'root'),
147
- attr_type=opts.get('attr_type', False),
148
- item_func=opts.get('item_func', lambda x: 'item')
394
+ xml_str = xmltodict.unparse(
395
+ wrapped_value,
396
+ pretty=opts.get('pretty', False),
397
+ indent=opts.get('indent', ' '),
398
+ full_document=opts.get('full_document', True)
149
399
  )
150
-
151
- # Convert to string
152
- xml_str = xml_bytes.decode('utf-8')
153
-
154
- # Pretty print if requested
155
- if opts.get('pretty', False):
156
- import xml.dom.minidom
157
- dom = xml.dom.minidom.parseString(xml_bytes)
158
- xml_str = dom.toprettyxml(indent=opts.get('indent', ' '))
159
-
160
- return xml_str
161
-
162
- except Exception as e:
400
+ except (ValueError, TypeError) as e:
163
401
  raise SerializationError(
164
- f"Failed to encode XML: {e}",
402
+ f"Failed to encode XML: {e}. "
403
+ f"Data may contain invalid XML characters or unsupported types.",
165
404
  format_name=self.format_name,
166
405
  original_error=e
167
- )
406
+ ) from e
407
+
408
+ return xml_str
168
409
 
169
410
  def decode(self, repr: Union[bytes, str], *, options: Optional[DecodeOptions] = None) -> Any:
170
411
  """
171
412
  Decode XML string to data.
172
413
 
173
- Uses xmltodict.parse().
414
+ Uses xmltodict.parse() with security features enabled.
174
415
 
175
416
  Args:
176
417
  repr: XML string (bytes or str)
177
- options: XML options (process_namespaces, etc.)
418
+ options: XML options (process_namespaces, root, preserve_types, etc.)
178
419
 
179
420
  Returns:
180
421
  Decoded Python dict
@@ -182,29 +423,163 @@ class XmlSerializer(ASerialization):
182
423
  Raises:
183
424
  SerializationError: If decoding fails
184
425
  """
426
+ # Convert bytes to str if needed
427
+ if isinstance(repr, bytes):
428
+ repr = repr.decode('utf-8')
429
+
430
+ opts = options or {}
431
+ root_name = opts.get('root', 'root')
432
+ preserve_types = opts.get('preserve_types', False)
433
+
434
+ # Decode from XML string with security features enabled
435
+ # Root cause fixed: Use available security features based on xmltodict version.
436
+ # Priority #1: Security - Use all available security features.
437
+ parse_kwargs = {
438
+ 'process_namespaces': opts.get('process_namespaces', False),
439
+ 'namespace_separator': opts.get('namespace_separator', ':'),
440
+ 'disable_entities': True, # Security: disable external entities (available in >=0.12.0)
441
+ }
442
+
443
+ # Add additional security features if available (>=0.13.0)
444
+ if self._has_forbid_dtd:
445
+ parse_kwargs['forbid_dtd'] = True # Security: forbid DTD
446
+ if self._has_forbid_entities:
447
+ parse_kwargs['forbid_entities'] = True # Security: forbid entities
448
+
185
449
  try:
186
- # Convert bytes to str if needed
187
- if isinstance(repr, bytes):
188
- repr = repr.decode('utf-8')
189
-
190
- opts = options or {}
450
+ data = xmltodict.parse(repr, **parse_kwargs)
451
+ except Exception as e:
452
+ # Provide better error context for XML parsing failures
453
+ error_msg = str(e)
454
+ if "not well-formed" in error_msg or "ExpatError" in str(type(e).__name__):
455
+ # Try to find the problematic character position
456
+ raise SerializationError(
457
+ f"Failed to decode XML: {error_msg}. "
458
+ f"The XML may contain invalid characters or be malformed.",
459
+ format_name=self.format_name,
460
+ original_error=e
461
+ ) from e
462
+ else:
463
+ raise SerializationError(
464
+ f"Failed to decode XML: {error_msg}",
465
+ format_name=self.format_name,
466
+ original_error=e
467
+ ) from e
468
+
469
+ # Unwrap root element if it matches expected root name
470
+ # Root cause fixed: Proper root element handling - check if root matches expected name.
471
+ # Priority #2: Usability - Round-trip serialization should preserve data structure.
472
+ if isinstance(data, dict) and len(data) == 1:
473
+ # Check if the single key matches root_name or if it's a generic 'root'
474
+ keys = list(data.keys())
475
+ if keys[0] == root_name or (root_name == 'root' and keys[0] == 'root'):
476
+ data = data[keys[0]]
477
+ # If root doesn't match, keep wrapped (might be intentional)
478
+
479
+ # Restore original keys if they were preserved
480
+ # Root cause fixed: Dictionary keys were sanitized during encoding.
481
+ # Solution: Restore original keys from @_original_key attributes.
482
+ # Priority #2: Usability - Round-trip serialization must preserve key names.
483
+ data = self._restore_original_keys(data)
484
+
485
+ # Restore types if they were preserved
486
+ if preserve_types:
487
+ data = self._restore_types(data)
488
+
489
+ return data
490
+
491
+ def _infer_type(self, value: str) -> Any:
492
+ """
493
+ Infer Python type from XML string value.
494
+
495
+ Root cause fixed: XML is text-based and converts all values to strings.
496
+ Solution: Attempt to infer and restore original types (int, float, bool, None).
497
+ Priority #2: Usability - Round-trip serialization should preserve types when possible.
498
+
499
+ Args:
500
+ value: String value from XML
191
501
 
192
- # Decode from XML string
193
- data = xmltodict.parse(
194
- repr,
195
- process_namespaces=opts.get('process_namespaces', False),
196
- namespace_separator=opts.get('namespace_separator', ':'),
197
- disable_entities=True, # Security: disable external entities
198
- forbid_dtd=True, # Security: forbid DTD
199
- forbid_entities=True # Security: forbid entities
200
- )
502
+ Returns:
503
+ Value with inferred type (int, float, bool, None, or original string)
504
+ """
505
+ if not isinstance(value, str):
506
+ return value
507
+
508
+ value = value.strip()
509
+
510
+ # Check for None/empty
511
+ if not value or value.lower() in ('none', 'null', ''):
512
+ return None
513
+
514
+ # Check for boolean
515
+ if value.lower() in ('true', 'false'):
516
+ return value.lower() == 'true'
517
+
518
+ # Check for integer
519
+ try:
520
+ # Try int first (more common)
521
+ if value.isdigit() or (value.startswith('-') and value[1:].isdigit()):
522
+ return int(value)
523
+ except (ValueError, OverflowError):
524
+ pass
525
+
526
+ # Check for float
527
+ try:
528
+ return float(value)
529
+ except (ValueError, OverflowError):
530
+ pass
531
+
532
+ # Return original string if no type matches
533
+ return value
534
+
535
+ def _restore_original_keys(self, data: Any) -> Any:
536
+ """
537
+ Restore original dictionary keys from @_original_key attributes.
538
+
539
+ Root cause fixed: Keys were sanitized during encoding (e.g., UUIDs starting with digits).
540
+ Solution: Check for @_original_key attributes and restore original key names.
541
+ Priority #2: Usability - Round-trip serialization must preserve key names.
542
+
543
+ Args:
544
+ data: Decoded XML data structure
201
545
 
546
+ Returns:
547
+ Data structure with original keys restored and types inferred
548
+ """
549
+ if isinstance(data, dict):
550
+ result = {}
551
+ for key, value in data.items():
552
+ # Skip xmltodict internal attributes
553
+ if key.startswith('@') and key != '@_original_key':
554
+ continue
555
+
556
+ # Check if this value has an original key attribute
557
+ if isinstance(value, dict) and '@_original_key' in value:
558
+ original_key = value['@_original_key']
559
+ # Remove the attribute from value
560
+ clean_value = {k: v for k, v in value.items() if k != '@_original_key'}
561
+
562
+ # If clean_value has only #text, unwrap it
563
+ if len(clean_value) == 1 and '#text' in clean_value:
564
+ clean_value = clean_value['#text']
565
+ # Infer type for unwrapped text value
566
+ clean_value = self._infer_type(clean_value)
567
+
568
+ # Recursively restore keys in the value
569
+ clean_value = self._restore_original_keys(clean_value)
570
+ result[original_key] = clean_value
571
+ else:
572
+ # Recursively restore keys in the value
573
+ restored_value = self._restore_original_keys(value)
574
+ # Infer type for leaf string values
575
+ if isinstance(restored_value, str):
576
+ restored_value = self._infer_type(restored_value)
577
+ result[key] = restored_value
578
+ return result
579
+ elif isinstance(data, list):
580
+ return [self._restore_original_keys(item) for item in data]
581
+ else:
582
+ # Infer type for leaf string values
583
+ if isinstance(data, str):
584
+ return self._infer_type(data)
202
585
  return data
203
-
204
- except (Exception, UnicodeDecodeError) as e:
205
- raise SerializationError(
206
- f"Failed to decode XML: {e}",
207
- format_name=self.format_name,
208
- original_error=e
209
- )
210
-
@@ -2,7 +2,7 @@
2
2
  Company: eXonware.com
3
3
  Author: Eng. Muhammad AlShehri
4
4
  Email: connect@exonware.com
5
- Version: 0.0.1.408
5
+ Version: 0.0.1.410
6
6
  Generation Date: November 2, 2025
7
7
 
8
8
  YAML serialization - Human-readable data serialization format.
@@ -2,7 +2,7 @@
2
2
  Company: eXonware.com
3
3
  Author: Eng. Muhammad AlShehri
4
4
  Email: connect@exonware.com
5
- Version: 0.0.1.408
5
+ Version: 0.0.1.410
6
6
  Generation Date: November 2, 2025
7
7
 
8
8
  Serialization Registry - Delegates to UniversalCodecRegistry.
@@ -10,7 +10,7 @@ Serialization Registry - Delegates to UniversalCodecRegistry.
10
10
  Provides serialization-specific convenience methods for format discovery.
11
11
  """
12
12
 
13
- from typing import Optional, List, Union
13
+ from typing import Optional, Union
14
14
  from pathlib import Path
15
15
 
16
16
  from ..codec.registry import UniversalCodecRegistry, get_registry
@@ -117,7 +117,7 @@ class SerializationRegistry:
117
117
  """
118
118
  return self._codec_registry.get_by_mime_type(mime_type)
119
119
 
120
- def list_formats(self) -> List[str]:
120
+ def list_formats(self) -> list[str]:
121
121
  """
122
122
  List all registered format IDs.
123
123
 
@@ -130,7 +130,7 @@ class SerializationRegistry:
130
130
  """
131
131
  return self._codec_registry.list_codecs()
132
132
 
133
- def list_extensions(self) -> List[str]:
133
+ def list_extensions(self) -> list[str]:
134
134
  """
135
135
  List all registered file extensions.
136
136
 
@@ -143,7 +143,7 @@ class SerializationRegistry:
143
143
  """
144
144
  return self._codec_registry.list_extensions()
145
145
 
146
- def list_mime_types(self) -> List[str]:
146
+ def list_mime_types(self) -> list[str]:
147
147
  """
148
148
  List all registered MIME types.
149
149