"""Small XML metadata extractor used as validation oracle for SQL payloads.""" from __future__ import annotations import xml.etree.ElementTree as ET from dataclasses import asdict, dataclass from pathlib import Path from typing import Any def local_name(tag: str) -> str: return tag.rsplit("}", 1)[-1] if "}" in tag else tag @dataclass(frozen=True) class XmlMetadataItem: category: str name: str synonym: str uuid: str | None value_type: dict[str, Any] | None = None parent_category: str | None = None parent_name: str | None = None parent_uuid: str | None = None def to_dict(self) -> dict[str, Any]: return asdict(self) def direct_child(parent: ET.Element, name: str) -> ET.Element | None: for child in list(parent): if local_name(child.tag) == name: return child return None def child_text(parent: ET.Element, name: str) -> str: child = direct_child(parent, name) return (child.text or "").strip() if child is not None else "" def synonym_text(properties: ET.Element | None) -> str: if properties is None: return "" synonym = direct_child(properties, "Synonym") if synonym is None: return "" for node in synonym.iter(): if local_name(node.tag) == "content" and node.text: return node.text.strip() return "" def value_type(properties: ET.Element | None) -> dict[str, Any] | None: if properties is None: return None type_node = direct_child(properties, "Type") if type_node is None: return None types = [] qualifiers: dict[str, Any] = {} for node in type_node.iter(): name = local_name(node.tag) text = (node.text or "").strip() if name == "Type" and node is not type_node and text: types.append(text) elif name in {"Length", "AllowedLength", "Digits", "FractionDigits", "AllowedSign", "DateFractions"} and text: qualifiers[name] = text if not types and not qualifiers: return None return { "types": types, "qualifiers": qualifiers, "is_composite": len(types) > 1, } def item_from_properties( category: str, node: ET.Element, properties: ET.Element | None, *, parent: XmlMetadataItem | None = None, ) -> XmlMetadataItem: return XmlMetadataItem( category=category, name=child_text(properties, "Name") if properties is not None else node.attrib.get("name", ""), synonym=synonym_text(properties), uuid=node.attrib.get("uuid"), value_type=value_type(properties), parent_category=parent.category if parent else None, parent_name=parent.name if parent else None, parent_uuid=parent.uuid.lower() if parent and parent.uuid else None, ) def extract_xml_metadata_items(path: Path) -> list[XmlMetadataItem]: root = ET.parse(path).getroot() metadata_node = next(iter(list(root)), None) if metadata_node is None: return [] result: list[XmlMetadataItem] = [] properties = direct_child(metadata_node, "Properties") root_item = item_from_properties(local_name(metadata_node.tag), metadata_node, properties) result.append(root_item) internal = direct_child(metadata_node, "InternalInfo") if internal is not None: for generated in internal.iter(): if local_name(generated.tag) == "GeneratedType": result.append( XmlMetadataItem( category="GeneratedType", name=generated.attrib.get("name", ""), synonym=generated.attrib.get("category", ""), uuid=None, ) ) if properties is not None: standard = direct_child(properties, "StandardAttributes") if standard is not None: for node in list(standard): if local_name(node.tag) == "StandardAttribute": result.append( XmlMetadataItem( category="StandardAttribute", name=node.attrib.get("name", ""), synonym=synonym_text(node), uuid=None, ) ) child_objects = direct_child(metadata_node, "ChildObjects") if child_objects is not None: for node in list(child_objects): category = local_name(node.tag) props = direct_child(node, "Properties") child_item = item_from_properties(category, node, props, parent=root_item) result.append(child_item) append_nested_child_objects(result, node, child_item) return result def append_nested_child_objects(result: list[XmlMetadataItem], parent_node: ET.Element, parent_item: XmlMetadataItem) -> None: child_objects = direct_child(parent_node, "ChildObjects") if child_objects is None: return for node in list(child_objects): category = local_name(node.tag) props = direct_child(node, "Properties") child_item = item_from_properties(category, node, props, parent=parent_item) result.append(child_item) append_nested_child_objects(result, node, child_item) def group_xml_items(items: list[XmlMetadataItem]) -> dict[str, list[XmlMetadataItem]]: grouped: dict[str, list[XmlMetadataItem]] = {} for item in items: grouped.setdefault(item.category, []).append(item) return grouped