163 lines
5.4 KiB
Python
163 lines
5.4 KiB
Python
"""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
|