# xdto-decompile v1.0 — Convert 1C XDTO package to XML Schema (XSD) (Python port) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import os import re import sys from lxml import etree # newline="" — иначе Windows транслирует \n и CRLF схемы удваивается в \r\r\n sys.stdout.reconfigure(encoding="utf-8", newline="") sys.stderr.reconfigure(encoding="utf-8") XDTO_NS = "http://v8.1c.ru/8.1/xdto" XS_NS = "http://www.w3.org/2001/XMLSchema" XSI_NS = "http://www.w3.org/2001/XMLSchema-instance" MD_NS = "http://v8.1c.ru/8.3/MDClasses" V8_NS = "http://v8.1c.ru/8.1/data/core" parser = argparse.ArgumentParser(allow_abbrev=False) parser.add_argument("-PackagePath", "-Path", required=True) parser.add_argument("-OutFile", default="") args = parser.parse_args() def _parse_xml(source, from_string=False): """Разбор с узким отступлением для не-URI пространств имён. Платформа допускает в targetNamespace произвольную строку (в выгрузке БП есть пакет с кириллическим «ДопФайлУниверсальный»), .NET такое принимает, а libxml2 отвергает. Откатываемся на восстанавливающий разбор ТОЛЬКО на этой ошибке, иначе по-настоящему битый XML перестал бы отличаться от корректного. """ try: return (etree.fromstring(source) if from_string else etree.parse(source)) except etree.XMLSyntaxError as e: if "is not a valid URI" not in str(e): raise p = etree.XMLParser(recover=True) return (etree.fromstring(source, p) if from_string else etree.parse(source, p)) # ── resolve paths ──────────────────────────────────────────── package_path = args.PackagePath bin_path = None md_path = None if os.path.isfile(package_path): if os.path.basename(package_path) == "Package.bin": bin_path = package_path pkg_dir = os.path.dirname(os.path.dirname(package_path)) if os.path.exists(pkg_dir + ".xml"): md_path = pkg_dir + ".xml" elif package_path.endswith(".xml"): md_path = package_path stem = os.path.join(os.path.dirname(package_path), os.path.splitext(os.path.basename(package_path))[0]) c = os.path.join(stem, "Ext", "Package.bin") if os.path.exists(c): bin_path = c elif os.path.isdir(package_path): c = os.path.join(package_path, "Ext", "Package.bin") if os.path.exists(c): bin_path = c m = package_path.rstrip("\\/") + ".xml" if os.path.exists(m): md_path = m if not bin_path: print(f"Не найден Ext/Package.bin для пути: {package_path}", file=sys.stderr) sys.exit(1) doc = _parse_xml(bin_path) pkg = doc.getroot() def local(el): return etree.QName(el).localname if local(pkg) != "package": print(f"Ожидался корневой , получен <{local(pkg)}>", file=sys.stderr) sys.exit(1) target_ns = pkg.get("targetNamespace") # ── namespace -> prefix map for the emitted schema ─────────── ns_prefix = {XS_NS: "xs"} if target_ns: ns_prefix[target_ns] = "tns" imports = [] for imp in pkg: if isinstance(imp.tag, str) and local(imp) == "import": ns = imp.get("namespace") imports.append(ns) if ns not in ns_prefix: ns_prefix[ns] = "ns" + str(len(ns_prefix)) def register_ns(ns): if ns and ns not in ns_prefix: ns_prefix[ns] = "ns" + str(len(ns_prefix)) # ── output buffer ──────────────────────────────────────────── lines = [] def X(line): lines.append(line) def esc(s): if s is None: return "" return str(s).replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """) def esc_text(s): if s is None: return "" return str(s).replace("&", "&").replace("<", "<").replace(">", ">") # ── QName conversion: bin prefix -> schema prefix ──────────── def convert_qname(el, qname): if not qname: return None # Нотация Кларка {ns}local — так записаны почти все memberTypes if qname.startswith("{"): close = qname.find("}") if close > 0: ns = qname[1:close] loc = qname[close + 1:] if not ns: return loc register_ns(ns) return f"{ns_prefix[ns]}:{loc}" parts = qname.split(":") if len(parts) == 2: ns = el.nsmap.get(parts[0]) loc = parts[1] else: ns = el.nsmap.get(None) loc = parts[0] if not ns: return qname register_ns(ns) return f"{ns_prefix[ns]}:{loc}" def convert_qname_list(el, lst): if not lst: return None return " ".join(convert_qname(el, q) for q in lst.split() if q) def attrs(pairs): out = "" for i in range(0, len(pairs), 2): v = pairs[i + 1] if v is not None: out += f' {pairs[i]}="{esc(v)}"' return out # ── xdto: mirror attributes ────────────────────────────────── state = {"uses_xdto": False} def mirror(name, value): if value is None: return "" state["uses_xdto"] = True return f' xdto:{name}="{esc(value)}"' DNPM = re.compile(r"^d\d+p\d+$") def ns_decls_of(el): """Локальные объявления xmlns на самом узле, как (префикс, uri).""" parent_map = el.getparent().nsmap if el.getparent() is not None else {} for px, uri in el.nsmap.items(): if px is None: continue if parent_map.get(px) == uri: continue yield px, uri def mirror_prefix(el): # Обычно префиксы генерируются схемой dNpM, но изредка узел несёт осмысленный # префикс (например dcsset) — его надо сохранить, иначе round-trip не сойдётся. for px, _uri in ns_decls_of(el): if DNPM.match(px): continue return mirror("prefix", px) return "" FACET_ATTRS = ["length", "minLength", "maxLength", "totalDigits", "fractionDigits", "minInclusive", "maxInclusive", "minExclusive", "maxExclusive", "whiteSpace"] def emit_facets(el, indent): for f in FACET_ATTRS: v = el.get(f) if v is not None: X(f'{indent}') for child in el: if not isinstance(child.tag, str): continue ln = local(child) if ln == "pattern": X(f'{indent}') elif ln == "enumeration": xsi_type = child.get(f"{{{XSI_NS}}}type") m = mirror("type", convert_qname(child, xsi_type)) if xsi_type else "" X(f'{indent}') def has_simple_content(el): for c in el: if isinstance(c.tag, str) and local(c) in ("pattern", "enumeration"): return True return any(el.get(f) is not None for f in FACET_ATTRS) # ── simple type body (valueType / typeDef xsi:type=ValueType) ─ def emit_simple_type_body(el, indent): variety = el.get("variety") base = convert_qname(el, el.get("base")) item_type = convert_qname(el, el.get("itemType")) member_types = convert_qname_list(el, el.get("memberTypes")) mv = mirror("variety", variety) raw_members = el.get("memberTypes") if raw_members is not None and not raw_members.startswith("{"): mv += mirror("memberTypesForm", "prefixed") # При нотации Кларка объявление xmlns:dNpM иногда присутствует, иногда нет — # из значения это не выводится (зависит от состояния сериализатора), зеркалим факт if raw_members is not None and raw_members.startswith("{"): for px, uri in ns_decls_of(el): if DNPM.match(px): mv += mirror("declareNs", uri) break if variety == "List" or item_type: X(f'{indent}') return if variety == "Union" or member_types: anon = [c for c in el if isinstance(c.tag, str) and local(c) == "typeDef"] if not anon: X(f'{indent}') else: X(f'{indent}') for c in anon: X(f"{indent}\t") emit_simple_type_body(c, indent + "\t\t") X(f"{indent}\t") X(f"{indent}") return # Базовый тип может быть задан не атрибутом base, а вложенным анонимным typeDef anon_base = next((c for c in el if isinstance(c.tag, str) and local(c) == "typeDef"), None) if has_simple_content(el) or anon_base is not None: X(f'{indent}') if anon_base is not None: X(f"{indent}\t") emit_simple_type_body(anon_base, indent + "\t\t") X(f"{indent}\t") emit_facets(el, indent + "\t") X(f"{indent}") else: X(f'{indent}') # ── property classification ────────────────────────────────── def prop_form(p): f = p.get("form") return "Element" if f is None else f def anon_type_def(p): return next((c for c in p if isinstance(c.tag, str) and local(c) == "typeDef"), None) # ── property emission ──────────────────────────────────────── def emit_property(p, indent): form = prop_form(p) name = p.get("name") type_ = convert_qname(p, p.get("type")) ref = convert_qname(p, p.get("ref")) local_name = p.get("localName") lower = p.get("lowerBound") upper = p.get("upperBound") nill = p.get("nillable") default = p.get("default") fixed = p.get("fixed") # В модели fixed — булев флаг, значение лежит в default; в XSD наоборот: # fixed="V" несёт само значение. Переводим, а не копируем. def_out, fix_out, fix_mirror = default, None, "" if fixed == "true" and default is not None: fix_out, def_out = default, None elif fixed is not None: fix_mirror = mirror("fixed", fixed) anon = anon_type_def(p) qual = p.get(f"{{{XDTO_NS}}}qualified") min_occurs = lower max_occurs = None if upper is not None: max_occurs = "unbounded" if upper == "-1" else upper xml_name = local_name if local_name is not None else name mirror_name = mirror("name", name) if local_name is not None else "" m = "" if form == "Attribute": if qual is not None: m += mirror("qualified", qual) if nill is not None: m += mirror("nillable", nill) if lower is not None: m += mirror("lowerBound", lower) if upper is not None: m += mirror("upperBound", upper) m += mirror_name m += fix_mirror body = attrs(["name", xml_name, "ref", ref, "type", type_, "default", def_out, "fixed", fix_out]) if anon is not None: X(f"{indent}") X(f"{indent}\t") emit_simple_type_body(anon, indent + "\t\t") X(f"{indent}\t") X(f"{indent}") else: X(f"{indent}") return if form == "Text": # handled by the owning complexType (xs:simpleContent) return if p.get("form") is not None: m += mirror("form", form) if qual is not None: m += mirror("qualified", qual) m += mirror_name m += mirror_prefix(p) m += fix_mirror body = attrs(["name", xml_name, "ref", ref, "type", type_, "minOccurs", min_occurs, "maxOccurs", max_occurs, "nillable", nill, "default", def_out, "fixed", fix_out]) if anon is not None: X(f"{indent}") if anon.get(f"{{{XSI_NS}}}type") == "ObjectType": anon_base = convert_qname(anon, anon.get("base")) if anon_base: X(f"{indent}\t") X(f"{indent}\t\t") X(f'{indent}\t\t\t') emit_complex_type_body(anon, indent + "\t\t\t\t") X(f"{indent}\t\t\t") X(f"{indent}\t\t") X(f"{indent}\t") else: X(f"{indent}\t") emit_complex_type_body(anon, indent + "\t\t") X(f"{indent}\t") else: X(f"{indent}\t") emit_simple_type_body(anon, indent + "\t\t") X(f"{indent}\t") X(f"{indent}") else: X(f"{indent}") # ── complex type body ──────────────────────────────────────── def emit_complex_type_body(el, indent): open_ = el.get("open") ordered = el.get("ordered") props = [c for c in el if isinstance(c.tag, str) and local(c) == "property"] elems, attr_props, text = [], [], None for p in props: f = prop_form(p) if f == "Attribute": attr_props.append(p) elif f == "Text": text = p else: elems.append(p) if text is not None: t_type = convert_qname(text, text.get("type")) tm = "" t_name = text.get("name") if t_name != "__content": tm += mirror("textName", t_name) for extra in ("lowerBound", "upperBound", "nillable"): v = text.get(extra) if v is not None: tm += mirror("text" + extra, v) X(f"{indent}") X(f'{indent}\t') for a in attr_props: emit_property(a, indent + "\t\t") X(f"{indent}\t") X(f"{indent}") return particle_tag = "xs:choice" if ordered == "false" else "xs:sequence" if elems or open_ == "true": X(f"{indent}<{particle_tag}>") for e in elems: emit_property(e, indent + "\t") if open_ == "true": X(f'{indent}\t') X(f"{indent}") for a in attr_props: emit_property(a, indent) if open_ == "true": X(f'{indent}') def complex_type_attrs(el): open_ = el.get("open") ordered = el.get("ordered") sequenced = el.get("sequenced") abstract = el.get("abstract") mixed = el.get("mixed") out = "" if abstract == "true": out += ' abstract="true"' elif abstract is not None: out += mirror("abstract", abstract) if mixed == "true": out += ' mixed="true"' elif mixed is not None: out += mirror("mixed", mixed) # XSD требует объявлять атрибуты после частицы, поэтому исходный порядок свойств # восстановим как «сначала form=Attribute, потом остальные» — верно для 96.5% типов order, kinds = [], [] for c in el: if not isinstance(c.tag, str) or local(c) != "property": continue nm = c.get("name") if nm is None: nm = "@" + (c.get("ref") or "").split(":")[-1] order.append(nm) kinds.append(0 if prop_form(c) == "Attribute" else 1) if len(order) > 1: natural = all(kinds[i] >= kinds[i - 1] for i in range(1, len(kinds))) if not natural: out += mirror("order", "|".join(order)) if open_ is not None and open_ != "true": out += mirror("open", open_) if ordered is not None and ordered != "false": out += mirror("ordered", ordered) if sequenced is not None: out += mirror("sequenced", sequenced) return out # ── metadata properties ────────────────────────────────────── meta = None if md_path and os.path.exists(md_path): md = _parse_xml(md_path) props_el = md.find(f".//{{{MD_NS}}}XDTOPackage/{{{MD_NS}}}Properties") if props_el is not None: meta = {"Name": None, "Comment": None, "Synonym": []} n = props_el.find(f"{{{MD_NS}}}Name") if n is not None: meta["Name"] = n.text or "" c = props_el.find(f"{{{MD_NS}}}Comment") if c is not None: meta["Comment"] = c.text or "" for item in props_el.iterfind(f"{{{MD_NS}}}Synonym/{{{V8_NS}}}item"): lang = item.find(f"{{{V8_NS}}}lang") cont = item.find(f"{{{V8_NS}}}content") meta["Synonym"].append({ "Lang": (lang.text or "") if lang is not None else "", "Content": (cont.text or "") if cont is not None else "", }) # ── emit ───────────────────────────────────────────────────── # Тело первым: при его генерации регистрируются все использованные пространства # имён, поэтому корневой элемент может объявить полную карту префиксов. for node in pkg: if not isinstance(node.tag, str): continue ln = local(node) if ln == "import": X(f'\t') elif ln == "property": emit_property(node, "\t") elif ln == "valueType": X(f'\t') emit_simple_type_body(node, "\t\t") X("\t") elif ln == "objectType": name = node.get("name") base = convert_qname(node, node.get("base")) cta = complex_type_attrs(node) if base: X(f'\t') X("\t\t") X(f'\t\t\t') emit_complex_type_body(node, "\t\t\t\t") X("\t\t\t") X("\t\t") X("\t") else: has_body = any(isinstance(c.tag, str) for c in node) if not has_body and node.get("open") != "true": X(f'\t') else: X(f'\t') emit_complex_type_body(node, "\t\t") X("\t") body_lines = lines lines = [] # ── schema element ─────────────────────────────────────────── ns_decls = "" for uri, px in sorted(ns_prefix.items(), key=lambda kv: kv[1]): ns_decls += f' xmlns:{px}="{esc(uri)}"' if state["uses_xdto"]: ns_decls += f' xmlns:xdto="{XDTO_NS}"' schema_attrs = "" efq = pkg.get("elementFormQualified") afq = pkg.get("attributeFormQualified") if efq is not None: schema_attrs += f' elementFormDefault="{"qualified" if efq == "true" else "unqualified"}"' if afq is not None: schema_attrs += f' attributeFormDefault="{"qualified" if afq == "true" else "unqualified"}"' X(f'') if meta: X("\t") X("\t\t") X(f'\t\t\t') if meta["Name"] is not None: X(f'\t\t\t\t{esc_text(meta["Name"])}') if meta["Comment"]: X(f'\t\t\t\t{esc_text(meta["Comment"])}') for s in meta["Synonym"]: X(f'\t\t\t\t{esc_text(s["Content"])}') X("\t\t\t") X("\t\t") X("\t") lines.extend(body_lines) X("") text = "\r\n".join(lines) + "\r\n" if args.OutFile: d = os.path.dirname(args.OutFile) if d and not os.path.isdir(d): os.makedirs(d, exist_ok=True) with open(args.OutFile, "wb") as f: f.write(b"\xef\xbb\xbf" + text.encode("utf-8")) print(f"✓ XSD записана: {args.OutFile}") print(f" targetNamespace: {target_ns}") else: sys.stdout.write(text)