Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions packages/opencode/src/lsp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import fs from "fs/promises"
import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance"
import { Flag } from "../flag/flag"
import { Archive } from "../util/archive"

export namespace LSPServer {
const log = Log.create({ service: "lsp.server" })
Expand Down Expand Up @@ -176,7 +177,7 @@ export namespace LSPServer {
const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip")
await Bun.file(zipPath).write(response)

await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
await Archive.extractZip(zipPath, Global.Path.bin)
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling: The original code used .nothrow() which prevents exceptions from propagating. The new Archive.extractZip call can throw exceptions. If extraction fails, the subsequent cleanup code (removing zip file, renaming directories) will not execute, potentially leaving the system in an inconsistent state.

Copilot uses AI. Check for mistakes.
await fs.rm(zipPath, { force: true })

const extractedPath = path.join(Global.Path.bin, "vscode-eslint-main")
Expand Down Expand Up @@ -438,7 +439,7 @@ export namespace LSPServer {
const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
await Bun.file(zipPath).write(response)

await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
await Archive.extractZip(zipPath, Global.Path.bin)
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling: The original code used .nothrow() which prevents exceptions from propagating. The new Archive.extractZip call can throw exceptions. If extraction fails, the subsequent cleanup and build steps will not execute, potentially leaving the system in an inconsistent state.

Copilot uses AI. Check for mistakes.

await fs.rm(zipPath, {
force: true,
Expand Down Expand Up @@ -541,7 +542,7 @@ export namespace LSPServer {
await Bun.file(tempPath).write(downloadResponse)

if (ext === "zip") {
await $`unzip -o -q ${tempPath}`.quiet().cwd(Global.Path.bin).nothrow()
await Archive.extractZip(tempPath, Global.Path.bin)
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling: The original code used .nothrow() which prevents exceptions from propagating. The new Archive.extractZip call can throw exceptions. If extraction fails, the subsequent cleanup and verification steps will not execute, potentially leaving the system in an inconsistent state.

Copilot uses AI. Check for mistakes.
} else {
await $`tar -xf ${tempPath}`.cwd(Global.Path.bin).nothrow()
}
Expand Down Expand Up @@ -840,7 +841,7 @@ export namespace LSPServer {
}

if (zip) {
await $`unzip -o -q ${archive}`.quiet().cwd(Global.Path.bin).nothrow()
await Archive.extractZip(archive, Global.Path.bin)
}
Comment on lines +844 to 845
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling: The original code used .nothrow() which prevents exceptions from propagating. The new Archive.extractZip call can throw exceptions. If extraction fails, the subsequent cleanup and verification steps will not execute, potentially leaving the system in an inconsistent state.

Suggested change
await Archive.extractZip(archive, Global.Path.bin)
}
try {
await Archive.extractZip(archive, Global.Path.bin)
} catch (err) {
log.error("Failed to extract clangd zip archive", { error: err });
await fs.rm(archive, { force: true });
return;
}

Copilot uses AI. Check for mistakes.
if (tar) {
await $`tar -xf ${archive}`.cwd(Global.Path.bin).nothrow()
Expand Down Expand Up @@ -1188,14 +1189,21 @@ export namespace LSPServer {
await fs.mkdir(installDir, { recursive: true })

if (ext === "zip") {
const ok = await $`unzip -o -q ${tempPath} -d ${installDir}`.quiet().catch((error) => {
log.error("Failed to extract lua-language-server archive", { error })
})
const ok = await Archive.extractZip(tempPath, installDir)
.then(() => true)
.catch((error) => {
log.error("Failed to extract lua-language-server archive", { error })
return false
})
if (!ok) return
} else {
const ok = await $`tar -xzf ${tempPath} -C ${installDir}`.quiet().catch((error) => {
log.error("Failed to extract lua-language-server archive", { error })
})
const ok = await $`tar -xzf ${tempPath} -C ${installDir}`
.quiet()
.then(() => true)
.catch((error) => {
log.error("Failed to extract lua-language-server archive", { error })
return false
})
if (!ok) return
}

Expand Down Expand Up @@ -1396,7 +1404,7 @@ export namespace LSPServer {
const tempPath = path.join(Global.Path.bin, assetName)
await Bun.file(tempPath).write(downloadResponse)

await $`unzip -o -q ${tempPath}`.cwd(Global.Path.bin).nothrow()
await Archive.extractZip(tempPath, Global.Path.bin)
await fs.rm(tempPath, { force: true })
Comment on lines +1407 to 1408
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling: The original code used .nothrow() which prevents exceptions from propagating. The new Archive.extractZip call can throw exceptions. If extraction fails, the subsequent cleanup and verification steps will not execute, potentially leaving the system in an inconsistent state.

Suggested change
await Archive.extractZip(tempPath, Global.Path.bin)
await fs.rm(tempPath, { force: true })
try {
await Archive.extractZip(tempPath, Global.Path.bin)
} catch (err) {
log.error("Failed to extract terraform-ls zip", { error: err })
return
} finally {
await fs.rm(tempPath, { force: true })
}

Copilot uses AI. Check for mistakes.

bin = path.join(Global.Path.bin, "terraform-ls" + (platform === "win32" ? ".exe" : ""))
Expand Down Expand Up @@ -1481,7 +1489,7 @@ export namespace LSPServer {
await Bun.file(tempPath).write(downloadResponse)

if (ext === "zip") {
await $`unzip -o -q ${tempPath}`.cwd(Global.Path.bin).nothrow()
await Archive.extractZip(tempPath, Global.Path.bin)
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling: The original code used .nothrow() which prevents exceptions from propagating. The new Archive.extractZip call can throw exceptions. If extraction fails, the subsequent cleanup and verification steps will not execute, potentially leaving the system in an inconsistent state.

Copilot uses AI. Check for mistakes.
}
if (ext === "tar.gz") {
await $`tar -xzf ${tempPath}`.cwd(Global.Path.bin).nothrow()
Expand Down
16 changes: 16 additions & 0 deletions packages/opencode/src/util/archive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { $ } from "bun"
import path from "path"

export namespace Archive {
export async function extractZip(zipPath: string, destDir: string) {
if (process.platform === "win32") {
const winZipPath = path.resolve(zipPath)
const winDestDir = path.resolve(destDir)
// $global:ProgressPreference suppresses PowerShell's blue progress bar popup
const cmd = `$global:ProgressPreference = 'SilentlyContinue'; Expand-Archive -Path '${winZipPath}' -DestinationPath '${winDestDir}' -Force`
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Command injection vulnerability: The zipPath and destDir variables are interpolated directly into a PowerShell command string without proper escaping. If these paths contain special characters like single quotes, backticks, or dollar signs, they could break out of the string context and execute arbitrary PowerShell commands. Use proper argument passing instead of string interpolation.

Copilot uses AI. Check for mistakes.
await $`powershell -NoProfile -NonInteractive -Command ${cmd}`.quiet()
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error suppression: The original code used .nothrow() to prevent exceptions from bubbling up during extraction. Without it, extraction failures will now throw exceptions that may not be properly handled by all callers. Consider adding .nothrow() or documenting that callers should handle exceptions.

Copilot uses AI. Check for mistakes.
} else {
await $`unzip -o -q ${zipPath} -d ${destDir}`.quiet()
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error suppression: The original code used .nothrow() to prevent exceptions from bubbling up during extraction. Without it, extraction failures will now throw exceptions that may not be properly handled by all callers. Consider adding .nothrow() or documenting that callers should handle exceptions.

Copilot uses AI. Check for mistakes.
}
Comment on lines +5 to +14
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage: This new utility function lacks test coverage. Given that it handles critical LSP installation functionality across multiple platforms and has potential security implications, tests should verify correct extraction on both Windows and Unix-like systems, proper error handling, and safe handling of edge cases like paths with special characters.

Copilot uses AI. Check for mistakes.
}
}