prompt: You will be provided with a partial code base and an issue statement explaining a problem to resolve.
  <issue>
  [V3 Config] Config does not reject bad keys
  Including a `.` or starting a keyname with `$` needs to be invalid in config because of mongoDB backend support. 
  
  Config will work just fine for this on the json backend, but not with the mongodb one because these characters are not allowed in field names for mongo. Instead, it works with json, and throws uncaught exceptions on mongo.
  
  I'd be in favor of limiting keynames to alpha-numeric and space, underscore, and hyphens. to prevent future issues like this with other future possible backends without possibly breaking cogs twice for this.
  
  </issue>
  <code>
  [start of redbot/core/drivers/red_mongo.py]
  1 import motor.motor_asyncio
  2 from .red_base import BaseDriver
  3 from urllib.parse import quote_plus
  4 
  5 __all__ = ["Mongo"]
  6 
  7 
  8 _conn = None
  9 
  10 
  11 def _initialize(**kwargs):
  12     uri = kwargs.get("URI", "mongodb")
  13     host = kwargs["HOST"]
  14     port = kwargs["PORT"]
  15     admin_user = kwargs["USERNAME"]
  16     admin_pass = kwargs["PASSWORD"]
  17     db_name = kwargs.get("DB_NAME", "default_db")
  18 
  19     if port is 0:
  20         ports = ""
  21     else:
  22         ports = ":{}".format(port)
  23 
  24     if admin_user is not None and admin_pass is not None:
  25         url = "{}://{}:{}@{}{}/{}".format(
  26             uri, quote_plus(admin_user), quote_plus(admin_pass), host, ports, db_name
  27         )
  28     else:
  29         url = "{}://{}{}/{}".format(uri, host, ports, db_name)
  30 
  31     global _conn
  32     _conn = motor.motor_asyncio.AsyncIOMotorClient(url)
  33 
  34 
  35 class Mongo(BaseDriver):
  36     """
  37     Subclass of :py:class:`.red_base.BaseDriver`.
  38     """
  39 
  40     def __init__(self, cog_name, identifier, **kwargs):
  41         super().__init__(cog_name, identifier)
  42 
  43         if _conn is None:
  44             _initialize(**kwargs)
  45 
  46     @property
  47     def db(self) -> motor.core.Database:
  48         """
  49         Gets the mongo database for this cog's name.
  50 
  51         .. warning::
  52 
  53             Right now this will cause a new connection to be made every time the
  54             database is accessed. We will want to create a connection pool down the
  55             line to limit the number of connections.
  56 
  57         :return:
  58             PyMongo Database object.
  59         """
  60         return _conn.get_database()
  61 
  62     def get_collection(self) -> motor.core.Collection:
  63         """
  64         Gets a specified collection within the PyMongo database for this cog.
  65 
  66         Unless you are doing custom stuff ``collection_name`` should be one of the class
  67         attributes of :py:class:`core.config.Config`.
  68 
  69         :param str collection_name:
  70         :return:
  71             PyMongo collection object.
  72         """
  73         return self.db[self.cog_name]
  74 
  75     @staticmethod
  76     def _parse_identifiers(identifiers):
  77         uuid, identifiers = identifiers[0], identifiers[1:]
  78         return uuid, identifiers
  79 
  80     async def get(self, *identifiers: str):
  81         mongo_collection = self.get_collection()
  82 
  83         dot_identifiers = ".".join(identifiers)
  84 
  85         partial = await mongo_collection.find_one(
  86             filter={"_id": self.unique_cog_identifier}, projection={dot_identifiers: True}
  87         )
  88 
  89         if partial is None:
  90             raise KeyError("No matching document was found and Config expects a KeyError.")
  91 
  92         for i in identifiers:
  93             partial = partial[i]
  94         return partial
  95 
  96     async def set(self, *identifiers: str, value=None):
  97         dot_identifiers = ".".join(identifiers)
  98 
  99         mongo_collection = self.get_collection()
  100 
  101         await mongo_collection.update_one(
  102             {"_id": self.unique_cog_identifier},
  103             update={"$set": {dot_identifiers: value}},
  104             upsert=True,
  105         )
  106 
  107     async def clear(self, *identifiers: str):
  108         dot_identifiers = ".".join(identifiers)
  109         mongo_collection = self.get_collection()
  110 
  111         if len(identifiers) > 0:
  112             await mongo_collection.update_one(
  113                 {"_id": self.unique_cog_identifier}, update={"$unset": {dot_identifiers: 1}}
  114             )
  115         else:
  116             await mongo_collection.delete_one({"_id": self.unique_cog_identifier})
  117 
  118 
  119 def get_config_details():
  120     uri = None
  121     while True:
  122         uri = input("Enter URI scheme (mongodb or mongodb+srv): ")
  123         if uri is "":
  124             uri = "mongodb"
  125 
  126         if uri in ["mongodb", "mongodb+srv"]:
  127             break
  128         else:
  129             print("Invalid URI scheme")
  130 
  131     host = input("Enter host address: ")
  132     if uri is "mongodb":
  133         port = int(input("Enter host port: "))
  134     else:
  135         port = 0
  136 
  137     admin_uname = input("Enter login username: ")
  138     admin_password = input("Enter login password: ")
  139 
  140     db_name = input("Enter mongodb database name: ")
  141 
  142     if admin_uname == "":
  143         admin_uname = admin_password = None
  144 
  145     ret = {
  146         "HOST": host,
  147         "PORT": port,
  148         "USERNAME": admin_uname,
  149         "PASSWORD": admin_password,
  150         "DB_NAME": db_name,
  151         "URI": uri,
  152     }
  153     return ret
  154
  [end of redbot/core/drivers/red_mongo.py]
  </code>
  
  I need you to solve this issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the following format.
  <patch>
  diff --git a/file.py b/file.py
  --- a/file.py
  +++ b/file.py
  @@ -1,27 +1,35 @@
   def euclidean(a, b):
  -    while b:
  -        a, b = b, a % b
  -    return a
  +    if b == 0:
  +        return a
  +    return euclidean(b, a % b)
   
   
   def bresenham(x0, y0, x1, y1):
       points = []
       dx = abs(x1 - x0)
       dy = abs(y1 - y0)
  -    sx = 1 if x0 < x1 else -1
  -    sy = 1 if y0 < y1 else -1
  -    err = dx - dy
  +    x, y = x0, y0
  +    sx = -1 if x0 > x1 else 1
  +    sy = -1 if y0 > y1 else 1
   
  -    while True:
  -        points.append((x0, y0))
  -        if x0 == x1 and y0 == y1:
  -            break
  -        e2 = 2 * err
  -        if e2 > -dy:
  -            err -= dy
  -            x0 += sx
  -        if e2 < dx:
  -            err += dx
  -            y0 += sy
  +    if dx > dy:
  +        err = dx / 2.0
  +        while x != x1:
  +            points.append((x, y))
  +            err -= dy
  +            if err < 0:
  +                y += sy
  +                err += dx
  +            x += sx
  +    else:
  +        err = dy / 2.0
  +        while y != y1:
  +            points.append((x, y))
  +            err -= dx
  +            if err < 0:
  +                x += sx
  +                err += dy
  +            y += sy
  +
  +    points.append((x, y))
       return points
  
  </patch>
golden_diff: diff --git a/redbot/core/drivers/red_mongo.py b/redbot/core/drivers/red_mongo.py
  --- a/redbot/core/drivers/red_mongo.py
  +++ b/redbot/core/drivers/red_mongo.py
  @@ -1,6 +1,11 @@
  +import re
  +from typing import Match, Pattern
  +from urllib.parse import quote_plus
  +
  +import motor.core
   import motor.motor_asyncio
  +
   from .red_base import BaseDriver
  -from urllib.parse import quote_plus
   
   __all__ = ["Mongo"]
   
  @@ -80,6 +85,7 @@
       async def get(self, *identifiers: str):
           mongo_collection = self.get_collection()
   
  +        identifiers = (*map(self._escape_key, identifiers),)
           dot_identifiers = ".".join(identifiers)
   
           partial = await mongo_collection.find_one(
  @@ -91,10 +97,14 @@
   
           for i in identifiers:
               partial = partial[i]
  +        if isinstance(partial, dict):
  +            return self._unescape_dict_keys(partial)
           return partial
   
       async def set(self, *identifiers: str, value=None):
  -        dot_identifiers = ".".join(identifiers)
  +        dot_identifiers = ".".join(map(self._escape_key, identifiers))
  +        if isinstance(value, dict):
  +            value = self._escape_dict_keys(value)
   
           mongo_collection = self.get_collection()
   
  @@ -105,7 +115,7 @@
           )
   
       async def clear(self, *identifiers: str):
  -        dot_identifiers = ".".join(identifiers)
  +        dot_identifiers = ".".join(map(self._escape_key, identifiers))
           mongo_collection = self.get_collection()
   
           if len(identifiers) > 0:
  @@ -115,6 +125,62 @@
           else:
               await mongo_collection.delete_one({"_id": self.unique_cog_identifier})
   
  +    @staticmethod
  +    def _escape_key(key: str) -> str:
  +        return _SPECIAL_CHAR_PATTERN.sub(_replace_with_escaped, key)
  +
  +    @staticmethod
  +    def _unescape_key(key: str) -> str:
  +        return _CHAR_ESCAPE_PATTERN.sub(_replace_with_unescaped, key)
  +
  +    @classmethod
  +    def _escape_dict_keys(cls, data: dict) -> dict:
  +        """Recursively escape all keys in a dict."""
  +        ret = {}
  +        for key, value in data.items():
  +            key = cls._escape_key(key)
  +            if isinstance(value, dict):
  +                value = cls._escape_dict_keys(value)
  +            ret[key] = value
  +        return ret
  +
  +    @classmethod
  +    def _unescape_dict_keys(cls, data: dict) -> dict:
  +        """Recursively unescape all keys in a dict."""
  +        ret = {}
  +        for key, value in data.items():
  +            key = cls._unescape_key(key)
  +            if isinstance(value, dict):
  +                value = cls._unescape_dict_keys(value)
  +            ret[key] = value
  +        return ret
  +
  +
  +_SPECIAL_CHAR_PATTERN: Pattern[str] = re.compile(r"([.$]|\\U0000002E|\\U00000024)")
  +_SPECIAL_CHARS = {
  +    ".": "\\U0000002E",
  +    "$": "\\U00000024",
  +    "\\U0000002E": "\\U&0000002E",
  +    "\\U00000024": "\\U&00000024",
  +}
  +
  +
  +def _replace_with_escaped(match: Match[str]) -> str:
  +    return _SPECIAL_CHARS[match[0]]
  +
  +
  +_CHAR_ESCAPE_PATTERN: Pattern[str] = re.compile(r"(\\U0000002E|\\U00000024)")
  +_CHAR_ESCAPES = {
  +    "\\U0000002E": ".",
  +    "\\U00000024": "$",
  +    "\\U&0000002E": "\\U0000002E",
  +    "\\U&00000024": "\\U00000024",
  +}
  +
  +
  +def _replace_with_unescaped(match: Match[str]) -> str:
  +    return _CHAR_ESCAPES[match[0]]
  +
   
   def get_config_details():
       uri = None
  
verification_info: {"golden_diff": "diff --git a/redbot/core/drivers/red_mongo.py b/redbot/core/drivers/red_mongo.py\n--- a/redbot/core/drivers/red_mongo.py\n+++ b/redbot/core/drivers/red_mongo.py\n@@ -1,6 +1,11 @@\n+import re\n+from typing import Match, Pattern\n+from urllib.parse import quote_plus\n+\n+import motor.core\n import motor.motor_asyncio\n+\n from .red_base import BaseDriver\n-from urllib.parse import quote_plus\n \n __all__ = [\"Mongo\"]\n \n@@ -80,6 +85,7 @@\n     async def get(self, *identifiers: str):\n         mongo_collection = self.get_collection()\n \n+        identifiers = (*map(self._escape_key, identifiers),)\n         dot_identifiers = \".\".join(identifiers)\n \n         partial = await mongo_collection.find_one(\n@@ -91,10 +97,14 @@\n \n         for i in identifiers:\n             partial = partial[i]\n+        if isinstance(partial, dict):\n+            return self._unescape_dict_keys(partial)\n         return partial\n \n     async def set(self, *identifiers: str, value=None):\n-        dot_identifiers = \".\".join(identifiers)\n+        dot_identifiers = \".\".join(map(self._escape_key, identifiers))\n+        if isinstance(value, dict):\n+            value = self._escape_dict_keys(value)\n \n         mongo_collection = self.get_collection()\n \n@@ -105,7 +115,7 @@\n         )\n \n     async def clear(self, *identifiers: str):\n-        dot_identifiers = \".\".join(identifiers)\n+        dot_identifiers = \".\".join(map(self._escape_key, identifiers))\n         mongo_collection = self.get_collection()\n \n         if len(identifiers) > 0:\n@@ -115,6 +125,62 @@\n         else:\n             await mongo_collection.delete_one({\"_id\": self.unique_cog_identifier})\n \n+    @staticmethod\n+    def _escape_key(key: str) -> str:\n+        return _SPECIAL_CHAR_PATTERN.sub(_replace_with_escaped, key)\n+\n+    @staticmethod\n+    def _unescape_key(key: str) -> str:\n+        return _CHAR_ESCAPE_PATTERN.sub(_replace_with_unescaped, key)\n+\n+    @classmethod\n+    def _escape_dict_keys(cls, data: dict) -> dict:\n+        \"\"\"Recursively escape all keys in a dict.\"\"\"\n+        ret = {}\n+        for key, value in data.items():\n+            key = cls._escape_key(key)\n+            if isinstance(value, dict):\n+                value = cls._escape_dict_keys(value)\n+            ret[key] = value\n+        return ret\n+\n+    @classmethod\n+    def _unescape_dict_keys(cls, data: dict) -> dict:\n+        \"\"\"Recursively unescape all keys in a dict.\"\"\"\n+        ret = {}\n+        for key, value in data.items():\n+            key = cls._unescape_key(key)\n+            if isinstance(value, dict):\n+                value = cls._unescape_dict_keys(value)\n+            ret[key] = value\n+        return ret\n+\n+\n+_SPECIAL_CHAR_PATTERN: Pattern[str] = re.compile(r\"([.$]|\\\\U0000002E|\\\\U00000024)\")\n+_SPECIAL_CHARS = {\n+    \".\": \"\\\\U0000002E\",\n+    \"$\": \"\\\\U00000024\",\n+    \"\\\\U0000002E\": \"\\\\U&0000002E\",\n+    \"\\\\U00000024\": \"\\\\U&00000024\",\n+}\n+\n+\n+def _replace_with_escaped(match: Match[str]) -> str:\n+    return _SPECIAL_CHARS[match[0]]\n+\n+\n+_CHAR_ESCAPE_PATTERN: Pattern[str] = re.compile(r\"(\\\\U0000002E|\\\\U00000024)\")\n+_CHAR_ESCAPES = {\n+    \"\\\\U0000002E\": \".\",\n+    \"\\\\U00000024\": \"$\",\n+    \"\\\\U&0000002E\": \"\\\\U0000002E\",\n+    \"\\\\U&00000024\": \"\\\\U00000024\",\n+}\n+\n+\n+def _replace_with_unescaped(match: Match[str]) -> str:\n+    return _CHAR_ESCAPES[match[0]]\n+\n \n def get_config_details():\n     uri = None\n", "issue": "[V3 Config] Config does not reject bad keys\nIncluding a `.` or starting a keyname with `$` needs to be invalid in config because of mongoDB backend support. \r\n\r\nConfig will work just fine for this on the json backend, but not with the mongodb one because these characters are not allowed in field names for mongo. Instead, it works with json, and throws uncaught exceptions on mongo.\r\n\r\nI'd be in favor of limiting keynames to alpha-numeric and space, underscore, and hyphens. to prevent future issues like this with other future possible backends without possibly breaking cogs twice for this.\n", "before_files": [{"content": "import motor.motor_asyncio\nfrom .red_base import BaseDriver\nfrom urllib.parse import quote_plus\n\n__all__ = [\"Mongo\"]\n\n\n_conn = None\n\n\ndef _initialize(**kwargs):\n    uri = kwargs.get(\"URI\", \"mongodb\")\n    host = kwargs[\"HOST\"]\n    port = kwargs[\"PORT\"]\n    admin_user = kwargs[\"USERNAME\"]\n    admin_pass = kwargs[\"PASSWORD\"]\n    db_name = kwargs.get(\"DB_NAME\", \"default_db\")\n\n    if port is 0:\n        ports = \"\"\n    else:\n        ports = \":{}\".format(port)\n\n    if admin_user is not None and admin_pass is not None:\n        url = \"{}://{}:{}@{}{}/{}\".format(\n            uri, quote_plus(admin_user), quote_plus(admin_pass), host, ports, db_name\n        )\n    else:\n        url = \"{}://{}{}/{}\".format(uri, host, ports, db_name)\n\n    global _conn\n    _conn = motor.motor_asyncio.AsyncIOMotorClient(url)\n\n\nclass Mongo(BaseDriver):\n    \"\"\"\n    Subclass of :py:class:`.red_base.BaseDriver`.\n    \"\"\"\n\n    def __init__(self, cog_name, identifier, **kwargs):\n        super().__init__(cog_name, identifier)\n\n        if _conn is None:\n            _initialize(**kwargs)\n\n    @property\n    def db(self) -> motor.core.Database:\n        \"\"\"\n        Gets the mongo database for this cog's name.\n\n        .. warning::\n\n            Right now this will cause a new connection to be made every time the\n            database is accessed. We will want to create a connection pool down the\n            line to limit the number of connections.\n\n        :return:\n            PyMongo Database object.\n        \"\"\"\n        return _conn.get_database()\n\n    def get_collection(self) -> motor.core.Collection:\n        \"\"\"\n        Gets a specified collection within the PyMongo database for this cog.\n\n        Unless you are doing custom stuff ``collection_name`` should be one of the class\n        attributes of :py:class:`core.config.Config`.\n\n        :param str collection_name:\n        :return:\n            PyMongo collection object.\n        \"\"\"\n        return self.db[self.cog_name]\n\n    @staticmethod\n    def _parse_identifiers(identifiers):\n        uuid, identifiers = identifiers[0], identifiers[1:]\n        return uuid, identifiers\n\n    async def get(self, *identifiers: str):\n        mongo_collection = self.get_collection()\n\n        dot_identifiers = \".\".join(identifiers)\n\n        partial = await mongo_collection.find_one(\n            filter={\"_id\": self.unique_cog_identifier}, projection={dot_identifiers: True}\n        )\n\n        if partial is None:\n            raise KeyError(\"No matching document was found and Config expects a KeyError.\")\n\n        for i in identifiers:\n            partial = partial[i]\n        return partial\n\n    async def set(self, *identifiers: str, value=None):\n        dot_identifiers = \".\".join(identifiers)\n\n        mongo_collection = self.get_collection()\n\n        await mongo_collection.update_one(\n            {\"_id\": self.unique_cog_identifier},\n            update={\"$set\": {dot_identifiers: value}},\n            upsert=True,\n        )\n\n    async def clear(self, *identifiers: str):\n        dot_identifiers = \".\".join(identifiers)\n        mongo_collection = self.get_collection()\n\n        if len(identifiers) > 0:\n            await mongo_collection.update_one(\n                {\"_id\": self.unique_cog_identifier}, update={\"$unset\": {dot_identifiers: 1}}\n            )\n        else:\n            await mongo_collection.delete_one({\"_id\": self.unique_cog_identifier})\n\n\ndef get_config_details():\n    uri = None\n    while True:\n        uri = input(\"Enter URI scheme (mongodb or mongodb+srv): \")\n        if uri is \"\":\n            uri = \"mongodb\"\n\n        if uri in [\"mongodb\", \"mongodb+srv\"]:\n            break\n        else:\n            print(\"Invalid URI scheme\")\n\n    host = input(\"Enter host address: \")\n    if uri is \"mongodb\":\n        port = int(input(\"Enter host port: \"))\n    else:\n        port = 0\n\n    admin_uname = input(\"Enter login username: \")\n    admin_password = input(\"Enter login password: \")\n\n    db_name = input(\"Enter mongodb database name: \")\n\n    if admin_uname == \"\":\n        admin_uname = admin_password = None\n\n    ret = {\n        \"HOST\": host,\n        \"PORT\": port,\n        \"USERNAME\": admin_uname,\n        \"PASSWORD\": admin_password,\n        \"DB_NAME\": db_name,\n        \"URI\": uri,\n    }\n    return ret\n", "path": "redbot/core/drivers/red_mongo.py"}]}