Skip to content

Commit

Permalink
feat: support uri path mappings (#879)
Browse files Browse the repository at this point in the history
* Re-implement path mapping logic

* Fix typos.

* Test Phar specific cases

* Add exact file mapping match.

* Changelog.
  • Loading branch information
zobo authored Feb 1, 2023
1 parent af38cb8 commit 453ebc3
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 126 deletions.
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

0 comments on commit 453ebc3

Please sign in to comment.