Add managed_runtime.go (paths, config validation, identity, runtime file changes, status checks) and shell_command.go (quoting, PowerShell encoding, command parsing) and move the four providers onto them. Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
199 lines
5.6 KiB
Go
199 lines
5.6 KiB
Go
package plugins
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"unicode/utf16"
|
|
)
|
|
|
|
const (
|
|
posixApostrophe = `'"'"'`
|
|
powerShellExitSuffix = "; exit $LASTEXITCODE"
|
|
powerShellEncodedFlags = " -NoLogo -NoProfile -NonInteractive -EncodedCommand "
|
|
)
|
|
|
|
func resolveNodeRuntime() (string, error) {
|
|
nodePath, err := exec.LookPath("node")
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolve Node.js runtime: %w", err)
|
|
}
|
|
absolute, err := filepath.Abs(nodePath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolve Node.js runtime path: %w", err)
|
|
}
|
|
return absolute, nil
|
|
}
|
|
|
|
func resolveAbsoluteNodeRuntime(product string) (string, error) {
|
|
nodePath, err := resolveNodeRuntime()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !filepath.IsAbs(nodePath) || !isNodeExecutable(nodePath) {
|
|
return "", fmt.Errorf("%s hooks require an absolute Node.js executable path", product)
|
|
}
|
|
return nodePath, nil
|
|
}
|
|
|
|
func isNodeExecutable(path string) bool {
|
|
name := filepath.Base(path)
|
|
return name == "node" || strings.EqualFold(name, "node.exe")
|
|
}
|
|
|
|
func quotePOSIXArgument(value string) string {
|
|
return "'" + strings.ReplaceAll(value, "'", posixApostrophe) + "'"
|
|
}
|
|
|
|
func quotePowerShellArgument(value string) string {
|
|
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
|
|
}
|
|
|
|
func posixSource(runtimePath, scriptPath string) string {
|
|
return quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath)
|
|
}
|
|
|
|
func powerShellSource(runtimePath, scriptPath string) string {
|
|
return "& " + quotePowerShellArgument(runtimePath) + " " +
|
|
quotePowerShellArgument(scriptPath) + powerShellExitSuffix
|
|
}
|
|
|
|
func windowsPowerShellPath() string {
|
|
root := "C:\\Windows"
|
|
if runtime.GOOS == "windows" {
|
|
configured := os.Getenv("SystemRoot")
|
|
if filepath.IsAbs(configured) && !strings.ContainsAny(configured, "\"%\r\n") {
|
|
root = configured
|
|
}
|
|
}
|
|
return strings.TrimRight(root, `\/`) + `\System32\WindowsPowerShell\v1.0\powershell.exe`
|
|
}
|
|
|
|
func encodePowerShellSource(source string) string {
|
|
units := utf16.Encode([]rune(source))
|
|
raw := make([]byte, len(units)*2)
|
|
for index, value := range units {
|
|
binary.LittleEndian.PutUint16(raw[index*2:], value)
|
|
}
|
|
return base64.StdEncoding.EncodeToString(raw)
|
|
}
|
|
|
|
func decodePowerShellSource(encoded string) (string, bool) {
|
|
raw, err := base64.StdEncoding.DecodeString(encoded)
|
|
if err != nil || base64.StdEncoding.EncodeToString(raw) != encoded || len(raw)%2 != 0 {
|
|
return "", false
|
|
}
|
|
units := make([]uint16, len(raw)/2)
|
|
for index := range units {
|
|
units[index] = uint16(raw[index*2]) | uint16(raw[index*2+1])<<8
|
|
}
|
|
return string(utf16.Decode(units)), true
|
|
}
|
|
|
|
func encodedWindowsCommand(runtimePath, scriptPath string) string {
|
|
return `"` + windowsPowerShellPath() + `"` + powerShellEncodedFlags +
|
|
encodePowerShellSource(powerShellSource(runtimePath, scriptPath))
|
|
}
|
|
|
|
func readPOSIXArgument(command string, start int) (string, int, bool) {
|
|
if start >= len(command) || command[start] != '\'' {
|
|
return "", start, false
|
|
}
|
|
var value strings.Builder
|
|
for index := start + 1; index < len(command); {
|
|
if strings.HasPrefix(command[index:], posixApostrophe) {
|
|
value.WriteByte('\'')
|
|
index += len(posixApostrophe)
|
|
continue
|
|
}
|
|
if command[index] == '\'' {
|
|
return value.String(), index + 1, true
|
|
}
|
|
value.WriteByte(command[index])
|
|
index++
|
|
}
|
|
return "", start, false
|
|
}
|
|
|
|
func parsePOSIXCommand(command string) (string, string, bool) {
|
|
runtimePath, next, ok := readPOSIXArgument(command, 0)
|
|
if !ok || next >= len(command) || command[next] != ' ' {
|
|
return "", "", false
|
|
}
|
|
scriptPath, end, ok := readPOSIXArgument(command, next+1)
|
|
return runtimePath, scriptPath, ok && end == len(command)
|
|
}
|
|
|
|
func readPowerShellArgument(command string, start int) (string, int, bool) {
|
|
if start >= len(command) || command[start] != '\'' {
|
|
return "", start, false
|
|
}
|
|
var value strings.Builder
|
|
for index := start + 1; index < len(command); index++ {
|
|
if command[index] != '\'' {
|
|
value.WriteByte(command[index])
|
|
continue
|
|
}
|
|
if index+1 < len(command) && command[index+1] == '\'' {
|
|
value.WriteByte('\'')
|
|
index++
|
|
continue
|
|
}
|
|
return value.String(), index + 1, true
|
|
}
|
|
return "", start, false
|
|
}
|
|
|
|
func parsePowerShellSource(source string) (string, string, bool) {
|
|
if !strings.HasPrefix(source, "& ") {
|
|
return "", "", false
|
|
}
|
|
runtimePath, next, ok := readPowerShellArgument(source, 2)
|
|
if !ok || next >= len(source) || source[next] != ' ' {
|
|
return "", "", false
|
|
}
|
|
scriptPath, end, ok := readPowerShellArgument(source, next+1)
|
|
return runtimePath, scriptPath, ok && source[end:] == powerShellExitSuffix
|
|
}
|
|
|
|
func windowsPathBase(value string) string {
|
|
index := strings.LastIndexAny(value, `\/`)
|
|
return value[index+1:]
|
|
}
|
|
|
|
func isAbsoluteWindowsPath(value string) bool {
|
|
return len(value) >= 3 && ((value[0] >= 'A' && value[0] <= 'Z') ||
|
|
(value[0] >= 'a' && value[0] <= 'z')) && value[1] == ':' &&
|
|
(value[2] == '\\' || value[2] == '/') || strings.HasPrefix(value, `\\`)
|
|
}
|
|
|
|
func isPowerShellExecutable(value string) bool {
|
|
return isAbsoluteWindowsPath(value) &&
|
|
strings.EqualFold(windowsPathBase(value), "powershell.exe")
|
|
}
|
|
|
|
func parseEncodedWindowsCommand(command string) (string, string, bool) {
|
|
if !strings.HasPrefix(command, `"`) {
|
|
return "", "", false
|
|
}
|
|
closing := strings.IndexByte(command[1:], '"')
|
|
if closing < 0 {
|
|
return "", "", false
|
|
}
|
|
closing++
|
|
prefix := `"` + powerShellEncodedFlags
|
|
if !isPowerShellExecutable(command[1:closing]) || !strings.HasPrefix(command[closing:], prefix) {
|
|
return "", "", false
|
|
}
|
|
source, ok := decodePowerShellSource(command[closing+len(prefix):])
|
|
if !ok {
|
|
return "", "", false
|
|
}
|
|
return parsePowerShellSource(source)
|
|
}
|