mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 11:07:42 +00:00
git mv plus import, vi.mock and path-literal rewrites; one e2e route regex follows the moved dynamic-forward process.
186 lines
5.2 KiB
TypeScript
186 lines
5.2 KiB
TypeScript
import type { RelayPlatform } from './remote-exec/remote-frame-protocol'
|
|
|
|
export type RemotePathFlavor = 'posix' | 'windows'
|
|
export type RemoteCommandDialect = 'posix' | 'powershell'
|
|
export type RemoteOperatingSystem = 'linux' | 'darwin' | 'win32'
|
|
export type RemoteArchitecture = 'x64' | 'arm64'
|
|
|
|
export type RemoteHostPlatform = {
|
|
relayPlatform: RelayPlatform
|
|
os: RemoteOperatingSystem
|
|
arch: RemoteArchitecture
|
|
pathFlavor: RemotePathFlavor
|
|
commandDialect: RemoteCommandDialect
|
|
pathSeparator: '/' | '\\'
|
|
pathDelimiter: ':' | ';'
|
|
}
|
|
|
|
const PLATFORM_INFO: Record<RelayPlatform, RemoteHostPlatform> = {
|
|
'linux-x64': {
|
|
relayPlatform: 'linux-x64',
|
|
os: 'linux',
|
|
arch: 'x64',
|
|
pathFlavor: 'posix',
|
|
commandDialect: 'posix',
|
|
pathSeparator: '/',
|
|
pathDelimiter: ':'
|
|
},
|
|
'linux-arm64': {
|
|
relayPlatform: 'linux-arm64',
|
|
os: 'linux',
|
|
arch: 'arm64',
|
|
pathFlavor: 'posix',
|
|
commandDialect: 'posix',
|
|
pathSeparator: '/',
|
|
pathDelimiter: ':'
|
|
},
|
|
'darwin-x64': {
|
|
relayPlatform: 'darwin-x64',
|
|
os: 'darwin',
|
|
arch: 'x64',
|
|
pathFlavor: 'posix',
|
|
commandDialect: 'posix',
|
|
pathSeparator: '/',
|
|
pathDelimiter: ':'
|
|
},
|
|
'darwin-arm64': {
|
|
relayPlatform: 'darwin-arm64',
|
|
os: 'darwin',
|
|
arch: 'arm64',
|
|
pathFlavor: 'posix',
|
|
commandDialect: 'posix',
|
|
pathSeparator: '/',
|
|
pathDelimiter: ':'
|
|
},
|
|
'win32-x64': {
|
|
relayPlatform: 'win32-x64',
|
|
os: 'win32',
|
|
arch: 'x64',
|
|
pathFlavor: 'windows',
|
|
commandDialect: 'powershell',
|
|
pathSeparator: '\\',
|
|
pathDelimiter: ';'
|
|
},
|
|
'win32-arm64': {
|
|
relayPlatform: 'win32-arm64',
|
|
os: 'win32',
|
|
arch: 'arm64',
|
|
pathFlavor: 'windows',
|
|
commandDialect: 'powershell',
|
|
pathSeparator: '\\',
|
|
pathDelimiter: ';'
|
|
}
|
|
}
|
|
|
|
export function getRemoteHostPlatform(platform: RelayPlatform): RemoteHostPlatform {
|
|
return PLATFORM_INFO[platform]
|
|
}
|
|
|
|
export function isWindowsRemoteHost(host: RemoteHostPlatform): boolean {
|
|
return host.os === 'win32'
|
|
}
|
|
|
|
export function normalizeWindowsRemotePath(path: string): string {
|
|
return path.replace(/\\/g, '/')
|
|
}
|
|
|
|
export function assertSafeRemotePathSegment(segment: string, pathFlavor: RemotePathFlavor): void {
|
|
if (
|
|
!segment ||
|
|
segment === '.' ||
|
|
segment === '..' ||
|
|
segment.includes('/') ||
|
|
segment.includes('\0')
|
|
) {
|
|
throw new Error(`Unsafe remote path segment: ${JSON.stringify(segment)}`)
|
|
}
|
|
if (pathFlavor === 'posix') {
|
|
return
|
|
}
|
|
|
|
// Why: Win32 canonicalizes names and exposes devices/NTFS streams through
|
|
// otherwise ordinary-looking path segments, bypassing no-clobber checks.
|
|
const normalized = normalizeWindowsRemotePath(segment)
|
|
const deviceStem = normalized.split('.')[0]?.toUpperCase()
|
|
if (
|
|
normalized.includes('/') ||
|
|
hasUnsafeWindowsPathChar(normalized) ||
|
|
/[ .]$/u.test(normalized) ||
|
|
/^(?:CON|PRN|AUX|NUL|CONIN\$|CONOUT\$|CLOCK\$|COM[1-9¹²³]|LPT[1-9¹²³])$/u.test(deviceStem ?? '')
|
|
) {
|
|
throw new Error(`Unsafe remote path segment: ${JSON.stringify(segment)}`)
|
|
}
|
|
}
|
|
|
|
function hasUnsafeWindowsPathChar(value: string): boolean {
|
|
for (let i = 0; i < value.length; i += 1) {
|
|
const char = value[i]
|
|
if (value.charCodeAt(i) <= 31 || (char !== undefined && '<>:"|?*'.includes(char))) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
export function normalizeRemoteHome(rawHome: string, host: RemoteHostPlatform): string {
|
|
const home = rawHome.trim()
|
|
return isWindowsRemoteHost(host) ? normalizeWindowsRemotePath(home).replace(/\/+$/, '') : home
|
|
}
|
|
|
|
function hasUnsafeRemotePathChar(value: string): boolean {
|
|
for (let i = 0; i < value.length; i += 1) {
|
|
const code = value.charCodeAt(i)
|
|
if (code === 0 || code === 10 || code === 13) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
export function validateRemoteHome(home: string, host: RemoteHostPlatform): boolean {
|
|
if (!home || hasUnsafeRemotePathChar(home)) {
|
|
return false
|
|
}
|
|
if (host.pathFlavor === 'windows') {
|
|
return /^[a-zA-Z]:\//.test(home) || home.startsWith('//')
|
|
}
|
|
return home.startsWith('/')
|
|
}
|
|
|
|
export function joinRemotePath(host: RemoteHostPlatform, ...segments: string[]): string {
|
|
const cleaned = segments.filter(Boolean)
|
|
if (cleaned.length === 0) {
|
|
return ''
|
|
}
|
|
if (host.pathFlavor === 'windows') {
|
|
const [first, ...rest] = cleaned.map((segment) => normalizeWindowsRemotePath(segment))
|
|
return rest.reduce((acc, segment) => {
|
|
const left = acc.replace(/\/+$/, '')
|
|
const right = segment.replace(/^\/+/, '')
|
|
return `${left}/${right}`
|
|
}, first)
|
|
}
|
|
const [first, ...rest] = cleaned
|
|
return rest.reduce((acc, segment) => {
|
|
const left = acc.replace(/\/+$/, '')
|
|
const right = segment.replace(/^\/+/, '')
|
|
return `${left}/${right}`
|
|
}, first)
|
|
}
|
|
|
|
export function remoteBasename(path: string, host: RemoteHostPlatform): string {
|
|
const normalized = host.pathFlavor === 'windows' ? normalizeWindowsRemotePath(path) : path
|
|
return normalized.split('/').findLast(Boolean) ?? ''
|
|
}
|
|
|
|
export function remoteDirname(path: string, host: RemoteHostPlatform): string {
|
|
const normalized = host.pathFlavor === 'windows' ? normalizeWindowsRemotePath(path) : path
|
|
const parts = normalized.split('/')
|
|
parts.pop()
|
|
const joined = parts.join('/')
|
|
if (host.pathFlavor === 'windows') {
|
|
return joined
|
|
}
|
|
return joined || '/'
|
|
}
|