# xdto-edit v1.0 — Point edits of a 1C XDTO package # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory=$true)] [Alias('Path')] [string]$PackagePath, [Parameter(Mandatory=$true)] [ValidateSet("add-property", "replace-property", "remove-property", "add-type", "remove-type", "add-enum", "add-import", "rename", "set-synonym", "set-comment", "set-namespace")] [string]$Operation, [string]$Target, [string]$Value, [switch]$NoValidate ) $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" $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 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 { $d = $targetPath for ($i = 0; $i -lt 20 -and $d; $i++) { foreach ($x in @(Get-ChildItem -Path $d -Filter "*.xml" -File -ErrorAction SilentlyContinue)) { if (Test-ExternalObjectRoot $x.FullName) { return } } $cfgXml = Join-Path $d "Configuration.xml" $supportBin = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" 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 {} } # --- Resolve package ------------------------------------------------------------ if (-not [System.IO.Path]::IsPathRooted($PackagePath)) { $PackagePath = Join-Path (Get-Location).Path $PackagePath } $pkgDir = $null if (Test-Path $PackagePath -PathType Container) { if (Test-Path (Join-Path (Join-Path $PackagePath "Ext") "Package.bin")) { $pkgDir = $PackagePath } } elseif ((Test-Path $PackagePath -PathType Leaf) -and ([System.IO.Path]::GetFileName($PackagePath) -eq "Package.bin")) { $pkgDir = Split-Path (Split-Path $PackagePath -Parent) -Parent } elseif ($PackagePath.EndsWith(".xml")) { $stem = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($PackagePath), [System.IO.Path]::GetFileNameWithoutExtension($PackagePath)) if (Test-Path (Join-Path (Join-Path $stem "Ext") "Package.bin")) { $pkgDir = $stem } } if (-not $pkgDir) { throw "Не найден пакет XDTO по пути: $PackagePath" } $pkgName = [System.IO.Path]::GetFileName($pkgDir) $xdtoRoot = Split-Path $pkgDir -Parent $configRoot = Split-Path $xdtoRoot -Parent $binFile = Join-Path (Join-Path $pkgDir "Ext") "Package.bin" $mdFile = Join-Path $xdtoRoot "$pkgName.xml" $configXml = Join-Path $configRoot "Configuration.xml" # -Value "@путь" — содержимое берётся из файла. Передавать XSD-фрагмент инлайном # через powershell.exe -File ненадёжно: вложенные кавычки схлопываются на границе # процессов, и вместо понятной ошибки получается сырой сбой разбора XML. if ($Value -and $Value.StartsWith("@")) { $valueFile = $Value.Substring(1) if (-not [System.IO.Path]::IsPathRooted($valueFile)) { $valueFile = Join-Path (Get-Location).Path $valueFile } if (-not (Test-Path $valueFile -PathType Leaf)) { throw "Файл значения не найден: $valueFile" } $Value = [System.IO.File]::ReadAllText($valueFile).Trim() } Assert-EditAllowed $pkgDir $encBom = New-Object System.Text.UTF8Encoding($true) # --- Sibling skills ------------------------------------------------------------- # Правка идёт через уже проверенный round-trip: пакет выгружается в XSD, операция # применяется к схеме, пакет собирается обратно. Второго эмиттера не заводим — # байт-точность для всего нетронутого достаётся от компилятора. $decompileScript = Join-Path (Join-Path $PSScriptRoot "..\..\xdto-decompile") "scripts\xdto-decompile.ps1" $compileScript = Join-Path (Join-Path $PSScriptRoot "..\..\xdto-compile") "scripts\xdto-compile.ps1" $validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\xdto-validate") "scripts\xdto-validate.ps1" # Исключение из автономности навыков, сделанное осознанно: конвертер XSD ↔ модель # нельзя скопировать буквально (xdto-compile — скрипт со сквозным потоком, не библиотека), # а вторая его реализация разошлась бы с первой. Обещание «правка не меняет ни байта # в нетронутом» держится именно на том, что код тот же самый. # Проверяем комплектность заранее, чтобы не падать на середине правки. function Assert-SiblingsPresent([string]$operation) { $needed = @{} if (@("rename", "set-synonym", "set-comment") -notcontains $operation) { $needed["xdto-decompile"] = $decompileScript $needed["xdto-compile"] = $compileScript } $missing = @() foreach ($k in $needed.Keys) { if (-not (Test-Path $needed[$k])) { $missing += $k } } if ($missing.Count -gt 0) { $skillsRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent throw ("Навык неработоспособен: рядом нет " + ($missing -join ", ") + ".`n" + "Операция `"$operation`" выполняется через " + $(if ($missing.Count -gt 1) { "них" } else { "него" }) + ".`n" + "Ожидаются в каталоге навыков: $skillsRoot") } } function Invoke-Sibling([string]$script, [string[]]$argList, [string]$what) { if (-not (Test-Path $script)) { throw "Не найден навык $what по пути: $script" } $out = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $script @argList 2>&1 if ($LASTEXITCODE -ne 0) { throw "$what завершился с ошибкой:`n$($out -join "`n")" } return $out } # --- Metadata object edits ------------------------------------------------------ function Edit-Metadata([string]$field, [string]$newValue) { $doc = New-Object System.Xml.XmlDocument $doc.PreserveWhitespace = $true $doc.Load($mdFile) $nsm = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) $nsm.AddNamespace("md", $MD_NS) $nsm.AddNamespace("v8", $V8_NS) $props = $doc.SelectSingleNode("//md:XDTOPackage/md:Properties", $nsm) if (-not $props) { throw "В объекте метаданных не найден блок " } switch ($field) { "Name" { $n = $props.SelectSingleNode("md:Name", $nsm) if (-not $n) { throw "В объекте метаданных нет " } $n.InnerText = $newValue } "Comment" { $c = $props.SelectSingleNode("md:Comment", $nsm) if (-not $c) { $c = $doc.CreateElement("Comment", $MD_NS) $props.AppendChild($c) | Out-Null } $c.InnerText = $newValue } "Namespace" { $ns = $props.SelectSingleNode("md:Namespace", $nsm) if (-not $ns) { throw "В объекте метаданных нет " } $ns.InnerText = $newValue } "Synonym" { $syn = $props.SelectSingleNode("md:Synonym", $nsm) if (-not $syn) { $syn = $doc.CreateElement("Synonym", $MD_NS) $props.AppendChild($syn) | Out-Null } $item = $syn.SelectSingleNode("v8:item[v8:lang='ru']", $nsm) if (-not $item) { $item = $doc.CreateElement("v8", "item", $V8_NS) $lang = $doc.CreateElement("v8", "lang", $V8_NS); $lang.InnerText = "ru" $cont = $doc.CreateElement("v8", "content", $V8_NS) $item.AppendChild($lang) | Out-Null $item.AppendChild($cont) | Out-Null $syn.AppendChild($item) | Out-Null } $item.SelectSingleNode("v8:content", $nsm).InnerText = $newValue } } $settings = New-Object System.Xml.XmlWriterSettings $settings.Encoding = $encBom $settings.Indent = $false $stream = New-Object System.IO.FileStream($mdFile, [System.IO.FileMode]::Create) $writer = [System.Xml.XmlWriter]::Create($stream, $settings) $doc.Save($writer) $writer.Close(); $stream.Close() } function Rename-Package([string]$newName) { if ($newName -notmatch '^[\wЀ-ӿ]+$' -or $newName -match '^\d') { throw "`"$newName`" не является допустимым идентификатором 1С" } $newMd = Join-Path $xdtoRoot "$newName.xml" $newDir = Join-Path $xdtoRoot $newName if ((Test-Path $newMd) -or (Test-Path $newDir)) { throw "Имя `"$newName`" уже занято другим пакетом" } Edit-Metadata "Name" $newName Move-Item $mdFile $newMd Move-Item $pkgDir $newDir if (Test-Path $configXml) { $cfg = New-Object System.Xml.XmlDocument $cfg.PreserveWhitespace = $true $cfg.Load($configXml) $nsm = New-Object System.Xml.XmlNamespaceManager($cfg.NameTable) $nsm.AddNamespace("md", $MD_NS) $found = $false foreach ($e in $cfg.SelectNodes("//md:Configuration/md:ChildObjects/md:XDTOPackage", $nsm)) { if ($e.InnerText -eq $pkgName) { $e.InnerText = $newName; $found = $true; break } } if ($found) { $s = New-Object System.Xml.XmlWriterSettings $s.Encoding = $encBom; $s.Indent = $false $st = New-Object System.IO.FileStream($configXml, [System.IO.FileMode]::Create) $w = [System.Xml.XmlWriter]::Create($st, $s) $cfg.Save($w); $w.Close(); $st.Close() Write-Host " Configuration.xml: переименован в $newName" } else { Write-Warning "В Configuration.xml не найдена запись $pkgName — зарегистрируйте пакет вручную" } } Write-Host "✓ Пакет переименован: $pkgName → $newName" Write-Host " Перемещены: $newName.xml, $newName/" } # --- Model edits through the XSD round-trip ------------------------------------- $XSD_DECL = @{ "add-property" = "element"; "replace-property" = "element"; "remove-property" = "element" } function Get-SchemaChildren([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) } } return ,$res } function Get-SchemaFirst([System.Xml.XmlElement]$el, [string]$local) { $r = Get-SchemaChildren $el $local if ($r.Count -gt 0) { return $r[0] } return $null } function Find-TypeElement($schema, [string]$typeName) { foreach ($kind in @("complexType", "simpleType")) { foreach ($t in (Get-SchemaChildren $schema $kind)) { if ($t.GetAttribute("name") -eq $typeName) { return $t } } } throw "В пакете нет типа `"$typeName`"" } # Тело типа: внутрь xs:complexContent/xs:extension, если тип наследуется function Get-TypeBody([System.Xml.XmlElement]$ct) { $content = Get-SchemaFirst $ct "complexContent" if ($content) { $ext = Get-SchemaFirst $content "extension" if ($ext) { return $ext } } return $ct } function Find-Declaration([System.Xml.XmlElement]$body, [string]$propName) { foreach ($node in $body.SelectNodes(".//*")) { if ($node.NamespaceURI -ne $XS_NS) { continue } if (@("element", "attribute") -notcontains $node.get_LocalName()) { continue } if ($node.GetAttribute("name") -eq $propName) { return $node } } return $null } # Путь Тип.Свойство[.Вложенное...] — точка безопасна: имена в модели XDTO # являются идентификаторами 1С и точку содержать не могут function Resolve-Path($schema, [string]$path) { $segments = $path.Split(".") $typeEl = Find-TypeElement $schema $segments[0] if ($segments.Count -eq 1) { return [pscustomobject]@{ Type = $typeEl; Decl = $null } } $body = Get-TypeBody $typeEl $decl = $null for ($i = 1; $i -lt $segments.Count; $i++) { $decl = Find-Declaration $body $segments[$i] if (-not $decl) { throw "По пути `"$path`" не найдено свойство `"$($segments[$i])`"" } if ($i -lt $segments.Count - 1) { $inner = Get-SchemaFirst $decl "complexType" if (-not $inner) { throw "Свойство `"$($segments[$i])`" не содержит вложенного типа — путь дальше не идёт" } $body = Get-TypeBody $inner } } return [pscustomobject]@{ Type = $typeEl; Decl = $decl } } function Import-Fragment($schema, [string]$xml) { $tmp = New-Object System.Xml.XmlDocument $nsAttrs = " xmlns:xs=`"$XS_NS`" xmlns:xdto=`"$XDTO_NS`"" $tns = $schema.GetAttribute("targetNamespace") if ($tns) { $nsAttrs += " xmlns:tns=`"$tns`"" } foreach ($a in $schema.Attributes) { if ($a.Prefix -eq "xmlns" -and $a.get_LocalName() -notin @("xs", "xdto", "tns")) { $nsAttrs += " xmlns:$($a.get_LocalName())=`"$($a.Value)`"" } } try { $tmp.LoadXml("$xml") } catch { throw ("Не удалось разобрать -Value как фрагмент XML-схемы: " + $_.Exception.InnerException.Message + "`n" + "Получено: " + $xml + "`n" + "Если фрагмент передан инлайном, кавычки могли схлопнуться на границе процессов — " + "положите его в файл и укажите -Value `"@путь`".") } $res = New-Object System.Collections.ArrayList foreach ($c in $tmp.DocumentElement.ChildNodes) { if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element) { [void]$res.Add($schema.OwnerDocument.ImportNode($c, $true)) } } if ($res.Count -eq 0) { throw "Во фрагменте нет ни одного элемента: $xml" } return ,$res } function Apply-ModelOperation($schema) { switch ($Operation) { "add-property" { if (-not $Target) { throw "add-property требует -Target <Тип>" } $loc = Resolve-Path $schema $Target $body = Get-TypeBody $(if ($loc.Decl) { Get-SchemaFirst $loc.Decl "complexType" } else { $loc.Type }) foreach ($frag in (Import-Fragment $schema $Value)) { $kind = $frag.get_LocalName() if ($kind -eq "attribute") { $body.AppendChild($frag) | Out-Null } elseif ($kind -eq "element") { $particle = Get-SchemaFirst $body "sequence" if (-not $particle) { $particle = Get-SchemaFirst $body "choice" } if (-not $particle) { $particle = Get-SchemaFirst $body "all" } if (-not $particle) { $particle = $schema.OwnerDocument.CreateElement("xs", "sequence", $XS_NS) $firstAttr = Get-SchemaFirst $body "attribute" if ($firstAttr) { $body.InsertBefore($particle, $firstAttr) | Out-Null } else { $body.AppendChild($particle) | Out-Null } } $particle.AppendChild($frag) | Out-Null } else { throw "add-property ожидает или , получен " } Write-Host " + $($frag.GetAttribute('name')) в тип $Target" } } "replace-property" { if (-not $Target) { throw "replace-property требует -Target `"Тип.Свойство`"" } $loc = Resolve-Path $schema $Target if (-not $loc.Decl) { throw "replace-property требует путь вида `"Тип.Свойство`"" } $frags = Import-Fragment $schema $Value if ($frags.Count -ne 1) { throw "replace-property ожидает ровно одно объявление" } $loc.Decl.ParentNode.ReplaceChild($frags[0], $loc.Decl) | Out-Null Write-Host " ~ $Target заменено" } "remove-property" { if (-not $Target) { throw "remove-property требует путь `"Тип.Свойство`"" } foreach ($one in ($Target -split "\s*;;\s*")) { if (-not $one) { continue } $loc = Resolve-Path $schema $one if (-not $loc.Decl) { throw "remove-property требует путь вида `"Тип.Свойство`", получено `"$one`"" } $loc.Decl.ParentNode.RemoveChild($loc.Decl) | Out-Null Write-Host " − $one удалено" } } "add-type" { foreach ($frag in (Import-Fragment $schema $Value)) { if (@("complexType", "simpleType") -notcontains $frag.get_LocalName()) { throw "add-type ожидает или , получен " } $schema.AppendChild($frag) | Out-Null Write-Host " + тип $($frag.GetAttribute('name'))" } } "remove-type" { if (-not $Target) { throw "remove-type требует -Target <Тип>" } foreach ($one in ($Target -split "\s*;;\s*")) { if (-not $one) { continue } $t = Find-TypeElement $schema $one $t.ParentNode.RemoveChild($t) | Out-Null Write-Host " − тип $one удалён" } } "add-enum" { if (-not $Target) { throw "add-enum требует -Target <ТипЗначения>" } $t = Find-TypeElement $schema $Target $restriction = Get-SchemaFirst $t "restriction" if (-not $restriction) { throw "Тип `"$Target`" не является ограничением простого типа" } foreach ($lit in ($Value -split "\s*;;\s*")) { if (-not $lit) { continue } $e = $schema.OwnerDocument.CreateElement("xs", "enumeration", $XS_NS) $e.SetAttribute("value", $lit) $restriction.AppendChild($e) | Out-Null Write-Host " + значение `"$lit`" в тип $Target" } } "add-import" { foreach ($uri in ($Value -split "\s*;;\s*")) { if (-not $uri) { continue } $exists = $false foreach ($i in (Get-SchemaChildren $schema "import")) { if ($i.GetAttribute("namespace") -eq $uri) { $exists = $true; break } } if ($exists) { Write-Host " = импорт $uri уже объявлен"; continue } $imp = $schema.OwnerDocument.CreateElement("xs", "import", $XS_NS) $imp.SetAttribute("namespace", $uri) $firstOther = $null foreach ($c in $schema.ChildNodes) { if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -notin @("annotation", "import")) { $firstOther = $c; break } } if ($firstOther) { $schema.InsertBefore($imp, $firstOther) | Out-Null } else { $schema.AppendChild($imp) | Out-Null } Write-Host " + импорт $uri" } } "set-namespace" { if (-not $Value) { throw "set-namespace требует -Value " } $old = $schema.GetAttribute("targetNamespace") # Установка того же значения не отбрасывается: пакет пересобирается вхолостую, # и это заодно проба точности пути «выгрузить → собрать» на любом пакете if ($old -eq $Value) { Write-Host " = namespace уже $Value, пакет пересобран без изменений" } # Меняем и targetNamespace, и объявление префикса, который на него указывал: # иначе ссылки на собственные типы станут ссылками в чужое пространство имён $schema.SetAttribute("targetNamespace", $Value) foreach ($a in @($schema.Attributes)) { if ($a.Prefix -eq "xmlns" -and $a.Value -eq $old) { $schema.SetAttribute("xmlns:$($a.get_LocalName())", $Value) } } Write-Host " ~ namespace: $old → $Value" } } } # --- Dispatch ------------------------------------------------------------------- $metaOps = @("rename", "set-synonym", "set-comment") $touchesModel = ($metaOps -notcontains $Operation) Assert-SiblingsPresent $Operation Write-Host "Пакет: $pkgName" if ($Operation -eq "rename") { if (-not $Value) { throw "rename требует -Value <НовоеИмя>" } Rename-Package $Value $pkgName = $Value $pkgDir = Join-Path $xdtoRoot $Value } elseif ($Operation -eq "set-synonym") { if (-not $Value) { throw "set-synonym требует -Value <текст>" } Edit-Metadata "Synonym" $Value Write-Host "✓ Синоним: $Value" } elseif ($Operation -eq "set-comment") { Edit-Metadata "Comment" $Value Write-Host "✓ Комментарий обновлён" } else { $tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) ("xdto-edit_" + [guid]::NewGuid().ToString("N").Substring(0, 8)) New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null try { $xsdPath = Join-Path $tmpDir "schema.xsd" Invoke-Sibling $decompileScript @("-PackagePath", $binFile, "-OutFile", $xsdPath) "xdto-decompile" | Out-Null $doc = New-Object System.Xml.XmlDocument $doc.PreserveWhitespace = $false $doc.Load($xsdPath) $schema = $doc.DocumentElement $oldNamespace = $schema.GetAttribute("targetNamespace") Apply-ModelOperation $schema $doc.Save($xsdPath) Invoke-Sibling $compileScript @("-XsdPath", $xsdPath, "-OutputDir", $configRoot, "-Name", $pkgName, "-Force") "xdto-compile" | Out-Null if ($Operation -eq "set-namespace") { Edit-Metadata "Namespace" $Value # Зависящие пакеты не трогаем: при версионировании они обязаны продолжать # смотреть на прежний namespace. Но молчать о них нельзя. $dependents = @() foreach ($d in (Get-ChildItem $xdtoRoot -Directory -ErrorAction SilentlyContinue)) { if ($d.Name -eq $pkgName) { continue } $ob = Join-Path (Join-Path $d.FullName "Ext") "Package.bin" if (-not (Test-Path $ob)) { continue } try { $od = New-Object System.Xml.XmlDocument $od.Load($ob) foreach ($imp in $od.DocumentElement.ChildNodes) { if ($imp.NodeType -eq [System.Xml.XmlNodeType]::Element -and $imp.get_LocalName() -eq "import" -and $imp.GetAttribute("namespace") -eq $oldNamespace) { $dependents += $d.Name; break } } } catch {} } if ($dependents.Count -gt 0) { Write-Host "" Write-Warning ("Старый namespace импортируют пакеты ($($dependents.Count)): " + ($dependents -join ", ") + ". Они не изменены — при версионировании это верно; если нет, поправьте их импорты.") } } Write-Host "✓ Пакет пересобран: XDTOPackages/$pkgName/Ext/Package.bin" } finally { Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue } } # --- Validate ------------------------------------------------------------------- if (-not $NoValidate) { if (Test-Path $validateScript) { Write-Host "" Write-Host "--- xdto-validate ---" & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $validateScript -PackagePath (Join-Path $xdtoRoot $pkgName) } else { Write-Host "[SKIP] xdto-validate не найден: $validateScript" } } exit 0