Auto-build: opencode (powershell) from da6ac2b

This commit is contained in:
github-actions[bot]
2026-05-25 10:08:58 +00:00
commit 874aaf7b68
210 changed files with 108303 additions and 0 deletions
@@ -0,0 +1,611 @@
# cf-validate v1.3 — Validate 1C configuration root structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$ConfigPath,
[switch]$Detailed,
[int]$MaxErrors = 30,
[string]$OutFile
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve path ---
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath
}
if (Test-Path $ConfigPath -PathType Container) {
$candidate = Join-Path $ConfigPath "Configuration.xml"
if (Test-Path $candidate) {
$ConfigPath = $candidate
} else {
Write-Host "[ERROR] No Configuration.xml found in directory: $ConfigPath"
exit 1
}
}
if (-not (Test-Path $ConfigPath)) {
Write-Host "[ERROR] File not found: $ConfigPath"
exit 1
}
$resolvedPath = (Resolve-Path $ConfigPath).Path
$configDir = Split-Path $resolvedPath -Parent
# --- Output infrastructure ---
$script:errors = 0
$script:warnings = 0
$script:okCount = 0
$script:stopped = $false
$script:output = New-Object System.Text.StringBuilder 8192
function Out-Line {
param([string]$msg)
$script:output.AppendLine($msg) | Out-Null
}
function Report-OK {
param([string]$msg)
$script:okCount++
if ($Detailed) { Out-Line "[OK] $msg" }
}
function Report-Error {
param([string]$msg)
$script:errors++
Out-Line "[ERROR] $msg"
if ($script:errors -ge $MaxErrors) {
$script:stopped = $true
}
}
function Report-Warn {
param([string]$msg)
$script:warnings++
Out-Line "[WARN] $msg"
}
$finalize = {
$checks = $script:okCount + $script:errors + $script:warnings
if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) {
$result = "=== Validation OK: Configuration.$objName ($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, $result, $utf8Bom)
Write-Host "Written to: $OutFile"
}
}
# --- Reference tables ---
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
# 7 fixed ClassIds for Configuration
$validClassIds = @(
"9cd510cd-abfc-11d4-9434-004095e12fc7", # managed application module
"9fcd25a0-4822-11d4-9414-008048da11f9", # ordinary application module
"e3687481-0a87-462c-a166-9f34594f9bba", # session module
"9de14907-ec23-4a07-96f0-85521cb6b53b", # external connection module
"51f2d5d8-ea4d-4064-8892-82951750031e", # command interface
"e68182ea-4237-4383-967f-90c1e3370bc7", # main section command interface
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
)
# 44 types in canonical order
$childObjectTypes = @(
"Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
"Constant","CommonForm","Catalog","Document",
"DocumentNumerator","Sequence","DocumentJournal","Enum",
"Report","DataProcessor","InformationRegister","AccumulationRegister",
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
"ChartOfCalculationTypes","CalculationRegister",
"BusinessProcess","Task","IntegrationService"
)
# Type -> directory mapping
$childTypeDirMap = @{
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
"SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"
"CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"; "Constant"="Constants"
"CommonForm"="CommonForms"; "Catalog"="Catalogs"; "Document"="Documents"
"DocumentNumerator"="DocumentNumerators"; "Sequence"="Sequences"
"DocumentJournal"="DocumentJournals"; "Enum"="Enums"; "Report"="Reports"
"DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters"
"AccumulationRegister"="AccumulationRegisters"
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
"ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
"CalculationRegister"="CalculationRegisters"
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
"IntegrationService"="IntegrationServices"
}
# Valid enum values for Configuration properties
$validEnumValues = @{
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
"ScriptVariant" = @("Russian","English")
"DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged")
"ObjectAutonumerationMode" = @("NotAutoFree","AutoFree")
"ModalityUseMode" = @("DontUse","Use","UseWithWarnings")
"SynchronousPlatformExtensionAndAddInCallUseMode" = @("DontUse","Use","UseWithWarnings")
"InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5")
"DatabaseTablespacesUseMode" = @("DontUse","Use")
"MainClientApplicationWindowMode" = @("Normal","Fullscreen","Kiosk")
"CompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
}
# --- 1. Parse XML ---
Out-Line ""
$xmlDoc = $null
try {
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $false
$xmlDoc.Load($resolvedPath)
} catch {
Out-Line "=== Validation: Configuration (parse failed) ==="
Out-Line ""
Report-Error "1. XML parse failed: $($_.Exception.Message)"
& $finalize
exit 1
}
# --- Register namespaces ---
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema")
$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core")
$root = $xmlDoc.DocumentElement
# --- Check 1: Root structure ---
$check1Ok = $true
$expectedNs = "http://v8.1c.ru/8.3/MDClasses"
if ($root.LocalName -ne "MetaDataObject") {
Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'"
& $finalize
exit 1
}
if ($root.NamespaceURI -ne $expectedNs) {
Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'"
$check1Ok = $false
}
$version = $root.GetAttribute("version")
if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
}
# Must have Configuration child
$cfgNode = $null
foreach ($child in $root.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration" -and $child.NamespaceURI -eq $expectedNs) {
$cfgNode = $child; break
}
}
if (-not $cfgNode) {
Report-Error "1. No <Configuration> element found inside MetaDataObject"
& $finalize
exit 1
}
# UUID
$cfgUuid = $cfgNode.GetAttribute("uuid")
if (-not $cfgUuid) {
Report-Error "1. Missing uuid on <Configuration>"
$check1Ok = $false
} elseif ($cfgUuid -notmatch $guidPattern) {
Report-Error "1. Invalid uuid '$cfgUuid' on <Configuration>"
$check1Ok = $false
}
# Get name early for header
$propsNode = $cfgNode.SelectSingleNode("md:Properties", $ns)
$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null }
$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" }
$script:output.Insert(0, "=== Validation: Configuration.$objName ===$([Environment]::NewLine)") | Out-Null
if ($check1Ok) {
Report-OK "1. Root structure: MetaDataObject/Configuration, version $version"
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 2: InternalInfo ---
$internalInfo = $cfgNode.SelectSingleNode("md:InternalInfo", $ns)
$check2Ok = $true
if (-not $internalInfo) {
Report-Error "2. InternalInfo: missing"
} else {
$contained = $internalInfo.SelectNodes("xr:ContainedObject", $ns)
if ($contained.Count -ne 7) {
Report-Warn "2. InternalInfo: expected 7 ContainedObject, found $($contained.Count)"
}
$foundClassIds = @{}
foreach ($co in $contained) {
$classId = $co.SelectSingleNode("xr:ClassId", $ns)
$objectId = $co.SelectSingleNode("xr:ObjectId", $ns)
if (-not $classId -or -not $classId.InnerText) {
Report-Error "2. ContainedObject missing ClassId"
$check2Ok = $false
continue
}
$cid = $classId.InnerText
if ($validClassIds -notcontains $cid) {
Report-Error "2. Unknown ClassId: $cid"
$check2Ok = $false
}
if ($foundClassIds.ContainsKey($cid)) {
Report-Error "2. Duplicate ClassId: $cid"
$check2Ok = $false
}
$foundClassIds[$cid] = $true
if (-not $objectId -or -not $objectId.InnerText) {
Report-Error "2. ContainedObject missing ObjectId for ClassId $cid"
$check2Ok = $false
} elseif ($objectId.InnerText -notmatch $guidPattern) {
Report-Error "2. Invalid ObjectId '$($objectId.InnerText)' for ClassId $cid"
$check2Ok = $false
}
}
# Check missing ClassIds
$missingIds = @($validClassIds | Where-Object { -not $foundClassIds.ContainsKey($_) })
if ($missingIds.Count -gt 0) {
Report-Warn "2. Missing ClassIds: $($missingIds.Count) of 7"
}
if ($check2Ok) {
Report-OK "2. InternalInfo: $($contained.Count) ContainedObject, all ClassIds valid"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 3: Properties — Name, Synonym, DefaultLanguage, DefaultRunMode ---
if (-not $propsNode) {
Report-Error "3. Properties block missing"
} else {
$check3Ok = $true
# Name
if (-not $nameNode -or -not $nameNode.InnerText) {
Report-Error "3. Properties: Name is missing or empty"
$check3Ok = $false
} else {
$nameVal = $nameNode.InnerText
if ($nameVal -notmatch $identPattern) {
Report-Error "3. Properties: Name '$nameVal' is not a valid 1C identifier"
$check3Ok = $false
}
}
# Synonym
$synNode = $propsNode.SelectSingleNode("md:Synonym", $ns)
$synPresent = $false
if ($synNode) {
$synItem = $synNode.SelectSingleNode("v8:item", $ns)
if ($synItem) {
$synContent = $synItem.SelectSingleNode("v8:content", $ns)
if ($synContent -and $synContent.InnerText) { $synPresent = $true }
}
}
# DefaultLanguage
$defLangNode = $propsNode.SelectSingleNode("md:DefaultLanguage", $ns)
$defLang = if ($defLangNode -and $defLangNode.InnerText) { $defLangNode.InnerText } else { "" }
if (-not $defLang) {
Report-Error "3. Properties: DefaultLanguage is missing or empty"
$check3Ok = $false
}
# DefaultRunMode
$defRunNode = $propsNode.SelectSingleNode("md:DefaultRunMode", $ns)
if (-not $defRunNode -or -not $defRunNode.InnerText) {
Report-Warn "3. Properties: DefaultRunMode is missing or empty"
}
if ($check3Ok) {
$synInfo = if ($synPresent) { "Synonym present" } else { "no Synonym" }
Report-OK "3. Properties: Name=`"$objName`", $synInfo, DefaultLanguage=$defLang"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 4: Property values — enum properties ---
if ($propsNode) {
$enumChecked = 0
$check4Ok = $true
foreach ($propName in $validEnumValues.Keys) {
$propNode = $propsNode.SelectSingleNode("md:$propName", $ns)
if ($propNode -and $propNode.InnerText) {
$val = $propNode.InnerText
$allowed = $validEnumValues[$propName]
if ($allowed -notcontains $val) {
Report-Error "4. Property '$propName' has invalid value '$val'"
$check4Ok = $false
}
$enumChecked++
}
}
if ($check4Ok) {
Report-OK "4. Property values: $enumChecked enum properties checked"
}
} else {
Report-Warn "4. No Properties block to check"
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 5: ChildObjects — valid types, no duplicates, order ---
$childObjNode = $cfgNode.SelectSingleNode("md:ChildObjects", $ns)
if (-not $childObjNode) {
Report-Error "5. ChildObjects block missing"
} else {
$check5Ok = $true
$totalCount = 0
$typeCounts = @{}
$duplicates = @{}
$typeFirstIndex = @{} # type -> first position index
$lastTypeOrder = -1
$orderOk = $true
$idx = 0
foreach ($child in $childObjNode.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
$typeName = $child.LocalName
$objNameVal = $child.InnerText
# Valid type?
$typeIdx = $childObjectTypes.IndexOf($typeName)
if ($typeIdx -lt 0) {
Report-Error "5. Unknown type '$typeName' in ChildObjects"
$check5Ok = $false
} else {
# Check order
if (-not $typeFirstIndex.ContainsKey($typeName)) {
$typeFirstIndex[$typeName] = $typeIdx
if ($typeIdx -lt $lastTypeOrder) {
Report-Warn "5. Type '$typeName' is out of canonical order (after type at position $lastTypeOrder)"
$orderOk = $false
}
$lastTypeOrder = $typeIdx
}
}
# Count and dedup
if (-not $typeCounts.ContainsKey($typeName)) { $typeCounts[$typeName] = @{} }
if ($typeCounts[$typeName].ContainsKey($objNameVal)) {
if (-not $duplicates.ContainsKey("$typeName.$objNameVal")) {
Report-Error "5. Duplicate: $typeName.$objNameVal"
$duplicates["$typeName.$objNameVal"] = $true
$check5Ok = $false
}
} else {
$typeCounts[$typeName][$objNameVal] = $true
}
$totalCount++
$idx++
}
$typeCount = $typeCounts.Count
if ($check5Ok) {
$orderInfo = if ($orderOk) { ", order correct" } else { "" }
Report-OK "5. ChildObjects: $typeCount types, $totalCount objects${orderInfo}"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 6: DefaultLanguage references existing Language in ChildObjects ---
if ($defLang -and $childObjNode) {
# DefaultLanguage is like "Language.Русский"
$langName = $defLang
if ($langName.StartsWith("Language.")) {
$langName = $langName.Substring(9)
}
$found = $false
foreach ($child in $childObjNode.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language" -and $child.InnerText -eq $langName) {
$found = $true; break
}
}
if ($found) {
Report-OK "6. DefaultLanguage `"$defLang`" found in ChildObjects"
} else {
Report-Error "6. DefaultLanguage `"$defLang`" not found in ChildObjects"
}
} else {
if (-not $defLang) {
Report-Warn "6. Cannot check DefaultLanguage (empty)"
} else {
Report-Warn "6. Cannot check DefaultLanguage (no ChildObjects)"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 7: Language files exist ---
if ($childObjNode) {
$langNames = @()
foreach ($child in $childObjNode.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language") {
$langNames += $child.InnerText
}
}
if ($langNames.Count -gt 0) {
$existCount = 0
foreach ($ln in $langNames) {
$langFile = Join-Path (Join-Path $configDir "Languages") "$ln.xml"
if (Test-Path $langFile) {
$existCount++
} else {
Report-Warn "7. Language file missing: Languages/$ln.xml"
}
}
if ($existCount -eq $langNames.Count) {
Report-OK "7. Language files: $existCount/$($langNames.Count) exist"
}
} else {
Report-Warn "7. No Language entries in ChildObjects"
}
} else {
Report-Warn "7. Cannot check language files (no ChildObjects)"
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 8: Object directories exist (spot-check) ---
if ($childObjNode) {
$dirsToCheck = @{}
foreach ($child in $childObjNode.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
$typeName = $child.LocalName
if ($typeName -eq "Language") { continue } # Already checked
if ($childTypeDirMap.ContainsKey($typeName)) {
$dirName = $childTypeDirMap[$typeName]
if (-not $dirsToCheck.ContainsKey($dirName)) {
$dirsToCheck[$dirName] = 0
}
$dirsToCheck[$dirName] = $dirsToCheck[$dirName] + 1
}
}
$missingDirs = @()
foreach ($dir in $dirsToCheck.Keys) {
$dirPath = Join-Path $configDir $dir
if (-not (Test-Path $dirPath -PathType Container)) {
$missingDirs += "$dir ($($dirsToCheck[$dir]) objects)"
}
}
if ($missingDirs.Count -eq 0) {
Report-OK "8. Object directories: $($dirsToCheck.Count) directories, all exist"
} else {
foreach ($md in $missingDirs) {
Report-Warn "8. Missing directory: $md"
}
}
}
# --- Check 9: Form references (HomePageWorkArea + Properties) ---
function Test-FormRef([string]$ref) {
if (-not $ref) { return $true }
# UUID — cannot verify without scanning all forms; skip
if ($ref -match $guidPattern) { return $true }
$parts = $ref.Split(".")
if ($parts.Count -eq 2 -and $parts[0] -eq "CommonForm") {
$p = Join-Path (Join-Path (Join-Path $configDir "CommonForms") $parts[1]) "Form.xml"
$pExt = Join-Path (Join-Path (Join-Path (Join-Path $configDir "CommonForms") $parts[1]) "Ext") "Form.xml"
return (Test-Path $p) -or (Test-Path $pExt)
}
if ($parts.Count -eq 4 -and $parts[2] -eq "Form" -and $childTypeDirMap.ContainsKey($parts[0])) {
$dir = $childTypeDirMap[$parts[0]]
$p = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $configDir $dir) $parts[1]) "Forms") $parts[3]) "Form.xml"
$pExt = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $configDir $dir) $parts[1]) "Forms") $parts[3]) "Ext") "Form.xml"
return (Test-Path $p) -or (Test-Path $pExt)
}
return $false
}
$formRefsChecked = 0
$formRefErrors = @()
# HomePageWorkArea
$hpPath = Join-Path (Join-Path $configDir "Ext") "HomePageWorkArea.xml"
if (Test-Path $hpPath) {
try {
[xml]$hpDoc = Get-Content -Path $hpPath -Encoding UTF8
$hpNs = New-Object System.Xml.XmlNamespaceManager($hpDoc.NameTable)
$hpNs.AddNamespace("hp", "http://v8.1c.ru/8.3/xcf/extrnprops")
foreach ($f in $hpDoc.DocumentElement.SelectNodes("//hp:Item/hp:Form", $hpNs)) {
$ref = $f.InnerText.Trim()
if (-not $ref) { continue }
$formRefsChecked++
if (-not (Test-FormRef $ref)) {
$formRefErrors += "HomePageWorkArea.Form '$ref' — file not found"
}
}
} catch {
$formRefErrors += "HomePageWorkArea.xml: parse error — $($_.Exception.Message)"
}
}
# Properties: DefaultXxxForm refs
if ($propsNode) {
$formProps = @("DefaultReportForm","DefaultReportVariantForm","DefaultReportSettingsForm","DefaultDynamicListSettingsForm","DefaultSearchForm","DefaultDataHistoryChangeHistoryForm","DefaultDataHistoryVersionDataForm","DefaultDataHistoryVersionDifferencesForm","DefaultCollaborationSystemUsersChoiceForm","DefaultConstantsForm")
foreach ($pn in $formProps) {
$node = $propsNode.SelectSingleNode("md:$pn", $ns)
if ($node -and $node.InnerText.Trim()) {
$ref = $node.InnerText.Trim()
$formRefsChecked++
if (-not (Test-FormRef $ref)) {
$formRefErrors += "Properties.$pn '$ref' — form not found"
}
}
}
}
if ($formRefsChecked -eq 0) {
Report-OK "9. Form references: none to check"
} elseif ($formRefErrors.Count -eq 0) {
Report-OK "9. Form references: $formRefsChecked verified"
} else {
foreach ($err in $formRefErrors) { Report-Error "9. $err" }
}
# --- Final output ---
& $finalize
if ($script:errors -gt 0) {
exit 1
}
exit 0
@@ -0,0 +1,600 @@
#!/usr/bin/env python3
# cf-validate v1.3 — Validate 1C configuration XML structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
import sys, os, argparse, re
from lxml import etree
NS = {
'md': 'http://v8.1c.ru/8.3/MDClasses',
'v8': 'http://v8.1c.ru/8.1/data/core',
'xr': 'http://v8.1c.ru/8.3/xcf/readable',
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'xs': 'http://www.w3.org/2001/XMLSchema',
'app': 'http://v8.1c.ru/8.2/managed-application/core',
}
GUID_PATTERN = re.compile(
r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
)
IDENT_PATTERN = re.compile(
r'^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_]'
r'[A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
)
# 7 fixed ClassIds for Configuration
VALID_CLASS_IDS = [
'9cd510cd-abfc-11d4-9434-004095e12fc7', # managed application module
'9fcd25a0-4822-11d4-9414-008048da11f9', # ordinary application module
'e3687481-0a87-462c-a166-9f34594f9bba', # session module
'9de14907-ec23-4a07-96f0-85521cb6b53b', # external connection module
'51f2d5d8-ea4d-4064-8892-82951750031e', # command interface
'e68182ea-4237-4383-967f-90c1e3370bc7', # main section command interface
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
]
# 44 types in canonical order
CHILD_OBJECT_TYPES = [
'Language', 'Subsystem', 'StyleItem', 'Style',
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
'Constant', 'CommonForm', 'Catalog', 'Document',
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister',
'ChartOfCalculationTypes', 'CalculationRegister',
'BusinessProcess', 'Task', 'IntegrationService',
]
# Type -> directory mapping
CHILD_TYPE_DIR_MAP = {
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
'SettingsStorage': 'SettingsStorages', 'FunctionalOption': 'FunctionalOptions',
'FunctionalOptionsParameter': 'FunctionalOptionsParameters', 'DefinedType': 'DefinedTypes',
'CommonCommand': 'CommonCommands', 'CommandGroup': 'CommandGroups', 'Constant': 'Constants',
'CommonForm': 'CommonForms', 'Catalog': 'Catalogs', 'Document': 'Documents',
'DocumentNumerator': 'DocumentNumerators', 'Sequence': 'Sequences',
'DocumentJournal': 'DocumentJournals', 'Enum': 'Enums', 'Report': 'Reports',
'DataProcessor': 'DataProcessors', 'InformationRegister': 'InformationRegisters',
'AccumulationRegister': 'AccumulationRegisters',
'ChartOfCharacteristicTypes': 'ChartsOfCharacteristicTypes',
'ChartOfAccounts': 'ChartsOfAccounts', 'AccountingRegister': 'AccountingRegisters',
'ChartOfCalculationTypes': 'ChartsOfCalculationTypes',
'CalculationRegister': 'CalculationRegisters',
'BusinessProcess': 'BusinessProcesses', 'Task': 'Tasks',
'IntegrationService': 'IntegrationServices',
}
# Valid enum values for Configuration properties
VALID_ENUM_VALUES = {
'ConfigurationExtensionCompatibilityMode': [
'DontUse', 'Version8_1', 'Version8_2_13', 'Version8_2_16',
'Version8_3_1', 'Version8_3_2', 'Version8_3_3', 'Version8_3_4', 'Version8_3_5',
'Version8_3_6', 'Version8_3_7', 'Version8_3_8', 'Version8_3_9', 'Version8_3_10',
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
],
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
'ScriptVariant': ['Russian', 'English'],
'DataLockControlMode': ['Automatic', 'Managed', 'AutomaticAndManaged'],
'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'],
'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
'SynchronousPlatformExtensionAndAddInCallUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
'InterfaceCompatibilityMode': [
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
],
'DatabaseTablespacesUseMode': ['DontUse', 'Use'],
'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'],
'CompatibilityMode': [
'DontUse', 'Version8_1', 'Version8_2_13', 'Version8_2_16',
'Version8_3_1', 'Version8_3_2', 'Version8_3_3', 'Version8_3_4', 'Version8_3_5',
'Version8_3_6', 'Version8_3_7', 'Version8_3_8', 'Version8_3_9', 'Version8_3_10',
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
],
}
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
class Reporter:
def __init__(self, max_errors, detailed=False):
self.errors = 0
self.warnings = 0
self.ok_count = 0
self.stopped = False
self.max_errors = max_errors
self.detailed = detailed
self.lines = []
self.obj_name = '(unknown)'
def out(self, msg=''):
self.lines.append(msg)
def ok(self, msg):
self.ok_count += 1
if self.detailed:
self.lines.append(f'[OK] {msg}')
def error(self, msg):
self.errors += 1
self.lines.append(f'[ERROR] {msg}')
if self.errors >= self.max_errors:
self.stopped = True
def warn(self, msg):
self.warnings += 1
self.lines.append(f'[WARN] {msg}')
def text(self):
return '\r\n'.join(self.lines) + '\r\n'
def finalize(self, out_file):
checks = self.ok_count + self.errors + self.warnings
if self.errors == 0 and self.warnings == 0 and not self.detailed:
result = f'=== Validation OK: Configuration.{self.obj_name} ({checks} checks) ==='
else:
self.out('')
self.out(f'=== Result: {self.errors} errors, {self.warnings} warnings ({checks} checks) ===')
result = self.text()
print(result, end='' if '\r\n' in result else '\n')
if out_file:
with open(out_file, 'w', encoding='utf-8-sig', newline='') as f:
f.write(result)
print(f'Written to: {out_file}')
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description='Validate 1C configuration XML structure', allow_abbrev=False
)
parser.add_argument('-ConfigPath', '-Path', dest='ConfigPath', required=True)
parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
args = parser.parse_args()
config_path = args.ConfigPath
max_errors = args.MaxErrors
out_file = args.OutFile
# --- Resolve path ---
if not os.path.isabs(config_path):
config_path = os.path.join(os.getcwd(), config_path)
if os.path.isdir(config_path):
candidate = os.path.join(config_path, 'Configuration.xml')
if os.path.exists(candidate):
config_path = candidate
else:
print(f'[ERROR] No Configuration.xml found in directory: {config_path}')
sys.exit(1)
if not os.path.exists(config_path):
print(f'[ERROR] File not found: {config_path}')
sys.exit(1)
resolved_path = os.path.abspath(config_path)
config_dir = os.path.dirname(resolved_path)
if out_file and not os.path.isabs(out_file):
out_file = os.path.join(os.getcwd(), out_file)
r = Reporter(max_errors, detailed=args.Detailed)
r.out('')
# --- 1. Parse XML ---
xml_doc = None
try:
xml_parser = etree.XMLParser(remove_blank_text=False)
xml_doc = etree.parse(resolved_path, xml_parser)
except etree.XMLSyntaxError as e:
r.lines.insert(0, '=== Validation: Configuration (parse failed) ===')
r.out('')
r.error(f'1. XML parse failed: {e}')
r.finalize(out_file)
sys.exit(1)
root = xml_doc.getroot()
# --- Check 1: Root structure ---
check1_ok = True
root_local = etree.QName(root.tag).localname
root_ns = etree.QName(root.tag).namespace or ''
if root_local != 'MetaDataObject':
r.error(f"1. Root element is '{root_local}', expected 'MetaDataObject'")
r.finalize(out_file)
sys.exit(1)
if root_ns != EXPECTED_NS:
r.error(f"1. Root namespace is '{root_ns}', expected '{EXPECTED_NS}'")
check1_ok = False
version = root.get('version', '')
if not version:
r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20', '2.21'):
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
# Must have Configuration child
cfg_node = None
for child in root:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname == 'Configuration' and etree.QName(child.tag).namespace == EXPECTED_NS:
cfg_node = child
break
if cfg_node is None:
r.error('1. No <Configuration> element found inside MetaDataObject')
r.finalize(out_file)
sys.exit(1)
# UUID
cfg_uuid = cfg_node.get('uuid', '')
if not cfg_uuid:
r.error('1. Missing uuid on <Configuration>')
check1_ok = False
elif not GUID_PATTERN.match(cfg_uuid):
r.error(f"1. Invalid uuid '{cfg_uuid}' on <Configuration>")
check1_ok = False
# Get name early for header
props_node = cfg_node.find('md:Properties', NS)
name_node = props_node.find('md:Name', NS) if props_node is not None else None
obj_name = (name_node.text or '') if name_node is not None and name_node.text else '(unknown)'
r.obj_name = obj_name
r.lines.insert(0, f'=== Validation: Configuration.{obj_name} ===')
if check1_ok:
r.ok(f'1. Root structure: MetaDataObject/Configuration, version {version}')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 2: InternalInfo ---
internal_info = cfg_node.find('md:InternalInfo', NS)
check2_ok = True
if internal_info is None:
r.error('2. InternalInfo: missing')
else:
contained = internal_info.findall('xr:ContainedObject', NS)
if len(contained) != 7:
r.warn(f'2. InternalInfo: expected 7 ContainedObject, found {len(contained)}')
found_class_ids = {}
for co in contained:
class_id_el = co.find('xr:ClassId', NS)
object_id_el = co.find('xr:ObjectId', NS)
if class_id_el is None or not (class_id_el.text or ''):
r.error('2. ContainedObject missing ClassId')
check2_ok = False
continue
cid = class_id_el.text
if cid not in VALID_CLASS_IDS:
r.error(f'2. Unknown ClassId: {cid}')
check2_ok = False
if cid in found_class_ids:
r.error(f'2. Duplicate ClassId: {cid}')
check2_ok = False
found_class_ids[cid] = True
if object_id_el is None or not (object_id_el.text or ''):
r.error(f'2. ContainedObject missing ObjectId for ClassId {cid}')
check2_ok = False
elif not GUID_PATTERN.match(object_id_el.text):
r.error(f"2. Invalid ObjectId '{object_id_el.text}' for ClassId {cid}")
check2_ok = False
# Check missing ClassIds
missing_ids = [cid for cid in VALID_CLASS_IDS if cid not in found_class_ids]
if len(missing_ids) > 0:
r.warn(f'2. Missing ClassIds: {len(missing_ids)} of 7')
if check2_ok:
r.ok(f'2. InternalInfo: {len(contained)} ContainedObject, all ClassIds valid')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 3: Properties -- Name, Synonym, DefaultLanguage, DefaultRunMode ---
def_lang = ''
syn_present = False
if props_node is None:
r.error('3. Properties block missing')
else:
check3_ok = True
# Name
if name_node is None or not (name_node.text or ''):
r.error('3. Properties: Name is missing or empty')
check3_ok = False
else:
name_val = name_node.text
if not IDENT_PATTERN.match(name_val):
r.error(f"3. Properties: Name '{name_val}' is not a valid 1C identifier")
check3_ok = False
# Synonym
syn_node = props_node.find('md:Synonym', NS)
if syn_node is not None:
syn_item = syn_node.find('v8:item', NS)
if syn_item is not None:
syn_content = syn_item.find('v8:content', NS)
if syn_content is not None and syn_content.text:
syn_present = True
# DefaultLanguage
def_lang_node = props_node.find('md:DefaultLanguage', NS)
def_lang = (def_lang_node.text or '') if def_lang_node is not None else ''
if not def_lang:
r.error('3. Properties: DefaultLanguage is missing or empty')
check3_ok = False
# DefaultRunMode
def_run_node = props_node.find('md:DefaultRunMode', NS)
if def_run_node is None or not (def_run_node.text or ''):
r.warn('3. Properties: DefaultRunMode is missing or empty')
if check3_ok:
syn_info = 'Synonym present' if syn_present else 'no Synonym'
r.ok(f'3. Properties: Name="{obj_name}", {syn_info}, DefaultLanguage={def_lang}')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 4: Property values -- enum properties ---
if props_node is not None:
enum_checked = 0
check4_ok = True
for prop_name, allowed in VALID_ENUM_VALUES.items():
prop_node = props_node.find(f'md:{prop_name}', NS)
if prop_node is not None and prop_node.text:
val = prop_node.text
if val not in allowed:
r.error(f"4. Property '{prop_name}' has invalid value '{val}'")
check4_ok = False
enum_checked += 1
if check4_ok:
r.ok(f'4. Property values: {enum_checked} enum properties checked')
else:
r.warn('4. No Properties block to check')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 5: ChildObjects -- valid types, no duplicates, order ---
child_obj_node = cfg_node.find('md:ChildObjects', NS)
if child_obj_node is None:
r.error('5. ChildObjects block missing')
else:
check5_ok = True
total_count = 0
type_counts = {} # type_name -> {obj_name: True}
duplicates = {}
type_first_index = {}
last_type_order = -1
order_ok = True
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
type_name = etree.QName(child.tag).localname
obj_name_val = child.text or ''
# Valid type?
if type_name in CHILD_OBJECT_TYPES:
type_idx = CHILD_OBJECT_TYPES.index(type_name)
else:
type_idx = -1
if type_idx < 0:
r.error(f"5. Unknown type '{type_name}' in ChildObjects")
check5_ok = False
else:
# Check order
if type_name not in type_first_index:
type_first_index[type_name] = type_idx
if type_idx < last_type_order:
r.warn(f"5. Type '{type_name}' is out of canonical order (after type at position {last_type_order})")
order_ok = False
last_type_order = type_idx
# Count and dedup
if type_name not in type_counts:
type_counts[type_name] = {}
if obj_name_val in type_counts[type_name]:
dup_key = f'{type_name}.{obj_name_val}'
if dup_key not in duplicates:
r.error(f'5. Duplicate: {dup_key}')
duplicates[dup_key] = True
check5_ok = False
else:
type_counts[type_name][obj_name_val] = True
total_count += 1
type_count = len(type_counts)
if check5_ok:
order_info = ', order correct' if order_ok else ''
r.ok(f'5. ChildObjects: {type_count} types, {total_count} objects{order_info}')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 6: DefaultLanguage references existing Language in ChildObjects ---
if def_lang and child_obj_node is not None:
lang_name = def_lang
if lang_name.startswith('Language.'):
lang_name = lang_name[9:]
found = False
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname == 'Language' and (child.text or '') == lang_name:
found = True
break
if found:
r.ok(f'6. DefaultLanguage "{def_lang}" found in ChildObjects')
else:
r.error(f'6. DefaultLanguage "{def_lang}" not found in ChildObjects')
else:
if not def_lang:
r.warn('6. Cannot check DefaultLanguage (empty)')
else:
r.warn('6. Cannot check DefaultLanguage (no ChildObjects)')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 7: Language files exist ---
if child_obj_node is not None:
lang_names = []
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname == 'Language':
lang_names.append(child.text or '')
if len(lang_names) > 0:
exist_count = 0
for ln in lang_names:
lang_file = os.path.join(config_dir, 'Languages', ln + '.xml')
if os.path.exists(lang_file):
exist_count += 1
else:
r.warn(f'7. Language file missing: Languages/{ln}.xml')
if exist_count == len(lang_names):
r.ok(f'7. Language files: {exist_count}/{len(lang_names)} exist')
else:
r.warn('7. No Language entries in ChildObjects')
else:
r.warn('7. Cannot check language files (no ChildObjects)')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 8: Object directories exist (spot-check) ---
if child_obj_node is not None:
dirs_to_check = {}
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
type_name = etree.QName(child.tag).localname
if type_name == 'Language':
continue
if type_name in CHILD_TYPE_DIR_MAP:
dir_name = CHILD_TYPE_DIR_MAP[type_name]
dirs_to_check[dir_name] = dirs_to_check.get(dir_name, 0) + 1
missing_dirs = []
for dir_name, count in dirs_to_check.items():
dir_path = os.path.join(config_dir, dir_name)
if not os.path.isdir(dir_path):
missing_dirs.append(f'{dir_name} ({count} objects)')
if len(missing_dirs) == 0:
r.ok(f'8. Object directories: {len(dirs_to_check)} directories, all exist')
else:
for md in missing_dirs:
r.warn(f'8. Missing directory: {md}')
else:
pass # no ChildObjects
# --- Check 9: Form references (HomePageWorkArea + Properties) ---
def test_form_ref(ref):
if not ref:
return True
if GUID_PATTERN.match(ref):
return True
parts = ref.split('.')
if len(parts) == 2 and parts[0] == 'CommonForm':
p = os.path.join(config_dir, 'CommonForms', parts[1], 'Form.xml')
p_ext = os.path.join(config_dir, 'CommonForms', parts[1], 'Ext', 'Form.xml')
return os.path.isfile(p) or os.path.isfile(p_ext)
if len(parts) == 4 and parts[2] == 'Form' and parts[0] in CHILD_TYPE_DIR_MAP:
d = CHILD_TYPE_DIR_MAP[parts[0]]
p = os.path.join(config_dir, d, parts[1], 'Forms', parts[3], 'Form.xml')
p_ext = os.path.join(config_dir, d, parts[1], 'Forms', parts[3], 'Ext', 'Form.xml')
return os.path.isfile(p) or os.path.isfile(p_ext)
return False
form_refs_checked = 0
form_ref_errors = []
hp_path = os.path.join(config_dir, 'Ext', 'HomePageWorkArea.xml')
if os.path.isfile(hp_path):
try:
hp_tree = etree.parse(hp_path)
HP_NS = 'http://v8.1c.ru/8.3/xcf/extrnprops'
for f in hp_tree.getroot().iter(f'{{{HP_NS}}}Form'):
ref = (f.text or '').strip()
if not ref:
continue
form_refs_checked += 1
if not test_form_ref(ref):
form_ref_errors.append(f"HomePageWorkArea.Form '{ref}' — file not found")
except Exception as e:
form_ref_errors.append(f'HomePageWorkArea.xml: parse error — {e}')
if props_node is not None:
form_props = ['DefaultReportForm','DefaultReportVariantForm','DefaultReportSettingsForm','DefaultDynamicListSettingsForm','DefaultSearchForm','DefaultDataHistoryChangeHistoryForm','DefaultDataHistoryVersionDataForm','DefaultDataHistoryVersionDifferencesForm','DefaultCollaborationSystemUsersChoiceForm','DefaultConstantsForm']
for pn in form_props:
node = props_node.find(f'md:{pn}', NS)
if node is not None and node.text and node.text.strip():
ref = node.text.strip()
form_refs_checked += 1
if not test_form_ref(ref):
form_ref_errors.append(f"Properties.{pn} '{ref}' — form not found")
if form_refs_checked == 0:
r.ok('9. Form references: none to check')
elif not form_ref_errors:
r.ok(f'9. Form references: {form_refs_checked} verified')
else:
for err in form_ref_errors:
r.error(f'9. {err}')
# --- Final output ---
r.finalize(out_file)
sys.exit(1 if r.errors > 0 else 0)
if __name__ == '__main__':
main()