docs: add offline b-route reverse decision
This commit is contained in:
342
scripts/extract-broute-file-send-map.ps1
Normal file
342
scripts/extract-broute-file-send-map.ps1
Normal file
@@ -0,0 +1,342 @@
|
||||
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-file-send-static-map.md",
|
||||
[int]$WindowRadius = 5,
|
||||
[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 client IL; UC_FileSend, OffLineFileSend, MessageScheduling" },
|
||||
@{ path = "IMPP.Service.dll.il"; role = "file transfer service implementation" },
|
||||
@{ path = "IMPP.ServiceBase.dll.il"; role = "file transfer contracts, parameters, and result DTOs" },
|
||||
@{ path = "IMPP.Interface.dll.il"; role = "transport/event interface contracts" },
|
||||
@{ path = "smack.dll.il"; role = "Chat.sendFileMessage and XMPP message construction" },
|
||||
@{ path = "TcpFileTransfer.dll.il"; role = "native TCP transfer candidate; IL may be unavailable" },
|
||||
@{ path = "IMPP.Model.dll.il"; role = "chat record models if extracted" }
|
||||
)
|
||||
|
||||
$targets = @(
|
||||
@{
|
||||
key = "IMPP.Client.OffLineFileSend.sendFile"
|
||||
display = "IMPP.Client.OffLineFileSend.sendFile"
|
||||
phase = "upload"
|
||||
intent = "offline file upload orchestration from UI file sender"
|
||||
patterns = @("OffLineFileSend::sendFile", "^\s*sendFile\(\) cil managed")
|
||||
},
|
||||
@{
|
||||
key = "IMPP.Client.OffLineFileSend.trans_UploadCompleted"
|
||||
display = "IMPP.Client.OffLineFileSend.trans_UploadCompleted"
|
||||
phase = "upload_to_message_boundary"
|
||||
intent = "upload-completed handler that converts FileUploadResult.FileID into a chat file message"
|
||||
patterns = @("OffLineFileSend::trans_UploadCompleted", "trans_UploadCompleted\(string fileId\)")
|
||||
},
|
||||
@{
|
||||
key = "IMPP.Service.BLL.FileTransfer.FileUpload"
|
||||
display = "IMPP.Service.BLL.FileTransfer.FileUpload"
|
||||
phase = "upload"
|
||||
intent = "synchronous service upload entry returning FileUploadResult"
|
||||
patterns = @("FileTransfer::FileUpload", "FileUpload\(class \[IMPP\.ServiceBase\].*FileUploadPara")
|
||||
},
|
||||
@{
|
||||
key = "IMPP.Service.BLL.FileTransfer.AsynFileUpload"
|
||||
display = "IMPP.Service.BLL.FileTransfer.AsynFileUpload"
|
||||
phase = "upload"
|
||||
intent = "asynchronous upload queue entry"
|
||||
patterns = @("FileTransfer::AsynFileUpload", "AsynFileUpload\(class \[IMPP\.ServiceBase\].*FileUploadPara")
|
||||
},
|
||||
@{
|
||||
key = "FileUploadPara"
|
||||
display = "FileUploadPara"
|
||||
phase = "upload_parameters"
|
||||
intent = "host/port/domain/local file path/cache/timeout/upload flags"
|
||||
patterns = @("FileUploadPara", "FileUploadPara::\.ctor")
|
||||
},
|
||||
@{
|
||||
key = "FileUploadResult.FileID"
|
||||
display = "FileUploadResult.FileID"
|
||||
phase = "upload_result"
|
||||
intent = "server-side file id and URL returned by upload"
|
||||
patterns = @("FileUploadResult::get_FileID", "FileUploadResult::set_FileID", "FileUploadResult", "get_FileID\(\) cil managed")
|
||||
},
|
||||
@{
|
||||
key = "MessageScheduling.SendFileMessage"
|
||||
display = "MessageScheduling.SendFileMessage"
|
||||
phase = "message"
|
||||
intent = "scheduled/file-message finalization path using RosterContactsArgs and FileTransferArgs"
|
||||
patterns = @("MessageScheduling::SendFileMessage", "SendFileMessage\(class IMPP\.Client\.Business\.RosterContacts\.RosterContactsArgs")
|
||||
},
|
||||
@{
|
||||
key = "Chat.sendFileMessage"
|
||||
display = "Chat.sendFileMessage"
|
||||
phase = "message"
|
||||
intent = "smack API that sends the XMPP file message after upload metadata is known"
|
||||
patterns = @("Chat::sendFileMessage", "P2PChat::sendFileMessage", "ChatRoom::sendFileMessage", "sendFileMessage\(string p_message")
|
||||
},
|
||||
@{
|
||||
key = "Required values: path id size target jid"
|
||||
display = "Required values: path id size target jid"
|
||||
phase = "requirements"
|
||||
intent = "static evidence for local file path, remote folder/file id, file size/name, target JID/chat, and server auth"
|
||||
patterns = @("get_LocalFilePath", "get_RemoteFolder", "get__localFilePath", "get__fileSize", "FileInfo::get_Length", "FileSystemInfo::get_Name", "get_RosterJid", "GetChat\(", "HTTPFileServer", "UnifiedAuthentication", "Auth::get_token")
|
||||
}
|
||||
)
|
||||
|
||||
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 "\bnewobj\b") { return "constructor_call" }
|
||||
if ($Line -match "\bldstr\b") { return "string_literal" }
|
||||
if ($Line -match "^\s*(instance|static|FileUpload|SendFileMessage|sendFileMessage|sendFile|trans_UploadCompleted)\b.*\(") { return "signature_continuation" }
|
||||
return "reference"
|
||||
}
|
||||
|
||||
function Get-MethodContext($Lines, [int]$Index) {
|
||||
$start = [Math]::Max(0, $Index - 120)
|
||||
$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 "type_definition" -or $kinds -contains "method_header" -or $kinds -contains "signature_continuation" -or $kinds -contains "method_end") { return "confirmed_signature" }
|
||||
if ($kinds -contains "callsite" -or $kinds -contains "constructor_call") { 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 @("type_definition", "method_header", "signature_continuation", "method_end")) { 0 } elseif ($_.kind -in @("callsite", "constructor_call")) { 1 } elseif ($_.kind -eq "string_literal") { 2 } else { 3 } } }, source, line_number |
|
||||
Select-Object -First $MaxEvidencePerTarget
|
||||
)
|
||||
$targetResults.Add([pscustomobject]@{
|
||||
key = $targetKey
|
||||
display = $target['display']
|
||||
phase = $target['phase']
|
||||
intent = $target['intent']
|
||||
classification = Get-Classification -Evidence $allEvidence
|
||||
match_count = $allEvidence.Count
|
||||
evidence = $evidenceForReport
|
||||
})
|
||||
}
|
||||
|
||||
$requirements = @(
|
||||
[pscustomobject]@{ value = "local file path"; evidence = "OffLineFileSend constructor receives localFilePath; sendFile uses get__localFilePath; MessageScheduling.SendFileMessage uses FileTransferArgs.get_LocalFilePath."; status = "required_confirmed" },
|
||||
[pscustomobject]@{ value = "server file id / remote folder"; evidence = "Upload returns FileUploadResult.FileID; trans_UploadCompleted(fileId) passes it to Chat.sendFileMessage and Packet.set_id. MessageScheduling path uses FileTransferArgs.get_RemoteFolder."; status = "required_confirmed" },
|
||||
[pscustomobject]@{ value = "file name and file size"; evidence = "SendFileMessage builds OFFLINE_FILE_TRANSFER payload from FileInfo/FileSystemInfo name and length; OffLineFileSend keeps _fileSize."; status = "required_confirmed" },
|
||||
[pscustomobject]@{ value = "target JID / chat object"; evidence = "UC_FileSend and MessageScheduling both require a live smack Chat; scheduled path derives it from RosterContactsArgs.get_RosterJid + UtilsChat.GetChat."; status = "required_confirmed" },
|
||||
[pscustomobject]@{ value = "HTTP file server and auth"; evidence = "FileUploadPara construction uses Config.ServerInfo.HTTPFileServer IP/Port/domain; service upload builds /FS/auth/resource/upload URL with UnifiedAuthentication token/app key/client secret/time/user id."; status = "required_confirmed" },
|
||||
[pscustomobject]@{ value = "file hash"; evidence = "No file-transfer-path MD5/SHA/checksum requirement was confirmed in the configured IL. MD5 hits belong to image/RTF or auth digest areas, not the offline file-send path."; status = "not_confirmed_in_file_send_path" }
|
||||
)
|
||||
|
||||
$summary = [pscustomobject]@{
|
||||
generated_at = (Get-Date).ToUniversalTime().ToString("o")
|
||||
repository = $repo
|
||||
il_dir = $IlDir
|
||||
sources = $sourceResults
|
||||
targets = $targetResults
|
||||
phase_decision = "Offline static evidence shows upload first, then chat file-message finalization. Direct Chat.sendFileMessage alone is not enough unless a valid uploaded file id/remote folder already exists."
|
||||
requirements = $requirements
|
||||
originals_copied = $false
|
||||
originals_modified = $false
|
||||
}
|
||||
|
||||
$jsonPath = Join-Path $outDir "file-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 File 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 upload files, send file messages, replay traffic, inject into a process, or mutate the original iSphere binaries.")
|
||||
$linesOut.Add("")
|
||||
$linesOut.Add("Machine-readable evidence: ``$OutputDir/file-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 | Phase | Intent | Classification | Matches |")
|
||||
$linesOut.Add("| --- | --- | --- | --- | ---: |")
|
||||
foreach ($targetResult in $targetResults) {
|
||||
$linesOut.Add("| ``$($targetResult.display)`` | ``$($targetResult.phase)`` | $($targetResult.intent) | ``$($targetResult.classification)`` | $($targetResult.match_count) |")
|
||||
}
|
||||
$linesOut.Add("")
|
||||
$linesOut.Add("## Upload step vs chat-message step")
|
||||
$linesOut.Add("")
|
||||
$linesOut.Add("| Step | Static evidence | Business meaning |")
|
||||
$linesOut.Add("| --- | --- | --- |")
|
||||
$linesOut.Add("| 1. Upload | ``IMPP.Client.OffLineFileSend.sendFile`` constructs ``FileUploadPara`` and calls ``IFileTransfer.FileUpload``; ``IMPP.Service.BLL.FileTransfer.FileUpload`` delegates to ``SyncFileTransfer`` and private ``Upload``. | The local file must be uploaded to the HTTP file service first. |")
|
||||
$linesOut.Add("| 2. Upload result | ``FileUploadResult.FileID`` is read; upload notification calls ``IMPP.Client.OffLineFileSend.trans_UploadCompleted(fileId)``. | The server-issued file id becomes the durable reference. |")
|
||||
$linesOut.Add("| 3. Chat file message | ``trans_UploadCompleted`` and ``MessageScheduling.SendFileMessage`` build ``OFFLINE_FILE_TRANSFER;...`` payloads and call ``Chat.sendFileMessage``. | The chat send step publishes metadata/reference, not raw file bytes. |")
|
||||
$linesOut.Add("")
|
||||
$linesOut.Add("Conclusion: file send is upload-first, chat-message-second. A direct chat message is insufficient without a valid uploaded file id or remote folder reference.")
|
||||
$linesOut.Add("")
|
||||
$linesOut.Add("## Required values")
|
||||
$linesOut.Add("")
|
||||
$linesOut.Add("| Value | Status | Evidence |")
|
||||
$linesOut.Add("| --- | --- | --- |")
|
||||
foreach ($req in $requirements) {
|
||||
$linesOut.Add("| $($req.value) | ``$($req.status)`` | $($req.evidence) |")
|
||||
}
|
||||
$linesOut.Add("")
|
||||
$linesOut.Add("## Evidence excerpts")
|
||||
foreach ($targetResult in $targetResults) {
|
||||
$linesOut.Add("")
|
||||
$linesOut.Add("### $($targetResult.display)")
|
||||
$linesOut.Add("")
|
||||
$linesOut.Add("Classification: ``$($targetResult.classification)``; phase: ``$($targetResult.phase)``; 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: upload and file-message finalization are separate steps.")
|
||||
$linesOut.Add("- High confidence: ``FileUploadPara`` needs HTTP file server host/port/domain plus local file path and upload options.")
|
||||
$linesOut.Add("- High confidence: ``Chat.sendFileMessage`` produces the XMPP file-message envelope after file metadata is available.")
|
||||
$linesOut.Add("- Medium confidence: B-route file send must either call the same logged-in service/runtime or reproduce the authenticated HTTP upload plus XMPP file-message sequence.")
|
||||
$linesOut.Add("- Low confidence until bridge evidence: an external sidecar can obtain the same service/auth/chat objects without being inside the logged-in client process.")
|
||||
$linesOut.Add("")
|
||||
$linesOut.Add("## Decision")
|
||||
$linesOut.Add("")
|
||||
$linesOut.Add("Offline static evidence supports B-route research, but file sending needs more than one method call: upload first, then chat file-message finalization. The next blocker is reachability: whether an IPC/plugin/in-process bridge can access the logged-in ``IFileTransfer`` service and smack ``Chat`` object.")
|
||||
$linesOut.Add("")
|
||||
$linesOut.Add("## Next work")
|
||||
$linesOut.Add("")
|
||||
$linesOut.Add("1. Extract bridge/IPC/plugin candidates before coding any file-send adapter.")
|
||||
$linesOut.Add("2. If a bridge exists, prototype non-mutating discovery first: service availability, chat object lookup, and upload endpoint metadata only.")
|
||||
$linesOut.Add("3. Keep A-route/RPA backup-only.")
|
||||
|
||||
($linesOut -join "`n") + "`n" | Set-Content -LiteralPath $reportFullPath -Encoding UTF8
|
||||
|
||||
$requiredNames = @("IMPP.Client.OffLineFileSend.sendFile", "IMPP.Client.OffLineFileSend.trans_UploadCompleted", "IMPP.Service.BLL.FileTransfer.FileUpload", "IMPP.Service.BLL.FileTransfer.AsynFileUpload", "FileUploadPara", "sendFileMessage", "Upload step vs chat-message step")
|
||||
$reportText = Get-Content -LiteralPath $reportFullPath -Raw
|
||||
$missing = @($requiredNames | Where-Object { $reportText -notlike "*$_*" })
|
||||
if ($missing.Count -gt 0) {
|
||||
throw "Report missing required names: $($missing -join ', ')"
|
||||
}
|
||||
|
||||
[pscustomobject]@{
|
||||
ok = $true
|
||||
json_path = $jsonPath
|
||||
report_path = $reportFullPath
|
||||
sources = $sourceResults.Count
|
||||
targets = $targetResults.Count
|
||||
classifications = @($targetResults | ForEach-Object { "$($_.display)=$($_.classification)" })
|
||||
phase_decision = $summary.phase_decision
|
||||
originals_copied = $false
|
||||
originals_modified = $false
|
||||
} | ConvertTo-Json -Depth 6
|
||||
Reference in New Issue
Block a user