Repository: sdispater/tomlkit (python)

Issue:
Parsing breaks when adding a closing bracket in a quoted key
parsing breaks when the quoted key contains a closing bracket.

```python
>>> import tomlkit
>>> tomlkit.parse("""
... ["["]
... foo = 1
... """)
{'[': {'foo': 1}}

>>> tomlkit.parse("""
... ["]"]
... foo = 1
... """)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/stephanos/.local/share/virtualenvs/tomlkit/lib/python3.8/site-packages/tomlkit/api.py", line 51, in parse
    return Parser(string).parse()
  File "/home/stephanos/.local/share/virtualenvs/tomlkit/lib/python3.8/site-packages/tomlkit/parser.py", line 153, in parse
    key, value = self._parse_table()
  File "/home/stephanos/.local/share/virtualenvs/tomlkit/lib/python3.8/site-packages/tomlkit/parser.py", line 946, in _parse_table
    cws, comment, trail = self._parse_comment_trail()
  File "/home/stephanos/.local/share/virtualenvs/tomlkit/lib/python3.8/site-packages/tomlkit/parser.py", line 314, in _parse_comment_trail
    raise self.parse_error(UnexpectedCharError, c)
tomlkit.exceptions.UnexpectedCharError: Unexpected character: '"' at line 2 col 3
```

Pull request description:
Fix the way table names are parsed
Fixes #74 

Patch:
diff --git a/tomlkit/parser.py b/tomlkit/parser.py
index 0ff26f0..c5cc139 100644
--- a/tomlkit/parser.py
+++ b/tomlkit/parser.py
@@ -201,7 +201,8 @@ class Parser:
         in_name = False
         current = ""
         t = KeyType.Bare
-        for c in name:
+        parts = 0
+        for c in name.strip():
             c = TOMLChar(c)
 
             if c == ".":
@@ -212,7 +213,8 @@ class Parser:
                 if not current:
                     raise self.parse_error()
 
-                yield Key(current, t=t, sep="")
+                yield Key(current.strip(), t=t, sep="")
+                parts += 1
 
                 current = ""
                 t = KeyType.Bare
@@ -233,17 +235,35 @@ class Parser:
 
                     in_name = False
                 else:
+                    if current and TOMLChar(current[-1]).is_spaces() and not parts:
+                        raise self.parse_error()
+
                     in_name = True
                     t = KeyType.Literal if c == "'" else KeyType.Basic
 
                 continue
             elif in_name or c.is_bare_key_char():
+                if (
+                    not in_name
+                    and current
+                    and TOMLChar(current[-1]).is_spaces()
+                    and not parts
+                ):
+                    raise self.parse_error()
+
                 current += c
+            elif c.is_spaces():
+                # A space is only valid at this point
+                # if it's in between parts.
+                # We store it for now and will check
+                # later if it's valid
+                current += c
+                continue
             else:
                 raise self.parse_error()
 
-        if current:
-            yield Key(current, t=t, sep="")
+        if current.strip():
+            yield Key(current.strip(), t=t, sep="")
 
     def _parse_item(self):  # type: () -> Optional[Tuple[Optional[Key], Item]]
         """
@@ -916,15 +936,46 @@ class Parser:
 
             is_aot = True
 
-        # Key
+        # Consume any whitespace
         self.mark()
-        while self._current != "]" and self.inc():
-            if self.end():
-                raise self.parse_error(UnexpectedEofError)
-
+        while self._current.is_spaces() and self.inc():
             pass
 
-        name = self.extract()
+        ws_prefix = self.extract()
+
+        # Key
+        if self._current in [StringType.SLL.value, StringType.SLB.value]:
+            delimiter = (
+                StringType.SLL
+                if self._current == StringType.SLL.value
+                else StringType.SLB
+            )
+            name = self._parse_string(delimiter)
+            name = "{delimiter}{name}{delimiter}".format(
+                delimiter=delimiter.value, name=name
+            )
+
+            self.mark()
+            while self._current != "]" and self.inc():
+                if self.end():
+                    raise self.parse_error(UnexpectedEofError)
+
+                pass
+
+            ws_suffix = self.extract()
+            name += ws_suffix
+        else:
+            self.mark()
+            while self._current != "]" and self.inc():
+                if self.end():
+                    raise self.parse_error(UnexpectedEofError)
+
+                pass
+
+            name = self.extract()
+
+        name = ws_prefix + name
+
         if not name.strip():
             raise self.parse_error(EmptyTableNameError)
 


Test patch:
diff --git a/tests/examples/json/table_names.json b/tests/examples/json/table_names.json
new file mode 100644
index 0000000..2485730
--- /dev/null
+++ b/tests/examples/json/table_names.json
@@ -0,0 +1,21 @@
+{
+    "Special \"table\"": {
+        "foo": "bar"
+    },
+    "BJ's Restaurant": {
+        "account": "dining"
+    },
+    "]": {
+        "foo": 1
+    },
+    "[bracket]": {
+        "bar": 2
+    },
+    "a": {
+        "b.c": {
+            "d": {
+                "baz": 3
+            }
+        }
+    }
+}
diff --git a/tests/examples/table_names_with_string_delimiters.toml b/tests/examples/table_names.toml
similarity index 50%
rename from tests/examples/table_names_with_string_delimiters.toml
rename to tests/examples/table_names.toml
index 8efd3ef..c9b7cc4 100644
--- a/tests/examples/table_names_with_string_delimiters.toml
+++ b/tests/examples/table_names.toml
@@ -3,3 +3,12 @@ foo = "bar"
 
 ["BJ's Restaurant"]
 account = "dining"
+
+["]"]
+foo = 1
+
+[  "[bracket]"  ]
+bar = 2
+
+[  a . "b.c" . d  ]
+baz = 3
diff --git a/tests/test_api.py b/tests/test_api.py
index a19bdda..279f81a 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -53,7 +53,7 @@ def json_serial(obj):
         "newline_in_strings",
         "preserve_quotes_in_string",
         "string_slash_whitespace_newline",
-        "table_names_with_string_delimiters",
+        "table_names",
     ],
 )
 def test_parse_can_parse_valid_toml_files(example, example_name):
@@ -61,7 +61,7 @@ def test_parse_can_parse_valid_toml_files(example, example_name):
     assert isinstance(loads(example(example_name)), TOMLDocument)
 
 
-@pytest.mark.parametrize("example_name", ["0.5.0", "pyproject"])
+@pytest.mark.parametrize("example_name", ["0.5.0", "pyproject", "table_names"])
 def test_parsed_document_are_properly_json_representable(
     example, json_example, example_name
 ):
@@ -111,6 +111,7 @@ def test_parse_raises_errors_for_invalid_toml_files(
         "pyproject",
         "0.5.0",
         "test",
+        "table_names",
     ],
 )
 def test_original_string_and_dumped_string_are_equal(example, example_name):