We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `rasterio/_path.py`
Content:
```
1 """Dataset paths, identifiers, and filenames
2 
3 Note: this module is not part of Rasterio's API. It is for internal use
4 only.
5 
6 """
7 
8 import pathlib
9 import re
10 import sys
11 from urllib.parse import urlparse
12 
13 import attr
14 
15 from rasterio.errors import PathError
16 
17 # Supported URI schemes and their mapping to GDAL's VSI suffix.
18 # TODO: extend for other cloud plaforms.
19 SCHEMES = {
20     'ftp': 'curl',
21     'gzip': 'gzip',
22     'http': 'curl',
23     'https': 'curl',
24     's3': 's3',
25     'tar': 'tar',
26     'zip': 'zip',
27     'file': 'file',
28     'oss': 'oss',
29     'gs': 'gs',
30     'az': 'az',
31 }
32 
33 CURLSCHEMES = set([k for k, v in SCHEMES.items() if v == 'curl'])
34 
35 # TODO: extend for other cloud plaforms.
36 REMOTESCHEMES = set([k for k, v in SCHEMES.items() if v in ('curl', 's3', 'oss', 'gs', 'az',)])
37 
38 
39 class _Path:
40     """Base class for dataset paths"""
41 
42     def as_vsi(self):
43         return _vsi_path(self)
44 
45 
46 @attr.s(slots=True)
47 class _ParsedPath(_Path):
48     """Result of parsing a dataset URI/Path
49 
50     Attributes
51     ----------
52     path : str
53         Parsed path. Includes the hostname and query string in the case
54         of a URI.
55     archive : str
56         Parsed archive path.
57     scheme : str
58         URI scheme such as "https" or "zip+s3".
59     """
60     path = attr.ib()
61     archive = attr.ib()
62     scheme = attr.ib()
63 
64     @classmethod
65     def from_uri(cls, uri):
66         parts = urlparse(uri)
67         path = parts.path
68         scheme = parts.scheme or None
69 
70         if parts.query:
71             path += "?" + parts.query
72 
73         if parts.scheme and parts.netloc:
74             path = parts.netloc + path
75 
76         parts = path.split('!')
77         path = parts.pop() if parts else None
78         archive = parts.pop() if parts else None
79         return _ParsedPath(pathlib.Path(path).as_posix(), archive, scheme)
80 
81     @property
82     def name(self):
83         """The parsed path's original URI"""
84         if not self.scheme:
85             return self.path
86         elif self.archive:
87             return "{}://{}!{}".format(self.scheme, self.archive, self.path)
88         else:
89             return "{}://{}".format(self.scheme, self.path)
90 
91     @property
92     def is_remote(self):
93         """Test if the path is a remote, network URI"""
94         return bool(self.scheme) and self.scheme.split("+")[-1] in REMOTESCHEMES
95 
96     @property
97     def is_local(self):
98         """Test if the path is a local URI"""
99         return not self.scheme or (self.scheme and self.scheme.split('+')[-1] not in REMOTESCHEMES)
100 
101 
102 @attr.s(slots=True)
103 class _UnparsedPath(_Path):
104     """Encapsulates legacy GDAL filenames
105 
106     Attributes
107     ----------
108     path : str
109         The legacy GDAL filename.
110     """
111     path = attr.ib()
112 
113     @property
114     def name(self):
115         """The unparsed path's original path"""
116         return self.path
117 
118 
119 def _parse_path(path):
120     """Parse a dataset's identifier or path into its parts
121 
122     Parameters
123     ----------
124     path : str or path-like object
125         The path to be parsed.
126 
127     Returns
128     -------
129     ParsedPath or UnparsedPath
130 
131     Notes
132     -----
133     When legacy GDAL filenames are encountered, they will be returned
134     in a UnparsedPath.
135 
136     """
137     if isinstance(path, _Path):
138         return path
139 
140     elif pathlib and isinstance(path, pathlib.PurePath):
141         return _ParsedPath(path.as_posix(), None, None)
142 
143     elif isinstance(path, str):
144 
145         if sys.platform == "win32" and re.match(r"^[a-zA-Z]\:", path):
146             if pathlib:
147                 return _ParsedPath(pathlib.Path(path).as_posix(), None, None)
148             else:
149                 return _UnparsedPath(path)
150 
151         elif path.startswith('/vsi'):
152             return _UnparsedPath(path)
153 
154         else:
155             parts = urlparse(path)
156 
157     else:
158         raise PathError("invalid path '{!r}'".format(path))
159 
160     # if the scheme is not one of Rasterio's supported schemes, we
161     # return an UnparsedPath.
162     if parts.scheme:
163 
164         if all(p in SCHEMES for p in parts.scheme.split('+')):
165             return _ParsedPath.from_uri(path)
166 
167     return _UnparsedPath(path)
168 
169 
170 def _vsi_path(path):
171     """Convert a parsed path to a GDAL VSI path
172 
173     Parameters
174     ----------
175     path : Path
176         A ParsedPath or UnparsedPath object.
177 
178     Returns
179     -------
180     str
181 
182     """
183     if isinstance(path, _UnparsedPath):
184         return path.path
185 
186     elif isinstance(path, _ParsedPath):
187 
188         if not path.scheme:
189             return path.path
190 
191         else:
192             if path.scheme.split('+')[-1] in CURLSCHEMES:
193                 suffix = '{}://'.format(path.scheme.split('+')[-1])
194             else:
195                 suffix = ''
196 
197             prefix = '/'.join('vsi{0}'.format(SCHEMES[p]) for p in path.scheme.split('+') if p != 'file')
198 
199             if prefix:
200                 if path.archive:
201                     result = '/{}/{}{}/{}'.format(prefix, suffix, path.archive, path.path.lstrip('/'))
202                 else:
203                     result = '/{}/{}{}'.format(prefix, suffix, path.path)
204             else:
205                 result = path.path
206             return result
207 
208     else:
209         raise ValueError("path must be a ParsedPath or UnparsedPath object")
210
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/rasterio/_path.py b/rasterio/_path.py
--- a/rasterio/_path.py
+++ b/rasterio/_path.py
@@ -64,7 +64,7 @@
     @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 @@
         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):

{"golden_diff": "diff --git a/rasterio/_path.py b/rasterio/_path.py\n--- a/rasterio/_path.py\n+++ b/rasterio/_path.py\n@@ -64,7 +64,7 @@\n     @classmethod\n     def from_uri(cls, uri):\n         parts = urlparse(uri)\n-        path = parts.path\n+        path = pathlib.Path(parts.path).as_posix() if parts.path else parts.path\n         scheme = parts.scheme or None\n \n         if parts.query:\n@@ -76,7 +76,7 @@\n         parts = path.split('!')\n         path = parts.pop() if parts else None\n         archive = parts.pop() if parts else None\n-        return _ParsedPath(pathlib.Path(path).as_posix(), archive, scheme)\n+        return _ParsedPath(path, archive, scheme)\n \n     @property\n     def name(self):\n", "issue": "Path parsing breaks URLs with `//` in the query string.\n## Expected behavior and actual behavior.\r\n\r\nI'm opening a file over HTTP and the URL includes a querystring with a double forward slash `//`.\r\n\r\nI 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 `/`.\r\n\r\n```python\r\nIn [18]: pathlib.Path(\"foo.tif?bar=a//b\").as_posix()\r\nOut[18]: 'foo.tif?bar=a/b'\r\n```\r\n\r\nI 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?).\r\n\r\nThe 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`).\r\n\r\n## Steps to reproduce the problem.\r\n\r\nI've redacted the secret token so this doesn't actually run.\r\n\r\n```python\r\nIn [1]: import rasterio\r\n\r\nIn [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\r\n   ...: MmzYr4aIKDg%2BAP2//vywA%3D')\r\n---------------------------------------------------------------------------\r\nCPLE_HttpResponseError                    Traceback (most recent call last)\r\nFile rasterio/_base.pyx:302, in rasterio._base.DatasetBase.__init__()\r\n\r\nFile rasterio/_base.pyx:213, in rasterio._base.open_dataset()\r\n\r\nFile rasterio/_err.pyx:217, in rasterio._err.exc_wrap_pointer()\r\n\r\nCPLE_HttpResponseError: HTTP response code: 403\r\n\r\nDuring handling of the above exception, another exception occurred:\r\n\r\nRasterioIOError                           Traceback (most recent call last)\r\nCell In [2], line 1\r\n----> 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')\r\n\r\nFile ~/mambaforge/envs/gdal/lib/python3.10/site-packages/rasterio/env.py:442, in ensure_env_with_credentials.<locals>.wrapper(*args, **kwds)\r\n    439     session = DummySession()\r\n    441 with env_ctor(session=session):\r\n--> 442     return f(*args, **kwds)\r\n\r\nFile ~/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)\r\n    274 path = _parse_path(raw_dataset_path)\r\n    276 if mode == \"r\":\r\n--> 277     dataset = DatasetReader(path, driver=driver, sharing=sharing, **kwargs)\r\n    278 elif mode == \"r+\":\r\n    279     dataset = get_writer_for_path(path, driver=driver)(\r\n    280         path, mode, driver=driver, sharing=sharing, **kwargs\r\n    281     )\r\n\r\nFile rasterio/_base.pyx:304, in rasterio._base.DatasetBase.__init__()\r\n\r\nRasterioIOError: HTTP response code: 403\r\n\r\n```\r\n\r\n#### Environment Information\r\n\r\n```\r\n\u276f rio --show-versions\r\nrasterio info:\r\n  rasterio: 1.3.2\r\n      GDAL: 3.5.2\r\n      PROJ: 9.0.1\r\n      GEOS: 3.11.0\r\n PROJ DATA: /home/taugspurger/mambaforge/envs/gdal/share/proj\r\n GDAL DATA: None\r\n\r\nSystem:\r\n    python: 3.10.6 | packaged by conda-forge | (main, Aug 22 2022, 20:36:39) [GCC 10.4.0]\r\nexecutable: /home/taugspurger/mambaforge/envs/gdal/bin/python\r\n   machine: Linux-5.15.57.1-microsoft-standard-WSL2-x86_64-with-glibc2.31\r\n\r\nPython deps:\r\n    affine: 2.3.1\r\n     attrs: 22.1.0\r\n   certifi: 2022.09.24\r\n     click: 8.1.3\r\n     cligj: 0.7.2\r\n    cython: None\r\n     numpy: 1.23.3\r\n    snuggs: 1.4.7\r\nclick-plugins: None\r\nsetuptools: 65.4.1\r\n```\r\n\r\n## Installation Method\r\n\r\nconda\n", "before_files": [{"content": "\"\"\"Dataset paths, identifiers, and filenames\n\nNote: this module is not part of Rasterio's API. It is for internal use\nonly.\n\n\"\"\"\n\nimport pathlib\nimport re\nimport sys\nfrom urllib.parse import urlparse\n\nimport attr\n\nfrom rasterio.errors import PathError\n\n# Supported URI schemes and their mapping to GDAL's VSI suffix.\n# TODO: extend for other cloud plaforms.\nSCHEMES = {\n    'ftp': 'curl',\n    'gzip': 'gzip',\n    'http': 'curl',\n    'https': 'curl',\n    's3': 's3',\n    'tar': 'tar',\n    'zip': 'zip',\n    'file': 'file',\n    'oss': 'oss',\n    'gs': 'gs',\n    'az': 'az',\n}\n\nCURLSCHEMES = set([k for k, v in SCHEMES.items() if v == 'curl'])\n\n# TODO: extend for other cloud plaforms.\nREMOTESCHEMES = set([k for k, v in SCHEMES.items() if v in ('curl', 's3', 'oss', 'gs', 'az',)])\n\n\nclass _Path:\n    \"\"\"Base class for dataset paths\"\"\"\n\n    def as_vsi(self):\n        return _vsi_path(self)\n\n\n@attr.s(slots=True)\nclass _ParsedPath(_Path):\n    \"\"\"Result of parsing a dataset URI/Path\n\n    Attributes\n    ----------\n    path : str\n        Parsed path. Includes the hostname and query string in the case\n        of a URI.\n    archive : str\n        Parsed archive path.\n    scheme : str\n        URI scheme such as \"https\" or \"zip+s3\".\n    \"\"\"\n    path = attr.ib()\n    archive = attr.ib()\n    scheme = attr.ib()\n\n    @classmethod\n    def from_uri(cls, uri):\n        parts = urlparse(uri)\n        path = parts.path\n        scheme = parts.scheme or None\n\n        if parts.query:\n            path += \"?\" + parts.query\n\n        if parts.scheme and parts.netloc:\n            path = parts.netloc + path\n\n        parts = path.split('!')\n        path = parts.pop() if parts else None\n        archive = parts.pop() if parts else None\n        return _ParsedPath(pathlib.Path(path).as_posix(), archive, scheme)\n\n    @property\n    def name(self):\n        \"\"\"The parsed path's original URI\"\"\"\n        if not self.scheme:\n            return self.path\n        elif self.archive:\n            return \"{}://{}!{}\".format(self.scheme, self.archive, self.path)\n        else:\n            return \"{}://{}\".format(self.scheme, self.path)\n\n    @property\n    def is_remote(self):\n        \"\"\"Test if the path is a remote, network URI\"\"\"\n        return bool(self.scheme) and self.scheme.split(\"+\")[-1] in REMOTESCHEMES\n\n    @property\n    def is_local(self):\n        \"\"\"Test if the path is a local URI\"\"\"\n        return not self.scheme or (self.scheme and self.scheme.split('+')[-1] not in REMOTESCHEMES)\n\n\n@attr.s(slots=True)\nclass _UnparsedPath(_Path):\n    \"\"\"Encapsulates legacy GDAL filenames\n\n    Attributes\n    ----------\n    path : str\n        The legacy GDAL filename.\n    \"\"\"\n    path = attr.ib()\n\n    @property\n    def name(self):\n        \"\"\"The unparsed path's original path\"\"\"\n        return self.path\n\n\ndef _parse_path(path):\n    \"\"\"Parse a dataset's identifier or path into its parts\n\n    Parameters\n    ----------\n    path : str or path-like object\n        The path to be parsed.\n\n    Returns\n    -------\n    ParsedPath or UnparsedPath\n\n    Notes\n    -----\n    When legacy GDAL filenames are encountered, they will be returned\n    in a UnparsedPath.\n\n    \"\"\"\n    if isinstance(path, _Path):\n        return path\n\n    elif pathlib and isinstance(path, pathlib.PurePath):\n        return _ParsedPath(path.as_posix(), None, None)\n\n    elif isinstance(path, str):\n\n        if sys.platform == \"win32\" and re.match(r\"^[a-zA-Z]\\:\", path):\n            if pathlib:\n                return _ParsedPath(pathlib.Path(path).as_posix(), None, None)\n            else:\n                return _UnparsedPath(path)\n\n        elif path.startswith('/vsi'):\n            return _UnparsedPath(path)\n\n        else:\n            parts = urlparse(path)\n\n    else:\n        raise PathError(\"invalid path '{!r}'\".format(path))\n\n    # if the scheme is not one of Rasterio's supported schemes, we\n    # return an UnparsedPath.\n    if parts.scheme:\n\n        if all(p in SCHEMES for p in parts.scheme.split('+')):\n            return _ParsedPath.from_uri(path)\n\n    return _UnparsedPath(path)\n\n\ndef _vsi_path(path):\n    \"\"\"Convert a parsed path to a GDAL VSI path\n\n    Parameters\n    ----------\n    path : Path\n        A ParsedPath or UnparsedPath object.\n\n    Returns\n    -------\n    str\n\n    \"\"\"\n    if isinstance(path, _UnparsedPath):\n        return path.path\n\n    elif isinstance(path, _ParsedPath):\n\n        if not path.scheme:\n            return path.path\n\n        else:\n            if path.scheme.split('+')[-1] in CURLSCHEMES:\n                suffix = '{}://'.format(path.scheme.split('+')[-1])\n            else:\n                suffix = ''\n\n            prefix = '/'.join('vsi{0}'.format(SCHEMES[p]) for p in path.scheme.split('+') if p != 'file')\n\n            if prefix:\n                if path.archive:\n                    result = '/{}/{}{}/{}'.format(prefix, suffix, path.archive, path.path.lstrip('/'))\n                else:\n                    result = '/{}/{}{}'.format(prefix, suffix, path.path)\n            else:\n                result = path.path\n            return result\n\n    else:\n        raise ValueError(\"path must be a ParsedPath or UnparsedPath object\")\n", "path": "rasterio/_path.py"}], "after_files": [{"content": "\"\"\"Dataset paths, identifiers, and filenames\n\nNote: this module is not part of Rasterio's API. It is for internal use\nonly.\n\n\"\"\"\n\nimport pathlib\nimport re\nimport sys\nfrom urllib.parse import urlparse\n\nimport attr\n\nfrom rasterio.errors import PathError\n\n# Supported URI schemes and their mapping to GDAL's VSI suffix.\n# TODO: extend for other cloud plaforms.\nSCHEMES = {\n    'ftp': 'curl',\n    'gzip': 'gzip',\n    'http': 'curl',\n    'https': 'curl',\n    's3': 's3',\n    'tar': 'tar',\n    'zip': 'zip',\n    'file': 'file',\n    'oss': 'oss',\n    'gs': 'gs',\n    'az': 'az',\n}\n\nCURLSCHEMES = set([k for k, v in SCHEMES.items() if v == 'curl'])\n\n# TODO: extend for other cloud plaforms.\nREMOTESCHEMES = set([k for k, v in SCHEMES.items() if v in ('curl', 's3', 'oss', 'gs', 'az',)])\n\n\nclass _Path:\n    \"\"\"Base class for dataset paths\"\"\"\n\n    def as_vsi(self):\n        return _vsi_path(self)\n\n\n@attr.s(slots=True)\nclass _ParsedPath(_Path):\n    \"\"\"Result of parsing a dataset URI/Path\n\n    Attributes\n    ----------\n    path : str\n        Parsed path. Includes the hostname and query string in the case\n        of a URI.\n    archive : str\n        Parsed archive path.\n    scheme : str\n        URI scheme such as \"https\" or \"zip+s3\".\n    \"\"\"\n    path = attr.ib()\n    archive = attr.ib()\n    scheme = attr.ib()\n\n    @classmethod\n    def from_uri(cls, uri):\n        parts = urlparse(uri)\n        path = pathlib.Path(parts.path).as_posix() if parts.path else parts.path\n        scheme = parts.scheme or None\n\n        if parts.query:\n            path += \"?\" + parts.query\n\n        if parts.scheme and parts.netloc:\n            path = parts.netloc + path\n\n        parts = path.split('!')\n        path = parts.pop() if parts else None\n        archive = parts.pop() if parts else None\n        return _ParsedPath(path, archive, scheme)\n\n    @property\n    def name(self):\n        \"\"\"The parsed path's original URI\"\"\"\n        if not self.scheme:\n            return self.path\n        elif self.archive:\n            return \"{}://{}!{}\".format(self.scheme, self.archive, self.path)\n        else:\n            return \"{}://{}\".format(self.scheme, self.path)\n\n    @property\n    def is_remote(self):\n        \"\"\"Test if the path is a remote, network URI\"\"\"\n        return bool(self.scheme) and self.scheme.split(\"+\")[-1] in REMOTESCHEMES\n\n    @property\n    def is_local(self):\n        \"\"\"Test if the path is a local URI\"\"\"\n        return not self.scheme or (self.scheme and self.scheme.split('+')[-1] not in REMOTESCHEMES)\n\n\n@attr.s(slots=True)\nclass _UnparsedPath(_Path):\n    \"\"\"Encapsulates legacy GDAL filenames\n\n    Attributes\n    ----------\n    path : str\n        The legacy GDAL filename.\n    \"\"\"\n    path = attr.ib()\n\n    @property\n    def name(self):\n        \"\"\"The unparsed path's original path\"\"\"\n        return self.path\n\n\ndef _parse_path(path):\n    \"\"\"Parse a dataset's identifier or path into its parts\n\n    Parameters\n    ----------\n    path : str or path-like object\n        The path to be parsed.\n\n    Returns\n    -------\n    ParsedPath or UnparsedPath\n\n    Notes\n    -----\n    When legacy GDAL filenames are encountered, they will be returned\n    in a UnparsedPath.\n\n    \"\"\"\n    if isinstance(path, _Path):\n        return path\n\n    elif pathlib and isinstance(path, pathlib.PurePath):\n        return _ParsedPath(path.as_posix(), None, None)\n\n    elif isinstance(path, str):\n\n        if sys.platform == \"win32\" and re.match(r\"^[a-zA-Z]\\:\", path):\n            if pathlib:\n                return _ParsedPath(pathlib.Path(path).as_posix(), None, None)\n            else:\n                return _UnparsedPath(path)\n\n        elif path.startswith('/vsi'):\n            return _UnparsedPath(path)\n\n        else:\n            parts = urlparse(path)\n\n    else:\n        raise PathError(\"invalid path '{!r}'\".format(path))\n\n    # if the scheme is not one of Rasterio's supported schemes, we\n    # return an UnparsedPath.\n    if parts.scheme:\n\n        if all(p in SCHEMES for p in parts.scheme.split('+')):\n            return _ParsedPath.from_uri(path)\n\n    return _UnparsedPath(path)\n\n\ndef _vsi_path(path):\n    \"\"\"Convert a parsed path to a GDAL VSI path\n\n    Parameters\n    ----------\n    path : Path\n        A ParsedPath or UnparsedPath object.\n\n    Returns\n    -------\n    str\n\n    \"\"\"\n    if isinstance(path, _UnparsedPath):\n        return path.path\n\n    elif isinstance(path, _ParsedPath):\n\n        if not path.scheme:\n            return path.path\n\n        else:\n            if path.scheme.split('+')[-1] in CURLSCHEMES:\n                suffix = '{}://'.format(path.scheme.split('+')[-1])\n            else:\n                suffix = ''\n\n            prefix = '/'.join('vsi{0}'.format(SCHEMES[p]) for p in path.scheme.split('+') if p != 'file')\n\n            if prefix:\n                if path.archive:\n                    result = '/{}/{}{}/{}'.format(prefix, suffix, path.archive, path.path.lstrip('/'))\n                else:\n                    result = '/{}/{}{}'.format(prefix, suffix, path.path)\n            else:\n                result = path.path\n            return result\n\n    else:\n        raise ValueError(\"path must be a ParsedPath or UnparsedPath object\")\n", "path": "rasterio/_path.py"}]}