diff --git a/dvc/state.py b/dvc/state.py index 136f99a44f..9803e40c24 100644 --- a/dvc/state.py +++ b/dvc/state.py @@ -496,8 +496,14 @@ def _connect_sqlite(filename, options): def _build_sqlite_uri(filename, options): + # In the doc mentioned below we only need to replace ? -> %3f and + # # -> %23, but, if present, we also need to replace % -> %25 first + # (happens when we are on a weird FS that shows urlencoded filenames + # instead of proper ones) to not confuse sqlite. + uri_path = filename.replace("%", "%25") + # Convert filename to uri according to https://www.sqlite.org/uri.html, 3.1 - uri_path = filename.replace("?", "%3f").replace("#", "%23") + uri_path = uri_path.replace("?", "%3f").replace("#", "%23") if os.name == "nt": uri_path = uri_path.replace("\\", "/") uri_path = re.sub(r"^([a-z]:)", "/\\1", uri_path, flags=re.I) diff --git a/tests/unit/test_state.py b/tests/unit/test_state.py new file mode 100644 index 0000000000..27f55b8528 --- /dev/null +++ b/tests/unit/test_state.py @@ -0,0 +1,22 @@ +import pytest +from dvc.state import _build_sqlite_uri + + +@pytest.mark.parametrize( + "path, osname, result", + [ + ("/abs/path", "posix", "file:///abs/path"), + ("C:\\abs\\path", "nt", "file:///C:/abs/path"), + ("/abs/p?ath", "posix", "file:///abs/p%3fath"), + ("C:\\abs\\p?ath", "nt", "file:///C:/abs/p%3fath"), + ("/abs/p#ath", "posix", "file:///abs/p%23ath"), + ("C:\\abs\\p#ath", "nt", "file:///C:/abs/p%23ath"), + ("/abs/path space", "posix", "file:///abs/path space"), + ("C:\\abs\\path space", "nt", "file:///C:/abs/path space"), + ("/abs/path%20encoded", "posix", "file:///abs/path%2520encoded"), + ("C:\\abs\\path%20encoded", "nt", "file:///C:/abs/path%2520encoded"), + ], +) +def test_build_uri(path, osname, result, mocker): + mocker.patch("os.name", osname) + assert _build_sqlite_uri(path, {}) == result