feat(form-validate): проверки необъявленного префикса и версии формата

Обе проверки — про ошибки, которые делает не платформа, а тот, кто пишет
XML руками. Обе вскрыты на наших же фикстурах, обе платформенно-фатальны:
файл не читается вовсе, а прежний валидатор говорил OK.

Check 13 — префикс в значении типа обязан резолвиться. `cfg:CatalogRef.X`
в <v8:Type> при незадекларированном xmlns:cfg даёт «Исключение XDTO при
чтении файла». Область видимости считается по узлу, а не по корню:
локальная xmlns на элементе законна и в типовых встречается (d4p1, mxl).

Check 14 — версия формата формы против версии конфигурации. В пределах
одной выгрузки версия едина; форма из более новой выгрузки даёт
«Неизвестная версия формата N загружаемого файла».

Ложных срабатываний нет: корпус УТ/БП/ERP, 21 097 форм — те же 8 форм с
ошибками, что и до правки. Регресс 15/15 на обоих рантаймах, паритет
портов сверен построчно.
This commit is contained in:
Nick Shirokov
2026-08-12 18:59:55 +03:00
parent 1b40dc5b03
commit 7dc2e6e443
12 changed files with 582 additions and 2 deletions
@@ -1,4 +1,4 @@
# form-validate v1.13 — Validate 1C managed form
# form-validate v1.14 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -56,6 +56,7 @@ try {
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
$nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
$root = $xmlDoc.DocumentElement
@@ -68,6 +69,7 @@ for ($i = 0; $i -lt 15; $i++) {
if (-not $walkDir -or $walkDir -eq (Split-Path $walkDir)) { break }
if (Test-Path (Join-Path $walkDir "Configuration.xml")) {
$script:isConfigContext = $true
$script:configXmlPath = Join-Path $walkDir "Configuration.xml"
break
}
$walkDir = Split-Path $walkDir
@@ -879,6 +881,75 @@ if (-not $stopped) {
}
}
# --- Check 13: префиксы в значениях объявлены в самом файле ---
# `cfg:DataProcessorObject.X` в <v8:Type> при незадекларированном xmlns:cfg — валидный XML, который
# платформа не читает вовсе: «Исключение XDTO произошло при чтении файла». Ошибка типична для
# рукописного XML: префикс скопирован из чужой формы, а объявление в корне забыто. Область видимости
# считаем по узлу (GetNamespaceOfPrefix), а не по корню: локальная xmlns на элементе законна.
if (-not $stopped) {
$prefixErrors = 0
$prefixChecked = 0
$prefixPattern = '^([A-Za-z_][A-Za-z0-9_.-]*):.+$'
# Значения, где префикс обязан резолвиться: тип реквизита/колонки и xsi:type
# Только листовые узлы: под local-name()='Type' подходит и обёртка <Type>, и вложенный <v8:Type>,
# а InnerText обёртки — то же значение, иначе одна ошибка сообщалась бы дважды.
foreach ($node in $xmlDoc.SelectNodes("//*[local-name()='Type' or local-name()='TypeSet']", $nsMgr)) {
if ($node.SelectSingleNode("*")) { continue }
$val = $node.InnerText.Trim()
if (-not $val) { continue }
$m = [regex]::Match($val, $prefixPattern)
if (-not $m.Success) { continue }
$prefixChecked++
$pfx = $m.Groups[1].Value
if (-not $node.GetNamespaceOfPrefix($pfx)) {
Report-Error "13. Type '$val': namespace prefix '${pfx}:' is not declared — the platform cannot read the file (XDTO)"
$prefixErrors++
}
}
foreach ($node in $xmlDoc.SelectNodes("//*[@xsi:type]", $nsMgr)) {
$val = $node.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
$m = [regex]::Match($val, $prefixPattern)
if (-not $m.Success) { continue }
$prefixChecked++
$pfx = $m.Groups[1].Value
if (-not $node.GetNamespaceOfPrefix($pfx)) {
Report-Error "13. xsi:type='$val': namespace prefix '${pfx}:' is not declared — the platform cannot read the file (XDTO)"
$prefixErrors++
}
}
if ($prefixChecked -eq 0) {
Report-OK "13. Namespace prefixes: nothing to check"
} elseif ($prefixErrors -eq 0) {
Report-OK "13. Namespace prefixes: $prefixChecked values, all declared"
}
}
# --- Check 14: версия формата формы совпадает с версией конфигурации ---
# Версию задаёт платформа, которой выгружали, и в пределах одной выгрузки она едина. Форма из
# другой версии — «Неизвестная версия формата N загружаемого файла»: платформа не читает файл,
# который новее её самой. Типичный след ручной сборки: форму скопировали из свежей конфигурации.
if (-not $stopped -and $script:configXmlPath) {
$formVer = $root.GetAttribute("version")
$cfgVer = ""
try {
$cfgHead = [System.IO.File]::ReadAllText($script:configXmlPath, [System.Text.Encoding]::UTF8)
$vm = [regex]::Match($cfgHead.Substring(0, [Math]::Min(4000, $cfgHead.Length)), '<MetaDataObject[^>]*\bversion="([^"]+)"')
if ($vm.Success) { $cfgVer = $vm.Groups[1].Value }
} catch { }
if (-not $cfgVer -or -not $formVer) {
Report-OK "14. Format version: not comparable"
} elseif ($formVer -ne $cfgVer) {
Report-Error "14. Format version $formVer differs from configuration ($cfgVer) — a dump carries one version, the platform refuses a file it cannot read"
} else {
Report-OK "14. Format version: $formVer, matches configuration"
}
}
# --- Summary ---
$checks = $script:okCount + $errors + $warnings
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-validate v1.13 — Validate 1C managed form
# form-validate v1.14 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -128,6 +128,7 @@ def main():
# Detect context: config vs EPF/ERF
is_config_context = False
config_xml_path = ''
walk_dir = os.path.dirname(os.path.abspath(form_path))
for _ in range(15):
parent = os.path.dirname(walk_dir)
@@ -135,6 +136,7 @@ def main():
break
if os.path.isfile(os.path.join(walk_dir, 'Configuration.xml')):
is_config_context = True
config_xml_path = os.path.join(walk_dir, 'Configuration.xml')
break
walk_dir = parent
@@ -808,6 +810,69 @@ def main():
else:
report_ok('12. Types: no type values to check')
# --- Check 13: префиксы в значениях объявлены в самом файле ---
# `cfg:DataProcessorObject.X` в <v8:Type> при незадекларированном xmlns:cfg — валидный XML, который
# платформа не читает вовсе: «Исключение XDTO произошло при чтении файла». Ошибка типична для
# рукописного XML: префикс скопирован из чужой формы, а объявление в корне забыто. Область видимости
# считаем по узлу (nsmap элемента), а не по корню: локальная xmlns на элементе законна.
if not stopped:
prefix_errors = 0
prefix_checked = 0
prefix_re = re.compile(r'^([A-Za-z_][A-Za-z0-9_.-]*):.+$')
for node in root.iter():
if not isinstance(node.tag, str):
continue
ln = localname(node)
values = []
if ln in ('Type', 'TypeSet'):
values.append((node.text or '').strip())
xsi_type = node.get(f'{{{"http://www.w3.org/2001/XMLSchema-instance"}}}type')
if xsi_type:
values.append(xsi_type.strip())
for val in values:
if not val:
continue
m = prefix_re.match(val)
if not m:
continue
prefix_checked += 1
pfx = m.group(1)
if pfx not in node.nsmap:
kind = "xsi:type" if val == xsi_type else "Type"
report_error(f"13. {kind} '{val}': namespace prefix '{pfx}:' is not declared "
"— the platform cannot read the file (XDTO)")
prefix_errors += 1
if prefix_checked == 0:
report_ok('13. Namespace prefixes: nothing to check')
elif prefix_errors == 0:
report_ok(f'13. Namespace prefixes: {prefix_checked} values, all declared')
# --- Check 14: версия формата формы совпадает с версией конфигурации ---
# Версию задаёт платформа, которой выгружали, и в пределах одной выгрузки она едина. Форма из
# другой версии — «Неизвестная версия формата N загружаемого файла»: платформа не читает файл,
# который новее её самой. Типичный след ручной сборки: форму скопировали из свежей конфигурации.
if not stopped and config_xml_path:
form_ver = root.get('version', '')
cfg_ver = ''
try:
with open(config_xml_path, 'r', encoding='utf-8-sig', errors='ignore') as fh:
head = fh.read(4000)
vm = re.search(r'<MetaDataObject[^>]*[ \t]version="([^"]+)"', head)
if vm:
cfg_ver = vm.group(1)
except OSError:
pass
if not cfg_ver or not form_ver:
report_ok('14. Format version: not comparable')
elif form_ver != cfg_ver:
report_error(f'14. Format version {form_ver} differs from configuration ({cfg_ver}) '
'— a dump carries one version, the platform refuses a file it cannot read')
else:
report_ok(f'14. Format version: {form_ver}, matches configuration')
# --- Finalize ---
checks = ok_count + errors + warnings
if errors == 0 and warnings == 0 and not detailed: