Repository: rasterio/rasterio

Issue:
Path parsing breaks URLs with `//` in the query string.
## Expected behavior and actual behavior.

I'm opening a file over HTTP and the URL includes a querystring with a double forward slash `//`.

I can't share the full URL since it includes a token, but I've traced the call to the call to `pathlib.Path(path).as_posix()` at https://github.com/rasterio/rasterio/blob/5cf71dc806adc299108543def00647845ab4fc42/rasterio/_path.py#L79. The root issue is that `pathlib.Path` seems to normalize `//` to `/`.

```python
In [18]: pathlib.Path("foo.tif?bar=a//b").as_posix()
Out[18]: 'foo.tif?bar=a/b'
```

I don't actually know what to do here... I'm not sure if that call to `pathlib.Path` can be avoided (maybe only if the scheme is http?).

The relatively easy workaround is to prepend the `fp` argument to `rasterio.open` with `/vsicurl/`, but that might be hard for libraries to figure out (without reimplementing parts of `_parse_path`).

## Steps to reproduce the problem.

I've redacted the secret token so this doesn't actually run.

```python
In [1]: import rasterio

In [2]: rasterio.open('https://sentinel2l2a01.blob.core.windows.net/sentinel2-l2/10/T/ET/2016/02/08/S2A_MSIL2A_20160208T190542_N0300_R013_T10TET_20210528T041016.SAFE/GRANULE/L2A_T10TET_A003299_20160208T190605/IMG_DATA/R10m/T10TET_20160208T190542_B03_10m.tif?<token>&sig=QalzEsDqH2wtl%2BTouR7lVh
   ...: MmzYr4aIKDg%2BAP2//vywA%3D')
---------------------------------------------------------------------------
CPLE_HttpResponseError                    Traceback (most recent call last)
File rasterio/_base.pyx:302, in rasterio._base.DatasetBase.__init__()

File rasterio/_base.pyx:213, in rasterio._base.open_dataset()

File rasterio/_err.pyx:217, in rasterio._err.exc_wrap_pointer()

CPLE_HttpResponseError: HTTP response code: 403

During handling of the above exception, another exception occurred:

RasterioIOError                           Traceback (most recent call last)
Cell In [2], line 1
----> 1 rasterio.open('https://sentinel2l2a01.blob.core.windows.net/sentinel2-l2/10/T/ET/2016/02/08/S2A_MSIL2A_20160208T190542_N0300_R013_T10TET_20210528T041016.SAFE/GRANULE/L2A_T10TET_A003299_20160208T190605/IMG_DATA/R10m/T10TET_20160208T190542_B03_10m.tif?<token>&sig=QalzEsDqH2wtl%2BTouR7lVhMmzYr4aIKDg%2BAP2//vywA%3D')

File ~/mambaforge/envs/gdal/lib/python3.10/site-packages/rasterio/env.py:442, in ensure_env_with_credentials.<locals>.wrapper(*args, **kwds)
    439     session = DummySession()
    441 with env_ctor(session=session):
--> 442     return f(*args, **kwds)

File ~/mambaforge/envs/gdal/lib/python3.10/site-packages/rasterio/__init__.py:277, in open(fp, mode, driver, width, height, count, crs, transform, dtype, nodata, sharing, **kwargs)
    274 path = _parse_path(raw_dataset_path)
    276 if mode == "r":
--> 277     dataset = DatasetReader(path, driver=driver, sharing=sharing, **kwargs)
    278 elif mode == "r+":
    279     dataset = get_writer_for_path(path, driver=driver)(
    280         path, mode, driver=driver, sharing=sharing, **kwargs
    281     )

File rasterio/_base.pyx:304, in rasterio._base.DatasetBase.__init__()

RasterioIOError: HTTP response code: 403

```

#### Environment Information

```
❯ rio --show-versions
rasterio info:
  rasterio: 1.3.2
      GDAL: 3.5.2
      PROJ: 9.0.1
      GEOS: 3.11.0
 PROJ DATA: /home/taugspurger/mambaforge/envs/gdal/share/proj
 GDAL DATA: None

System:
    python: 3.10.6 | packaged by conda-forge | (main, Aug 22 2022, 20:36:39) [GCC 10.4.0]
executable: /home/taugspurger/mambaforge/envs/gdal/bin/python
   machine: Linux-5.15.57.1-microsoft-standard-WSL2-x86_64-with-glibc2.31

Python deps:
    affine: 2.3.1
     attrs: 22.1.0
   certifi: 2022.09.24
     click: 8.1.3
     cligj: 0.7.2
    cython: None
     numpy: 1.23.3
    snuggs: 1.4.7
click-plugins: None
setuptools: 65.4.1
```

## Installation Method

conda


Discussion:

Resolving patch:
diff --git a/rasterio/_path.py b/rasterio/_path.py
--- a/rasterio/_path.py
+++ b/rasterio/_path.py
@@ -64,7 +64,7 @@ class _ParsedPath(_Path):
     @classmethod
     def from_uri(cls, uri):
         parts = urlparse(uri)
-        path = parts.path
+        path = pathlib.Path(parts.path).as_posix() if parts.path else parts.path
         scheme = parts.scheme or None
 
         if parts.query:
@@ -76,7 +76,7 @@ def from_uri(cls, uri):
         parts = path.split('!')
         path = parts.pop() if parts else None
         archive = parts.pop() if parts else None
-        return _ParsedPath(pathlib.Path(path).as_posix(), archive, scheme)
+        return _ParsedPath(path, archive, scheme)
 
     @property
     def name(self):


Test patch:
diff --git a/tests/test_path.py b/tests/test_path.py
--- a/tests/test_path.py
+++ b/tests/test_path.py
@@ -99,6 +99,14 @@ def test_parse_gdal():
     assert _parse_path('GDAL:filepath:varname').path == 'GDAL:filepath:varname'
 
 
+def test_parse_http_password():
+    """Make sure password unmodified GH2602"""
+    parsed = _parse_path('https://foo.tif?bar=a//b')
+    assert parsed.path == 'foo.tif?bar=a//b'
+    assert parsed.archive is None
+    assert parsed.scheme == 'https'
+
+
 @pytest.mark.skipif(sys.platform == 'win32', reason="Checking behavior on posix, not win32")
 def test_parse_windows_path(monkeypatch):
     """Return Windows paths unparsed"""