Skip to content

Array-of-tables header accepted with a single closing bracket at EOF: [[a] parses as {a: [{}]} #65

Description

@mehrad31415

Summary

An array-of-tables header closed by a single ] is accepted when it is the last
thing in the document. [[a] parses as { a: [ {} ] } instead of throwing. One
character of trailing content is also silently discarded, so [[a]x parses the
same way.

Version

  • smol-toml 1.8.0 (also reproduces on 1.7.1)
  • Node v22.17.0

Reproduction

import { parse } from 'smol-toml'

parse('[[a]')   // => { a: [ {} ] }   expected: throw
parse('[[a]x')  // => { a: [ {} ] }   expected: throw ('x' is dropped)

Why it is invalid

TOML 1.1.0's grammar requires two closing brackets:

array-table       = array-table-open key array-table-close
array-table-open  = %x5B.5B ws  ; [[ Double left square bracket
array-table-close = ws %x5D.5D  ; ]] Double right square bracket

array-table-close is %x5D.5D, so [[a] cannot derive from array-table.

Observed behaviour

input 1.8.0 expected
[[a] accepts{a:[{}]} reject
[[a]x accepts{a:[{}]}, x discarded reject
[[a]xyz rejects reject
[[a]]x rejects reject
[[a] + newline + b = 1 rejects reject
[a]x rejects reject

Likely cause

In parse.js:

let k = parseKey(ctx, ']');           // consumes through the first ']'
if (isTableArray) {
    if (toml.charCodeAt(ctx.p - 1) !== 0x5d /* ] */) {
        throw new TomlError('expected end of table declaration', { ... });
    }
    ctx.p++;
}

The guard tests ctx.p - 1, which is the ] that parseKey has already
consumed and so is always 0x5d — the condition can never fire. The second
bracket is therefore never required, and ctx.p++ advances past whatever
happens to be there.

That accounts for the whole table: at EOF the increment is a no-op ([[a]), with
exactly one trailing character it swallows it ([[a]x), and with more the
leftovers produce an unrelated error ([[a]xyz).

Testing charCodeAt(ctx.p) for the closing bracket before advancing looks like
the intended check.

Why the test suite doesn't catch it

smol-toml passes all five existing toml-test cases for unclosed table headers
(invalid/table/array-no-close-01..04 and nested-brackets-close). Every one
of them has a line following the header, so none exercises the end-of-input path.
I've opened toml-lang/toml-test#205 with the two EOF cases ([[a] and [[a]x).

Other implementations

All reject [[a]:

  • BurntSushi/toml (Go) v1.6.0 — expected end of table array name delimiter ']'
  • CPython tomllib (3.14.7) — raises
  • Rust toml 1.1.4+spec-1.1.0 — rejects

How this was found

Generated by a conformance-suite generator that derives one test per named error
branch of an executable TOML 1.1.0 semantics.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions