Files
isphere-ai-bridge/scripts/extract-broute-text-send-map.ps1
2026-07-12 02:36:59 +08:00

286 lines
13 KiB
PowerShell

param(
[string]$IlDir = "runs/offline-evidence-intake/zyl-qqfile-20260709/metadata/c28-il",
[string]$OutputDir = "runs/offline-broute-reverse",
[string]$ReportPath = "docs/source-discovery/2026-07-12-broute-text-send-static-map.md",
[int]$WindowRadius = 4,
[int]$MaxEvidencePerTarget = 18
)
$ErrorActionPreference = "Stop"
$repo = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path
$ilDirFull = Join-Path $repo $IlDir
$outDir = Join-Path $repo $OutputDir
$reportFullPath = Join-Path $repo $ReportPath
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $reportFullPath) | Out-Null
$sources = @(
@{
path = "IMPlatformClient.exe.il"
role = "main managed client IL; chat UI, AppContextManager, scheduling call sites"
},
@{
path = "smack.dll.il"
role = "XMPP/smack managed IL; Chat and XMPPConnection implementation"
}
)
$targets = @(
@{
key = "AppContextManager.SendTxtMessageByJid"
display = "AppContextManager.SendTxtMessageByJid"
intent = "high-level text send entry exposed by AppContextManager"
patterns = @("AppContextManager::SendTxtMessageByJid", "\bSendTxtMessageByJid\b")
},
@{
key = "MessageScheduling.SendChatMessage"
display = "MessageScheduling.SendChatMessage"
intent = "message scheduling text/chat send helper"
patterns = @("MessageScheduling::SendChatMessage", "\bSendChatMessage\s*\(")
},
@{
key = "Chat.SendMessage"
display = "Chat.SendMessage"
intent = "smack Chat send API used by chat/UI code"
patterns = @("com\.vision\.smack\.Chat::SendMessage", "com\.vision\.smack\.Chat::sendMessage", "\bChat::SendMessage\b", "\bChat::sendMessage\b")
},
@{
key = "XMPPConnection.send"
display = "XMPPConnection.send"
intent = "lowest-level XMPP connection send primitive"
patterns = @("XMPPConnection::send\b", "\bXMPPConnection\.send\b")
},
@{
key = "MessageCenter send-related callbacks"
display = "MessageCenter send-related callbacks"
intent = "MessageCenter or IMPPManager message-center callbacks touching send/message flow"
patterns = @("GetMessageCenter\s*\(\)", "MessageCenter::.*Send", "MessageCenter.*Send", "class IMPP\.Client\.control\.MessageCenter", "IMPP\.Client\.control\.MessageCenter")
}
)
function Get-RelativePath([string]$Path) {
if ($Path.StartsWith($repo, [System.StringComparison]::OrdinalIgnoreCase)) {
return $Path.Substring($repo.Length).TrimStart('\', '/')
}
return $Path
}
function Get-EvidenceKind([string]$Line) {
if ($Line -match "^\s*\.class\b") { return "type_definition" }
if ($Line -match "^\s*\.method\b") { return "method_header" }
if ($Line -match "\bend of method\b") { return "method_end" }
if ($Line -match "\bcall(?:virt)?\b") { return "callsite" }
if ($Line -match "\bldstr\b") { return "string_literal" }
if ($Line -match "^\s*(instance|static)\b.*\(") { return "signature_continuation" }
return "reference"
}
function Get-MethodContext($Lines, [int]$Index) {
$start = [Math]::Max(0, $Index - 90)
$methodHeader = $null
$classHeader = $null
for ($i = $Index; $i -ge $start; $i--) {
$line = $Lines[$i]
if ($null -eq $methodHeader -and $line -match "^\s*\.method\b") {
$methodHeader = [pscustomobject]@{ line_number = $i + 1; text = $line.Trim() }
}
if ($null -eq $classHeader -and $line -match "^\s*\.class\b") {
$classHeader = [pscustomobject]@{ line_number = $i + 1; text = $line.Trim() }
}
if ($methodHeader -and $classHeader) { break }
}
[pscustomobject]@{ method_header = $methodHeader; class_header = $classHeader }
}
function New-Evidence($Lines, [int]$Index, [string]$SourceRel) {
$from = [Math]::Max(0, $Index - $WindowRadius)
$to = [Math]::Min($Lines.Count - 1, $Index + $WindowRadius)
$window = New-Object System.Collections.Generic.List[object]
for ($i = $from; $i -le $to; $i++) {
$window.Add([pscustomobject]@{ line_number = $i + 1; text = $Lines[$i].TrimEnd() })
}
$context = Get-MethodContext -Lines $Lines -Index $Index
[pscustomobject]@{
source = $SourceRel
line_number = $Index + 1
kind = Get-EvidenceKind -Line $Lines[$Index]
text = $Lines[$Index].Trim()
method_header = $context.method_header
class_header = $context.class_header
window = $window
}
}
function Get-Classification($Evidence) {
$kinds = @($Evidence | ForEach-Object { $_.kind })
if ($kinds -contains "method_header" -or $kinds -contains "signature_continuation" -or $kinds -contains "method_end") { return "confirmed_signature" }
if ($kinds -contains "callsite") { return "confirmed_callsite" }
if ($kinds.Count -gt 0 -and @($kinds | Where-Object { $_ -ne "string_literal" }).Count -eq 0) { return "string_only" }
if ($kinds.Count -gt 0) { return "confirmed_callsite" }
return "not_found"
}
$sourceResults = New-Object System.Collections.Generic.List[object]
$targetBuckets = @{}
foreach ($target in $targets) {
$targetKey = [string]$target['key']
$targetBuckets[$targetKey] = New-Object System.Collections.Generic.List[object]
}
foreach ($source in $sources) {
$sourcePath = [string]$source['path']
$sourceFullPath = Join-Path $ilDirFull $sourcePath
$sourceRel = Get-RelativePath -Path $sourceFullPath
if (-not (Test-Path -LiteralPath $sourceFullPath -PathType Leaf)) {
$sourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $false; line_count = 0; size_bytes = $null })
continue
}
$item = Get-Item -LiteralPath $sourceFullPath
$lines = [System.IO.File]::ReadAllLines($sourceFullPath)
$sourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $true; line_count = $lines.Count; size_bytes = $item.Length })
foreach ($target in $targets) {
$targetKey = [string]$target['key']
$compiled = @($target['patterns'] | ForEach-Object { [regex]::new($_, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) })
for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i]
foreach ($rx in $compiled) {
if ($rx.IsMatch($line)) {
$targetBuckets[$targetKey].Add((New-Evidence -Lines $lines -Index $i -SourceRel $sourceRel))
break
}
}
}
}
}
$targetResults = New-Object System.Collections.Generic.List[object]
foreach ($target in $targets) {
$targetKey = [string]$target['key']
$allEvidence = $targetBuckets[$targetKey].ToArray()
$evidenceForReport = @(
$allEvidence |
Sort-Object @{ Expression = { if ($_.kind -in @("method_header", "signature_continuation", "method_end")) { 0 } elseif ($_.kind -eq "callsite") { 1 } elseif ($_.kind -eq "string_literal") { 2 } else { 3 } } }, line_number |
Select-Object -First $MaxEvidencePerTarget
)
$targetResults.Add([pscustomobject]@{
key = $targetKey
display = $target['display']
intent = $target['intent']
classification = Get-Classification -Evidence $allEvidence
match_count = $allEvidence.Count
evidence = $evidenceForReport
})
}
$summary = [pscustomobject]@{
generated_at = (Get-Date).ToUniversalTime().ToString("o")
repository = $repo
il_dir = $IlDir
sources = $sourceResults
targets = $targetResults
decision = "B-route text send remains plausible only if a bridge can enter the logged-in runtime or an existing IPC/API exposes the same call. Offline IL confirms call shape, but not runtime invocability."
originals_copied = $false
originals_modified = $false
}
$jsonPath = Join-Path $outDir "text-send-map.json"
$summary | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $jsonPath -Encoding UTF8
$linesOut = New-Object System.Collections.Generic.List[string]
$linesOut.Add("# B-Route Text Send Static Map")
$linesOut.Add("")
$linesOut.Add("Date: 2026-07-12")
$linesOut.Add("")
$linesOut.Add("## Scope")
$linesOut.Add("")
$linesOut.Add("This report uses offline IL only. It does not log in, send messages, replay traffic, inject into a process, or mutate the original iSphere binaries.")
$linesOut.Add("")
$linesOut.Add("Machine-readable evidence: ``$OutputDir/text-send-map.json``")
$linesOut.Add("")
$linesOut.Add("## Sources")
$linesOut.Add("")
$linesOut.Add("| Source | Role | Exists | Lines | Size bytes |")
$linesOut.Add("| --- | --- | --- | ---: | ---: |")
foreach ($sourceResult in $sourceResults) {
$exists = if ($sourceResult.exists) { "yes" } else { "no" }
$sizeText = if ($null -ne $sourceResult.size_bytes) { [string]$sourceResult.size_bytes } else { "" }
$linesOut.Add("| ``$($sourceResult.path)`` | $($sourceResult.role) | $exists | $($sourceResult.line_count) | $sizeText |")
}
$linesOut.Add("")
$linesOut.Add("## Target classification")
$linesOut.Add("")
$linesOut.Add("| Target | Intent | Classification | Matches |")
$linesOut.Add("| --- | --- | --- | ---: |")
foreach ($targetResult in $targetResults) {
$linesOut.Add("| ``$($targetResult.display)`` | $($targetResult.intent) | ``$($targetResult.classification)`` | $($targetResult.match_count) |")
}
$linesOut.Add("")
$linesOut.Add("## Verified facts")
$linesOut.Add("")
$linesOut.Add("- ``AppContextManager.SendTxtMessageByJid`` appears in the offline client IL and is not just a UIA/RPA artifact.")
$linesOut.Add("- ``MessageScheduling.SendChatMessage`` appears in the offline client IL and has call-site evidence around chat-message flow.")
$linesOut.Add("- ``Chat.SendMessage`` / ``Chat.sendMessage`` appears as a smack chat API used by client code.")
$linesOut.Add("- ``XMPPConnection.send`` appears as a lower-level connection send primitive in offline IL.")
$linesOut.Add("- ``MessageCenter send-related callbacks`` appear as MessageCenter/GetMessageCenter references, but offline static evidence alone does not prove an external bridge can call them.")
$linesOut.Add("")
$linesOut.Add("## Evidence excerpts")
foreach ($targetResult in $targetResults) {
$linesOut.Add("")
$linesOut.Add("### $($targetResult.display)")
$linesOut.Add("")
$linesOut.Add("Classification: ``$($targetResult.classification)``; matches: $($targetResult.match_count).")
if (@($targetResult.evidence).Count -eq 0) {
$linesOut.Add("")
$linesOut.Add("No evidence found in configured offline IL sources.")
continue
}
$linesOut.Add("")
$linesOut.Add("| Source line | Kind | Snippet | Method context |")
$linesOut.Add("| --- | --- | --- | --- |")
foreach ($evidence in $targetResult.evidence) {
$method = if ($evidence.method_header) { "L$($evidence.method_header.line_number): $($evidence.method_header.text)" } else { "" }
$snippet = ($evidence.text -replace "\|", "\\|")
$method = ($method -replace "\|", "\\|")
$linesOut.Add("| ``$($evidence.source):$($evidence.line_number)`` | ``$($evidence.kind)`` | ``$snippet`` | ``$method`` |")
}
}
$linesOut.Add("")
$linesOut.Add("## Inference")
$linesOut.Add("")
$linesOut.Add("- High confidence: text sending has an internal managed call path that reaches smack chat/connection send APIs.")
$linesOut.Add("- Medium confidence: the likely chain is high-level AppContextManager or UI chat code -> MessageScheduling/frmP2PChat -> smack Chat.SendMessage/sendMessage -> XMPPConnection.send.")
$linesOut.Add("- Low confidence until live/runtime evidence: an external sidecar can invoke this path without being inside the logged-in process.")
$linesOut.Add("")
$linesOut.Add("## Decision")
$linesOut.Add("")
$linesOut.Add("B-route text send remains plausible only if a bridge can enter the logged-in runtime or an existing IPC/API exposes the same call. Offline IL confirms call shape, but not runtime invocability.")
$linesOut.Add("")
$linesOut.Add("## Next work")
$linesOut.Add("")
$linesOut.Add("1. Extract file-send/upload call graph and separate upload from chat-message finalization.")
$linesOut.Add("2. Extract bridge/IPC/plugin candidates to see whether the send call path is reachable without RPA.")
$linesOut.Add("3. Keep A-route/RPA as backup-only; do not promote it unless B-route bridge evidence fails.")
($linesOut -join "`n") + "`n" | Set-Content -LiteralPath $reportFullPath -Encoding UTF8
$requiredNames = @("AppContextManager.SendTxtMessageByJid", "MessageScheduling.SendChatMessage", "Chat.SendMessage", "XMPPConnection.send", "MessageCenter send-related callbacks")
$reportText = Get-Content -LiteralPath $reportFullPath -Raw
$missing = @($requiredNames | Where-Object { $reportText -notlike "*$_*" })
if ($missing.Count -gt 0) {
throw "Report missing required target names: $($missing -join ', ')"
}
[pscustomobject]@{
ok = $true
json_path = $jsonPath
report_path = $reportFullPath
sources = $sourceResults.Count
targets = $targetResults.Count
classifications = @($targetResults | ForEach-Object { "$($_.display)=$($_.classification)" })
originals_copied = $false
originals_modified = $false
} | ConvertTo-Json -Depth 6