Files
llm/scripts/compare_1c_saved_state_objects.ps1

462 lines
17 KiB
PowerShell

param(
[string]$Server = $env:ONEC_SQL_SERVER,
[string]$Database = $env:ONEC_SQL_DATABASE,
[string]$User = $env:ONEC_SQL_USER,
[string]$Password = $env:ONEC_SQL_PASSWORD,
[string]$BaseMetadataDir = "reports\1c-sql\upo\structured-metadata-all-kinds",
[string]$ExtensionGuidIndex = "reports\1c-sql\upo\xml-guid-index-extensions.json",
[string]$Output
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
if (-not $Server) { throw "Server is required. Use -Server or ONEC_SQL_SERVER." }
if (-not $Database) { throw "Database is required. Use -Database or ONEC_SQL_DATABASE." }
if (-not $User) { throw "User is required. Use -User or ONEC_SQL_USER." }
if (-not $Password) { throw "Password is required. Use -Password or ONEC_SQL_PASSWORD." }
function U {
param([string]$Base64)
return [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Base64))
}
$ruExtension = U "0KDQsNGB0YjQuNGA0LXQvdC40LU="
$ruConfigObject = U "0J7QsdGK0LXQutGC0JrQvtC90YTQuNCz0YPRgNCw0YbQuNC4"
$ruForm = U "0KTQvtGA0LzQsA=="
$kindRu = @{
Configuration = (U "0JrQvtC90YTQuNCz0YPRgNCw0YbQuNGP")
CommonModule = (U "0J7QsdGJ0LjQudCc0L7QtNGD0LvRjA==")
CommonForm = (U "0J7QsdGJ0LDRj9Ck0L7RgNC80LA=")
CommonCommand = (U "0J7QsdGJ0LDRj9Ca0L7QvNCw0L3QtNCw")
CommonAttribute = (U "0J7QsdGJ0LjQudCg0LXQutCy0LjQt9C40YI=")
CommonTemplate = (U "0J7QsdGJ0LjQudCc0LDQutC10YI=")
CommonPicture = (U "0J7QsdGJ0LDRj9Ca0LDRgNGC0LjQvdC60LA=")
Catalog = (U "0KHQv9GA0LDQstC+0YfQvdC40Lo=")
Document = (U "0JTQvtC60YPQvNC10L3Rgg==")
DataProcessor = (U "0J7QsdGA0LDQsdC+0YLQutCw")
Report = (U "0J7RgtGH0LXRgg==")
InformationRegister = (U "0KDQtdCz0LjRgdGC0YDQodCy0LXQtNC10L3QuNC5")
AccumulationRegister = (U "0KDQtdCz0LjRgdGC0YDQndCw0LrQvtC/0LvQtdC90LjRjw==")
AccountingRegister = (U "0KDQtdCz0LjRgdGC0YDQkdGD0YXQs9Cw0LvRgtC10YDQuNC4")
CalculationRegister = (U "0KDQtdCz0LjRgdGC0YDQoNCw0YHRh9C10YLQsA==")
Enum = (U "0J/QtdGA0LXRh9C40YHQu9C10L3QuNC1")
Form = $ruForm
Template = (U "0JzQsNC60LXRgg==")
Role = (U "0KDQvtC70Yw=")
Subsystem = (U "0J/QvtC00YHQuNGB0YLQtdC80LA=")
ExchangePlan = (U "0J/Qu9Cw0L3QntCx0LzQtdC90LA=")
BusinessProcess = (U "0JHQuNC30L3QtdGB0J/RgNC+0YbQtdGB0YE=")
Task = (U "0JfQsNC00LDRh9Cw")
Constant = (U "0JrQvtC90YHRgtCw0L3RgtCw")
ChartOfCharacteristicTypes = (U "0J/Qu9Cw0L3QktC40LTQvtCy0KXQsNGA0LDQutGC0LXRgNC40YHRgtC40Lo=")
ChartOfAccounts = (U "0J/Qu9Cw0L3QodGH0LXRgtC+0LI=")
ChartOfCalculationTypes = (U "0J/Qu9Cw0L3QktC40LTQvtCy0KDQsNGB0YfQtdGC0LA=")
}
$folderKind = @{
CommonModules = "CommonModule"
CommonForms = "CommonForm"
CommonCommands = "CommonCommand"
CommonAttributes = "CommonAttribute"
CommonTemplates = "CommonTemplate"
CommonPictures = "CommonPicture"
Catalogs = "Catalog"
Documents = "Document"
DataProcessors = "DataProcessor"
Reports = "Report"
InformationRegisters = "InformationRegister"
AccumulationRegisters = "AccumulationRegister"
AccountingRegisters = "AccountingRegister"
CalculationRegisters = "CalculationRegister"
Enums = "Enum"
Forms = "Form"
Templates = "Template"
Roles = "Role"
Subsystems = "Subsystem"
ExchangePlans = "ExchangePlan"
BusinessProcesses = "BusinessProcess"
Tasks = "Task"
Constants = "Constant"
ChartsOfCharacteristicTypes = "ChartOfCharacteristicTypes"
ChartsOfAccounts = "ChartOfAccounts"
ChartsOfCalculationTypes = "ChartOfCalculationTypes"
}
function Convert-RefBytesToGuid {
param([byte[]]$Bytes)
if ($Bytes.Length -ne 16) { return $null }
$hex = ($Bytes | ForEach-Object { $_.ToString("x2") }) -join ""
return "{0}-{1}-{2}-{3}-{4}" -f $hex.Substring(24, 8), $hex.Substring(20, 4), $hex.Substring(16, 4), $hex.Substring(0, 4), $hex.Substring(4, 12)
}
function Convert-ToSha256Hex {
param([byte[]]$Bytes)
$sha = [System.Security.Cryptography.SHA256]::Create()
try {
return (($sha.ComputeHash($Bytes) | ForEach-Object { $_.ToString("x2") }) -join "")
}
finally {
$sha.Dispose()
}
}
function Join-ByteArrays {
param([object[]]$Rows)
$ordered = @($Rows | Sort-Object part_no)
$total = 0
foreach ($row in $ordered) { $total += $row.bytes.Length }
$buffer = [byte[]]::new($total)
$offset = 0
foreach ($row in $ordered) {
[Array]::Copy($row.bytes, 0, $buffer, $offset, $row.bytes.Length)
$offset += $row.bytes.Length
}
return $buffer
}
function New-Connection {
$connectionString = "Server=$Server;Database=$Database;User ID=$User;Password=$Password;Encrypt=False;TrustServerCertificate=True;MultipleActiveResultSets=True;Application Name=Codex 1C Saved State Object Compare;"
$connection = [System.Data.SqlClient.SqlConnection]::new($connectionString)
$connection.Open()
return $connection
}
function Read-TableFiles {
param(
[System.Data.SqlClient.SqlConnection]$Connection,
[string]$Table,
[string[]]$FileNames
)
$command = $Connection.CreateCommand()
if ($FileNames -and $FileNames.Count -gt 0) {
$placeholders = @()
for ($i = 0; $i -lt $FileNames.Count; $i++) {
$paramName = "@p$i"
$placeholders += $paramName
$null = $command.Parameters.Add($paramName, [System.Data.SqlDbType]::NVarChar, 512)
$command.Parameters[$paramName].Value = $FileNames[$i]
}
$command.CommandText = "SELECT FileName, PartNo, BinaryData FROM dbo.[$Table] WHERE FileName IN ($($placeholders -join ', ')) ORDER BY FileName, PartNo"
}
else {
$command.CommandText = "SELECT FileName, PartNo, BinaryData FROM dbo.[$Table] ORDER BY FileName, PartNo"
}
$reader = $null
$groups = @{}
try {
$reader = $command.ExecuteReader()
while ($reader.Read()) {
$fileName = [string]$reader.GetValue(0)
if (-not $groups.ContainsKey($fileName)) { $groups[$fileName] = @() }
$groups[$fileName] += [pscustomobject]@{
part_no = [int]$reader.GetValue(1)
bytes = [byte[]]$reader.GetValue(2)
}
}
}
finally {
if ($reader -ne $null) { $reader.Dispose() }
if ($command -ne $null) { $command.Dispose() }
}
$result = @{}
foreach ($fileName in $groups.Keys) {
$bytes = Join-ByteArrays @($groups[$fileName])
$result[$fileName] = [pscustomobject]@{
file_name = $fileName
chunks = @($groups[$fileName]).Count
bytes = $bytes.Length
sha256 = Convert-ToSha256Hex $bytes
}
}
return $result
}
function Read-Extensions {
param([System.Data.SqlClient.SqlConnection]$Connection)
$command = $Connection.CreateCommand()
$command.CommandText = "SELECT [_IDRRef], [_ExtName], [_ExtensionOrder], [_ExtensionUsePurpose], [_ExtensionScope] FROM dbo.[_ExtensionsInfo] ORDER BY [_ExtensionOrder], [_ExtName]"
$reader = $null
$map = @{}
try {
$reader = $command.ExecuteReader()
while ($reader.Read()) {
$bytes = [byte[]]$reader.GetValue(0)
$guid = Convert-RefBytesToGuid $bytes
$map[$guid] = [pscustomobject]@{
guid = $guid
name = [string]$reader.GetValue(1)
order = [decimal]$reader.GetValue(2)
use_purpose = [decimal]$reader.GetValue(3)
scope = [decimal]$reader.GetValue(4)
}
}
}
finally {
if ($reader -ne $null) { $reader.Dispose() }
if ($command -ne $null) { $command.Dispose() }
}
return $map
}
function Get-BaseObjectInfo {
param([string]$Guid)
$metadataRoot = [System.IO.Path]::GetFullPath($BaseMetadataDir)
if (-not (Test-Path -LiteralPath $metadataRoot)) { return $null }
$match = Get-ChildItem -LiteralPath $metadataRoot -Recurse -Filter *.json -File |
Select-String -SimpleMatch $Guid -List |
Select-Object -First 1
if (-not $match) { return $null }
$data = Get-Content -LiteralPath $match.Path -Raw -Encoding UTF8 | ConvertFrom-Json
$kind = [string]$data.kind
$name = [string]$data.identity.name
$synonym = $null
if ($data.identity.synonyms -and $data.identity.synonyms.ru) { $synonym = [string]$data.identity.synonyms.ru }
return [pscustomobject]@{
layer = "base"
kind = $kind
kind_ru = if ($kindRu.ContainsKey($kind)) { $kindRu[$kind] } else { $kind }
name = $name
synonym = $synonym
full_name = "$(if ($kindRu.ContainsKey($kind)) { $kindRu[$kind] } else { $kind }).$name"
guid = $Guid
evidence = [pscustomobject]@{
metadata_file = $match.Path
xml_file = $data.xml_file
}
}
}
function Get-ExtensionGuidMap {
$path = [System.IO.Path]::GetFullPath($ExtensionGuidIndex)
if (-not (Test-Path -LiteralPath $path)) { return @{} }
$data = Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json
return $data.guid_map
}
function Get-ExtensionObjectInfo {
param(
[string]$ExtensionName,
[string]$ObjectGuid,
[object]$GuidMap
)
$entry = $GuidMap.$ObjectGuid
$top = $null
if ($entry -and $entry.top_objects -and @($entry.top_objects).Count -gt 0) {
$top = @($entry.top_objects)[0]
}
$relativePath = if ($top) { [string]$top.relative_path } else { $null }
$parts = if ($relativePath) { $relativePath -split "[\\/]" } else { @() }
$parentKind = $null
$parentName = $null
$artifactKind = if ($top) { [string]$top.xml_kind } else { "ConfigCASObject" }
$artifactName = if ($top) { [string]$top.name } else { $ObjectGuid }
$synonym = if ($top -and $top.synonym) { [string]$top.synonym } else { $null }
for ($i = 0; $i -lt $parts.Count - 1; $i++) {
if ($folderKind.ContainsKey($parts[$i]) -and $parts[$i] -ne "Forms" -and $parts[$i] -ne "Templates") {
$parentKind = $folderKind[$parts[$i]]
if ($i + 1 -lt $parts.Count) { $parentName = $parts[$i + 1] }
break
}
}
$artifactKindRu = if ($kindRu.ContainsKey($artifactKind)) { $kindRu[$artifactKind] } else { $artifactKind }
$parentKindRu = if ($parentKind -and $kindRu.ContainsKey($parentKind)) { $kindRu[$parentKind] } else { $parentKind }
$fullName = if ($parentKind -and $artifactKind -eq "Form") {
"$ruExtension.$ExtensionName.$parentKindRu.$parentName.$ruForm.$artifactName"
}
elseif ($parentKind) {
"$ruExtension.$ExtensionName.$parentKindRu.$parentName.$artifactKindRu.$artifactName"
}
else {
"$ruExtension.$ExtensionName.$artifactKindRu.$artifactName"
}
return [pscustomobject]@{
layer = "extension"
extension = $ExtensionName
parent_kind = $parentKind
parent_kind_ru = $parentKindRu
parent_name = $parentName
kind = $artifactKind
kind_ru = $artifactKindRu
name = $artifactName
synonym = $synonym
full_name = $fullName
guid = $ObjectGuid
relative_path = $relativePath
evidence = [pscustomobject]@{
xml_path = if ($top) { [string]$top.path } else { $null }
}
}
}
function Split-ObjectPart {
param([string]$FileName)
if ($FileName -match '^([0-9a-fA-F-]{36})(\..+)?$') {
return [pscustomobject]@{ object_id = $Matches[1].ToLowerInvariant(); suffix = if ($Matches[2]) { $Matches[2] } else { "" } }
}
return $null
}
function Split-ExtensionFileName {
param([string]$FileName)
if ($FileName -match '^([0-9a-fA-F-]{36})__(.+)$') {
$rest = $Matches[2]
if ($rest -eq "configinfo") {
return [pscustomobject]@{ extension_id = $Matches[1].ToLowerInvariant(); object_id = $null; suffix = ""; system_name = "configinfo" }
}
$part = Split-ObjectPart $rest
return [pscustomobject]@{ extension_id = $Matches[1].ToLowerInvariant(); object_id = $part.object_id; suffix = $part.suffix; system_name = $null }
}
return $null
}
function New-StorageEvidence {
param(
[string]$SavedTable,
[string]$ActiveTable,
[string]$FileName,
[object]$Saved,
[object]$Active
)
return [pscustomobject]@{
saved_table = $SavedTable
active_table = $ActiveTable
file_name = $FileName
saved_bytes = $Saved.bytes
active_bytes = if ($Active) { $Active.bytes } else { $null }
saved_sha256 = $Saved.sha256
active_sha256 = if ($Active) { $Active.sha256 } else { $null }
active_exists = [bool]$Active
changed = (-not $Active) -or ($Saved.sha256 -ne $Active.sha256) -or ($Saved.bytes -ne $Active.bytes)
}
}
$connection = New-Connection
try {
$configSave = Read-TableFiles $connection "ConfigSave" $null
$configCassave = Read-TableFiles $connection "ConfigCASSave" $null
$configActive = Read-TableFiles $connection "Config" @($configSave.Keys)
$configCasActive = Read-TableFiles $connection "ConfigCAS" @($configCassave.Keys)
$extensions = Read-Extensions $connection
}
finally {
if ($connection -ne $null) { $connection.Dispose() }
}
$objectChangesByKey = @{}
$systemChanges = @()
foreach ($fileName in $configSave.Keys) {
$saved = $configSave[$fileName]
$active = if ($configActive.ContainsKey($fileName)) { $configActive[$fileName] } else { $null }
$evidence = New-StorageEvidence "ConfigSave" "Config" $fileName $saved $active
if (-not $evidence.changed) { continue }
$part = Split-ObjectPart $fileName
if (-not $part) {
$systemChanges += [pscustomobject]@{ layer = "base"; name = $fileName; storage = $evidence }
continue
}
$info = Get-BaseObjectInfo $part.object_id
if (-not $info) {
$info = [pscustomobject]@{
layer = "base"
kind = "ConfigObject"
kind_ru = $ruConfigObject
name = $part.object_id
synonym = $null
full_name = "$ruConfigObject.$($part.object_id)"
guid = $part.object_id
evidence = $null
}
}
$key = "base::$($part.object_id)"
if (-not $objectChangesByKey.ContainsKey($key)) {
$objectChangesByKey[$key] = [ordered]@{
layer = $info.layer
kind = $info.kind
kind_ru = $info.kind_ru
name = $info.name
synonym = $info.synonym
full_name = $info.full_name
guid = $info.guid
change_state = "saved_not_applied"
storage = @()
evidence = $info.evidence
}
}
$objectChangesByKey[$key].storage += $evidence
}
$extensionGuidMap = Get-ExtensionGuidMap
foreach ($fileName in $configCassave.Keys) {
$saved = $configCassave[$fileName]
$active = if ($configCasActive.ContainsKey($fileName)) { $configCasActive[$fileName] } else { $null }
$evidence = New-StorageEvidence "ConfigCASSave" "ConfigCAS" $fileName $saved $active
if (-not $evidence.changed) { continue }
$part = Split-ExtensionFileName $fileName
if (-not $part) {
$systemChanges += [pscustomobject]@{ layer = "extension"; name = $fileName; storage = $evidence }
continue
}
$extension = if ($extensions.ContainsKey($part.extension_id)) { $extensions[$part.extension_id] } else { $null }
$extensionName = if ($extension) { $extension.name } else { $part.extension_id }
if ($part.system_name) {
$systemChanges += [pscustomobject]@{ layer = "extension"; extension = $extensionName; name = $part.system_name; storage = $evidence }
continue
}
$info = Get-ExtensionObjectInfo $extensionName $part.object_id $extensionGuidMap
$key = "extension::$($part.extension_id)::$($part.object_id)"
if (-not $objectChangesByKey.ContainsKey($key)) {
$objectChangesByKey[$key] = [ordered]@{
layer = $info.layer
extension = $info.extension
parent_kind = $info.parent_kind
parent_kind_ru = $info.parent_kind_ru
parent_name = $info.parent_name
kind = $info.kind
kind_ru = $info.kind_ru
name = $info.name
synonym = $info.synonym
full_name = $info.full_name
guid = $info.guid
relative_path = $info.relative_path
change_state = "saved_not_applied"
storage = @()
evidence = $info.evidence
}
}
$objectChangesByKey[$key].storage += $evidence
}
$objectChanges = @($objectChangesByKey.Values | ForEach-Object { [pscustomobject]$_ } | Sort-Object layer, extension, full_name)
$result = [pscustomobject]@{
schema = "onec_saved_state_object_comparison.v1"
server = $Server
database = $Database
view = "saved_not_applied_vs_active"
object_changes = $objectChanges
system_changes = $systemChanges
counts = [pscustomobject]@{
object_changes = $objectChanges.Count
system_changes = $systemChanges.Count
config_save_files = $configSave.Count
config_cas_save_files = $configCassave.Count
}
safety = [pscustomobject]@{
read_only = $true
sql_write_performed = $false
exposes_storage_evidence = $true
public_terms_are_1c_objects = $true
}
}
$json = $result | ConvertTo-Json -Depth 20
if ($Output) {
$parent = Split-Path -Parent $Output
if ($parent) { New-Item -ItemType Directory -Force -Path $parent | Out-Null }
$json | Set-Content -LiteralPath $Output -Encoding UTF8
}
$json