Repository: toml-rs/toml (rust)

Issue:
TOML parse error: Invalid table header: Duplicate key
```toml
# pyproject.toml

[tool.hatch]
version.source = "vcs"

[tool.hatch.version.raw-options]
local_scheme = "no-local-version"
```

Both `toml` and `toml_edit` fail to parse it:

```rust
// toml
Err(
    Error {
        inner: ErrorInner {
            kind: Custom,
            line: Some(
                5,
            ),
            col: 0,
            at: Some(
                55,
            ),
            message: "duplicate key: `version`",
            key: [
                "tool",
                "hatch",
            ],
        },
    },
)

// toml_edit
Err(
    TomlError {
        message: "TOML parse error at line 6, column 1\n  |\n6 | [tool.hatch.version.raw-options]\n  | ^\nInvalid table header\nDuplicate key `version`\n",
        line_col: Some(
            (
                5,
                0,
            ),
        ),
    },
)
```

while the Python `tomllib` has no issue with it

```bash
Python 3.11.0 (main, Oct 25 2022, 16:25:24) [Clang 14.0.0 (clang-1400.0.29.102)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import tomllib as toml
>>> s = '''# pyproject.toml
...
... [tool.hatch]
... version.source = "vcs"
...
... [tool.hatch.version.raw-options]
... local_scheme = "no-local-version"'''
>>> toml.loads(s)
{'tool': {'hatch': {'version': {'source': 'vcs', 'raw-options': {'local_scheme': 'no-local-version'}}}}}
```

[Rust Explorer Playground](https://www.rustexplorer.com/b/vthrz3)

See also https://github.com/charliermarsh/ruff/issues/1894

Pull request description:
fix(parser): Allow appending to dotted table
Fixes #439 

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index de16d1a1..fe2abd29 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -922,21 +922,21 @@ dependencies = [
 
 [[package]]
 name = "toml-test-data"
-version = "1.2.0"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffb12a952ab0d7f1d9fc67ba6ceb7736ed39628e1e843e9779dd7f5996a56ca1"
+checksum = "93f351b6d6005ee802b0d4a53ca1cdf05636f441df4d299e62cba57f1da52646"
 dependencies = [
  "include_dir",
 ]
 
 [[package]]
 name = "toml-test-harness"
-version = "0.4.2"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9dd8f1792ea1c5f58d0ff04297bfea7df05dc0d7924e7db7c72360058def550e"
+checksum = "0e00fda5710922fe6b3005bf6a5050c303d6f9625249c37b7386e8818f4af675"
 dependencies = [
  "ignore",
- "libtest-mimic 0.5.2",
+ "libtest-mimic 0.6.0",
  "toml-test",
  "toml-test-data",
 ]
diff --git a/crates/toml/Cargo.toml b/crates/toml/Cargo.toml
index 22a353a8..a885a09b 100644
--- a/crates/toml/Cargo.toml
+++ b/crates/toml/Cargo.toml
@@ -53,7 +53,7 @@ serde_spanned = { version = "0.5.0", path = "../serde_spanned", features = ["ser
 [dev-dependencies]
 serde = { version = "1.0.152", features = ["derive"] }
 serde_json = "1.0.91"
-toml-test-harness = "0.4.2"
+toml-test-harness = "0.4.3"
 snapbox = "0.4.3"
 
 [[test]]
diff --git a/crates/toml_edit/Cargo.toml b/crates/toml_edit/Cargo.toml
index 12d29868..91dac946 100644
--- a/crates/toml_edit/Cargo.toml
+++ b/crates/toml_edit/Cargo.toml
@@ -57,8 +57,8 @@ serde_spanned = { version = "0.5.0", path = "../serde_spanned", features = ["ser
 
 [dev-dependencies]
 serde_json = "1.0.91"
-toml-test-harness = "0.4.2"
-toml-test-data = "1.2.0"
+toml-test-harness = "0.4.3"
+toml-test-data = "1.3.0"
 libtest-mimic = "0.6.0"
 snapbox = { version = "0.4.3", features = ["harness"] }
 
diff --git a/crates/toml_edit/src/parser/state.rs b/crates/toml_edit/src/parser/state.rs
index e83c8577..299e39f7 100644
--- a/crates/toml_edit/src/parser/state.rs
+++ b/crates/toml_edit/src/parser/state.rs
@@ -242,12 +242,6 @@ impl ParseState {
                     // Since tables cannot be defined more than once, redefining such tables using a
                     // [table] header is not allowed. Likewise, using dotted keys to redefine tables
                     // already defined in [table] form is not allowed.
-                    if sweet_child_of_mine.is_dotted() && !dotted {
-                        return Err(CustomError::DuplicateKey {
-                            key: key.get().into(),
-                            table: None,
-                        });
-                    }
                     if dotted && !sweet_child_of_mine.is_implicit() {
                         return Err(CustomError::DuplicateKey {
                             key: key.get().into(),


Test patch:
diff --git a/crates/toml/tests/decoder_compliance.rs b/crates/toml/tests/decoder_compliance.rs
index 093725ef..fb1b2e8b 100644
--- a/crates/toml/tests/decoder_compliance.rs
+++ b/crates/toml/tests/decoder_compliance.rs
@@ -5,12 +5,18 @@ fn main() {
     let mut harness = toml_test_harness::DecoderHarness::new(decoder);
     harness
         .ignore([
-            "invalid/integer/positive-hex.toml",
-            "invalid/integer/positive-bin.toml",
-            "invalid/control/comment-del.toml",
             "invalid/control/comment-cr.toml",
-            "valid/string/escape-esc.toml",
+            "invalid/control/comment-del.toml",
             "invalid/datetime/hour-over.toml",
+            "invalid/integer/positive-bin.toml",
+            "invalid/integer/positive-hex.toml",
+            "valid/inline-table/newline.toml",
+            "valid/spec/float-0.toml",
+            "valid/spec/table-9.toml",
+            // Unreleased
+            "valid/string/escape-esc.toml",
+            "valid/string/hex-escape.toml",
+            "valid/datetime/no-seconds.toml",
         ])
         .unwrap();
     harness.test();
diff --git a/crates/toml/tests/encoder_compliance.rs b/crates/toml/tests/encoder_compliance.rs
index 3e2d5805..5621397a 100644
--- a/crates/toml/tests/encoder_compliance.rs
+++ b/crates/toml/tests/encoder_compliance.rs
@@ -9,6 +9,8 @@ fn main() {
         .ignore([
             "valid/array/mixed-string-table.toml",
             "valid/inline-table/nest.toml",
+            "valid/spec/float-0.toml",
+            "valid/spec/array-0.toml",
         ])
         .unwrap();
     harness.test();
diff --git a/crates/toml_edit/tests/decoder_compliance.rs b/crates/toml_edit/tests/decoder_compliance.rs
index b23fdca2..e6881dad 100644
--- a/crates/toml_edit/tests/decoder_compliance.rs
+++ b/crates/toml_edit/tests/decoder_compliance.rs
@@ -3,6 +3,15 @@ mod decoder;
 fn main() {
     let decoder = decoder::Decoder;
     let mut harness = toml_test_harness::DecoderHarness::new(decoder);
-    harness.ignore(["valid/string/escape-esc.toml"]).unwrap();
+    harness
+        .ignore([
+            "valid/spec/float-0.toml",
+            // Unreleased
+            "valid/string/escape-esc.toml",
+            "valid/string/hex-escape.toml",
+            "valid/datetime/no-seconds.toml",
+            "valid/inline-table/newline.toml",
+        ])
+        .unwrap();
     harness.test();
 }
diff --git a/crates/toml_edit/tests/easy_decoder_compliance.rs b/crates/toml_edit/tests/easy_decoder_compliance.rs
index 5deb2d41..e237aa77 100644
--- a/crates/toml_edit/tests/easy_decoder_compliance.rs
+++ b/crates/toml_edit/tests/easy_decoder_compliance.rs
@@ -5,7 +5,16 @@ mod easy_decoder;
 fn main() {
     let decoder = easy_decoder::Decoder;
     let mut harness = toml_test_harness::DecoderHarness::new(decoder);
-    harness.ignore(["valid/string/escape-esc.toml"]).unwrap();
+    harness
+        .ignore([
+            "valid/spec/float-0.toml",
+            // Unreleased
+            "valid/string/escape-esc.toml",
+            "valid/string/hex-escape.toml",
+            "valid/datetime/no-seconds.toml",
+            "valid/inline-table/newline.toml",
+        ])
+        .unwrap();
     harness.test();
 }
 
diff --git a/crates/toml_edit/tests/easy_encoder_compliance.rs b/crates/toml_edit/tests/easy_encoder_compliance.rs
index 372721dd..a53527f8 100644
--- a/crates/toml_edit/tests/easy_encoder_compliance.rs
+++ b/crates/toml_edit/tests/easy_encoder_compliance.rs
@@ -7,7 +7,8 @@ mod easy_encoder;
 fn main() {
     let encoder = easy_encoder::Encoder;
     let decoder = easy_decoder::Decoder;
-    let harness = toml_test_harness::EncoderHarness::new(encoder, decoder);
+    let mut harness = toml_test_harness::EncoderHarness::new(encoder, decoder);
+    harness.ignore(["valid/spec/float-0.toml"]).unwrap();
     harness.test();
 }
 
diff --git a/crates/toml_edit/tests/encoder_compliance.rs b/crates/toml_edit/tests/encoder_compliance.rs
index 09c3d0bf..9d77448a 100644
--- a/crates/toml_edit/tests/encoder_compliance.rs
+++ b/crates/toml_edit/tests/encoder_compliance.rs
@@ -4,6 +4,7 @@ mod encoder;
 fn main() {
     let encoder = encoder::Encoder;
     let decoder = decoder::Decoder;
-    let harness = toml_test_harness::EncoderHarness::new(encoder, decoder);
+    let mut harness = toml_test_harness::EncoderHarness::new(encoder, decoder);
+    harness.ignore(["valid/spec/float-0.toml"]).unwrap();
     harness.test();
 }
diff --git a/crates/toml_edit/tests/fixtures/invalid/encoding/bad-utf8-in-multiline-literal.stderr b/crates/toml_edit/tests/fixtures/invalid/encoding/bad-utf8-in-multiline-literal.stderr
new file mode 100644
index 00000000..0c70f286
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/encoding/bad-utf8-in-multiline-literal.stderr
@@ -0,0 +1,1 @@
+invalid utf-8 sequence of 1 bytes from index 66
\ No newline at end of file
diff --git a/crates/toml_edit/tests/fixtures/invalid/encoding/bad-utf8-in-multiline.stderr b/crates/toml_edit/tests/fixtures/invalid/encoding/bad-utf8-in-multiline.stderr
new file mode 100644
index 00000000..0c70f286
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/encoding/bad-utf8-in-multiline.stderr
@@ -0,0 +1,1 @@
+invalid utf-8 sequence of 1 bytes from index 66
\ No newline at end of file
diff --git a/crates/toml_edit/tests/fixtures/invalid/encoding/bad-utf8-in-string-literal.stderr b/crates/toml_edit/tests/fixtures/invalid/encoding/bad-utf8-in-string-literal.stderr
new file mode 100644
index 00000000..9e1a687c
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/encoding/bad-utf8-in-string-literal.stderr
@@ -0,0 +1,1 @@
+invalid utf-8 sequence of 1 bytes from index 64
\ No newline at end of file
diff --git a/crates/toml_edit/tests/fixtures/invalid/spec/inline-table-2-0.stderr b/crates/toml_edit/tests/fixtures/invalid/spec/inline-table-2-0.stderr
new file mode 100644
index 00000000..7fed2698
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/spec/inline-table-2-0.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 3, column 1
+  |
+3 | type.edible = false  # INVALID
+  | ^
+dotted key `type` attempted to extend non-table type (inline table)
diff --git a/crates/toml_edit/tests/fixtures/invalid/spec/inline-table-3-0.stderr b/crates/toml_edit/tests/fixtures/invalid/spec/inline-table-3-0.stderr
new file mode 100644
index 00000000..45ba696b
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/spec/inline-table-3-0.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 3, column 1
+  |
+3 | type = { edible = false }  # INVALID
+  | ^
+duplicate key `type` in table `product`
diff --git a/crates/toml_edit/tests/fixtures/invalid/spec/key-value-pair-1.stderr b/crates/toml_edit/tests/fixtures/invalid/spec/key-value-pair-1.stderr
new file mode 100644
index 00000000..ba765a82
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/spec/key-value-pair-1.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 7
+  |
+1 | key = # INVALID
+  |       ^
+invalid string
+expected `"`, `'`
diff --git a/crates/toml_edit/tests/fixtures/invalid/spec/keys-2.stderr b/crates/toml_edit/tests/fixtures/invalid/spec/keys-2.stderr
new file mode 100644
index 00000000..bdbcce45
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/spec/keys-2.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 1, column 1
+  |
+1 | = "no key name"  # INVALID
+  | ^
+invalid key
diff --git a/crates/toml_edit/tests/fixtures/invalid/spec/string-4-0.stderr b/crates/toml_edit/tests/fixtures/invalid/spec/string-4-0.stderr
new file mode 100644
index 00000000..e82601dd
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/spec/string-4-0.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 2, column 46
+  |
+2 | str5 = """Here are three quotation marks: """."""  # INVALID
+  |                                              ^
+expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/spec/string-7-0.stderr b/crates/toml_edit/tests/fixtures/invalid/spec/string-7-0.stderr
new file mode 100644
index 00000000..7a928e19
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/spec/string-7-0.stderr
@@ -0,0 +1,5 @@
+TOML parse error at line 3, column 48
+  |
+3 | apos15 = '''Here are fifteen apostrophes: ''''''''''''''''''  # INVALID
+  |                                                ^
+expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/spec/table-9-0.stderr b/crates/toml_edit/tests/fixtures/invalid/spec/table-9-0.stderr
new file mode 100644
index 00000000..a2e1d499
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/spec/table-9-0.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 5, column 1
+  |
+5 | [fruit.apple]  # INVALID
+  | ^
+invalid table header
+duplicate key `apple` in table `fruit`
diff --git a/crates/toml_edit/tests/fixtures/invalid/spec/table-9-1.stderr b/crates/toml_edit/tests/fixtures/invalid/spec/table-9-1.stderr
new file mode 100644
index 00000000..78dea214
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/spec/table-9-1.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 6, column 1
+  |
+6 | [fruit.apple.taste]  # INVALID
+  | ^
+invalid table header
+duplicate key `taste` in table `fruit.apple`
diff --git a/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc-1.stderr b/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc-1.stderr
new file mode 100644
index 00000000..5d5577fa
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc-1.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+  |
+1 | bad-hex-esc-1 = "\x0g"
+  |                    ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc-2.stderr b/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc-2.stderr
new file mode 100644
index 00000000..5a40ad44
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc-2.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+  |
+1 | bad-hex-esc-2 = "\xG0"
+  |                    ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc-3.stderr b/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc-3.stderr
new file mode 100644
index 00000000..70e1183f
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc-3.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+  |
+1 | bad-hex-esc-3 = "\x"
+  |                    ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc-4.stderr b/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc-4.stderr
new file mode 100644
index 00000000..df028ee8
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc-4.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+  |
+1 | bad-hex-esc-4 = "\x 50"
+  |                    ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc-5.stderr b/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc-5.stderr
new file mode 100644
index 00000000..4b5cd33d
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc-5.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+  |
+1 | bad-hex-esc-5 = "\x 50"
+  |                    ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc.stderr b/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc.stderr
new file mode 100644
index 00000000..5d5577fa
--- /dev/null
+++ b/crates/toml_edit/tests/fixtures/invalid/string/bad-hex-esc.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 20
+  |
+1 | bad-hex-esc-1 = "\x0g"
+  |                    ^
+invalid escape sequence
+expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/crates/toml_edit/tests/fixtures/invalid/table/duplicate-key-dotted-table2.stderr b/crates/toml_edit/tests/fixtures/invalid/table/duplicate-key-dotted-table2.stderr
index 5dd5e178..c5ab1eb5 100644
--- a/crates/toml_edit/tests/fixtures/invalid/table/duplicate-key-dotted-table2.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/table/duplicate-key-dotted-table2.stderr
@@ -3,4 +3,4 @@ TOML parse error at line 4, column 1
 4 | [fruit.apple.taste] # INVALID
   | ^
 invalid table header
-duplicate key `apple`
+duplicate key `taste` in table `fruit.apple`