feat(xdto-compile,xdto-decompile,xdto-validate): пакеты XDTO из XML-схемы

Три навыка для работы с пакетами XDTO. Формат описания — обычная XSD,
своего DSL нет: рутину снимает конвертер (локальные объявления префиксов
dNpM на каждой ссылке, инвертированная кратность lowerBound/upperBound,
фасеты атрибутами вместо дочерних элементов, обязательный порядок
элементов верхнего уровня). То, чего XSD выразить не может — nillable
у атрибута, qualified у свойства, «атрибут записан явно» — едет
атрибутами из пространства имён модели XDTO по правилу «то же имя,
что в Package.bin». Свойства объекта метаданных живут в xs:appinfo,
поэтому пара decompile → compile замыкается без потерь.

Инвариант bin → xsd → bin проверен побайтово на 760 пакетах выгрузок
Бухгалтерии и ERP 8.3.24 (харнесс debug/xdto/roundtrip-corpus.mjs).
Сборка из рукописной XSD проверена загрузкой в базу 8.3.24 — именно
она вскрыла обязательный порядок import→property→valueType→objectType,
невидимый для корпусной сверки: все выгрузки уже канонические.

xdto-validate ловит два класса тихих дефектов, которые платформа не
диагностирует: подмену неразрешённого чужого типа на xs:anyType при
импорте XML-схемы и nillable у свойства-атрибута, теряемый экспортом
схемы в Конфигураторе.

Тесты: 18 снэпшот-кейсов на синтетических схемах (типовые конфигурации
в репозиторий не тащим), паритет PS↔PY на общих эталонах. Раннер
получил caseFiles — копирование файлов кейса в workDir для навыков
с файловым, а не JSON входом.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-25 21:21:57 +03:00
co-authored by Claude Opus 5
parent d544071b1e
commit d05aef54b4
140 changed files with 9601 additions and 1 deletions
+93
View File
@@ -0,0 +1,93 @@
---
name: xdto-compile
description: Создание пакета XDTO 1С из XML-схемы (XSD). Используй когда нужно добавить пакет XDTO в конфигурацию — для обмена, интеграции, веб-сервиса, разбора внешнего XML-формата
argument-hint: -XsdPath <файл.xsd> -OutputDir <каталог-конфигурации> [-Name <имя>] [-Synonym <синоним>] [-Comment <текст>] [-Force]
allowed-tools:
- Bash
- Read
- Glob
---
# /xdto-compile — Создание пакета XDTO из XML-схемы
Собирает пакет XDTO по обычной XML-схеме: `XDTOPackages/<Имя>.xml`,
`XDTOPackages/<Имя>/Ext/Package.bin` и регистрацию в `Configuration.xml`.
Отдельного DSL нет — входной формат это XSD. Вся механика перевода (локальные объявления
префиксов `dNpM` на каждой ссылке, инвертированная кратность `lowerBound`/`upperBound`,
фасеты атрибутами вместо дочерних элементов) делается навыком.
## Параметры
| Параметр | Описание |
|----------|----------|
| `XsdPath` | Путь к файлу XML-схемы |
| `Xsd` | Схема строкой, вместо `-XsdPath` |
| `OutputDir` | Каталог конфигурации (где лежит `Configuration.xml`) |
| `Name` | Имя объекта метаданных. По умолчанию — из `xs:appinfo`, иначе имя файла XSD, санированное под идентификатор 1С |
| `Synonym` | Синоним: строка (русский) или хеш-таблица `@{ru='…'; en='…'}` |
| `Comment` | Комментарий |
| `Force` | Перезаписать существующий пакет |
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/xdto-compile.ps1" -XsdPath "<схема.xsd>" -OutputDir "<каталог>"
```
Примеры:
```powershell
... -XsdPath bank.xsd -OutputDir C:\cfsrc\erp -Name ОбменСБанком -Synonym "Обмен с банком"
... -XsdPath fss.xsd -OutputDir C:\cfsrc\erp -Force
```
## Формат входа
Обычная XML-схема. Две вещи, которые XSD выразить не может (`nillable` у атрибута,
`qualified` у отдельного свойства), пишутся атрибутами из пространства имён модели XDTO:
```xml
<xs:attribute name="Представление" type="xs:string"
xmlns:xdto="http://v8.1c.ru/8.1/xdto" xdto:nillable="true"/>
```
Правило: **чего XSD сказать не может — пиши атрибутом `xdto:` с тем же именем, что в модели**.
Схема при этом остаётся валидной. Полный список аннотаций — в справке ниже.
Свойства объекта метаданных можно задать прямо в схеме:
```xml
<xs:annotation>
<xs:appinfo>
<xdto:package xmlns:xdto="http://v8.1c.ru/8.1/xdto">
<xdto:name>ОбменСБанком</xdto:name>
<xdto:synonym lang="ru">Обмен с банком</xdto:synonym>
</xdto:package>
</xs:appinfo>
</xs:annotation>
```
Принимается и не вполне каноническая схема: имя типа без префикса трактуется как тип
целевого пространства имён, `form="qualified"` на глобальных объявлениях (так экспортирует
сам Конфигуратор) — читается.
## Зависимости между пакетами
`<xs:import namespace="…"/>` разрешается по namespace среди пакетов конфигурации.
Если пакета с таким пространством имён в конфигурации нет, платформа при загрузке молча
подменит тип на `xs:anyType` — без ошибки. Сначала соберите пакеты-зависимости,
затем зависящий, и проверьте результат через `/xdto-validate`.
## Типичный workflow
1. Получить XSD от контрагента (или выгрузить: Конфигуратор → пакет → Экспорт XML-схемы)
2. `/xdto-compile -XsdPath <файл> -OutputDir <конфигурация>`
3. `/xdto-validate` — убедиться, что все типы разрешились
4. `/db-load-xml` + `/db-update` — загрузить в базу
Правка существующего пакета: `/xdto-decompile` → правка XSD → `/xdto-compile -Force`.
Пара замыкается без потерь, включая свойства объекта метаданных.
## Верификация
```
/xdto-validate <каталог>/XDTOPackages/<Имя> — типы, импорты, регистрация
```
@@ -0,0 +1,772 @@
# xdto-compile v1.0 — Build a 1C XDTO package from an XML Schema (XSD)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true, ParameterSetName='File')]
[Alias('Path')]
[string]$XsdPath,
[Parameter(Mandatory=$true, ParameterSetName='Inline')]
[string]$Xsd,
[Parameter(Mandatory=$true)]
[string]$OutputDir,
[string]$Name,
[object]$Synonym,
[string]$Comment,
[switch]$Force
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$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"
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow.
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.get_LocalName() }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-EditMode([string]$cfgDir) {
$mode = "deny"
try {
$pj = Find-V8Project $cfgDir
if ($pj) {
$cfg = Get-Content -Path $pj -Raw -Encoding UTF8 | ConvertFrom-Json
if ($cfg.PSObject.Properties.Name -contains 'editingAllowedCheck' -and $cfg.editingAllowedCheck) {
$mode = [string]$cfg.editingAllowedCheck
}
}
} catch {}
return $mode
}
function Assert-EditAllowed([string]$targetPath) {
try {
$mode = $null
$d = $targetPath
for ($i = 0; $i -lt 20 -and $d; $i++) {
$cfgXml = Join-Path $d "Configuration.xml"
$supportBin = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
# Автономный объект (внешняя обработка/отчёт) — граница климба
foreach ($x in @(Get-ChildItem -Path $d -Filter "*.xml" -File -ErrorAction SilentlyContinue)) {
if (Test-ExternalObjectRoot $x.FullName) { return }
}
if (Test-Path $cfgXml) {
if (Test-Path $supportBin) {
$mode = Get-EditMode $d
if ($mode -eq "off") { return }
$msg = "Конфигурация находится на поддержке (Ext/ParentConfigurations.bin). Правка может быть запрещена."
if ($mode -eq "warn") { Write-Warning $msg; return }
throw "$msg Снимите с поддержки (/support-edit) или задайте editingAllowedCheck в .v8-project.json."
}
return
}
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
} catch [System.Management.Automation.RuntimeException] {
throw
} catch {}
}
# --- Load the schema ---
if ($PSCmdlet.ParameterSetName -eq 'Inline') {
$xsdText = $Xsd
$defaultName = "Package"
} else {
if (-not (Test-Path $XsdPath -PathType Leaf)) { throw "Файл XSD не найден: $XsdPath" }
$xsdText = [System.IO.File]::ReadAllText($XsdPath)
$defaultName = [System.IO.Path]::GetFileNameWithoutExtension($XsdPath)
}
$xdoc = New-Object System.Xml.XmlDocument
$xdoc.PreserveWhitespace = $false
try { $xdoc.LoadXml($xsdText) } catch { throw "Не удалось разобрать XSD: $($_.Exception.Message)" }
$schema = $xdoc.DocumentElement
if ($schema.get_LocalName() -ne "schema" -or $schema.NamespaceURI -ne $XS_NS) {
throw "Ожидался корневой <xs:schema> в пространстве имён $XS_NS"
}
$targetNs = $schema.GetAttribute("targetNamespace")
# --- Emit-tree primitives -----------------------------------------------------
# A node carries attributes in canonical order; QName values keep their namespace
# so prefixes can be assigned per depth at serialization time (the dNpN scheme).
function New-Node([string]$tag) {
return [pscustomobject]@{ Tag = $tag; Attrs = (New-Object System.Collections.ArrayList); Children = (New-Object System.Collections.ArrayList); Text = $null; Prefix = $null; DeclareNs = $null }
}
function Add-Attr($node, [string]$name, $value) {
# $value НЕ типизируем: [string]$null коэрсится в "" и атрибут ложно появляется
if ($null -eq $value) { return }
[void]$node.Attrs.Add([pscustomobject]@{ Name = $name; Value = [string]$value; Ns = $null; Local = $null })
}
function Add-QAttr($node, [string]$name, $ns, $local) {
if ($null -eq $local) { return }
[void]$node.Attrs.Add([pscustomobject]@{ Name = $name; Value = $null; Ns = [string]$ns; Local = [string]$local })
}
function Add-QListAttr($node, [string]$name, $pairs, [bool]$clark) {
if (-not $pairs -or $pairs.Count -eq 0) { return }
[void]$node.Attrs.Add([pscustomobject]@{ Name = $name; Value = $null; Ns = $null; Local = $null; List = $pairs; Clark = $clark })
}
function Add-Child($node, $child) { if ($child) { [void]$node.Children.Add($child) } }
# Canonical attribute order per element — derived by topological sort over the
# whole 8.3.24 corpus (acc + erp, 760 packages), see docs/1c-xdto-spec.md.
$ATTR_ORDER = @{
"package" = @("targetNamespace", "elementFormQualified", "attributeFormQualified")
"import" = @("namespace")
"objectType" = @("name", "base", "open", "abstract", "mixed", "ordered", "sequenced")
"property" = @("name", "ref", "type", "lowerBound", "upperBound", "nillable", "fixed", "default", "form", "localName", "qualified")
"valueType" = @("name", "base", "variety", "itemType", "length", "memberTypes", "minExclusive", "maxExclusive", "minInclusive", "maxInclusive", "minLength", "maxLength", "totalDigits", "fractionDigits", "whiteSpace")
"typeDef" = @("xsi:type", "base", "mixed", "open", "ordered", "sequenced", "variety", "itemType", "length", "memberTypes", "minExclusive", "maxExclusive", "minInclusive", "maxInclusive", "minLength", "maxLength", "totalDigits", "fractionDigits", "whiteSpace")
"enumeration" = @("xsi:type")
}
function Sort-Attrs($node) {
$order = $ATTR_ORDER[$node.Tag]
if (-not $order) { return $node.Attrs }
$sorted = New-Object System.Collections.ArrayList
foreach ($n in $order) {
foreach ($a in $node.Attrs) { if ($a.Name -eq $n) { [void]$sorted.Add($a) } }
}
foreach ($a in $node.Attrs) { if ($order -notcontains $a.Name) { [void]$sorted.Add($a) } }
return $sorted
}
function Esc([string]$s) {
if ($null -eq $s) { return "" }
return $s.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace('"', "&quot;")
}
function EscText([string]$s) {
if ($null -eq $s) { return "" }
return $s.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;")
}
# --- Serializer with the dNpN prefix scheme ---
$out = New-Object System.Text.StringBuilder
function Serialize-Node($node, [int]$depth, $inherited) {
$indent = "`t" * ($depth - 1)
$attrs = Sort-Attrs $node
# Namespaces needing a NEW declaration here: те, что ещё не в области видимости.
# Сериализатор платформы объявляет префикс на первом узле, где он нужен, а
# потомки его переиспользуют — отсюда d2p1 у property внутри objectType.
$localNs = New-Object System.Collections.ArrayList
function Need-Prefix([string]$ns) {
if (-not $ns -or $ns -eq $XS_NS -or $ns -eq $XSI_NS) { return }
if ($inherited.ContainsKey($ns)) { return }
if (-not $localNs.Contains($ns)) { [void]$localNs.Add($ns) }
}
foreach ($a in $attrs) {
if ($a.PSObject.Properties.Name -contains 'List' -and $a.List) {
# Нотация Кларка несёт ns в значении и префикса не требует; редкие
# случаи, где платформа его всё же объявила, приходят зеркалом declareNs
if (-not $a.Clark) { foreach ($p in $a.List) { Need-Prefix $p.Ns } }
} elseif ($a.Ns) { Need-Prefix $a.Ns }
}
# Свойство с qualified платформа сериализует с явным префиксом пространства
# имён XDTO — и в имени тега, и в имени самого атрибута
$hasQualified = $false
foreach ($a in $attrs) { if ($a.Name -eq "qualified") { $hasQualified = $true } }
if ($hasQualified) { Need-Prefix $XDTO_NS }
if ($node.DeclareNs) { Need-Prefix $node.DeclareNs }
$prefixOf = @{}
foreach ($k in $inherited.Keys) { $prefixOf[$k] = $inherited[$k] }
$nsDecls = ""
for ($i = 0; $i -lt $localNs.Count; $i++) {
# Осмысленный префикс из исходника (зеркало xdto:prefix) имеет приоритет
$px = if ($i -eq 0 -and $node.Prefix) { $node.Prefix } else { "d${depth}p$($i + 1)" }
$prefixOf[$localNs[$i]] = $px
$nsDecls += " xmlns:$px=`"$(Esc $localNs[$i])`""
}
function QVal([string]$ns, [string]$local) {
if (-not $ns) { return $local }
if ($ns -eq $XS_NS) { return "xs:$local" }
if ($ns -eq $XSI_NS) { return "xsi:$local" }
return "$($prefixOf[$ns]):$local"
}
$attrText = ""
foreach ($a in $attrs) {
if ($a.PSObject.Properties.Name -contains 'List' -and $a.List) {
$vals = @()
foreach ($p in $a.List) {
if ($a.Clark) { $vals += $(if ($p.Ns) { "{$($p.Ns)}$($p.Local)" } else { $p.Local }) }
else { $vals += (QVal $p.Ns $p.Local) }
}
$attrText += " $($a.Name)=`"$(Esc ($vals -join ' '))`""
} elseif ($a.Ns -or $a.Local) {
$attrText += " $($a.Name)=`"$(Esc (QVal $a.Ns $a.Local))`""
} elseif ($a.Name -eq "qualified") {
$attrText += " $($prefixOf[$XDTO_NS]):qualified=`"$(Esc $a.Value)`""
} else {
$attrText += " $($a.Name)=`"$(Esc $a.Value)`""
}
}
$tagName = $node.Tag
if ($hasQualified) { $tagName = "$($prefixOf[$XDTO_NS]):$($node.Tag)" }
$hasChildren = $node.Children.Count -gt 0
# Пустое значение пишется самозакрывающимся тегом: <enumeration/>, а не <enumeration></enumeration>
$hasText = ($null -ne $node.Text -and $node.Text -ne "")
if (-not $hasChildren -and -not $hasText) {
[void]$out.Append("$indent<$tagName$nsDecls$attrText/>`r`n")
return
}
if ($hasText -and -not $hasChildren) {
[void]$out.Append("$indent<$tagName$nsDecls$attrText>$(EscText $node.Text)</$tagName>`r`n")
return
}
[void]$out.Append("$indent<$tagName$nsDecls$attrText>`r`n")
foreach ($c in $node.Children) { Serialize-Node $c ($depth + 1) $prefixOf }
[void]$out.Append("$indent</$tagName>`r`n")
}
# --- XSD reading helpers ---
function XA([System.Xml.XmlElement]$el, [string]$name) {
if ($el.HasAttribute($name)) { return $el.GetAttribute($name) }
return $null
}
function MA([System.Xml.XmlElement]$el, [string]$name) {
# xdto: mirror attribute — the literal value to write into Package.bin.
# Префикс не фиксируем: ищем по namespace, а не по строке "xdto:".
$a = $el.Attributes.GetNamedItem($name, $XDTO_NS)
if ($null -eq $a) { return $null }
return $a.Value
}
function XChildren([System.Xml.XmlElement]$el, [string]$local) {
$res = New-Object System.Collections.ArrayList
foreach ($c in $el.ChildNodes) {
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.NamespaceURI -eq $XS_NS -and $c.get_LocalName() -eq $local) { [void]$res.Add($c) }
}
# ArrayList, а не @(): PowerShell разворачивает массив из одного элемента при return
return ,$res
}
function XFirst([System.Xml.XmlElement]$el, [string]$local) {
$r = XChildren $el $local
if ($r.Count -gt 0) { return $r[0] }
return $null
}
# Split a QName from the XSD into (ns, local) using that element's prefix scope
function Split-QName([System.Xml.XmlElement]$el, [string]$qname) {
if ($null -eq $qname -or $qname -eq "") { return $null }
$parts = $qname.Split(":")
if ($parts.Count -eq 2) {
$ns = $el.GetNamespaceOfPrefix($parts[0])
$local = $parts[1]
} else {
# Прощающий ввод: голое имя типа трактуем как тип целевого пространства
$ns = $el.GetNamespaceOfPrefix("")
if (-not $ns) { $ns = $targetNs }
$local = $parts[0]
}
return [pscustomobject]@{ Ns = $ns; Local = $local }
}
function Split-QNameList([System.Xml.XmlElement]$el, [string]$list) {
if (-not $list) { return @() }
$res = @()
foreach ($q in ($list -split "\s+")) { if ($q) { $res += (Split-QName $el $q) } }
return $res
}
$FACETS = @("length", "minLength", "maxLength", "totalDigits", "fractionDigits",
"minInclusive", "maxInclusive", "minExclusive", "maxExclusive", "whiteSpace")
# --- simpleType -> valueType / typeDef(ValueType) ---
function Fill-SimpleType($node, [System.Xml.XmlElement]$st) {
$restriction = XFirst $st "restriction"
$list = XFirst $st "list"
$union = XFirst $st "union"
if ($list) {
$it = Split-QName $list (XA $list "itemType")
$mv = MA $list "variety"
Add-Attr $node "variety" $(if ($null -ne $mv) { $mv } else { "List" })
if ($it) { Add-QAttr $node "itemType" $it.Ns $it.Local }
return
}
if ($union) {
$mv = MA $union "variety"
Set-AttrValue $node "variety" $(if ($null -ne $mv) { $mv } else { "Union" })
$members = @(Split-QNameList $union (XA $union "memberTypes"))
# По умолчанию нотация Кларка — так записано 125 из 135 memberTypes корпуса
$useClark = ((MA $union "memberTypesForm") -ne "prefixed")
if ($members.Count -gt 0) { Add-QListAttr $node "memberTypes" $members $useClark }
$node.DeclareNs = MA $union "declareNs"
foreach ($anon in (XChildren $union "simpleType")) {
# typeDef в контексте простого типа xsi:type не несёт (40 узлов корпуса)
$td = New-Node "typeDef"
Fill-SimpleType $td $anon
Add-Child $node $td
}
return
}
if ($restriction) {
$b = Split-QName $restriction (XA $restriction "base")
if ($b) { Add-QAttr $node "base" $b.Ns $b.Local }
$mv = MA $restriction "variety"
if ($null -ne $mv) { Add-Attr $node "variety" $mv }
# Анонимный базовый тип внутри xs:restriction — typeDef без xsi:type
$anonBase = XFirst $restriction "simpleType"
if ($anonBase) {
$td = New-Node "typeDef"
Fill-SimpleType $td $anonBase
Add-Child $node $td
}
foreach ($f in $FACETS) {
foreach ($fe in (XChildren $restriction $f)) { Add-Attr $node $f (XA $fe "value") }
}
foreach ($pe in (XChildren $restriction "pattern")) {
$pn = New-Node "pattern"; $pn.Text = (XA $pe "value"); Add-Child $node $pn
}
foreach ($en in (XChildren $restriction "enumeration")) {
$enode = New-Node "enumeration"
$mt = MA $en "type"
if ($null -ne $mt) {
$q = Split-QName $en $mt
Add-QAttr $enode "xsi:type" $q.Ns $q.Local
}
$enode.Text = (XA $en "value")
Add-Child $node $enode
}
}
}
function Get-PropKey($p) {
foreach ($a in $p.Attrs) { if ($a.Name -eq "name") { return $a.Value } }
foreach ($a in $p.Attrs) { if ($a.Name -eq "ref") { return "@" + $a.Local } }
return $null
}
# Восстановить исходный порядок свойств по зеркалу xdto:order
function Reorder-Properties($node, $names) {
$props = @($node.Children | Where-Object { $_.Tag -eq "property" })
if ($props.Count -lt 2) { return }
$byKey = @{}
foreach ($p in $props) {
$k = Get-PropKey $p
if ($null -ne $k -and -not $byKey.ContainsKey($k)) { $byKey[$k] = $p }
}
$ordered = New-Object System.Collections.ArrayList
foreach ($n in $names) {
if ($byKey.ContainsKey($n)) { [void]$ordered.Add($byKey[$n]); $byKey.Remove($n) }
}
foreach ($p in $props) { if ($ordered -notcontains $p) { [void]$ordered.Add($p) } }
$others = @($node.Children | Where-Object { $_.Tag -ne "property" })
$node.Children.Clear()
foreach ($p in $ordered) { [void]$node.Children.Add($p) }
foreach ($o in $others) { [void]$node.Children.Add($o) }
}
function Set-AttrValue($node, [string]$name, [string]$value) {
foreach ($a in $node.Attrs) { if ($a.Name -eq $name) { $a.Value = $value; return } }
Add-Attr $node $name $value
}
# --- element / attribute -> property ---
function Build-Property([System.Xml.XmlElement]$el, [bool]$isAttribute) {
$p = New-Node "property"
$xsdName = XA $el "name"
$mirrorName = MA $el "name"
if ($null -ne $mirrorName) {
Add-Attr $p "name" $mirrorName
$localName = $xsdName
} else {
Add-Attr $p "name" $xsdName
$localName = $null
}
$refQ = Split-QName $el (XA $el "ref")
if ($refQ) { Add-QAttr $p "ref" $refQ.Ns $refQ.Local }
$typeQ = Split-QName $el (XA $el "type")
if ($typeQ) { Add-QAttr $p "type" $typeQ.Ns $typeQ.Local }
if ($isAttribute) {
Add-Attr $p "lowerBound" (MA $el "lowerBound")
Add-Attr $p "upperBound" (MA $el "upperBound")
Add-Attr $p "nillable" (MA $el "nillable")
} else {
Add-Attr $p "lowerBound" (XA $el "minOccurs")
$maxOcc = XA $el "maxOccurs"
if ($null -ne $maxOcc) { Add-Attr $p "upperBound" $(if ($maxOcc -eq "unbounded") { "-1" } else { $maxOcc }) }
Add-Attr $p "nillable" (XA $el "nillable")
}
Add-Attr $p "fixed" (XA $el "fixed")
Add-Attr $p "default" (XA $el "default")
if ($isAttribute) {
Add-Attr $p "form" "Attribute"
} else {
$mf = MA $el "form"
if ($null -ne $mf) { Add-Attr $p "form" $mf }
}
Add-Attr $p "localName" $localName
Add-Attr $p "qualified" (MA $el "qualified")
$p.Prefix = MA $el "prefix"
# Anonymous inline type
$anonSimple = XFirst $el "simpleType"
$anonComplex = XFirst $el "complexType"
if ($anonSimple) {
$td = New-Node "typeDef"
Add-Attr $td "xsi:type" "ValueType"
Fill-SimpleType $td $anonSimple
Add-Child $p $td
} elseif ($anonComplex) {
$td = New-Node "typeDef"
Add-Attr $td "xsi:type" "ObjectType"
Fill-ComplexType $td $anonComplex
Add-Child $p $td
}
return $p
}
# --- complexType -> objectType / typeDef(ObjectType) ---
# open / ordered / sequenced / abstract / mixed: выводим где выводимо,
# остальное приходит зеркалом xdto:
function Set-TypeFlags($node, [System.Xml.XmlElement]$ct, $isOpen, $choice) {
$mOpen = MA $ct "open"
if ($null -ne $mOpen) { Add-Attr $node "open" $mOpen }
elseif ($isOpen) { Add-Attr $node "open" "true" }
$mOrdered = MA $ct "ordered"
if ($null -ne $mOrdered) { Add-Attr $node "ordered" $mOrdered }
elseif ($choice) { Add-Attr $node "ordered" "false" }
$mSeq = MA $ct "sequenced"
if ($null -ne $mSeq) { Add-Attr $node "sequenced" $mSeq }
$mAbstract = MA $ct "abstract"
if ($null -ne $mAbstract) { Add-Attr $node "abstract" $mAbstract }
elseif ((XA $ct "abstract") -eq "true") { Add-Attr $node "abstract" "true" }
$mMixed = MA $ct "mixed"
if ($null -ne $mMixed) { Add-Attr $node "mixed" $mMixed }
elseif ((XA $ct "mixed") -eq "true") { Add-Attr $node "mixed" "true" }
}
function Fill-ComplexType($node, [System.Xml.XmlElement]$ct) {
# xs:complexContent/xs:extension carries the base type
$content = XFirst $ct "complexContent"
$body = $ct
if ($content) {
$ext = XFirst $content "extension"
if ($ext) {
$b = Split-QName $ext (XA $ext "base")
if ($b) { Add-QAttr $node "base" $b.Ns $b.Local }
$body = $ext
}
}
# xs:simpleContent -> a "Text" property holding the element's own value
$simple = XFirst $ct "simpleContent"
if ($simple) {
$ext = XFirst $simple "extension"
if ($ext) {
foreach ($a in (XChildren $ext "attribute")) { Add-Child $node (Build-Property $a $true) }
$tp = New-Node "property"
$tName = MA $ext "textName"
Add-Attr $tp "name" $(if ($null -ne $tName) { $tName } else { "__content" })
$b = Split-QName $ext (XA $ext "base")
if ($b) { Add-QAttr $tp "type" $b.Ns $b.Local }
Add-Attr $tp "lowerBound" (MA $ext "textlowerBound")
Add-Attr $tp "upperBound" (MA $ext "textupperBound")
Add-Attr $tp "nillable" (MA $ext "textnillable")
Add-Attr $tp "form" "Text"
Add-Child $node $tp
# xs:simpleContent не отменяет флаги самого xs:complexType
Set-TypeFlags $node $ct $false $null
return
}
}
# Particle: xs:sequence (ordered) or xs:choice (ordered="false")
$seq = XFirst $body "sequence"
$cho = XFirst $body "choice"
$particle = if ($seq) { $seq } else { $cho }
$isOpen = $false
# Порядок в XDTO: сначала form="Attribute", потом остальные (верно для 96.5%
# типов корпуса). Отклонения приходят зеркалом xdto:order.
$elemProps = New-Object System.Collections.ArrayList
if ($particle) {
foreach ($c in $particle.ChildNodes) {
if ($c.NodeType -ne [System.Xml.XmlNodeType]::Element -or $c.NamespaceURI -ne $XS_NS) { continue }
switch ($c.get_LocalName()) {
"element" { [void]$elemProps.Add((Build-Property $c $false)) }
"any" { $isOpen = $true }
}
}
}
foreach ($a in (XChildren $body "attribute")) { Add-Child $node (Build-Property $a $true) }
foreach ($e in $elemProps) { Add-Child $node $e }
if ((XChildren $body "anyAttribute").Count -gt 0) { $isOpen = $true }
$mOrder = MA $ct "order"
if ($null -ne $mOrder) { Reorder-Properties $node ($mOrder -split "\|") }
Set-TypeFlags $node $ct $isOpen $cho
}
# --- Build the package tree ---
$pkgNode = New-Node "package"
Add-Attr $pkgNode "targetNamespace" $targetNs
$efqMirror = MA $schema "elementFormQualified"
$afqMirror = MA $schema "attributeFormQualified"
$efd = XA $schema "elementFormDefault"
$afd = XA $schema "attributeFormDefault"
if ($null -ne $efqMirror) { Add-Attr $pkgNode "elementFormQualified" $efqMirror }
elseif ($null -ne $efd) { Add-Attr $pkgNode "elementFormQualified" $(if ($efd -eq "qualified") { "true" } else { "false" }) }
if ($null -ne $afqMirror) { Add-Attr $pkgNode "attributeFormQualified" $afqMirror }
elseif ($null -ne $afd) { Add-Attr $pkgNode "attributeFormQualified" $(if ($afd -eq "qualified") { "true" } else { "false" }) }
# Metadata properties from xs:annotation/xs:appinfo
$metaName = $null; $metaComment = $null; $metaSynonym = @()
$ann = XFirst $schema "annotation"
if ($ann) {
$appinfo = XFirst $ann "appinfo"
if ($appinfo) {
foreach ($pk in $appinfo.ChildNodes) {
if ($pk.NodeType -ne [System.Xml.XmlNodeType]::Element -or $pk.NamespaceURI -ne $XDTO_NS) { continue }
foreach ($f in $pk.ChildNodes) {
if ($f.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
switch ($f.get_LocalName()) {
"name" { $metaName = $f.InnerText }
"comment" { $metaComment = $f.InnerText }
"synonym" { $metaSynonym += @{ Lang = $f.GetAttribute("lang"); Content = $f.InnerText } }
}
}
}
}
}
foreach ($node in $schema.ChildNodes) {
if ($node.NodeType -ne [System.Xml.XmlNodeType]::Element -or $node.NamespaceURI -ne $XS_NS) { continue }
switch ($node.get_LocalName()) {
"annotation" { }
"import" {
$n = New-Node "import"
Add-Attr $n "namespace" (XA $node "namespace")
Add-Child $pkgNode $n
}
"include" { }
"element" { Add-Child $pkgNode (Build-Property $node $false) }
"attribute" { Add-Child $pkgNode (Build-Property $node $true) }
"simpleType" {
$n = New-Node "valueType"
Add-Attr $n "name" (XA $node "name")
Fill-SimpleType $n $node
Add-Child $pkgNode $n
}
"complexType" {
$n = New-Node "objectType"
Add-Attr $n "name" (XA $node "name")
Fill-ComplexType $n $node
Add-Child $pkgNode $n
}
}
}
# --- Serialize Package.bin ---
# Модель XDTO требует строгой последовательности элементов верхнего уровня:
# import → property → valueType → objectType. Порядок объявлений в XSD произвольный,
# поэтому пересортировываем — иначе платформа отвергает пакет с «Ошибка преобразования
# данных XDTO». Все 760 пакетов корпуса этому порядку удовлетворяют, так что
# round-trip не затрагивается.
$TOP_ORDER = @("import", "property", "valueType", "objectType")
$sortedChildren = New-Object System.Collections.ArrayList
foreach ($t in $TOP_ORDER) {
foreach ($c in $pkgNode.Children) { if ($c.Tag -eq $t) { [void]$sortedChildren.Add($c) } }
}
foreach ($c in $pkgNode.Children) { if ($TOP_ORDER -notcontains $c.Tag) { [void]$sortedChildren.Add($c) } }
$pkgNode.Children.Clear()
foreach ($c in $sortedChildren) { [void]$pkgNode.Children.Add($c) }
$attrsRoot = Sort-Attrs $pkgNode
$rootAttrText = ""
foreach ($a in $attrsRoot) { $rootAttrText += " $($a.Name)=`"$(Esc $a.Value)`"" }
[void]$out.Append("<package xmlns=`"$XDTO_NS`" xmlns:xs=`"$XS_NS`" xmlns:xsi=`"$XSI_NS`"$rootAttrText>`r`n")
foreach ($c in $pkgNode.Children) { Serialize-Node $c 2 @{} }
[void]$out.Append("</package>")
$binText = $out.ToString()
# --- Resolve the package name ---
if (-not $Name) {
if ($metaName) { $Name = $metaName } else { $Name = $defaultName }
}
# Санация под идентификатор 1С
$Name = ($Name -replace '[^\wЀ-ӿ]', '_')
if ($Name -match '^\d') { $Name = "_$Name" }
Assert-EditAllowed $OutputDir
$pkgRoot = Join-Path $OutputDir "XDTOPackages"
$pkgDir = Join-Path $pkgRoot $Name
$extDir = Join-Path $pkgDir "Ext"
$mdFile = Join-Path $pkgRoot "$Name.xml"
$binFile = Join-Path $extDir "Package.bin"
if ((Test-Path $binFile) -and -not $Force) {
throw "Пакет уже существует: $binFile. Используйте -Force для перезаписи."
}
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
$encBom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($binFile, $binText, $encBom)
# --- Metadata object file ---
if (-not $Synonym -and $metaSynonym.Count -gt 0) {
$synItems = $metaSynonym
} elseif ($Synonym -is [System.Collections.IDictionary]) {
$synItems = @()
foreach ($k in $Synonym.Keys) { $synItems += @{ Lang = [string]$k; Content = [string]$Synonym[$k] } }
} elseif ($Synonym) {
$synItems = @(@{ Lang = "ru"; Content = [string]$Synonym })
} else {
$synItems = @(@{ Lang = "ru"; Content = $Name })
}
if (-not $Comment -and $metaComment) { $Comment = $metaComment }
$uuid = [guid]::NewGuid().ToString()
$md = New-Object System.Text.StringBuilder
function M([string]$s) { [void]$md.Append($s); [void]$md.Append("`r`n") }
M '<?xml version="1.0" encoding="UTF-8"?>'
M '<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">'
M "`t<XDTOPackage uuid=`"$uuid`">"
M "`t`t<Properties>"
M "`t`t`t<Name>$(EscText $Name)</Name>"
M "`t`t`t<Synonym>"
foreach ($s in $synItems) {
M "`t`t`t`t<v8:item>"
M "`t`t`t`t`t<v8:lang>$(EscText $s.Lang)</v8:lang>"
M "`t`t`t`t`t<v8:content>$(EscText $s.Content)</v8:content>"
M "`t`t`t`t</v8:item>"
}
M "`t`t`t</Synonym>"
if ($Comment) { M "`t`t`t<Comment>$(EscText $Comment)</Comment>" } else { M "`t`t`t<Comment/>" }
M "`t`t`t<Namespace>$(EscText $targetNs)</Namespace>"
M "`t`t</Properties>"
M "`t</XDTOPackage>"
[void]$md.Append("</MetaDataObject>")
[System.IO.File]::WriteAllText($mdFile, $md.ToString(), $encBom)
# --- Register in Configuration.xml ---
$configXmlPath = Join-Path $OutputDir "Configuration.xml"
$regResult = "no-config"
if (Test-Path $configXmlPath) {
$configDoc = New-Object System.Xml.XmlDocument
$configDoc.PreserveWhitespace = $true
$configDoc.Load($configXmlPath)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($configDoc.NameTable)
$nsMgr.AddNamespace("md", $MD_NS)
$childObjects = $configDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $nsMgr)
if ($childObjects) {
$existing = $childObjects.SelectNodes("md:XDTOPackage", $nsMgr)
$already = $false
foreach ($e in $existing) { if ($e.InnerText -eq $Name) { $already = $true; break } }
if ($already) {
$regResult = "already"
} else {
$newElem = $configDoc.CreateElement("XDTOPackage", $MD_NS)
$newElem.InnerText = $Name
if ($existing.Count -gt 0) {
$lastElem = $existing[$existing.Count - 1]
$newWs = $configDoc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertAfter($newWs, $lastElem) | Out-Null
$childObjects.InsertAfter($newElem, $newWs) | Out-Null
} else {
$lastChild = $childObjects.LastChild
if ($lastChild -and $lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
$newWs = $configDoc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertBefore($newWs, $lastChild) | Out-Null
$childObjects.InsertBefore($newElem, $lastChild) | Out-Null
} else {
$childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($newElem) | Out-Null
$childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t")) | Out-Null
}
}
$cfgSettings = New-Object System.Xml.XmlWriterSettings
$cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true)
$cfgSettings.Indent = $false
$stream = New-Object System.IO.FileStream($configXmlPath, [System.IO.FileMode]::Create)
$writer = [System.Xml.XmlWriter]::Create($stream, $cfgSettings)
$configDoc.Save($writer)
$writer.Close()
$stream.Close()
$regResult = "added"
}
}
}
# --- Report ---
$typeCount = 0
foreach ($c in $pkgNode.Children) { if ($c.Tag -eq "objectType" -or $c.Tag -eq "valueType") { $typeCount++ } }
Write-Host "✓ Пакет XDTO собран: $Name"
Write-Host " Namespace: $targetNs"
Write-Host " Типов: $typeCount"
Write-Host " Файлы: XDTOPackages/$Name.xml, XDTOPackages/$Name/Ext/Package.bin"
switch ($regResult) {
"added" { Write-Host " Configuration.xml: <XDTOPackage>$Name</XDTOPackage> добавлен в ChildObjects" }
"already" { Write-Host " Configuration.xml: <XDTOPackage>$Name</XDTOPackage> уже зарегистрирован" }
"no-config" { Write-Host " Configuration.xml не найден — регистрация пропущена" }
}
@@ -0,0 +1,780 @@
# xdto-compile v1.0 — Build a 1C XDTO package from an XML Schema (XSD) (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import sys
import uuid
from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
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"
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-XsdPath", "-Path", default="")
parser.add_argument("-Xsd", default="")
parser.add_argument("-OutputDir", required=True)
parser.add_argument("-Name", default="")
parser.add_argument("-Synonym", default="")
parser.add_argument("-Comment", default="")
parser.add_argument("-Force", action="store_true")
args = parser.parse_args()
# ── support guard (Ext/ParentConfigurations.bin) ─────────────
# См. docs/1c-support-state-spec.md. Блокирует правку объектов поставщика
# «на замке». Триггер — наличие bin; реакция из .v8-project.json
# editingAllowedCheck (deny|warn|off, по умолчанию deny).
def find_v8_project(start_dir):
d = os.path.abspath(start_dir)
for _ in range(20):
pj = os.path.join(d, ".v8-project.json")
if os.path.exists(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def get_edit_mode(cfg_dir):
try:
pj = find_v8_project(cfg_dir)
if pj:
with open(pj, encoding="utf-8-sig") as f:
cfg = json.load(f)
return str(cfg.get("editingAllowedCheck") or "deny")
except Exception: # noqa: BLE001
pass
return "deny"
def is_external_object_root(xml_path):
try:
root = etree.parse(xml_path).getroot()
for el in root:
if isinstance(el.tag, str):
return etree.QName(el).localname in ("ExternalDataProcessor", "ExternalReport")
except Exception: # noqa: BLE001
pass
return False
def assert_edit_allowed(target_path):
d = os.path.abspath(target_path)
for _ in range(20):
# Автономный объект (внешняя обработка/отчёт) — граница климба
try:
for f in os.listdir(d):
if f.endswith(".xml") and is_external_object_root(os.path.join(d, f)):
return
except OSError:
pass
cfg_xml = os.path.join(d, "Configuration.xml")
support_bin = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cfg_xml):
if os.path.exists(support_bin):
mode = get_edit_mode(d)
if mode == "off":
return
msg = ("Конфигурация находится на поддержке (Ext/ParentConfigurations.bin). "
"Правка может быть запрещена.")
if mode == "warn":
print(f"WARNING: {msg}", file=sys.stderr)
return
print(f"{msg} Снимите с поддержки (/support-edit) или задайте "
"editingAllowedCheck в .v8-project.json.", file=sys.stderr)
sys.exit(1)
return
parent = os.path.dirname(d)
if parent == d:
break
d = parent
# ── load the schema ──────────────────────────────────────────
if args.Xsd:
xsd_bytes = args.Xsd.encode("utf-8")
default_name = "Package"
elif args.XsdPath:
if not os.path.isfile(args.XsdPath):
print(f"Файл XSD не найден: {args.XsdPath}", file=sys.stderr)
sys.exit(1)
with open(args.XsdPath, "rb") as f:
xsd_bytes = f.read()
default_name = os.path.splitext(os.path.basename(args.XsdPath))[0]
else:
print("Укажите -XsdPath или -Xsd", file=sys.stderr)
sys.exit(1)
try:
schema = etree.fromstring(xsd_bytes)
except Exception as e: # noqa: BLE001
print(f"Не удалось разобрать XSD: {e}", file=sys.stderr)
sys.exit(1)
def local(el):
return etree.QName(el).localname
if local(schema) != "schema" or etree.QName(schema).namespace != XS_NS:
print(f"Ожидался корневой <xs:schema> в пространстве имён {XS_NS}", file=sys.stderr)
sys.exit(1)
target_ns = schema.get("targetNamespace") or ""
# ── emit-tree primitives ─────────────────────────────────────
class Node:
__slots__ = ("tag", "attrs", "children", "text", "prefix", "declare_ns")
def __init__(self, tag):
self.tag = tag
self.attrs = [] # список dict: name, value | (ns, local) | list
self.children = []
self.text = None
self.prefix = None
self.declare_ns = None
def add_attr(node, name, value):
if value is None:
return
node.attrs.append({"name": name, "value": str(value)})
def add_qattr(node, name, ns, loc):
if loc is None:
return
node.attrs.append({"name": name, "ns": ns, "local": loc})
def add_qlist_attr(node, name, pairs, clark):
if not pairs:
return
node.attrs.append({"name": name, "list": pairs, "clark": clark})
# Канонический порядок атрибутов — топологическая сортировка по корпусу 8.3.24
# (acc + erp, 760 пакетов), см. docs/1c-xdto-spec.md.
ATTR_ORDER = {
"package": ["targetNamespace", "elementFormQualified", "attributeFormQualified"],
"import": ["namespace"],
"objectType": ["name", "base", "open", "abstract", "mixed", "ordered", "sequenced"],
"property": ["name", "ref", "type", "lowerBound", "upperBound", "nillable",
"fixed", "default", "form", "localName", "qualified"],
"valueType": ["name", "base", "variety", "itemType", "length", "memberTypes",
"minExclusive", "maxExclusive", "minInclusive", "maxInclusive",
"minLength", "maxLength", "totalDigits", "fractionDigits", "whiteSpace"],
"typeDef": ["xsi:type", "base", "mixed", "open", "ordered", "sequenced", "variety",
"itemType", "length", "memberTypes", "minExclusive", "maxExclusive",
"minInclusive", "maxInclusive", "minLength", "maxLength",
"totalDigits", "fractionDigits", "whiteSpace"],
"enumeration": ["xsi:type"],
}
def sort_attrs(node):
order = ATTR_ORDER.get(node.tag)
if not order:
return node.attrs
res = []
for n in order:
res.extend(a for a in node.attrs if a["name"] == n)
res.extend(a for a in node.attrs if a["name"] not in order)
return res
def esc(s):
if s is None:
return ""
return str(s).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
def esc_text(s):
if s is None:
return ""
return str(s).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
# ── serializer with the dNpM prefix scheme ───────────────────
out = []
def serialize_node(node, depth, inherited):
indent = "\t" * (depth - 1)
attrs_sorted = sort_attrs(node)
# Объявляем здесь только те ns, которых ещё нет в области видимости:
# сериализатор платформы объявляет префикс на первом нуждающемся узле,
# а потомки переиспользуют — отсюда d2p1 у property внутри objectType.
local_ns = []
def need_prefix(ns):
if not ns or ns in (XS_NS, XSI_NS):
return
if ns in inherited:
return
if ns not in local_ns:
local_ns.append(ns)
for a in attrs_sorted:
if "list" in a:
# Нотация Кларка несёт ns в значении и префикса не требует
if not a["clark"]:
for p in a["list"]:
need_prefix(p[0])
elif a.get("ns"):
need_prefix(a["ns"])
has_qualified = any(a["name"] == "qualified" for a in attrs_sorted)
if has_qualified:
need_prefix(XDTO_NS)
if node.declare_ns:
need_prefix(node.declare_ns)
prefix_of = dict(inherited)
ns_decls = ""
for i, ns in enumerate(local_ns):
# Осмысленный префикс из исходника (зеркало xdto:prefix) имеет приоритет
px = node.prefix if (i == 0 and node.prefix) else f"d{depth}p{i + 1}"
prefix_of[ns] = px
ns_decls += f' xmlns:{px}="{esc(ns)}"'
def qval(ns, loc):
if not ns:
return loc
if ns == XS_NS:
return f"xs:{loc}"
if ns == XSI_NS:
return f"xsi:{loc}"
return f"{prefix_of[ns]}:{loc}"
attr_text = ""
for a in attrs_sorted:
if "list" in a:
vals = []
for ns, loc in a["list"]:
vals.append((f"{{{ns}}}{loc}" if ns else loc) if a["clark"] else qval(ns, loc))
attr_text += f' {a["name"]}="{esc(" ".join(vals))}"'
elif a.get("ns") or a.get("local"):
attr_text += f' {a["name"]}="{esc(qval(a.get("ns"), a["local"]))}"'
elif a["name"] == "qualified":
attr_text += f' {prefix_of[XDTO_NS]}:qualified="{esc(a["value"])}"'
else:
attr_text += f' {a["name"]}="{esc(a["value"])}"'
tag_name = f"{prefix_of[XDTO_NS]}:{node.tag}" if has_qualified else node.tag
has_children = bool(node.children)
# Пустое значение пишется самозакрывающимся тегом: <enumeration/>
has_text = node.text is not None and node.text != ""
if not has_children and not has_text:
out.append(f"{indent}<{tag_name}{ns_decls}{attr_text}/>\r\n")
return
if has_text and not has_children:
out.append(f"{indent}<{tag_name}{ns_decls}{attr_text}>{esc_text(node.text)}</{tag_name}>\r\n")
return
out.append(f"{indent}<{tag_name}{ns_decls}{attr_text}>\r\n")
for c in node.children:
serialize_node(c, depth + 1, prefix_of)
out.append(f"{indent}</{tag_name}>\r\n")
# ── XSD reading helpers ──────────────────────────────────────
def MA(el, name):
# xdto: mirror attribute — литеральное значение для Package.bin.
# Ищем по namespace, а не по строке префикса.
return el.get(f"{{{XDTO_NS}}}{name}")
def xchildren(el, name):
return [c for c in el if isinstance(c.tag, str)
and etree.QName(c).namespace == XS_NS and local(c) == name]
def xfirst(el, name):
r = xchildren(el, name)
return r[0] if r else None
def split_qname(el, qname):
if not qname:
return None
parts = qname.split(":")
if len(parts) == 2:
ns = el.nsmap.get(parts[0])
loc = parts[1]
else:
# Прощающий ввод: голое имя типа — тип целевого пространства имён
ns = el.nsmap.get(None) or target_ns
loc = parts[0]
return (ns, loc)
def split_qname_list(el, lst):
if not lst:
return []
return [split_qname(el, q) for q in lst.split() if q]
FACETS = ["length", "minLength", "maxLength", "totalDigits", "fractionDigits",
"minInclusive", "maxInclusive", "minExclusive", "maxExclusive", "whiteSpace"]
# ── simpleType -> valueType / typeDef(ValueType) ─────────────
def fill_simple_type(node, st):
restriction = xfirst(st, "restriction")
lst = xfirst(st, "list")
union = xfirst(st, "union")
if lst is not None:
it = split_qname(lst, lst.get("itemType"))
mv = MA(lst, "variety")
add_attr(node, "variety", mv if mv is not None else "List")
if it:
add_qattr(node, "itemType", it[0], it[1])
return
if union is not None:
mv = MA(union, "variety")
set_attr_value(node, "variety", mv if mv is not None else "Union")
members = split_qname_list(union, union.get("memberTypes"))
# По умолчанию нотация Кларка — так записано 125 из 135 memberTypes корпуса
use_clark = MA(union, "memberTypesForm") != "prefixed"
if members:
add_qlist_attr(node, "memberTypes", members, use_clark)
node.declare_ns = MA(union, "declareNs")
for anon in xchildren(union, "simpleType"):
# typeDef в контексте простого типа xsi:type не несёт (40 узлов корпуса)
td = Node("typeDef")
fill_simple_type(td, anon)
node.children.append(td)
return
if restriction is not None:
b = split_qname(restriction, restriction.get("base"))
if b:
add_qattr(node, "base", b[0], b[1])
mv = MA(restriction, "variety")
if mv is not None:
add_attr(node, "variety", mv)
# Анонимный базовый тип внутри xs:restriction — typeDef без xsi:type
anon_base = xfirst(restriction, "simpleType")
if anon_base is not None:
td = Node("typeDef")
fill_simple_type(td, anon_base)
node.children.append(td)
for f in FACETS:
for fe in xchildren(restriction, f):
add_attr(node, f, fe.get("value"))
for pe in xchildren(restriction, "pattern"):
pn = Node("pattern")
pn.text = pe.get("value")
node.children.append(pn)
for en in xchildren(restriction, "enumeration"):
enode = Node("enumeration")
mt = MA(en, "type")
if mt is not None:
q = split_qname(en, mt)
add_qattr(enode, "xsi:type", q[0], q[1])
enode.text = en.get("value")
node.children.append(enode)
def set_attr_value(node, name, value):
for a in node.attrs:
if a["name"] == name:
a["value"] = value
return
add_attr(node, name, value)
def get_prop_key(p):
for a in p.attrs:
if a["name"] == "name":
return a.get("value")
for a in p.attrs:
if a["name"] == "ref":
return "@" + a["local"]
return None
def reorder_properties(node, names):
props = [c for c in node.children if c.tag == "property"]
if len(props) < 2:
return
by_key = {}
for p in props:
k = get_prop_key(p)
if k is not None and k not in by_key:
by_key[k] = p
ordered = []
for n in names:
if n in by_key:
ordered.append(by_key.pop(n))
for p in props:
if p not in ordered:
ordered.append(p)
others = [c for c in node.children if c.tag != "property"]
node.children = ordered + others
# ── element / attribute -> property ──────────────────────────
def build_property(el, is_attribute):
p = Node("property")
xsd_name = el.get("name")
mirror_name = MA(el, "name")
if mirror_name is not None:
add_attr(p, "name", mirror_name)
local_name = xsd_name
else:
add_attr(p, "name", xsd_name)
local_name = None
ref_q = split_qname(el, el.get("ref"))
if ref_q:
add_qattr(p, "ref", ref_q[0], ref_q[1])
type_q = split_qname(el, el.get("type"))
if type_q:
add_qattr(p, "type", type_q[0], type_q[1])
if is_attribute:
add_attr(p, "lowerBound", MA(el, "lowerBound"))
add_attr(p, "upperBound", MA(el, "upperBound"))
add_attr(p, "nillable", MA(el, "nillable"))
else:
add_attr(p, "lowerBound", el.get("minOccurs"))
max_occ = el.get("maxOccurs")
if max_occ is not None:
add_attr(p, "upperBound", "-1" if max_occ == "unbounded" else max_occ)
add_attr(p, "nillable", el.get("nillable"))
add_attr(p, "fixed", el.get("fixed"))
add_attr(p, "default", el.get("default"))
if is_attribute:
add_attr(p, "form", "Attribute")
else:
mf = MA(el, "form")
if mf is not None:
add_attr(p, "form", mf)
add_attr(p, "localName", local_name)
add_attr(p, "qualified", MA(el, "qualified"))
p.prefix = MA(el, "prefix")
anon_simple = xfirst(el, "simpleType")
anon_complex = xfirst(el, "complexType")
if anon_simple is not None:
td = Node("typeDef")
add_attr(td, "xsi:type", "ValueType")
fill_simple_type(td, anon_simple)
p.children.append(td)
elif anon_complex is not None:
td = Node("typeDef")
add_attr(td, "xsi:type", "ObjectType")
fill_complex_type(td, anon_complex)
p.children.append(td)
return p
# ── complexType -> objectType / typeDef(ObjectType) ──────────
def set_type_flags(node, ct, is_open, choice):
m_open = MA(ct, "open")
if m_open is not None:
add_attr(node, "open", m_open)
elif is_open:
add_attr(node, "open", "true")
m_ordered = MA(ct, "ordered")
if m_ordered is not None:
add_attr(node, "ordered", m_ordered)
elif choice is not None:
add_attr(node, "ordered", "false")
m_seq = MA(ct, "sequenced")
if m_seq is not None:
add_attr(node, "sequenced", m_seq)
m_abstract = MA(ct, "abstract")
if m_abstract is not None:
add_attr(node, "abstract", m_abstract)
elif ct.get("abstract") == "true":
add_attr(node, "abstract", "true")
m_mixed = MA(ct, "mixed")
if m_mixed is not None:
add_attr(node, "mixed", m_mixed)
elif ct.get("mixed") == "true":
add_attr(node, "mixed", "true")
def fill_complex_type(node, ct):
body = ct
content = xfirst(ct, "complexContent")
if content is not None:
ext = xfirst(content, "extension")
if ext is not None:
b = split_qname(ext, ext.get("base"))
if b:
add_qattr(node, "base", b[0], b[1])
body = ext
# xs:simpleContent -> свойство "Text", хранящее значение самого элемента
simple = xfirst(ct, "simpleContent")
if simple is not None:
ext = xfirst(simple, "extension")
if ext is not None:
for a in xchildren(ext, "attribute"):
node.children.append(build_property(a, True))
tp = Node("property")
t_name = MA(ext, "textName")
add_attr(tp, "name", t_name if t_name is not None else "__content")
b = split_qname(ext, ext.get("base"))
if b:
add_qattr(tp, "type", b[0], b[1])
add_attr(tp, "lowerBound", MA(ext, "textlowerBound"))
add_attr(tp, "upperBound", MA(ext, "textupperBound"))
add_attr(tp, "nillable", MA(ext, "textnillable"))
add_attr(tp, "form", "Text")
node.children.append(tp)
# xs:simpleContent не отменяет флаги самого xs:complexType
set_type_flags(node, ct, False, None)
return
seq = xfirst(body, "sequence")
cho = xfirst(body, "choice")
particle = seq if seq is not None else cho
is_open = False
# Порядок в XDTO: сначала form="Attribute", потом остальные (96.5% типов корпуса)
elem_props = []
if particle is not None:
for c in particle:
if not isinstance(c.tag, str) or etree.QName(c).namespace != XS_NS:
continue
ln = local(c)
if ln == "element":
elem_props.append(build_property(c, False))
elif ln == "any":
is_open = True
for a in xchildren(body, "attribute"):
node.children.append(build_property(a, True))
node.children.extend(elem_props)
if xchildren(body, "anyAttribute"):
is_open = True
m_order = MA(ct, "order")
if m_order is not None:
reorder_properties(node, m_order.split("|"))
set_type_flags(node, ct, is_open, cho)
# ── build the package tree ───────────────────────────────────
pkg_node = Node("package")
add_attr(pkg_node, "targetNamespace", target_ns)
efq_mirror = MA(schema, "elementFormQualified")
afq_mirror = MA(schema, "attributeFormQualified")
efd = schema.get("elementFormDefault")
afd = schema.get("attributeFormDefault")
if efq_mirror is not None:
add_attr(pkg_node, "elementFormQualified", efq_mirror)
elif efd is not None:
add_attr(pkg_node, "elementFormQualified", "true" if efd == "qualified" else "false")
if afq_mirror is not None:
add_attr(pkg_node, "attributeFormQualified", afq_mirror)
elif afd is not None:
add_attr(pkg_node, "attributeFormQualified", "true" if afd == "qualified" else "false")
meta_name = meta_comment = None
meta_synonym = []
ann = xfirst(schema, "annotation")
if ann is not None:
appinfo = xfirst(ann, "appinfo")
if appinfo is not None:
for pk in appinfo:
if not isinstance(pk.tag, str) or etree.QName(pk).namespace != XDTO_NS:
continue
for f in pk:
if not isinstance(f.tag, str):
continue
ln = local(f)
if ln == "name":
meta_name = f.text or ""
elif ln == "comment":
meta_comment = f.text or ""
elif ln == "synonym":
meta_synonym.append({"Lang": f.get("lang") or "", "Content": f.text or ""})
for node in schema:
if not isinstance(node.tag, str) or etree.QName(node).namespace != XS_NS:
continue
ln = local(node)
if ln == "import":
n = Node("import")
add_attr(n, "namespace", node.get("namespace"))
pkg_node.children.append(n)
elif ln in ("annotation", "include"):
continue
elif ln == "element":
pkg_node.children.append(build_property(node, False))
elif ln == "attribute":
pkg_node.children.append(build_property(node, True))
elif ln == "simpleType":
n = Node("valueType")
add_attr(n, "name", node.get("name"))
fill_simple_type(n, node)
pkg_node.children.append(n)
elif ln == "complexType":
n = Node("objectType")
add_attr(n, "name", node.get("name"))
fill_complex_type(n, node)
pkg_node.children.append(n)
# ── serialize Package.bin ────────────────────────────────────
# Модель XDTO требует строгой последовательности элементов верхнего уровня:
# import → property → valueType → objectType. Порядок объявлений в XSD произвольный,
# поэтому пересортировываем — иначе платформа отвергает пакет с «Ошибка преобразования
# данных XDTO». Все 760 пакетов корпуса этому порядку удовлетворяют.
TOP_ORDER = ["import", "property", "valueType", "objectType"]
pkg_node.children = (
[c for t in TOP_ORDER for c in pkg_node.children if c.tag == t]
+ [c for c in pkg_node.children if c.tag not in TOP_ORDER]
)
root_attr_text = "".join(f' {a["name"]}="{esc(a["value"])}"' for a in sort_attrs(pkg_node))
out.append(f'<package xmlns="{XDTO_NS}" xmlns:xs="{XS_NS}" xmlns:xsi="{XSI_NS}"{root_attr_text}>\r\n')
for c in pkg_node.children:
serialize_node(c, 2, {})
out.append("</package>")
bin_text = "".join(out)
# ── resolve the package name ─────────────────────────────────
name = args.Name or meta_name or default_name
name = re.sub(r"[^\wЀ-ӿ]", "_", name, flags=re.UNICODE)
if re.match(r"^\d", name):
name = "_" + name
assert_edit_allowed(args.OutputDir)
pkg_root = os.path.join(args.OutputDir, "XDTOPackages")
pkg_dir = os.path.join(pkg_root, name)
ext_dir = os.path.join(pkg_dir, "Ext")
md_file = os.path.join(pkg_root, name + ".xml")
bin_file = os.path.join(ext_dir, "Package.bin")
if os.path.exists(bin_file) and not args.Force:
print(f"Пакет уже существует: {bin_file}. Используйте -Force для перезаписи.", file=sys.stderr)
sys.exit(1)
os.makedirs(ext_dir, exist_ok=True)
with open(bin_file, "wb") as f:
f.write(b"\xef\xbb\xbf" + bin_text.encode("utf-8"))
# ── metadata object file ─────────────────────────────────────
if not args.Synonym and meta_synonym:
syn_items = meta_synonym
elif args.Synonym:
syn_items = [{"Lang": "ru", "Content": args.Synonym}]
else:
syn_items = [{"Lang": "ru", "Content": name}]
comment = args.Comment or meta_comment or ""
md_lines = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" '
'xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" '
'xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" '
'xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" '
'xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" '
'xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" '
'xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" '
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" '
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">',
f'\t<XDTOPackage uuid="{uuid.uuid4()}">',
"\t\t<Properties>",
f"\t\t\t<Name>{esc_text(name)}</Name>",
"\t\t\t<Synonym>",
]
for s in syn_items:
md_lines += [
"\t\t\t\t<v8:item>",
f'\t\t\t\t\t<v8:lang>{esc_text(s["Lang"])}</v8:lang>',
f'\t\t\t\t\t<v8:content>{esc_text(s["Content"])}</v8:content>',
"\t\t\t\t</v8:item>",
]
md_lines.append("\t\t\t</Synonym>")
md_lines.append(f"\t\t\t<Comment>{esc_text(comment)}</Comment>" if comment else "\t\t\t<Comment/>")
md_lines.append(f"\t\t\t<Namespace>{esc_text(target_ns)}</Namespace>")
md_lines += ["\t\t</Properties>", "\t</XDTOPackage>", "</MetaDataObject>"]
with open(md_file, "wb") as f:
f.write(b"\xef\xbb\xbf" + "\r\n".join(md_lines).encode("utf-8"))
# ── register in Configuration.xml ────────────────────────────
config_xml = os.path.join(args.OutputDir, "Configuration.xml")
reg_result = "no-config"
if os.path.exists(config_xml):
with open(config_xml, "rb") as f:
raw = f.read()
had_bom = raw.startswith(b"\xef\xbb\xbf")
cfg_doc = etree.parse(config_xml)
child_objects = cfg_doc.find(f".//{{{MD_NS}}}Configuration/{{{MD_NS}}}ChildObjects")
if child_objects is not None:
existing = child_objects.findall(f"{{{MD_NS}}}XDTOPackage")
if any((e.text or "") == name for e in existing):
reg_result = "already"
else:
new_elem = etree.SubElement(child_objects, f"{{{MD_NS}}}XDTOPackage")
new_elem.text = name
if existing:
last = existing[-1]
new_elem.tail = last.tail
child_objects.remove(new_elem)
last.addnext(new_elem)
else:
new_elem.tail = child_objects.text
data = etree.tostring(cfg_doc, xml_declaration=True, encoding="UTF-8")
if had_bom:
data = b"\xef\xbb\xbf" + data
with open(config_xml, "wb") as f:
f.write(data)
reg_result = "added"
type_count = sum(1 for c in pkg_node.children if c.tag in ("objectType", "valueType"))
print(f"✓ Пакет XDTO собран: {name}")
print(f" Namespace: {target_ns}")
print(f" Типов: {type_count}")
print(f" Файлы: XDTOPackages/{name}.xml, XDTOPackages/{name}/Ext/Package.bin")
if reg_result == "added":
print(f" Configuration.xml: <XDTOPackage>{name}</XDTOPackage> добавлен в ChildObjects")
elif reg_result == "already":
print(f" Configuration.xml: <XDTOPackage>{name}</XDTOPackage> уже зарегистрирован")
else:
print(" Configuration.xml не найден — регистрация пропущена")
+68
View File
@@ -0,0 +1,68 @@
---
name: xdto-decompile
description: Выгрузка пакета XDTO 1С в XML-схему (XSD). Используй когда нужно прочитать или отредактировать существующий пакет XDTO, отдать схему контрагенту, перенести пакет между конфигурациями
argument-hint: <PackagePath> [-OutFile <файл.xsd>]
allowed-tools:
- Bash
- Read
- Glob
---
# /xdto-decompile — Выгрузка пакета XDTO в XML-схему
Превращает `Ext/Package.bin` в обычную XML-схему. Заменяет чтение модели XDTO с её
инвертированной кратностью, фасетами-атрибутами и локальными объявлениями префиксов
`dNpM` на каждой ссылке.
## Параметры
| Параметр | Описание |
|----------|----------|
| `PackagePath` | Каталог пакета, путь к `Ext/Package.bin` или к `<Имя>.xml` объекта метаданных |
| `OutFile` | Записать схему в файл (UTF-8 BOM). Без него — вывод в stdout |
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/xdto-decompile.ps1" -PackagePath "<путь>"
```
Примеры:
```powershell
... -PackagePath C:\cfsrc\erp\XDTOPackages\ClientBankExchange
... -PackagePath C:\cfsrc\erp\XDTOPackages\ClientBankExchange -OutFile bank.xsd
```
## Round-trip
`/xdto-decompile` → правка XSD → `/xdto-compile -Force` возвращает исходный `Package.bin`
байт-в-байт. Инвариант проверен на 760 пакетах выгрузок Бухгалтерии и ERP.
Это основной способ править существующий пакет: точечных операций не требуется, схема
целиком читаема и редактируема.
Свойства объекта метаданных (`Name`, `Synonym`, `Comment`) выгружаются в
`xs:annotation/xs:appinfo`, поэтому при обратной сборке не теряются. `Namespace` не
дублируется — его несёт `targetNamespace`.
## Отличия от экспорта XML-схемы в Конфигураторе
Штатный экспорт Конфигуратора теряет данные: `nillable` у свойств-атрибутов
(спецификация XSD не допускает его у атрибутов), а для пакетов с
`elementFormQualified="false"` выдаёт невалидную схему — `form="qualified"` на
глобальных объявлениях.
Навык пишет валидную схему, а то, что XSD выразить не может, выносит в атрибуты
пространства имён модели XDTO:
```xml
<xs:attribute name="Представление" type="xs:string" xdto:nillable="true"/>
```
Такие атрибуты валидаторы игнорируют, а `/xdto-compile` читает обратно. Схему без
потерь можно отдавать контрагенту как есть.
## Верификация
```
/xdto-decompile <путь> — схема в stdout
/xdto-decompile <путь> -OutFile schema.xsd — в файл
```
@@ -0,0 +1,603 @@
# xdto-decompile v1.0 — Convert 1C XDTO package to XML Schema (XSD)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)]
[Alias('Path')]
[string]$PackagePath,
[string]$OutFile
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$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"
# --- Resolve paths: accept package dir, Package.bin, or metadata .xml ---
function Resolve-PackagePaths([string]$p) {
$binPath = $null
$mdPath = $null
if (Test-Path $p -PathType Leaf) {
if ([System.IO.Path]::GetFileName($p) -eq "Package.bin") {
$binPath = $p
# <Name>/Ext/Package.bin -> <Name>.xml
$extDir = [System.IO.Path]::GetDirectoryName($p)
$pkgDir = [System.IO.Path]::GetDirectoryName($extDir)
$cand = "$pkgDir.xml"
if (Test-Path $cand) { $mdPath = $cand }
} elseif ($p.EndsWith(".xml")) {
$mdPath = $p
$pkgDir = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($p), [System.IO.Path]::GetFileNameWithoutExtension($p))
$cand = Join-Path (Join-Path $pkgDir "Ext") "Package.bin"
if (Test-Path $cand) { $binPath = $cand }
}
} elseif (Test-Path $p -PathType Container) {
$cand = Join-Path (Join-Path $p "Ext") "Package.bin"
if (Test-Path $cand) {
$binPath = $cand
$mdCand = "$($p.TrimEnd('\','/')).xml"
if (Test-Path $mdCand) { $mdPath = $mdCand }
}
}
if (-not $binPath) { throw "Не найден Ext/Package.bin для пути: $p" }
return @{ Bin = $binPath; Md = $mdPath }
}
$paths = Resolve-PackagePaths $PackagePath
# --- Load package model ---
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $false
$doc.Load($paths.Bin)
$pkg = $doc.DocumentElement
if ($pkg.get_LocalName() -ne "package") { throw "Ожидался корневой <package>, получен <$($pkg.get_LocalName())>" }
$targetNs = $pkg.GetAttribute("targetNamespace")
# --- Namespace -> prefix map for the emitted schema ---
$nsPrefix = @{}
$nsPrefix[$XS_NS] = "xs"
if ($targetNs) { $nsPrefix[$targetNs] = "tns" }
$imports = @()
foreach ($imp in $pkg.ChildNodes) {
if ($imp.NodeType -ne [System.Xml.XmlNodeType]::Element -or $imp.get_LocalName() -ne "import") { continue }
$ns = $imp.GetAttribute("namespace")
$imports += $ns
if (-not $nsPrefix.ContainsKey($ns)) { $nsPrefix[$ns] = "ns" + ($nsPrefix.Count) }
}
# Any foreign namespace referenced but not imported still needs a prefix
function Register-Ns([string]$ns) {
if (-not $ns) { return }
if (-not $nsPrefix.ContainsKey($ns)) { $nsPrefix[$ns] = "ns" + ($nsPrefix.Count) }
}
# --- Output buffer ---
$sb = New-Object System.Text.StringBuilder
function X([string]$line) { [void]$sb.Append($line); [void]$sb.Append("`r`n") }
function Esc([string]$s) {
if ($null -eq $s) { return "" }
return $s.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace('"', "&quot;")
}
function EscText([string]$s) {
if ($null -eq $s) { return "" }
return $s.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;")
}
# --- QName conversion: bin prefix -> schema prefix ---
function Convert-QName([System.Xml.XmlElement]$el, [string]$qname) {
if (-not $qname) { return $null }
# Нотация Кларка {ns}local — так записаны почти все memberTypes
if ($qname.StartsWith("{")) {
$close = $qname.IndexOf("}")
if ($close -gt 0) {
$ns = $qname.Substring(1, $close - 1)
$local = $qname.Substring($close + 1)
if (-not $ns) { return $local }
Register-Ns $ns
return "$($nsPrefix[$ns]):$local"
}
}
$parts = $qname.Split(":")
if ($parts.Count -eq 2) {
$ns = $el.GetNamespaceOfPrefix($parts[0])
$local = $parts[1]
} else {
$ns = $el.GetNamespaceOfPrefix("")
$local = $parts[0]
}
if (-not $ns) { return $qname }
Register-Ns $ns
return "$($nsPrefix[$ns]):$local"
}
function Convert-QNameList([System.Xml.XmlElement]$el, [string]$list) {
if (-not $list) { return $null }
$out = @()
foreach ($q in ($list -split "\s+")) {
if ($q) { $out += (Convert-QName $el $q) }
}
return ($out -join " ")
}
# --- Attribute helpers ---
function A([System.Xml.XmlElement]$el, [string]$name) {
if ($el.HasAttribute($name)) { return $el.GetAttribute($name) }
return $null
}
# Emits `key="value"` pairs, skipping nulls
function Attrs([object[]]$pairs) {
$out = ""
for ($i = 0; $i -lt $pairs.Count; $i += 2) {
$v = $pairs[$i + 1]
if ($null -ne $v) { $out += " $($pairs[$i])=`"$(Esc ([string]$v))`"" }
}
return $out
}
# --- xdto: mirror attributes ---
# Everything XSD cannot express literally rides as xdto:<same name as in Package.bin>.
# Mirrors are emitted only when the literal bin form is not recoverable from the XSD.
$usesXdtoNs = $false
function Mirror([string]$name, $value) {
# $value НЕ типизируем: [string]$null коэрсится в "" и зеркало ложно появляется
if ($null -eq $value) { return "" }
$script:usesXdtoNs = $true
return " xdto:$name=`"$(Esc ([string]$value))`""
}
# Обычно префиксы генерируются схемой dNpM, но изредка узел несёт осмысленный
# префикс (например dcsset) — его надо сохранить, иначе round-trip не сойдётся.
function Mirror-Prefix([System.Xml.XmlElement]$el) {
foreach ($a in $el.Attributes) {
if ($a.Prefix -ne "xmlns") { continue }
if ($a.get_LocalName() -match '^d\d+p\d+$') { continue }
return (Mirror "prefix" $a.get_LocalName())
}
return ""
}
# --- Facet emission (simple types) ---
$FACET_ATTRS = @("length", "minLength", "maxLength", "totalDigits", "fractionDigits",
"minInclusive", "maxInclusive", "minExclusive", "maxExclusive", "whiteSpace")
function Emit-Facets([System.Xml.XmlElement]$el, [string]$indent) {
foreach ($f in $FACET_ATTRS) {
$v = A $el $f
if ($null -ne $v) { X "$indent<xs:$f value=`"$(Esc $v)`"/>" }
}
foreach ($child in $el.ChildNodes) {
if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
if ($child.get_LocalName() -eq "pattern") {
X "$indent<xs:pattern value=`"$(Esc $child.InnerText)`"/>"
} elseif ($child.get_LocalName() -eq "enumeration") {
# xsi:type on enumeration has no XSD counterpart — mirror it
$xsiType = $child.GetAttribute("type", $XSI_NS)
$m = ""
if ($xsiType) { $m = Mirror "type" (Convert-QName $child $xsiType) }
X "$indent<xs:enumeration value=`"$(Esc $child.InnerText)`"$m/>"
}
}
}
function Has-SimpleContent([System.Xml.XmlElement]$el) {
foreach ($c in $el.ChildNodes) {
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -eq "pattern") { return $true }
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -eq "enumeration") { return $true }
}
foreach ($f in $FACET_ATTRS) { if ($null -ne (A $el $f)) { return $true } }
return $false
}
# --- Simple type body (valueType / typeDef xsi:type=ValueType) ---
function Emit-SimpleTypeBody([System.Xml.XmlElement]$el, [string]$indent) {
$variety = A $el "variety"
$base = Convert-QName $el (A $el "base")
$itemType = Convert-QName $el (A $el "itemType")
$memberTypes= Convert-QNameList $el (A $el "memberTypes")
# variety is mirrored: "Atomic" is written explicitly for only part of the corpus
$mv = Mirror "variety" $variety
# memberTypes почти всегда записаны нотацией Кларка; редкую префиксную форму зеркалим
$rawMembers = A $el "memberTypes"
if ($null -ne $rawMembers -and -not $rawMembers.StartsWith("{")) {
$mv += Mirror "memberTypesForm" "prefixed"
}
# При нотации Кларка объявление xmlns:dNpM иногда присутствует, иногда нет —
# из значения это не выводится (зависит от состояния сериализатора), зеркалим факт
if ($null -ne $rawMembers -and $rawMembers.StartsWith("{")) {
foreach ($a in $el.Attributes) {
if ($a.Prefix -eq "xmlns" -and $a.get_LocalName() -match '^d\d+p\d+$') {
$mv += Mirror "declareNs" $a.Value
break
}
}
}
if ($variety -eq "List" -or $itemType) {
X "$indent<xs:list$(Attrs @('itemType', $itemType))$mv/>"
return
}
if ($variety -eq "Union" -or $memberTypes) {
$anon = @()
foreach ($c in $el.ChildNodes) {
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -eq "typeDef") { $anon += $c }
}
if ($anon.Count -eq 0) {
X "$indent<xs:union$(Attrs @('memberTypes', $memberTypes))$mv/>"
} else {
X "$indent<xs:union$(Attrs @('memberTypes', $memberTypes))$mv>"
foreach ($c in $anon) {
X "$indent`t<xs:simpleType>"
Emit-SimpleTypeBody $c "$indent`t`t"
X "$indent`t</xs:simpleType>"
}
X "$indent</xs:union>"
}
return
}
# Базовый тип может быть задан не атрибутом base, а вложенным анонимным typeDef
$anonBase = $null
foreach ($c in $el.ChildNodes) {
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -eq "typeDef") { $anonBase = $c; break }
}
if ((Has-SimpleContent $el) -or $anonBase) {
X "$indent<xs:restriction$(Attrs @('base', $base))$mv>"
if ($anonBase) {
X "$indent`t<xs:simpleType>"
Emit-SimpleTypeBody $anonBase "$indent`t`t"
X "$indent`t</xs:simpleType>"
}
Emit-Facets $el "$indent`t"
X "$indent</xs:restriction>"
} else {
X "$indent<xs:restriction$(Attrs @('base', $base))$mv/>"
}
}
# --- Property classification ---
function Get-PropForm([System.Xml.XmlElement]$p) {
$f = A $p "form"
if ($null -eq $f) { return "Element" }
return $f
}
function Get-AnonTypeDef([System.Xml.XmlElement]$p) {
foreach ($c in $p.ChildNodes) {
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -eq "typeDef") { return $c }
}
return $null
}
# --- Property emission ---
function Emit-Property([System.Xml.XmlElement]$p, [string]$indent, [bool]$isGlobal) {
$form = Get-PropForm $p
$name = A $p "name"
$type = Convert-QName $p (A $p "type")
$ref = Convert-QName $p (A $p "ref")
$local = A $p "localName"
$lower = A $p "lowerBound"
$upper = A $p "upperBound"
$nill = A $p "nillable"
$def = A $p "default"
$fix = A $p "fixed"
$anon = Get-AnonTypeDef $p
# qualified записан как атрибут в пространстве имён XDTO
$qual = $p.GetAttribute("qualified", $XDTO_NS)
if ($qual -eq "") { $qual = $null }
# lowerBound/upperBound map 1:1 onto minOccurs/maxOccurs, including "written explicitly"
$minOccurs = $lower
$maxOccurs = $null
if ($null -ne $upper) { $maxOccurs = if ($upper -eq "-1") { "unbounded" } else { $upper } }
# localName carries the original XML name when it is not a valid 1C identifier
$xmlName = if ($null -ne $local) { $local } else { $name }
$mirrorName = if ($null -ne $local) { Mirror "name" $name } else { "" }
$m = ""
$isAttr = ($form -eq "Attribute")
if ($isAttr) {
# XSD forbids nillable on attributes, and has no minOccurs/maxOccurs
if ($null -ne $qual) { $m += Mirror "qualified" $qual }
if ($null -ne $nill) { $m += Mirror "nillable" $nill }
if ($null -ne $lower) { $m += Mirror "lowerBound" $lower }
if ($null -ne $upper) { $m += Mirror "upperBound" $upper }
$m += $mirrorName
$body = Attrs @('name', $xmlName, 'ref', $ref, 'type', $type, 'default', $def, 'fixed', $fix)
if ($anon) {
X "$indent<xs:attribute$body$m>"
X "$indent`t<xs:simpleType>"
Emit-SimpleTypeBody $anon "$indent`t`t"
X "$indent`t</xs:simpleType>"
X "$indent</xs:attribute>"
} else {
X "$indent<xs:attribute$body$m/>"
}
return
}
if ($form -eq "Text") {
# handled by the owning complexType (xs:simpleContent)
return
}
# form="Element" written explicitly is indistinguishable in XSD from the default
if ($null -ne (A $p "form")) { $m += Mirror "form" $form }
if ($null -ne $qual) { $m += Mirror "qualified" $qual }
$m += $mirrorName
$m += (Mirror-Prefix $p)
$body = Attrs @('name', $xmlName, 'ref', $ref, 'type', $type,
'minOccurs', $minOccurs, 'maxOccurs', $maxOccurs,
'nillable', $nill, 'default', $def, 'fixed', $fix)
if ($anon) {
X "$indent<xs:element$body$m>"
if ($anon.GetAttribute("type", $XSI_NS) -eq "ObjectType") {
$anonBase = Convert-QName $anon (A $anon "base")
if ($anonBase) {
X "$indent`t<xs:complexType$(ComplexTypeAttrs $anon)>"
X "$indent`t`t<xs:complexContent>"
X "$indent`t`t`t<xs:extension$(Attrs @('base', $anonBase))>"
Emit-ComplexTypeBody $anon "$indent`t`t`t`t"
X "$indent`t`t`t</xs:extension>"
X "$indent`t`t</xs:complexContent>"
X "$indent`t</xs:complexType>"
} else {
X "$indent`t<xs:complexType$(ComplexTypeAttrs $anon)>"
Emit-ComplexTypeBody $anon "$indent`t`t"
X "$indent`t</xs:complexType>"
}
} else {
X "$indent`t<xs:simpleType>"
Emit-SimpleTypeBody $anon "$indent`t`t"
X "$indent`t</xs:simpleType>"
}
X "$indent</xs:element>"
} else {
X "$indent<xs:element$body$m/>"
}
}
# --- Complex type body (objectType / typeDef xsi:type=ObjectType) ---
function Emit-ComplexTypeBody([System.Xml.XmlElement]$el, [string]$indent) {
$open = A $el "open"
$ordered = A $el "ordered"
$sequenced = A $el "sequenced"
$props = @()
foreach ($c in $el.ChildNodes) {
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -eq "property") { $props += $c }
}
$elems = @(); $attrs = @(); $text = $null
foreach ($p in $props) {
switch (Get-PropForm $p) {
"Attribute" { $attrs += $p }
"Text" { $text = $p }
default { $elems += $p }
}
}
# simpleContent: a "Text" property holds the element's own value
if ($null -ne $text) {
$tType = Convert-QName $text (A $text "type")
$tm = ""
$tName = A $text "name"
if ($tName -ne "__content") { $tm += Mirror "textName" $tName }
foreach ($extra in @("lowerBound", "upperBound", "nillable")) {
$v = A $text $extra
if ($null -ne $v) { $tm += Mirror "text$extra" $v }
}
X "$indent<xs:simpleContent>"
X "$indent`t<xs:extension$(Attrs @('base', $tType))$tm>"
foreach ($a in $attrs) { Emit-Property $a "$indent`t`t" $false }
X "$indent`t</xs:extension>"
X "$indent</xs:simpleContent>"
return
}
$particleTag = if ($ordered -eq "false") { "xs:choice" } else { "xs:sequence" }
$needParticle = ($elems.Count -gt 0) -or ($open -eq "true")
if ($needParticle) {
X "$indent<$particleTag>"
foreach ($e in $elems) { Emit-Property $e "$indent`t" $false }
if ($open -eq "true") {
X "$indent`t<xs:any namespace=`"##any`" processContents=`"lax`" minOccurs=`"0`" maxOccurs=`"unbounded`"/>"
}
X "$indent</$particleTag>"
}
foreach ($a in $attrs) { Emit-Property $a "$indent" $false }
if ($open -eq "true") {
X "$indent<xs:anyAttribute namespace=`"##any`" processContents=`"lax`"/>"
}
}
# Attributes of a complexType tag itself (mirrors + XSD-native abstract/mixed)
function ComplexTypeAttrs([System.Xml.XmlElement]$el) {
$open = A $el "open"
$ordered = A $el "ordered"
$sequenced = A $el "sequenced"
$abstract = A $el "abstract"
$mixed = A $el "mixed"
$out = ""
if ($abstract -eq "true") { $out += " abstract=`"true`"" } elseif ($null -ne $abstract) { $out += Mirror "abstract" $abstract }
if ($mixed -eq "true") { $out += " mixed=`"true`"" } elseif ($null -ne $mixed) { $out += Mirror "mixed" $mixed }
# XSD требует объявлять атрибуты после частицы, поэтому исходный порядок свойств
# восстановим как «сначала form=Attribute, потом остальные» — это верно для 96.5%
# типов корпуса. Расхождения (768 типов) зеркалим списком имён.
$order = @(); $kinds = @()
foreach ($c in $el.ChildNodes) {
if ($c.NodeType -ne [System.Xml.XmlNodeType]::Element -or $c.get_LocalName() -ne "property") { continue }
$nm = A $c "name"
if ($null -eq $nm) { $nm = "@" + ((A $c "ref") -split ":")[-1] }
$order += $nm
$kinds += $(if ((Get-PropForm $c) -eq "Attribute") { 0 } else { 1 })
}
if ($order.Count -gt 1) {
$natural = $true
for ($i = 1; $i -lt $kinds.Count; $i++) { if ($kinds[$i] -lt $kinds[$i - 1]) { $natural = $false; break } }
if (-not $natural) { $out += Mirror "order" ($order -join "|") }
}
# open="true" is rendered as xs:any + xs:anyAttribute; anything else is mirrored
if ($null -ne $open -and $open -ne "true") { $out += Mirror "open" $open }
# ordered="false" is rendered as xs:choice; "true" written explicitly is mirrored
if ($null -ne $ordered -and $ordered -ne "false") { $out += Mirror "ordered" $ordered }
# sequenced has no XSD counterpart at all
if ($null -ne $sequenced) { $out += Mirror "sequenced" $sequenced }
return $out
}
# --- Metadata properties (Name/Synonym/Comment) from the object's .xml ---
function Get-MetadataBlock() {
if (-not $paths.Md -or -not (Test-Path $paths.Md)) { return $null }
$md = New-Object System.Xml.XmlDocument
$md.Load($paths.Md)
$nsm = New-Object System.Xml.XmlNamespaceManager($md.NameTable)
$nsm.AddNamespace("md", $MD_NS)
$nsm.AddNamespace("v8", $V8_NS)
$props = $md.SelectSingleNode("//md:XDTOPackage/md:Properties", $nsm)
if (-not $props) { return $null }
$res = @{ Name = $null; Comment = $null; Synonym = @() }
$n = $props.SelectSingleNode("md:Name", $nsm); if ($n) { $res.Name = $n.InnerText }
$c = $props.SelectSingleNode("md:Comment", $nsm); if ($c) { $res.Comment = $c.InnerText }
foreach ($item in $props.SelectNodes("md:Synonym/v8:item", $nsm)) {
$lang = $item.SelectSingleNode("v8:lang", $nsm)
$cont = $item.SelectSingleNode("v8:content", $nsm)
$res.Synonym += @{ Lang = $(if ($lang) { $lang.InnerText } else { "" }); Content = $(if ($cont) { $cont.InnerText } else { "" }) }
}
return $res
}
$meta = Get-MetadataBlock
# --- Emit ---
# Body first: emitting it registers every namespace actually referenced, so the
# schema element can declare a complete prefix map.
$bodyBuilder = New-Object System.Text.StringBuilder
$mainBuilder = $sb
$sb = $bodyBuilder
foreach ($node in $pkg.ChildNodes) {
if ($node.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
switch ($node.get_LocalName()) {
"import" {
X "`t<xs:import namespace=`"$(Esc $node.GetAttribute('namespace'))`"/>"
}
"property" {
Emit-Property $node "`t" $true
}
"valueType" {
$name = A $node "name"
X "`t<xs:simpleType$(Attrs @('name', $name))>"
Emit-SimpleTypeBody $node "`t`t"
X "`t</xs:simpleType>"
}
"objectType" {
$name = A $node "name"
$base = Convert-QName $node (A $node "base")
$cta = ComplexTypeAttrs $node
if ($base) {
X "`t<xs:complexType$(Attrs @('name', $name))$cta>"
X "`t`t<xs:complexContent>"
X "`t`t`t<xs:extension$(Attrs @('base', $base))>"
Emit-ComplexTypeBody $node "`t`t`t`t"
X "`t`t`t</xs:extension>"
X "`t`t</xs:complexContent>"
X "`t</xs:complexType>"
} else {
$hasBody = $false
foreach ($c in $node.ChildNodes) { if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element) { $hasBody = $true; break } }
if (-not $hasBody -and (A $node "open") -ne "true") {
X "`t<xs:complexType$(Attrs @('name', $name))$cta/>"
} else {
X "`t<xs:complexType$(Attrs @('name', $name))$cta>"
Emit-ComplexTypeBody $node "`t`t"
X "`t</xs:complexType>"
}
}
}
}
}
$sb = $mainBuilder
# --- Schema element ---
$nsDecls = ""
foreach ($kv in ($nsPrefix.GetEnumerator() | Sort-Object { $_.Value })) {
$nsDecls += " xmlns:$($kv.Value)=`"$(Esc $kv.Key)`""
}
if ($usesXdtoNs) { $nsDecls += " xmlns:xdto=`"$XDTO_NS`"" }
$schemaAttrs = ""
$efq = A $pkg "elementFormQualified"
$afq = A $pkg "attributeFormQualified"
if ($null -ne $efq) { $schemaAttrs += " elementFormDefault=`"$(if ($efq -eq 'true') { 'qualified' } else { 'unqualified' })`"" }
if ($null -ne $afq) { $schemaAttrs += " attributeFormDefault=`"$(if ($afq -eq 'true') { 'qualified' } else { 'unqualified' })`"" }
X "<xs:schema$nsDecls$(Attrs @('targetNamespace', $targetNs))$schemaAttrs>"
if ($meta) {
X "`t<xs:annotation>"
X "`t`t<xs:appinfo>"
X "`t`t`t<xdto:package xmlns:xdto=`"$XDTO_NS`">"
if ($null -ne $meta.Name) { X "`t`t`t`t<xdto:name>$(EscText $meta.Name)</xdto:name>" }
if ($null -ne $meta.Comment -and $meta.Comment -ne "") { X "`t`t`t`t<xdto:comment>$(EscText $meta.Comment)</xdto:comment>" }
foreach ($s in $meta.Synonym) {
X "`t`t`t`t<xdto:synonym lang=`"$(Esc $s.Lang)`">$(EscText $s.Content)</xdto:synonym>"
}
X "`t`t`t</xdto:package>"
X "`t`t</xs:appinfo>"
X "`t</xs:annotation>"
}
[void]$sb.Append($bodyBuilder.ToString())
X "</xs:schema>"
# --- Output (UTF-8 with BOM, CRLF — matches the Designer's own XSD export) ---
$text = $sb.ToString()
if ($OutFile) {
$dir = [System.IO.Path]::GetDirectoryName($OutFile)
if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($OutFile, $text, $enc)
Write-Host "✓ XSD записана: $OutFile"
Write-Host " targetNamespace: $targetNs"
} else {
[Console]::Out.Write($text)
}
@@ -0,0 +1,567 @@
# 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()
# ── 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 = etree.parse(bin_path)
pkg = doc.getroot()
def local(el):
return etree.QName(el).localname
if local(pkg) != "package":
print(f"Ожидался корневой <package>, получен <{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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
def esc_text(s):
if s is None:
return ""
return str(s).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
# ── 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}<xs:{f} value="{esc(v)}"/>')
for child in el:
if not isinstance(child.tag, str):
continue
ln = local(child)
if ln == "pattern":
X(f'{indent}<xs:pattern value="{esc(child.text or "")}"/>')
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}<xs:enumeration value="{esc(child.text or "")}"{m}/>')
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}<xs:list{attrs(["itemType", item_type])}{mv}/>')
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}<xs:union{attrs(["memberTypes", member_types])}{mv}/>')
else:
X(f'{indent}<xs:union{attrs(["memberTypes", member_types])}{mv}>')
for c in anon:
X(f"{indent}\t<xs:simpleType>")
emit_simple_type_body(c, indent + "\t\t")
X(f"{indent}\t</xs:simpleType>")
X(f"{indent}</xs:union>")
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}<xs:restriction{attrs(["base", base])}{mv}>')
if anon_base is not None:
X(f"{indent}\t<xs:simpleType>")
emit_simple_type_body(anon_base, indent + "\t\t")
X(f"{indent}\t</xs:simpleType>")
emit_facets(el, indent + "\t")
X(f"{indent}</xs:restriction>")
else:
X(f'{indent}<xs:restriction{attrs(["base", base])}{mv}/>')
# ── 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")
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
body = attrs(["name", xml_name, "ref", ref, "type", type_, "default", default, "fixed", fixed])
if anon is not None:
X(f"{indent}<xs:attribute{body}{m}>")
X(f"{indent}\t<xs:simpleType>")
emit_simple_type_body(anon, indent + "\t\t")
X(f"{indent}\t</xs:simpleType>")
X(f"{indent}</xs:attribute>")
else:
X(f"{indent}<xs:attribute{body}{m}/>")
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)
body = attrs(["name", xml_name, "ref", ref, "type", type_,
"minOccurs", min_occurs, "maxOccurs", max_occurs,
"nillable", nill, "default", default, "fixed", fixed])
if anon is not None:
X(f"{indent}<xs:element{body}{m}>")
if anon.get(f"{{{XSI_NS}}}type") == "ObjectType":
anon_base = convert_qname(anon, anon.get("base"))
if anon_base:
X(f"{indent}\t<xs:complexType{complex_type_attrs(anon)}>")
X(f"{indent}\t\t<xs:complexContent>")
X(f'{indent}\t\t\t<xs:extension{attrs(["base", anon_base])}>')
emit_complex_type_body(anon, indent + "\t\t\t\t")
X(f"{indent}\t\t\t</xs:extension>")
X(f"{indent}\t\t</xs:complexContent>")
X(f"{indent}\t</xs:complexType>")
else:
X(f"{indent}\t<xs:complexType{complex_type_attrs(anon)}>")
emit_complex_type_body(anon, indent + "\t\t")
X(f"{indent}\t</xs:complexType>")
else:
X(f"{indent}\t<xs:simpleType>")
emit_simple_type_body(anon, indent + "\t\t")
X(f"{indent}\t</xs:simpleType>")
X(f"{indent}</xs:element>")
else:
X(f"{indent}<xs:element{body}{m}/>")
# ── 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}<xs:simpleContent>")
X(f'{indent}\t<xs:extension{attrs(["base", t_type])}{tm}>')
for a in attr_props:
emit_property(a, indent + "\t\t")
X(f"{indent}\t</xs:extension>")
X(f"{indent}</xs:simpleContent>")
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<xs:any namespace="##any" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>')
X(f"{indent}</{particle_tag}>")
for a in attr_props:
emit_property(a, indent)
if open_ == "true":
X(f'{indent}<xs:anyAttribute namespace="##any" processContents="lax"/>')
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 = etree.parse(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<xs:import namespace="{esc(node.get("namespace"))}"/>')
elif ln == "property":
emit_property(node, "\t")
elif ln == "valueType":
X(f'\t<xs:simpleType{attrs(["name", node.get("name")])}>')
emit_simple_type_body(node, "\t\t")
X("\t</xs:simpleType>")
elif ln == "objectType":
name = node.get("name")
base = convert_qname(node, node.get("base"))
cta = complex_type_attrs(node)
if base:
X(f'\t<xs:complexType{attrs(["name", name])}{cta}>')
X("\t\t<xs:complexContent>")
X(f'\t\t\t<xs:extension{attrs(["base", base])}>')
emit_complex_type_body(node, "\t\t\t\t")
X("\t\t\t</xs:extension>")
X("\t\t</xs:complexContent>")
X("\t</xs:complexType>")
else:
has_body = any(isinstance(c.tag, str) for c in node)
if not has_body and node.get("open") != "true":
X(f'\t<xs:complexType{attrs(["name", name])}{cta}/>')
else:
X(f'\t<xs:complexType{attrs(["name", name])}{cta}>')
emit_complex_type_body(node, "\t\t")
X("\t</xs:complexType>")
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'<xs:schema{ns_decls}{attrs(["targetNamespace", target_ns])}{schema_attrs}>')
if meta:
X("\t<xs:annotation>")
X("\t\t<xs:appinfo>")
X(f'\t\t\t<xdto:package xmlns:xdto="{XDTO_NS}">')
if meta["Name"] is not None:
X(f'\t\t\t\t<xdto:name>{esc_text(meta["Name"])}</xdto:name>')
if meta["Comment"]:
X(f'\t\t\t\t<xdto:comment>{esc_text(meta["Comment"])}</xdto:comment>')
for s in meta["Synonym"]:
X(f'\t\t\t\t<xdto:synonym lang="{esc(s["Lang"])}">{esc_text(s["Content"])}</xdto:synonym>')
X("\t\t\t</xdto:package>")
X("\t\t</xs:appinfo>")
X("\t</xs:annotation>")
lines.extend(body_lines)
X("</xs:schema>")
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)
+71
View File
@@ -0,0 +1,71 @@
---
name: xdto-validate
description: Валидация пакета XDTO 1С. Используй после создания или модификации пакета XDTO для проверки корректности
argument-hint: <PackagePath> [-ConfigDir <каталог>] [-Detailed] [-MaxErrors N] [-OutFile <файл>]
allowed-tools:
- Bash
- Read
- Glob
---
# /xdto-validate — Валидация пакета XDTO
Проверяет модель пакета, объект метаданных и его связь с конфигурацией.
Exit code `1` при наличии ошибок.
## Параметры
| Параметр | Описание |
|----------|----------|
| `PackagePath` | Каталог пакета, `Ext/Package.bin` или `<Имя>.xml` объекта метаданных |
| `ConfigDir` | Каталог конфигурации. По умолчанию определяется по расположению пакета |
| `Detailed` | Показывать успешные проверки, а не только ошибки |
| `MaxErrors` | Остановиться после N ошибок (по умолчанию 20) |
| `OutFile` | Записать отчёт в файл |
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/xdto-validate.ps1" -PackagePath "<путь>"
```
## Проверки
**Модель**
- Package.bin — корректный XML с корнем `<package>`, задан `targetNamespace`, кодировка UTF-8 с BOM
- Имена типов уникальны внутри пакета
- Каждая ссылка (`type`, `base`, `ref`, `itemType`, `memberTypes`) разрешается: собственный
тип, импортированное пространство имён или `xs:`. Поддержана нотация Кларка `{ns}local`
- Префиксы объявлены (нет ссылок на необъявленный `dNpM`)
- Фасеты согласованы: `length` не вместе с `minLength`/`maxLength`, `minLength ≤ maxLength`,
`fractionDigits ≤ totalDigits`, допустимые `whiteSpace` и `variety`
- Свойства: допустимая `form`, `lowerBound ≤ upperBound`, есть `name` либо `ref`
**Связь с конфигурацией**
- Объект метаданных существует, `Name` совпадает с именем каталога, `Namespace`с `targetNamespace`
- Пакет зарегистрирован в `ChildObjects` файла `Configuration.xml`
- `targetNamespace` уникален среди пакетов конфигурации
**Предупреждения о тихих дефектах**
Два класса проблем платформа не диагностирует — их ловит навык:
- **`xs:anyType` при объявленных импортах.** При импорте XML-схемы тип из чужого
пространства имён, для которого в конфигурации нет пакета, заменяется на `xs:anyType`
молча — ни ошибки, ни предупреждения. Пакет выглядит загруженным, а `ФабрикаXDTO`
потом отдаёт бесструктурное значение. Сопутствующий признак — объявленный, но
неиспользуемый `<import>`.
- **`nillable="true"` вместе с `form="Attribute"`.** Спецификация XSD не допускает
`nillable` у атрибутов, поэтому экспорт XML-схемы в Конфигураторе такое свойство теряет.
На саму работу пакета не влияет, но ломает перенос схемы через Конфигуратор.
## Типичный workflow
1. `/xdto-compile` или `/xdto-decompile` → правка → `/xdto-compile -Force`
2. `/xdto-validate <путь>` — до загрузки в базу
3. `/db-load-xml` + `/db-update`
## Верификация
```
/xdto-validate <путь> — только проблемы
/xdto-validate <путь> -Detailed — все проверки
```
@@ -0,0 +1,402 @@
# xdto-validate v1.0 — Validate a 1C XDTO package
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$PackagePath,
[string]$ConfigDir,
[switch]$Detailed,
[int]$MaxErrors = 20,
[string]$OutFile
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$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"
# --- Reporting ---
$script:errors = 0
$script:warnings = 0
$script:okCount = 0
$script:stopped = $false
$script:output = New-Object System.Text.StringBuilder
function Out-Line([string]$s) { [void]$script:output.AppendLine($s) }
function Report-OK([string]$msg) {
$script:okCount++
if ($Detailed) { Out-Line "[OK] $msg" }
}
function Report-Error([string]$msg) {
$script:errors++
Out-Line "[ERROR] $msg"
if ($script:errors -ge $MaxErrors) { $script:stopped = $true }
}
function Report-Warn([string]$msg) {
$script:warnings++
Out-Line "[WARN] $msg"
}
# --- Resolve paths ---
if (-not [System.IO.Path]::IsPathRooted($PackagePath)) {
$PackagePath = Join-Path (Get-Location).Path $PackagePath
}
$binPath = $null
$mdPath = $null
if (Test-Path $PackagePath -PathType Leaf) {
if ([System.IO.Path]::GetFileName($PackagePath) -eq "Package.bin") {
$binPath = $PackagePath
$pkgDir = [System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($PackagePath))
if (Test-Path "$pkgDir.xml") { $mdPath = "$pkgDir.xml" }
} elseif ($PackagePath.EndsWith(".xml")) {
$mdPath = $PackagePath
$stem = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($PackagePath), [System.IO.Path]::GetFileNameWithoutExtension($PackagePath))
$c = Join-Path (Join-Path $stem "Ext") "Package.bin"
if (Test-Path $c) { $binPath = $c }
}
} elseif (Test-Path $PackagePath -PathType Container) {
$c = Join-Path (Join-Path $PackagePath "Ext") "Package.bin"
if (Test-Path $c) {
$binPath = $c
$m = "$($PackagePath.TrimEnd('\','/')).xml"
if (Test-Path $m) { $mdPath = $m }
}
}
$fileName = if ($binPath) { [System.IO.Path]::GetFileName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($binPath))) } else { $PackagePath }
if (-not $binPath) {
Write-Host "[ERROR] Не найден Ext/Package.bin для пути: $PackagePath"
exit 1
}
# Каталог конфигурации — для проверок регистрации и зависимостей
if (-not $ConfigDir) {
$d = [System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($binPath)))
# .../XDTOPackages/<Имя>/Ext/Package.bin -> .../XDTOPackages -> корень
$ConfigDir = [System.IO.Path]::GetDirectoryName($d)
}
$finalize = {
$checks = $script:okCount + $script:errors + $script:warnings
if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) {
$result = "=== Validation OK: $fileName ($checks checks) ==="
} else {
Out-Line ""
Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ==="
$result = $script:output.ToString()
}
Write-Host $result
if ($OutFile) {
$utf8Bom = New-Object System.Text.UTF8Encoding $true
[System.IO.File]::WriteAllText($OutFile, $script:output.ToString(), $utf8Bom)
}
}
# --- 1. Well-formedness ---
$doc = New-Object System.Xml.XmlDocument
try {
$doc.Load($binPath)
} catch {
Report-Error "Package.bin не является корректным XML: $($_.Exception.Message)"
& $finalize
exit 1
}
$pkg = $doc.DocumentElement
if ($pkg.get_LocalName() -ne "package") {
Report-Error "Ожидался корневой <package>, найден <$($pkg.get_LocalName())>"
& $finalize
exit 1
}
Report-OK "Package.bin: корректный XML, корень <package>"
$targetNs = $pkg.GetAttribute("targetNamespace")
if (-not $targetNs) {
Report-Error "У <package> не задан targetNamespace"
} else {
Report-OK "targetNamespace: $targetNs"
}
# --- 2. Encoding / EOL ---
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if (-not ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF)) {
Report-Warn "Package.bin без BOM UTF-8 — платформа пишет файл с BOM"
} else {
Report-OK "Кодировка: UTF-8 с BOM"
}
# --- 3. Collect declarations ---
$imports = New-Object System.Collections.ArrayList
$localTypes = New-Object System.Collections.Generic.HashSet[string]
$globalProps = New-Object System.Collections.Generic.HashSet[string]
foreach ($n in $pkg.ChildNodes) {
if ($n.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
switch ($n.get_LocalName()) {
"import" { [void]$imports.Add($n.GetAttribute("namespace")) }
"objectType" { [void]$localTypes.Add($n.GetAttribute("name")) }
"valueType" { [void]$localTypes.Add($n.GetAttribute("name")) }
"property" { if ($n.HasAttribute("name")) { [void]$globalProps.Add($n.GetAttribute("name")) } }
}
}
# --- 4. Duplicate type names ---
$seen = @{}
foreach ($n in $pkg.ChildNodes) {
if ($n.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
if ($n.get_LocalName() -ne "objectType" -and $n.get_LocalName() -ne "valueType") { continue }
$nm = $n.GetAttribute("name")
if (-not $nm) { Report-Error "<$($n.get_LocalName())> без атрибута name"; continue }
if ($seen.ContainsKey($nm)) {
Report-Error "Дублирующееся имя типа: $nm"
} else {
$seen[$nm] = $true
}
}
if ($localTypes.Count -gt 0) { Report-OK "$($localTypes.Count) тип(ов), имена уникальны" }
# --- 5. Type references resolve ---
$usedNamespaces = New-Object System.Collections.Generic.HashSet[string]
$anyTypeProps = New-Object System.Collections.ArrayList
$refAttrs = @("type", "base", "ref", "itemType")
function Resolve-Ref([System.Xml.XmlElement]$el, [string]$attr, [string]$raw) {
if (-not $raw) { return }
# Нотация Кларка {ns}local
if ($raw.StartsWith("{")) {
$close = $raw.IndexOf("}")
if ($close -lt 0) { Report-Error "Некорректная нотация Кларка в $attr=`"$raw`""; return }
$ns = $raw.Substring(1, $close - 1)
$local = $raw.Substring($close + 1)
} else {
$parts = $raw.Split(":")
if ($parts.Count -eq 2) {
$ns = $el.GetNamespaceOfPrefix($parts[0])
$local = $parts[1]
if (-not $ns) {
Report-Error "Префикс `"$($parts[0])`" не объявлен: $attr=`"$raw`" (тип $($el.get_LocalName()))"
return
}
} else {
$ns = $null
$local = $parts[0]
}
}
if ($ns -eq $XS_NS -or $ns -eq $XSI_NS) {
if ($local -eq "anyType") { [void]$anyTypeProps.Add($el) }
return
}
if ($ns -eq $targetNs) {
if ($attr -eq "ref") {
if (-not $globalProps.Contains($local)) {
Report-Error "ref=`"$raw`" не разрешается: в пакете нет глобального свойства `"$local`""
}
} elseif (-not $localTypes.Contains($local)) {
Report-Error "$attr=`"$raw`" не разрешается: в пакете нет типа `"$local`""
}
return
}
if ($ns) {
[void]$usedNamespaces.Add($ns)
if (-not $imports.Contains($ns)) {
Report-Error "$attr=`"$raw`" ссылается на `"$ns`", но <import namespace=`"$ns`"/> не объявлен"
}
}
}
$nodeCount = 0
foreach ($el in $pkg.SelectNodes("//*")) {
if ($el.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
$nodeCount++
foreach ($a in $refAttrs) {
if ($el.HasAttribute($a)) { Resolve-Ref $el $a $el.GetAttribute($a) }
}
if ($el.HasAttribute("memberTypes")) {
foreach ($m in ($el.GetAttribute("memberTypes") -split "\s+")) {
if ($m) { Resolve-Ref $el "memberTypes" $m }
}
}
if ($script:stopped) { break }
}
if (-not $script:stopped) { Report-OK "$nodeCount узлов: ссылки на типы разрешаются" }
if ($script:stopped) { & $finalize; exit 1 }
# --- 6. Silent degradation to xs:anyType ---
# Платформа при импорте XML-схемы молча подменяет неразрешённый чужой тип на anyType.
if ($anyTypeProps.Count -gt 0 -and $imports.Count -gt 0) {
$names = @()
foreach ($p in $anyTypeProps) { if ($p.HasAttribute("name")) { $names += $p.GetAttribute("name") } }
$shown = ($names | Select-Object -First 5) -join ", "
Report-Warn "Свойств с type=`"xs:anyType`": $($anyTypeProps.Count) при объявленных импортах ($shown). Возможна тихая деградация: при импорте XML-схемы платформа заменяет неразрешённый чужой тип на anyType без ошибки"
}
# --- 7. Unused imports ---
foreach ($imp in $imports) {
if (-not $usedNamespaces.Contains($imp)) {
Report-Warn "<import namespace=`"$imp`"/> объявлен, но ни один тип из этого пространства имён не используется"
}
}
if ($imports.Count -gt 0 -and $script:warnings -eq 0) { Report-OK "$($imports.Count) импорт(ов) — все используются" }
# --- 8. nillable on attribute-form properties ---
$nillAttrs = @()
foreach ($p in $pkg.SelectNodes("//*[local-name()='property']")) {
if ($p.GetAttribute("form") -eq "Attribute" -and $p.GetAttribute("nillable") -eq "true") {
$nillAttrs += $p.GetAttribute("name")
}
}
if ($nillAttrs.Count -gt 0) {
Report-Warn "Свойств с nillable=`"true`" и form=`"Attribute`": $($nillAttrs.Count) ($(($nillAttrs | Select-Object -First 5) -join ', ')). Спецификация XSD не допускает nillable у атрибутов — экспорт XML-схемы в Конфигураторе их потеряет"
}
# --- 9. Facet consistency ---
$FACET_NUM = @("totalDigits", "fractionDigits")
$facetChecked = 0
foreach ($t in $pkg.SelectNodes("//*[local-name()='valueType' or local-name()='typeDef']")) {
if ($t.get_LocalName() -eq "typeDef" -and $t.GetAttribute("type", $XSI_NS) -eq "ObjectType") { continue }
$facetChecked++
$nm = if ($t.HasAttribute("name")) { $t.GetAttribute("name") } else { "(анонимный тип)" }
$len = $t.GetAttribute("length")
if ($len -and ($t.HasAttribute("minLength") -or $t.HasAttribute("maxLength"))) {
Report-Error "$nm : length несовместим с minLength/maxLength"
}
$minL = $t.GetAttribute("minLength"); $maxL = $t.GetAttribute("maxLength")
if ($minL -and $maxL -and ([int]$minL -gt [int]$maxL)) {
Report-Error "$nm : minLength ($minL) больше maxLength ($maxL)"
}
$td = $t.GetAttribute("totalDigits"); $fd = $t.GetAttribute("fractionDigits")
if ($td -and $fd -and ([int]$fd -gt [int]$td)) {
Report-Error "$nm : fractionDigits ($fd) больше totalDigits ($td)"
}
$ws = $t.GetAttribute("whiteSpace")
if ($ws -and @("preserve", "replace", "collapse") -notcontains $ws) {
Report-Error "$nm : недопустимое whiteSpace=`"$ws`""
}
$var = $t.GetAttribute("variety")
if ($var -and @("Atomic", "List", "Union") -notcontains $var) {
Report-Error "$nm : недопустимое variety=`"$var`""
}
if ($var -eq "List" -and -not $t.HasAttribute("itemType")) {
Report-Warn "$nm : variety=`"List`" без itemType"
}
if ($script:stopped) { break }
}
if ($facetChecked -gt 0 -and -not $script:stopped) { Report-OK "$facetChecked простых тип(ов): фасеты согласованы" }
if ($script:stopped) { & $finalize; exit 1 }
# --- 10. property form / bounds ---
foreach ($p in $pkg.SelectNodes("//*[local-name()='property']")) {
$form = $p.GetAttribute("form")
if ($form -and @("Element", "Attribute", "Text") -notcontains $form) {
Report-Error "Свойство `"$($p.GetAttribute('name'))`": недопустимое form=`"$form`""
}
$ub = $p.GetAttribute("upperBound")
if ($ub -and $ub -ne "-1" -and ([int]$ub -lt 1)) {
Report-Error "Свойство `"$($p.GetAttribute('name'))`": upperBound=`"$ub`" (допустимы -1 или число ≥ 1)"
}
$lb = $p.GetAttribute("lowerBound")
if ($lb -and $ub -and $ub -ne "-1" -and ([int]$lb -gt [int]$ub)) {
Report-Error "Свойство `"$($p.GetAttribute('name'))`": lowerBound ($lb) больше upperBound ($ub)"
}
if (-not $p.HasAttribute("name") -and -not $p.HasAttribute("ref")) {
Report-Error "Свойство без name и без ref"
}
if ($script:stopped) { break }
}
if (-not $script:stopped) { Report-OK "Свойства: form и кратности корректны" }
# --- 11. Metadata object ---
if ($mdPath) {
$md = New-Object System.Xml.XmlDocument
$md.Load($mdPath)
$nsm = New-Object System.Xml.XmlNamespaceManager($md.NameTable)
$nsm.AddNamespace("md", $MD_NS)
$mdName = $md.SelectSingleNode("//md:XDTOPackage/md:Properties/md:Name", $nsm)
$mdNs = $md.SelectSingleNode("//md:XDTOPackage/md:Properties/md:Namespace", $nsm)
if (-not $mdName) {
Report-Error "В объекте метаданных не задано <Name>"
} elseif ($mdName.InnerText -ne $fileName) {
Report-Error "<Name>$($mdName.InnerText)</Name> не совпадает с именем каталога `"$fileName`""
} else {
Report-OK "Объект метаданных: Name = $($mdName.InnerText)"
}
if ($mdNs -and $mdNs.InnerText -ne $targetNs) {
Report-Error "<Namespace>$($mdNs.InnerText)</Namespace> не совпадает с targetNamespace пакета ($targetNs)"
} elseif ($mdNs) {
Report-OK "Namespace объекта метаданных совпадает с targetNamespace"
}
} else {
Report-Warn "Файл объекта метаданных <Имя>.xml не найден рядом с каталогом пакета"
}
# --- 12. Registration in Configuration.xml + namespace uniqueness ---
$configXml = Join-Path $ConfigDir "Configuration.xml"
if (Test-Path $configXml) {
$cfg = New-Object System.Xml.XmlDocument
$cfg.Load($configXml)
$nsm2 = New-Object System.Xml.XmlNamespaceManager($cfg.NameTable)
$nsm2.AddNamespace("md", $MD_NS)
$registered = $false
foreach ($e in $cfg.SelectNodes("//md:Configuration/md:ChildObjects/md:XDTOPackage", $nsm2)) {
if ($e.InnerText -eq $fileName) { $registered = $true; break }
}
if ($registered) {
Report-OK "Зарегистрирован в Configuration.xml"
} else {
Report-Error "<XDTOPackage>$fileName</XDTOPackage> отсутствует в ChildObjects файла Configuration.xml — платформа пакет не увидит"
}
# Уникальность targetNamespace среди пакетов конфигурации
$pkgRoot = Join-Path $ConfigDir "XDTOPackages"
if (Test-Path $pkgRoot) {
$clash = @()
foreach ($other in (Get-ChildItem $pkgRoot -Directory -ErrorAction SilentlyContinue)) {
if ($other.Name -eq $fileName) { continue }
$ob = Join-Path (Join-Path $other.FullName "Ext") "Package.bin"
if (-not (Test-Path $ob)) { continue }
try {
$od = New-Object System.Xml.XmlDocument
$od.Load($ob)
if ($od.DocumentElement.GetAttribute("targetNamespace") -eq $targetNs) { $clash += $other.Name }
} catch {}
}
if ($clash.Count -gt 0) {
Report-Error "targetNamespace `"$targetNs`" уже занят пакет(ами): $($clash -join ', '). Платформа не допускает два пакета с одним пространством имён"
} else {
Report-OK "targetNamespace уникален в конфигурации"
}
}
} else {
Report-Warn "Configuration.xml не найден ($ConfigDir) — проверки регистрации и уникальности namespace пропущены"
}
# --- Final ---
& $finalize
if ($script:errors -gt 0) { exit 1 }
exit 0
@@ -0,0 +1,386 @@
# xdto-validate v1.0 — Validate a 1C XDTO package (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
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"
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-PackagePath", "-Path", required=True)
parser.add_argument("-ConfigDir", default="")
parser.add_argument("-Detailed", action="store_true")
parser.add_argument("-MaxErrors", type=int, default=20)
parser.add_argument("-OutFile", default="")
args = parser.parse_args()
package_path = os.path.abspath(args.PackagePath)
detailed = args.Detailed
max_errors = args.MaxErrors
out_file = args.OutFile
# ── reporting ────────────────────────────────────────────────
state = {"errors": 0, "warnings": 0, "ok": 0, "stopped": False}
output = []
def out_line(s):
output.append(s)
def report_ok(msg):
state["ok"] += 1
if detailed:
out_line(f"[OK] {msg}")
def report_error(msg):
state["errors"] += 1
out_line(f"[ERROR] {msg}")
if state["errors"] >= max_errors:
state["stopped"] = True
def report_warn(msg):
state["warnings"] += 1
out_line(f"[WARN] {msg}")
# ── resolve paths ────────────────────────────────────────────
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"[ERROR] Не найден Ext/Package.bin для пути: {package_path}")
sys.exit(1)
file_name = os.path.basename(os.path.dirname(os.path.dirname(bin_path)))
config_dir = args.ConfigDir
if not config_dir:
# .../XDTOPackages/<Имя>/Ext/Package.bin -> .../XDTOPackages -> корень
config_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(bin_path))))
def finalize():
checks = state["ok"] + state["errors"] + state["warnings"]
if state["errors"] == 0 and state["warnings"] == 0 and not detailed:
result = f"=== Validation OK: {file_name} ({checks} checks) ==="
else:
out_line("")
out_line(f"=== Result: {state['errors']} errors, {state['warnings']} warnings ({checks} checks) ===")
result = "\n".join(output)
print(result)
if out_file:
with open(out_file, "w", encoding="utf-8-sig", newline="") as f:
f.write("\n".join(output))
def local(el):
return etree.QName(el).localname
# ── 1. well-formedness ───────────────────────────────────────
try:
doc = etree.parse(bin_path)
except Exception as e: # noqa: BLE001
report_error(f"Package.bin не является корректным XML: {e}")
finalize()
sys.exit(1)
pkg = doc.getroot()
if local(pkg) != "package":
report_error(f"Ожидался корневой <package>, найден <{local(pkg)}>")
finalize()
sys.exit(1)
report_ok("Package.bin: корректный XML, корень <package>")
target_ns = pkg.get("targetNamespace")
if not target_ns:
report_error("У <package> не задан targetNamespace")
else:
report_ok(f"targetNamespace: {target_ns}")
# ── 2. encoding ──────────────────────────────────────────────
with open(bin_path, "rb") as f:
head = f.read(3)
if head != b"\xef\xbb\xbf":
report_warn("Package.bin без BOM UTF-8 — платформа пишет файл с BOM")
else:
report_ok("Кодировка: UTF-8 с BOM")
# ── 3. declarations ──────────────────────────────────────────
imports = []
local_types = set()
global_props = set()
for n in pkg:
if not isinstance(n.tag, str):
continue
ln = local(n)
if ln == "import":
imports.append(n.get("namespace"))
elif ln in ("objectType", "valueType"):
local_types.add(n.get("name"))
elif ln == "property" and n.get("name"):
global_props.add(n.get("name"))
# ── 4. duplicate type names ──────────────────────────────────
seen = set()
for n in pkg:
if not isinstance(n.tag, str) or local(n) not in ("objectType", "valueType"):
continue
nm = n.get("name")
if not nm:
report_error(f"<{local(n)}> без атрибута name")
continue
if nm in seen:
report_error(f"Дублирующееся имя типа: {nm}")
else:
seen.add(nm)
if local_types:
report_ok(f"{len(local_types)} тип(ов), имена уникальны")
# ── 5. type references resolve ───────────────────────────────
used_namespaces = set()
any_type_props = []
REF_ATTRS = ("type", "base", "ref", "itemType")
def resolve_ref(el, attr, raw):
if not raw:
return
if raw.startswith("{"):
close = raw.find("}")
if close < 0:
report_error(f'Некорректная нотация Кларка в {attr}="{raw}"')
return
ns = raw[1:close]
loc = raw[close + 1:]
else:
parts = raw.split(":")
if len(parts) == 2:
ns = el.nsmap.get(parts[0])
loc = parts[1]
if not ns:
report_error(f'Префикс "{parts[0]}" не объявлен: {attr}="{raw}" (тип {local(el)})')
return
else:
ns = None
loc = parts[0]
if ns in (XS_NS, XSI_NS):
if loc == "anyType":
any_type_props.append(el)
return
if ns == target_ns:
if attr == "ref":
if loc not in global_props:
report_error(f'ref="{raw}" не разрешается: в пакете нет глобального свойства "{loc}"')
elif loc not in local_types:
report_error(f'{attr}="{raw}" не разрешается: в пакете нет типа "{loc}"')
return
if ns:
used_namespaces.add(ns)
if ns not in imports:
report_error(f'{attr}="{raw}" ссылается на "{ns}", но <import namespace="{ns}"/> не объявлен')
node_count = 0
for el in pkg.iter():
if not isinstance(el.tag, str):
continue
node_count += 1
for a in REF_ATTRS:
if el.get(a) is not None:
resolve_ref(el, a, el.get(a))
if el.get("memberTypes"):
for m in el.get("memberTypes").split():
resolve_ref(el, "memberTypes", m)
if state["stopped"]:
break
if not state["stopped"]:
report_ok(f"{node_count} узлов: ссылки на типы разрешаются")
if state["stopped"]:
finalize()
sys.exit(1)
# ── 6. silent degradation to xs:anyType ──────────────────────
if any_type_props and imports:
names = [p.get("name") for p in any_type_props if p.get("name")]
shown = ", ".join(names[:5])
report_warn(
f'Свойств с type="xs:anyType": {len(any_type_props)} при объявленных импортах ({shown}). '
"Возможна тихая деградация: при импорте XML-схемы платформа заменяет неразрешённый "
"чужой тип на anyType без ошибки"
)
# ── 7. unused imports ────────────────────────────────────────
for imp in imports:
if imp not in used_namespaces:
report_warn(f'<import namespace="{imp}"/> объявлен, но ни один тип из этого пространства имён не используется')
if imports and state["warnings"] == 0:
report_ok(f"{len(imports)} импорт(ов) — все используются")
# ── 8. nillable on attribute-form properties ─────────────────
nill_attrs = [p.get("name") for p in pkg.iter()
if isinstance(p.tag, str) and local(p) == "property"
and p.get("form") == "Attribute" and p.get("nillable") == "true"]
if nill_attrs:
report_warn(
f'Свойств с nillable="true" и form="Attribute": {len(nill_attrs)} ({", ".join(nill_attrs[:5])}). '
"Спецификация XSD не допускает nillable у атрибутов — экспорт XML-схемы в Конфигураторе их потеряет"
)
# ── 9. facet consistency ─────────────────────────────────────
facet_checked = 0
for t in pkg.iter():
if not isinstance(t.tag, str) or local(t) not in ("valueType", "typeDef"):
continue
if local(t) == "typeDef" and t.get(f"{{{XSI_NS}}}type") == "ObjectType":
continue
facet_checked += 1
nm = t.get("name") or "(анонимный тип)"
if t.get("length") and (t.get("minLength") or t.get("maxLength")):
report_error(f"{nm} : length несовместим с minLength/maxLength")
min_l, max_l = t.get("minLength"), t.get("maxLength")
if min_l and max_l and int(min_l) > int(max_l):
report_error(f"{nm} : minLength ({min_l}) больше maxLength ({max_l})")
td, fd = t.get("totalDigits"), t.get("fractionDigits")
if td and fd and int(fd) > int(td):
report_error(f"{nm} : fractionDigits ({fd}) больше totalDigits ({td})")
ws = t.get("whiteSpace")
if ws and ws not in ("preserve", "replace", "collapse"):
report_error(f'{nm} : недопустимое whiteSpace="{ws}"')
var = t.get("variety")
if var and var not in ("Atomic", "List", "Union"):
report_error(f'{nm} : недопустимое variety="{var}"')
if var == "List" and t.get("itemType") is None:
report_warn(f'{nm} : variety="List" без itemType')
if state["stopped"]:
break
if facet_checked and not state["stopped"]:
report_ok(f"{facet_checked} простых тип(ов): фасеты согласованы")
if state["stopped"]:
finalize()
sys.exit(1)
# ── 10. property form / bounds ───────────────────────────────
for p in pkg.iter():
if not isinstance(p.tag, str) or local(p) != "property":
continue
form = p.get("form")
if form and form not in ("Element", "Attribute", "Text"):
report_error(f'Свойство "{p.get("name")}": недопустимое form="{form}"')
ub = p.get("upperBound")
if ub and ub != "-1" and int(ub) < 1:
report_error(f'Свойство "{p.get("name")}": upperBound="{ub}" (допустимы -1 или число ≥ 1)')
lb = p.get("lowerBound")
if lb and ub and ub != "-1" and int(lb) > int(ub):
report_error(f'Свойство "{p.get("name")}": lowerBound ({lb}) больше upperBound ({ub})')
if p.get("name") is None and p.get("ref") is None:
report_error("Свойство без name и без ref")
if state["stopped"]:
break
if not state["stopped"]:
report_ok("Свойства: form и кратности корректны")
# ── 11. metadata object ──────────────────────────────────────
if md_path:
md = etree.parse(md_path)
md_name = md.find(f".//{{{MD_NS}}}XDTOPackage/{{{MD_NS}}}Properties/{{{MD_NS}}}Name")
md_ns = md.find(f".//{{{MD_NS}}}XDTOPackage/{{{MD_NS}}}Properties/{{{MD_NS}}}Namespace")
if md_name is None:
report_error("В объекте метаданных не задано <Name>")
elif (md_name.text or "") != file_name:
report_error(f'<Name>{md_name.text}</Name> не совпадает с именем каталога "{file_name}"')
else:
report_ok(f"Объект метаданных: Name = {md_name.text}")
if md_ns is not None and (md_ns.text or "") != target_ns:
report_error(f"<Namespace>{md_ns.text}</Namespace> не совпадает с targetNamespace пакета ({target_ns})")
elif md_ns is not None:
report_ok("Namespace объекта метаданных совпадает с targetNamespace")
else:
report_warn("Файл объекта метаданных <Имя>.xml не найден рядом с каталогом пакета")
# ── 12. registration + namespace uniqueness ──────────────────
config_xml = os.path.join(config_dir, "Configuration.xml")
if os.path.exists(config_xml):
cfg = etree.parse(config_xml)
registered = any((e.text or "") == file_name
for e in cfg.iterfind(f".//{{{MD_NS}}}Configuration/{{{MD_NS}}}ChildObjects/{{{MD_NS}}}XDTOPackage"))
if registered:
report_ok("Зарегистрирован в Configuration.xml")
else:
report_error(f"<XDTOPackage>{file_name}</XDTOPackage> отсутствует в ChildObjects файла "
"Configuration.xml — платформа пакет не увидит")
pkg_root = os.path.join(config_dir, "XDTOPackages")
if os.path.isdir(pkg_root):
clash = []
for other in sorted(os.listdir(pkg_root)):
if other == file_name:
continue
ob = os.path.join(pkg_root, other, "Ext", "Package.bin")
if not os.path.exists(ob):
continue
try:
if etree.parse(ob).getroot().get("targetNamespace") == target_ns:
clash.append(other)
except Exception: # noqa: BLE001
pass
if clash:
report_error(f'targetNamespace "{target_ns}" уже занят пакет(ами): {", ".join(clash)}. '
"Платформа не допускает два пакета с одним пространством имён")
else:
report_ok("targetNamespace уникален в конфигурации")
else:
report_warn(f"Configuration.xml не найден ({config_dir}) — проверки регистрации и уникальности namespace пропущены")
finalize()
sys.exit(1 if state["errors"] > 0 else 0)