Skip to content

Commit 01fc44e

Browse files
committed
feat: normalize percent-encoded uris used as workspace dict keys
URIs used as dictionary keys in the Workspace class were stored and looked up without normalization. If a client sent a percent-encoded URI (e.g. file:///C%3A/foo) on one request and a decoded form (file:///C:/foo) on another, lookups would fail with KeyError since the raw strings differ. Apply urllib.parse.unquote to all URIs before using them as keys in _text_documents, _notebook_documents, _cell_in_notebook, and _folders dictionaries. This ensures equivalent URIs that differ only in percent encoding map to the same entry.
1 parent bdbe944 commit 01fc44e

2 files changed

Lines changed: 122 additions & 15 deletions

File tree

pygls/workspace/workspace.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import logging
2121
import os
2222
from typing import Dict, Optional, Sequence, Union
23+
from urllib.parse import unquote
2324

2425
from lsprotocol import types
2526
from lsprotocol.types import (
@@ -90,7 +91,7 @@ def _create_text_document(
9091
)
9192

9293
def add_folder(self, folder: WorkspaceFolder):
93-
self._folders[folder.uri] = folder
94+
self._folders[unquote(folder.uri)] = folder
9495

9596
@property
9697
def notebook_documents(self):
@@ -127,10 +128,10 @@ def get_notebook_document(
127128
The requested notebook document if found, ``None`` otherwise.
128129
"""
129130
if notebook_uri is not None:
130-
return self._notebook_documents.get(notebook_uri)
131+
return self._notebook_documents.get(unquote(notebook_uri))
131132

132133
if cell_uri is not None:
133-
notebook_uri = self._cell_in_notebook.get(cell_uri)
134+
notebook_uri = self._cell_in_notebook.get(unquote(cell_uri))
134135
if notebook_uri is None:
135136
return None
136137

@@ -145,7 +146,9 @@ def get_text_document(self, doc_uri: str) -> TextDocument:
145146
146147
See https://github.com/Microsoft/language-server-protocol/issues/177
147148
"""
148-
return self._text_documents.get(doc_uri) or self._create_text_document(doc_uri)
149+
return self._text_documents.get(unquote(doc_uri)) or self._create_text_document(
150+
doc_uri
151+
)
149152

150153
def is_local(self):
151154

@@ -161,7 +164,7 @@ def put_notebook_document(self, params: types.DidOpenNotebookDocumentParams):
161164
notebook = params.notebook_document
162165

163166
# Create a fresh instance to ensure our copy cannot be accidentally modified.
164-
self._notebook_documents[notebook.uri] = copy.deepcopy(notebook)
167+
self._notebook_documents[unquote(notebook.uri)] = copy.deepcopy(notebook)
165168

166169
for cell_document in params.cell_text_documents:
167170
self.put_text_document(cell_document, notebook_uri=notebook.uri)
@@ -184,31 +187,31 @@ def put_text_document(
184187
"""
185188
doc_uri = text_document.uri
186189

187-
self._text_documents[doc_uri] = self._create_text_document(
190+
self._text_documents[unquote(doc_uri)] = self._create_text_document(
188191
doc_uri,
189192
source=text_document.text,
190193
version=text_document.version,
191194
language_id=text_document.language_id,
192195
)
193196

194197
if notebook_uri:
195-
self._cell_in_notebook[doc_uri] = notebook_uri
198+
self._cell_in_notebook[unquote(doc_uri)] = unquote(notebook_uri)
196199

197200
def remove_notebook_document(self, params: types.DidCloseNotebookDocumentParams):
198201
notebook_uri = params.notebook_document.uri
199-
self._notebook_documents.pop(notebook_uri, None)
202+
self._notebook_documents.pop(unquote(notebook_uri), None)
200203

201204
for cell_document in params.cell_text_documents:
202205
self.remove_text_document(cell_document.uri)
203206

204207
def remove_text_document(self, doc_uri: str):
205-
self._text_documents.pop(doc_uri, None)
206-
self._cell_in_notebook.pop(doc_uri, None)
208+
self._text_documents.pop(unquote(doc_uri), None)
209+
self._cell_in_notebook.pop(unquote(doc_uri), None)
207210

208211
def remove_folder(self, folder_uri: str):
209-
self._folders.pop(folder_uri, None)
212+
self._folders.pop(unquote(folder_uri), None)
210213
try:
211-
del self._folders[folder_uri]
214+
del self._folders[unquote(folder_uri)]
212215
except KeyError:
213216
pass
214217

@@ -222,7 +225,7 @@ def root_uri(self):
222225

223226
def update_notebook_document(self, params: types.DidChangeNotebookDocumentParams):
224227
uri = params.notebook_document.uri
225-
notebook = self._notebook_documents[uri]
228+
notebook = self._notebook_documents[unquote(uri)]
226229
notebook.version = params.notebook_document.version
227230

228231
if params.change.metadata:
@@ -274,5 +277,5 @@ def update_text_document(
274277
change: types.TextDocumentContentChangeEvent,
275278
):
276279
doc_uri = text_doc.uri
277-
self._text_documents[doc_uri].apply_change(change)
278-
self._text_documents[doc_uri].version = text_doc.version
280+
self._text_documents[unquote(doc_uri)].apply_change(change)
281+
self._text_documents[unquote(doc_uri)].version = text_doc.version

tests/test_workspace.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,3 +440,107 @@ def test_null_workspace():
440440

441441
assert workspace.root_uri is None
442442
assert workspace.root_path is None
443+
444+
445+
# -- Percent-encoding normalization tests --
446+
447+
ENCODED_DOC_URI = "file:///C%3A/path/to/file.py"
448+
DECODED_DOC_URI = "file:///C:/path/to/file.py"
449+
ENCODED_DOC = types.TextDocumentItem(
450+
uri=ENCODED_DOC_URI, language_id="python", version=0, text="# encoded"
451+
)
452+
453+
ENCODED_NB_URI = "file:///C%3A/path/to/notebook.ipynb"
454+
DECODED_NB_URI = "file:///C:/path/to/notebook.ipynb"
455+
ENCODED_NOTEBOOK = types.NotebookDocument(
456+
uri=ENCODED_NB_URI,
457+
notebook_type="jupyter-notebook",
458+
version=0,
459+
cells=[
460+
types.NotebookCell(
461+
kind=types.NotebookCellKind.Code,
462+
document="nb-cell-scheme://C%3A/path/to/notebook.ipynb#cell1",
463+
),
464+
],
465+
)
466+
ENCODED_NB_CELL = types.TextDocumentItem(
467+
uri="nb-cell-scheme://C%3A/path/to/notebook.ipynb#cell1",
468+
language_id="python",
469+
version=0,
470+
text="# cell",
471+
)
472+
473+
474+
def test_get_text_document_percent_encoded(workspace):
475+
"""Looking up a document with a decoded URI after storing with an encoded one."""
476+
workspace.put_text_document(ENCODED_DOC)
477+
assert workspace.get_text_document(DECODED_DOC_URI).source == "# encoded"
478+
479+
480+
def test_get_text_document_percent_decoded(workspace):
481+
"""Looking up a document with an encoded URI after storing with a decoded one."""
482+
decoded_doc = types.TextDocumentItem(
483+
uri=DECODED_DOC_URI, language_id="python", version=0, text="# decoded"
484+
)
485+
workspace.put_text_document(decoded_doc)
486+
assert workspace.get_text_document(ENCODED_DOC_URI).source == "# decoded"
487+
488+
489+
def test_remove_text_document_percent_encoded(workspace):
490+
"""Removing a document stored with an encoded URI using a decoded URI."""
491+
workspace.put_text_document(ENCODED_DOC)
492+
workspace.remove_text_document(DECODED_DOC_URI)
493+
assert workspace.get_text_document(DECODED_DOC_URI)._source is None
494+
495+
496+
def test_get_notebook_document_percent_encoded(workspace):
497+
"""Looking up a notebook with a decoded URI after storing with an encoded one."""
498+
params = types.DidOpenNotebookDocumentParams(
499+
notebook_document=ENCODED_NOTEBOOK,
500+
cell_text_documents=[ENCODED_NB_CELL],
501+
)
502+
workspace.put_notebook_document(params)
503+
504+
notebook = workspace.get_notebook_document(notebook_uri=DECODED_NB_URI)
505+
assert notebook is not None
506+
assert notebook.uri == ENCODED_NB_URI
507+
508+
509+
def test_update_notebook_document_percent_encoded(workspace):
510+
"""Updating a notebook stored with an encoded URI using a decoded URI."""
511+
params = types.DidOpenNotebookDocumentParams(
512+
notebook_document=ENCODED_NOTEBOOK,
513+
cell_text_documents=[ENCODED_NB_CELL],
514+
)
515+
workspace.put_notebook_document(params)
516+
517+
update_params = types.DidChangeNotebookDocumentParams(
518+
notebook_document=types.VersionedNotebookDocumentIdentifier(
519+
uri=DECODED_NB_URI, version=5
520+
),
521+
change=types.NotebookDocumentChangeEvent(
522+
metadata={"updated": True},
523+
),
524+
)
525+
workspace.update_notebook_document(update_params)
526+
527+
notebook = workspace.get_notebook_document(notebook_uri=ENCODED_NB_URI)
528+
assert notebook.version == 5
529+
assert notebook.metadata == {"updated": True}
530+
531+
532+
def test_add_folder_percent_encoded(workspace):
533+
"""Looking up a folder with a decoded URI after adding with an encoded one."""
534+
encoded_uri = "file:///C%3A/workspace"
535+
decoded_uri = "file:///C:/workspace"
536+
workspace.add_folder(types.WorkspaceFolder(uri=encoded_uri, name="ws"))
537+
assert decoded_uri in workspace.folders
538+
539+
540+
def test_remove_folder_percent_encoded(workspace):
541+
"""Removing a folder stored with an encoded URI using a decoded URI."""
542+
encoded_uri = "file:///C%3A/workspace"
543+
decoded_uri = "file:///C:/workspace"
544+
workspace.add_folder(types.WorkspaceFolder(uri=encoded_uri, name="ws"))
545+
workspace.remove_folder(decoded_uri)
546+
assert decoded_uri not in workspace.folders

0 commit comments

Comments
 (0)