Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support uri path mappings #879

Merged
merged 5 commits into from
Feb 1, 2023
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).

## [1.31.0]

- Allow more flexible path mappings in url format.

## [1.30.0]

- Add skipFiles launch setting to skip over specified file patterns.
Expand Down
212 changes: 108 additions & 104 deletions src/paths.ts
Original file line number Diff line number Diff line change
@@ -1,149 +1,153 @@
import fileUrl from 'file-url'
import * as url from 'url'
import * as path from 'path'
import { decode } from 'urlencode'
import RelateUrl from 'relateurl'
import minimatch from 'minimatch'

/**
* Options to make sure that RelateUrl only outputs relative URLs and performs not other "smart" modifications.
* They would mess up things like prefix checking.
*/
const RELATE_URL_OPTIONS: RelateUrl.Options = {
// Make sure RelateUrl does not prefer root-relative URLs if shorter
output: RelateUrl.PATH_RELATIVE,
// Make sure RelateUrl does not remove trailing slash if present
removeRootTrailingSlash: false,
// Make sure RelateUrl does not remove default ports
defaultPorts: {},
}

/**
* Like `path.relative()` but for URLs.
* Inverse of `url.resolve()` or `new URL(relative, base)`.
*/
const relativeUrl = (from: string, to: string): string => RelateUrl.relate(from, to, RELATE_URL_OPTIONS)

/** converts a server-side Xdebug file URI to a local path for VS Code with respect to source root settings */
export function convertDebuggerPathToClient(
fileUri: string | url.Url,
pathMapping?: { [index: string]: string }
): string {
let localSourceRoot: string | undefined
let serverSourceRoot: string | undefined
if (typeof fileUri === 'string') {
fileUri = url.parse(fileUri)
}
// convert the file URI to a path
let serverPath = decode(fileUri.pathname!)
// strip the trailing slash from Windows paths (indicated by a drive letter with a colon)
const serverIsWindows = /^\/[a-zA-Z]:\//.test(serverPath)
if (serverIsWindows) {
serverPath = serverPath.substr(1)
}
export function convertDebuggerPathToClient(fileUri: string, pathMapping?: { [index: string]: string }): string {
let localSourceRootUrl: string | undefined
let serverSourceRootUrl: string | undefined

if (pathMapping) {
for (const mappedServerPath of Object.keys(pathMapping)) {
const mappedLocalSource = pathMapping[mappedServerPath]
// normalize slashes for windows-to-unix
const serverRelative = (serverIsWindows ? path.win32 : path.posix).relative(mappedServerPath, serverPath)
if (!serverRelative.startsWith('..')) {
let mappedServerPathUrl = pathOrUrlToUrl(mappedServerPath)
// try exact match
if (fileUri.length === mappedServerPathUrl.length && isSameUri(fileUri, mappedServerPathUrl)) {
// bail early
serverSourceRootUrl = mappedServerPathUrl
localSourceRootUrl = pathOrUrlToUrl(pathMapping[mappedServerPath])
break
}
// make sure it ends with a slash
if (!mappedServerPathUrl.endsWith('/')) {
mappedServerPathUrl += '/'
}
if (isSameUri(fileUri.substring(0, mappedServerPathUrl.length), mappedServerPathUrl)) {
// If a matching mapping has previously been found, only update
// it if the current server path is longer than the previous one
// (longest prefix matching)
if (!serverSourceRoot || mappedServerPath.length > serverSourceRoot.length) {
serverSourceRoot = mappedServerPath
localSourceRoot = mappedLocalSource
if (!serverSourceRootUrl || mappedServerPathUrl.length > serverSourceRootUrl.length) {
serverSourceRootUrl = mappedServerPathUrl
localSourceRootUrl = pathOrUrlToUrl(pathMapping[mappedServerPath])
if (!localSourceRootUrl.endsWith('/')) {
localSourceRootUrl += '/'
}
}
}
}
}
let localPath: string
if (serverSourceRoot && localSourceRoot) {
const clientIsWindows =
/^[a-zA-Z]:\\/.test(localSourceRoot) ||
/^\\\\/.test(localSourceRoot) ||
/^[a-zA-Z]:$/.test(localSourceRoot) ||
/^[a-zA-Z]:\//.test(localSourceRoot)
// get the part of the path that is relative to the source root
let pathRelativeToSourceRoot = (serverIsWindows ? path.win32 : path.posix).relative(
serverSourceRoot,
serverPath
)
if (serverIsWindows && !clientIsWindows) {
pathRelativeToSourceRoot = pathRelativeToSourceRoot.replace(/\\/g, path.posix.sep)
}
if (clientIsWindows && /^[a-zA-Z]:$/.test(localSourceRoot)) {
// if local source root mapping is only drive letter, add backslash
localSourceRoot += '\\'
if (serverSourceRootUrl && localSourceRootUrl) {
fileUri = localSourceRootUrl + fileUri.substring(serverSourceRootUrl.length)
}
if (fileUri.startsWith('file://')) {
const u = new URL(fileUri)
let pathname = u.pathname
if (isWindowsUri(fileUri)) {
// From Node.js lib/internal/url.js pathToFileURL
pathname = pathname.replace(/\//g, path.win32.sep)
pathname = decodeURIComponent(pathname)
if (u.hostname !== '') {
localPath = `\\\\${url.domainToUnicode(u.hostname)}${pathname}`
} else {
localPath = pathname.slice(1)
}
} else {
localPath = decodeURIComponent(pathname)
}
// resolve from the local source root
localPath = (clientIsWindows ? path.win32 : path.posix).resolve(localSourceRoot, pathRelativeToSourceRoot)
} else {
localPath = (serverIsWindows ? path.win32 : path.posix).normalize(serverPath)
// if it's not a file url it could be sshfs or something else
localPath = fileUri
}
return localPath
}

/** converts a local path from VS Code to a server-side Xdebug file URI with respect to source root settings */
export function convertClientPathToDebugger(localPath: string, pathMapping?: { [index: string]: string }): string {
let localSourceRoot: string | undefined
let serverSourceRoot: string | undefined
// Xdebug always lowercases Windows drive letters in file URIs
const localFileUri = fileUrl(
localPath.replace(/^[A-Z]:\\/, match => match.toLowerCase()),
{ resolve: false }
)
let localSourceRootUrl: string | undefined
let serverSourceRootUrl: string | undefined

// Parse or convert local path to URL
const localFileUri = pathOrUrlToUrl(localPath)

let serverFileUri: string
if (pathMapping) {
for (const mappedServerPath of Object.keys(pathMapping)) {
let mappedLocalSource = pathMapping[mappedServerPath]
if (/^[a-zA-Z]:$/.test(mappedLocalSource)) {
// if local source root mapping is only drive letter, add backslash
mappedLocalSource += '\\'
//let mappedLocalSource = pathMapping[mappedServerPath]
let mappedLocalSourceUrl = pathOrUrlToUrl(pathMapping[mappedServerPath])
// try exact match
if (localFileUri.length === mappedLocalSourceUrl.length && isSameUri(localFileUri, mappedLocalSourceUrl)) {
// bail early
localSourceRootUrl = mappedLocalSourceUrl
serverSourceRootUrl = pathOrUrlToUrl(mappedServerPath)
break
}
// make sure it ends with a slash
if (!mappedLocalSourceUrl.endsWith('/')) {
mappedLocalSourceUrl += '/'
}
const localRelative = path.relative(mappedLocalSource, localPath)
if (!localRelative.startsWith('..')) {

if (isSameUri(localFileUri.substring(0, mappedLocalSourceUrl.length), mappedLocalSourceUrl)) {
// If a matching mapping has previously been found, only update
// it if the current local path is longer than the previous one
// (longest prefix matching)
if (!localSourceRoot || mappedLocalSource.length > localSourceRoot.length) {
serverSourceRoot = mappedServerPath
localSourceRoot = mappedLocalSource
if (!localSourceRootUrl || mappedLocalSourceUrl.length > localSourceRootUrl.length) {
localSourceRootUrl = mappedLocalSourceUrl
serverSourceRootUrl = pathOrUrlToUrl(mappedServerPath)
if (!serverSourceRootUrl.endsWith('/')) {
serverSourceRootUrl += '/'
}
}
}
}
}
if (localSourceRoot) {
localSourceRoot = localSourceRoot.replace(/^[A-Z]:$/, match => match.toLowerCase())
localSourceRoot = localSourceRoot.replace(/^[A-Z]:\\/, match => match.toLowerCase())
localSourceRoot = localSourceRoot.replace(/^[A-Z]:\//, match => match.toLowerCase())
}
if (serverSourceRoot) {
serverSourceRoot = serverSourceRoot.replace(/^[A-Z]:$/, match => match.toLowerCase())
serverSourceRoot = serverSourceRoot.replace(/^[A-Z]:\\/, match => match.toLowerCase())
serverSourceRoot = serverSourceRoot.replace(/^[A-Z]:\//, match => match.toLowerCase())
}
if (serverSourceRoot && localSourceRoot) {
let localSourceRootUrl = fileUrl(localSourceRoot, { resolve: false })
if (!localSourceRootUrl.endsWith('/')) {
localSourceRootUrl += '/'
}
let serverSourceRootUrl = fileUrl(serverSourceRoot, { resolve: false })
if (!serverSourceRootUrl.endsWith('/')) {
serverSourceRootUrl += '/'
}
// get the part of the path that is relative to the source root
const urlRelativeToSourceRoot = relativeUrl(localSourceRootUrl, localFileUri)
// resolve from the server source root
serverFileUri = url.resolve(serverSourceRootUrl, urlRelativeToSourceRoot)
if (serverSourceRootUrl && localSourceRootUrl) {
serverFileUri = serverSourceRootUrl + localFileUri.substring(localSourceRootUrl.length)
} else {
serverFileUri = localFileUri
}
return serverFileUri
}

export function isWindowsUri(path: string): boolean {
return /^file:\/\/\/[a-zA-Z]:\//.test(path)
return /^file:\/\/\/[a-zA-Z]:\//.test(path) || /^file:\/\/[^/]/.test(path)
}

function isWindowsPath(path: string): boolean {
return /^[a-zA-Z]:\\/.test(path) || /^\\\\/.test(path) || /^[a-zA-Z]:$/.test(path) || /^[a-zA-Z]:\//.test(path)
}

function pathOrUrlToUrl(path: string): string {
// Do not try to parse windows drive letter paths
if (!isWindowsPath(path)) {
try {
// try to parse, but do not modify
new URL(path).toString()
return path
} catch (ex) {
// should be a path
}
}
// Not a URL, do some windows path mangling before it is converted to URL
if (path.startsWith('\\\\')) {
// UNC
const hostEndIndex = path.indexOf('\\', 2)
const host = path.substring(2, hostEndIndex)
const outURL = new URL('file://')
outURL.hostname = url.domainToASCII(host)
outURL.pathname = path.substring(hostEndIndex).replace(/\\/g, '/')
return outURL.toString()
}
if (/^[a-zA-Z]:$/.test(path)) {
// if local source root mapping is only drive letter, add backslash
path += '\\'
}
// Do not change drive later to lower case anymore
// if (/^[a-zA-Z]:/.test(path)) {
// // Xdebug always lowercases Windows drive letters in file URIs
// //path = path.replace(/^[A-Z]:/, match => match.toLowerCase())
// }
return fileUrl(path, { resolve: false })
}

export function isSameUri(clientUri: string, debuggerUri: string): boolean {
Expand Down
4 changes: 2 additions & 2 deletions src/phpDebug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -953,7 +953,7 @@ class PhpDebugSession extends vscode.DebugSession {
line++
} else {
// Xdebug paths are URIs, VS Code file paths
const filePath = convertDebuggerPathToClient(urlObject, this._args.pathMappings)
const filePath = convertDebuggerPathToClient(status.fileUri, this._args.pathMappings)
// "Name" of the source and the actual file path
source = { name: path.basename(filePath), path: filePath }
}
Expand Down Expand Up @@ -992,7 +992,7 @@ class PhpDebugSession extends vscode.DebugSession {
line++
} else {
// Xdebug paths are URIs, VS Code file paths
const filePath = convertDebuggerPathToClient(urlObject, this._args.pathMappings)
const filePath = convertDebuggerPathToClient(stackFrame.fileUri, this._args.pathMappings)
// "Name" of the source and the actual file path
source = { name: path.basename(filePath), path: filePath }
}
Expand Down
Loading