diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py index 1b34d8df..bc57f11d 100644 --- a/ptvsd/wrapper.py +++ b/ptvsd/wrapper.py @@ -17,6 +17,7 @@ import socket import sys import threading import traceback + try: import urllib urllib.unquote @@ -32,6 +33,7 @@ except ImportError: import Queue as queue import warnings from xml.sax import SAXParseException +from xml.sax.saxutils import unescape as xml_unescape import _pydevd_bundle.pydevd_comm as pydevd_comm # noqa import _pydevd_bundle.pydevd_comm_constants as pydevd_comm_constants # noqa @@ -212,6 +214,15 @@ def unquote(s): return urllib.unquote(s) +def unquote_xml_path(s): + """XML unescape after url unquote. This reverses the escapes and quotes done + by pydevd. + """ + if s is None: + return None + return xml_unescape(unquote(str(s))) + + class IDMap(object): """Maps VSCode entities to corresponding pydevd entities by ID. @@ -1686,7 +1697,7 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): fid = self.frame_map.to_vscode(key, autogen=True) name = unquote(xframe['name']) # pydevd encodes if necessary and then uses urllib.quote. - norm_path = self.path_casing.un_normcase(unquote(str(xframe['file']))) # noqa + norm_path = self.path_casing.un_normcase(unquote_xml_path(xframe['file'])) # noqa source_reference = self.get_source_reference(norm_path) if not self.internals_filter.is_internal_path(norm_path): module = self.modules_mgr.add_or_get_from_path(norm_path) @@ -2253,7 +2264,7 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): xframes = list(xml.thread.frame) frame_data = [] for f in xframes: - file_path = unquote(f['file']) + file_path = unquote_xml_path(f['file']) if not self.internals_filter.is_internal_path(file_path) \ and self._should_debug(file_path): line_no = int(f['line']) @@ -2270,7 +2281,7 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): func_name, None)) exc_stack = ''.join(traceback.format_list(frame_data)) - exc_source = unquote(xframes[0]['file']) + exc_source = unquote_xml_path(xframes[0]['file']) if self.internals_filter.is_internal_path(exc_source) or \ not self._should_debug(exc_source): exc_source = None @@ -2471,7 +2482,7 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): # do step over and step out xframes = list(xml.thread.frame) xframe = xframes[0] - filepath = unquote(xframe['file']) + filepath = unquote_xml_path(xframe['file']) if reason in STEP_REASONS or reason in EXCEPTION_REASONS: if self.internals_filter.is_internal_path(filepath) or \ not self._should_debug(filepath): diff --git a/pytests/func/test_breakpoints.py b/pytests/func/test_breakpoints.py new file mode 100644 index 00000000..84d61b78 --- /dev/null +++ b/pytests/func/test_breakpoints.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root +# for license information. + +from __future__ import print_function, with_statement, absolute_import + +import os.path +from pytests.helpers.timeline import Event +from ..helpers.pathutils import get_test_root, compare_path + +BP_TEST_ROOT = get_test_root('bp') + + +def test_path_with_ampersand(debug_session): + bp_line = 2 + testfile = os.path.join(BP_TEST_ROOT, 'a&b', 'test.py') + debug_session.common_setup(testfile, 'file', [bp_line]) + debug_session.start_debugging() + hit = debug_session.wait_for_thread_stopped() + frames = hit.stacktrace.body['stackFrames'] + assert compare_path(frames[0]['source']['path'], testfile) + + debug_session.send_request('continue').wait_for_response() + debug_session.wait_for_next(Event('continued')) + + debug_session.wait_for_exit() diff --git a/pytests/func/testfiles/bp/a&b/test.py b/pytests/func/testfiles/bp/a&b/test.py new file mode 100644 index 00000000..cd921c80 --- /dev/null +++ b/pytests/func/testfiles/bp/a&b/test.py @@ -0,0 +1,3 @@ +print('one') +print('two') +print('three') \ No newline at end of file