From 553a9613384bc40f5337647c2cd2b735ea20059a Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 4 Apr 2026 01:16:30 +0900 Subject: [PATCH] fix(shared): emit PowerShell zip entries as json lines Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../powershell-zip-entry-listing.ts | 91 ++++++++++++------- 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/src/shared/zip-entry-listing/powershell-zip-entry-listing.ts b/src/shared/zip-entry-listing/powershell-zip-entry-listing.ts index d1c9558e9..9169f510b 100644 --- a/src/shared/zip-entry-listing/powershell-zip-entry-listing.ts +++ b/src/shared/zip-entry-listing/powershell-zip-entry-listing.ts @@ -4,35 +4,74 @@ import type { ArchiveEntry } from "../archive-entry-validator" export type PowerShellZipExtractor = "pwsh" | "powershell" +type PowerShellZipEntryRecord = { + type: "file" | "directory" | "symlink" + name: string + target: string +} + +function isPowerShellZipEntryRecord(value: unknown): value is PowerShellZipEntryRecord { + if (!value || typeof value !== "object") { + return false + } + + const candidate = value as Record + return ( + (candidate.type === "file" || candidate.type === "directory" || candidate.type === "symlink") && + typeof candidate.name === "string" && + typeof candidate.target === "string" + ) +} + +export function parsePowerShellZipEntryLine(line: string): ArchiveEntry | null { + const parsedValue: unknown = JSON.parse(line) + if (!isPowerShellZipEntryRecord(parsedValue)) { + return null + } + + if (parsedValue.type === "symlink") { + return { + path: parsedValue.name, + type: parsedValue.type, + linkPath: parsedValue.target, + } + } + + return { + path: parsedValue.name, + type: parsedValue.type, + } +} + export async function listZipEntriesWithPowerShell( archivePath: string, escapePowerShellPath: (path: string) => string, extractor: PowerShellZipExtractor ): Promise { const proc = spawn( - [ - extractor, - "-Command", [ + extractor, + "-Command", + [ "Add-Type -AssemblyName System.IO.Compression.FileSystem", `$archive = [System.IO.Compression.ZipFile]::OpenRead('${escapePowerShellPath(archivePath)}')`, "try {", " foreach ($entry in $archive.Entries) {", " $mode = ($entry.ExternalAttributes -shr 16) -band 0xFFFF", " $type = if (($mode -band 0xF000) -eq 0xA000) { 'symlink' } elseif ($entry.FullName.EndsWith('/')) { 'directory' } else { 'file' }", - " $target = ''", - " if ($type -eq 'symlink') {", - " $stream = $entry.Open()", - " try {", - " $reader = New-Object System.IO.StreamReader($stream)", - " try { $target = $reader.ReadToEnd() } finally { $reader.Dispose() }", - " } finally { $stream.Dispose() }", - " }", - " Write-Output ($type + \"`t\" + $entry.FullName + \"`t\" + $target)", - " }", - "} finally {", - " $archive.Dispose()", - "}", + " $target = ''", + " if ($type -eq 'symlink') {", + " $stream = $entry.Open()", + " try {", + " $reader = New-Object System.IO.StreamReader($stream)", + " try { $target = $reader.ReadToEnd() } finally { $reader.Dispose() }", + " } finally { $stream.Dispose() }", + " }", + " Write-Output (ConvertTo-Json @{type=$type; name=$entry.FullName; target=$target} -Compress)", + " }", + "} finally {", + " $archive.Dispose()", + "}", ].join("; "), ], { @@ -55,24 +94,6 @@ export async function listZipEntriesWithPowerShell( .split(/\r?\n/) .map(line => line.trim()) .filter(Boolean) - .map((line): ArchiveEntry | null => { - const [type, entryPath, linkPath = ""] = line.split("\t") - if (type !== "file" && type !== "directory" && type !== "symlink") { - return null - } - - if (type === "symlink") { - return { - path: entryPath, - type, - linkPath, - } - } - - return { - path: entryPath, - type, - } - }) + .map(line => parsePowerShellZipEntryLine(line)) .filter((entry): entry is ArchiveEntry => entry !== null) }