Skip to content

TOML parser error for empty map #6568

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
DenisGorbachev opened this issue Apr 11, 2025 · 1 comment · May be fixed by #6587
Open

TOML parser error for empty map #6568

DenisGorbachev opened this issue Apr 11, 2025 · 1 comment · May be fixed by #6587
Labels
bug Something isn't working needs triage

Comments

@DenisGorbachev
Copy link

Steps to Reproduce

  1. Create a file:
    [package.metadata.details]
    readme = { }
  2. Notice that there is a space in { } (it's valid, but trips up the parser).
  3. Run a script to parse that file.
    import { parse as parseToml } from "jsr:@std/[email protected]"
    parseToml(/* the file above */)

Expected behavior

Parser succeeds.

Actual behavior

Parser fails.

Environment

OS: MacOS 15

deno --version
deno 1.46.1 (stable, release, aarch64-apple-darwin)
v8 12.9.202.2-rusty
typescript 5.5.2
import { parse as parseToml } from "jsr:@std/[email protected]"
@DenisGorbachev DenisGorbachev added bug Something isn't working needs triage labels Apr 11, 2025
@timreichen
Copy link
Contributor

I took the liberty to check the @std/toml implementation against npm:toml tests.
Empty map is tested in testEmptyInlineTables and fails.
Seems like this is not the only bug, there are a lot of tests that are failing with the current implementation.

import { assertEquals, assertThrows } from "@std/assert";
import { parse } from "./parse.ts";

Deno.test("testParsesExample", () => {
  const input = `# This is a TOML document. Boom.

title = "TOML Example"

[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\\n\\tLikes \\"tater tots\\" and beer and backslashes: \\\\"
dob = 1979-05-27T07:32:00Z # First class dates? Why not?

[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8003 ]
connection_max = 5000
connection_min = -2 # Don't ask me how
max_temp = 87.1 # It's a float
min_temp = -17.76
enabled = true

[servers]

  # You can indent as you please. Tabs or spaces. TOML don't care.
  [servers.alpha]
  ip = "10.0.0.1"
  dc = "eqdc10"

  [servers.beta]
  ip = "10.0.0.2"
  dc = "eqdc10"

[clients]
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
`;
  const expected = {
    title: "TOML Example",
    owner: {
      name: "Tom Preston-Werner",
      organization: "GitHub",
      bio:
        'GitHub Cofounder & CEO\n\tLikes "tater tots" and beer and backslashes: \\',
      dob: new Date("1979-05-27T07:32:00Z"),
    },
    database: {
      server: "192.168.1.1",
      ports: [8001, 8001, 8003],
      connection_max: 5000,
      connection_min: -2,
      max_temp: 87.1,
      min_temp: -17.76,
      enabled: true,
    },
    servers: {
      alpha: {
        ip: "10.0.0.1",
        dc: "eqdc10",
      },
      beta: {
        ip: "10.0.0.2",
        dc: "eqdc10",
      },
    },
    clients: {
      data: [["gamma", "delta"], [1, 2]],
    },
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testParsesHardExample", () => {
  const input = `# Test file for TOML
# Only this one tries to emulate a TOML file written by a user of the kind of parser writers probably hate
# This part you'll really hate

[the]
test_string = "You'll hate me after this - #"          # " Annoying, isn't it?

    [the.hard]
    test_array = [ "] ", " # "]      # ] There you go, parse this!
    test_array2 = [ "Test #11 ]proved that", "Experiment #9 was a success" ]
    # You didn't think it'd as easy as chucking out the last #, did you?
    another_test_string = " Same thing, but with a string #"
    harder_test_string = " And when \"'s are in the string, along with # \""   # "and comments are there too"
    # Things will get harder

        [the.hard."bit#"]
        "what?" = "You don't think some user won't do that?"
        multi_line_array = [
            "]",
            # ] Oh yes I did
            ]

# Each of the following keygroups/key value pairs should produce an error. Uncomment to them to test

#[error]   if you didn't catch this, your parser is broken
#string = "Anything other than tabs, spaces and newline after a keygroup or key value pair has ended should produce an error unless it is a comment"   like this
#array = [
#         "This might most likely happen in multiline arrays",
#         Like here,
#         "or here,
#         and here"
#         ]     End of array comment, forgot the #
#number = 3.14  pi <--again forgot the #
`;
  const expected = {
    the: {
      hard: {
        another_test_string: " Same thing, but with a string #",
        "bit#": {
          multi_line_array: ["]"],
          "what?": "You don't think some user won't do that?",
        },
        harder_test_string: ' And when "\'s are in the string, along with # "',
        test_array: ["] ", " # "],
        test_array2: ["Test #11 ]proved that", "Experiment #9 was a success"],
      },
      test_string: "You'll hate me after this - #",
    },
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testEasyTableArrays", () => {
  const input = `[[products]]
name = "Hammer"
sku = 738594937

[[products]]

[[products]]
name = "Nail"
sku = 284758393
color = "gray"
`;
  const expected = {
    "products": [
      { "name": "Hammer", "sku": 738594937 },
      {},
      { "name": "Nail", "sku": 284758393, "color": "gray" },
    ],
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testHarderTableArrays", () => {
  const input = `[[fruit]]
name = "durian"
variety = []

[[fruit]]
name = "apple"

  [fruit.physical]
    color = "red"
    shape = "round"

  [[fruit.variety]]
    name = "red delicious"

  [[fruit.variety]]
    name = "granny smith"

[[fruit]]

[[fruit]]
name = "banana"

  [[fruit.variety]]
    name = "plantain"

[[fruit]]
name = "orange"

[fruit.physical]
color = "orange"
shape = "round"
`;
  const expected = {
    "fruit": [
      {
        "name": "durian",
        "variety": [],
      },
      {
        "name": "apple",
        "physical": {
          "color": "red",
          "shape": "round",
        },
        "variety": [
          { "name": "red delicious" },
          { "name": "granny smith" },
        ],
      },
      {},
      {
        "name": "banana",
        "variety": [
          { "name": "plantain" },
        ],
      },
      {
        "name": "orange",
        "physical": {
          "color": "orange",
          "shape": "round",
        },
      },
    ],
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testSupportsTrailingCommasInArrays", () => {
  const input = "arr = [1, 2, 3,]";
  const expected = { arr: [1, 2, 3] };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testSingleElementArrayWithNoTrailingComma", () => {
  const input = "a = [1]";
  const expected = { a: [1] };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testEmptyArray", () => {
  const input = "a = []";
  const expected = { a: [] };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testArrayWithWhitespace", () => {
  const input = "[versions]\nfiles = [\n 3, \n    5 \n\n ]";
  const expected = { versions: { files: [3, 5] } };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testEmptyArrayWithWhitespace", () => {
  const input = "[versions]\nfiles = [\n  \n  ]";
  const expected = { versions: { files: [] } };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testDefineOnSuperkey", () => {
  const input = "[a.b]\nc = 1\n\n[a]\nd = 2";
  const expected = { a: { b: { c: 1 }, d: 2 } };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testWhitespace", () => {
  const input = "a = 1\n  \n  b = 2  ";
  const expected = { a: 1, b: 2 };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testUnicode", () => {
  const input = 'str = "My name is Jos\\u00E9"';
  const expected = { str: "My name is Jos\u00E9" };
  const actual = parse(input);
  assertEquals(actual, expected);

  const input2 = 'str = "My name is Jos\\U000000E9"';
  const expected2 = { str: "My name is Jos\u00E9" };
  const actual2 = parse(input2);
  assertEquals(actual2, expected2);
});

Deno.test("testMultilineStrings", () => {
  const input = `# The following strings are byte-for-byte equivalent:
key1 = "One\\nTwo"
key2 = """One\\nTwo"""
key3 = """
One
Two"""
`;
  const expected = {
    key1: "One\nTwo",
    key2: "One\nTwo",
    key3: "One\nTwo",
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testMultilineEatWhitespace", () => {
  const input = `# The following strings are byte-for-byte equivalent:
key1 = "The quick brown fox jumps over the lazy dog."

key2 = """
The quick brown \\


  fox jumps over \\
    the lazy dog."""

key3 = """\\
       The quick brown \\
       fox jumps over \\
       the lazy dog.\\
       """
`;

  const expected = {
    key1: "The quick brown fox jumps over the lazy dog.",
    key2: "The quick brown fox jumps over the lazy dog.",
    key3: "The quick brown fox jumps over the lazy dog.",
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testLiteralStrings", () => {
  const input = `# What you see is what you get.
winpath  = 'C:\\Users\\nodejs\\templates'
winpath2 = '\\\\ServerX\\admin$\\system32\\'
quoted   = 'Tom "Dubs" Preston-Werner'
regex    = '<\\i\\c*\\s*>'

`;
  const expected = {
    winpath: "C:\\Users\\nodejs\\templates",
    winpath2: "\\\\ServerX\\admin$\\system32\\",
    quoted: 'Tom "Dubs" Preston-Werner',
    regex: "<\\i\\c*\\s*>",
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testMultilineLiteralStrings", () => {
  const input = `regex2 = '''I [dw]on't need \\d{2} apples'''
lines  = '''
The first newline is
trimmed in raw strings.
   All other whitespace
   is preserved.
'''
`;
  const expected = {
    regex2: "I [dw]on't need \\d{2} apples",
    lines:
      "The first newline is\ntrimmed in raw strings.\n   All other whitespace\n   is preserved.\n",
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testIntegerFormats", () => {
  const input =
    "a = +99\nb = 42\nc = 0\nd = -17\ne = 1_000_001\nf = 1_2_3_4_5   # why u do dis";
  const expected = {
    a: 99,
    b: 42,
    c: 0,
    d: -17,
    e: 1000001,
    f: 12345,
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testFloatFormats", () => {
  const input = "a = +1.0\nb = 3.1415\nc = -0.01\n" +
    "d = 5e+22\ne = 1e6\nf = -2E-2\n" +
    "g = 6.626e-34\n" +
    "h = 9_224_617.445_991_228_313\n" +
    "i = 1e1_000";
  const expected = {
    a: 1.0,
    b: 3.1415,
    c: -0.01,
    d: 5e22,
    e: 1e6,
    f: -2e-2,
    g: 6.626e-34,
    h: 9224617.445991228313,
    i: 1e1000,
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testDate", () => {
  const date = new Date("1979-05-27T07:32:00Z");
  const expected = { a: date };
  const actual = parse("a = 1979-05-27T07:32:00Z");
  assertEquals(actual, expected);
});

Deno.test("testDateWithOffset", () => {
  const date1 = new Date("1979-05-27T07:32:00-07:00");
  const date2 = new Date("1979-05-27T07:32:00+02:00");
  const expected = {
    a: date1,
    b: date2,
  };
  const actual = parse(
    "a = 1979-05-27T07:32:00-07:00\nb = 1979-05-27T07:32:00+02:00",
  );
  assertEquals(expected, actual);
});

Deno.test("testDateWithSecondFraction", () => {
  const date = new Date("1979-05-27T00:32:00.999999-07:00");
  const expected = { a: date };
  const actual = parse("a = 1979-05-27T00:32:00.999999-07:00");
  assertEquals(actual, expected);
});

Deno.test("testDateFromIsoString", () => {
  // https://github.com/BinaryMuse/toml-node/issues/20
  const date = new Date();
  const dateStr = date.toISOString();
  const tomlStr = "a = " + dateStr;
  const expected = {
    a: date,
  };
  const actual = parse(tomlStr);
  assertEquals(actual, expected);
});

Deno.test("testLeadingNewlines", () => {
  // https://github.com/BinaryMuse/toml-node/issues/22
  const input = '\ntest = "ing"';
  const expected = {
    test: "ing",
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testInlineTables", () => {
  const input = `name = { first = "Tom", last = "Preston-Werner" }
point = { x = 1, y = 2 }
nested = { x = { a = { b = 3 } } }

points = [ { x = 1, y = 2, z = 3 },
           { x = 7, y = 8, z = 9 },
           { x = 2, y = 4, z = 8 } ]

arrays = [ { x = [1, 2, 3], y = [4, 5, 6] },
           { x = [7, 8, 9], y = [0, 1, 2] } ]
`;
  const expected = {
    name: {
      first: "Tom",
      last: "Preston-Werner",
    },
    point: {
      x: 1,
      y: 2,
    },
    nested: {
      x: {
        a: {
          b: 3,
        },
      },
    },
    points: [
      { x: 1, y: 2, z: 3 },
      { x: 7, y: 8, z: 9 },
      { x: 2, y: 4, z: 8 },
    ],
    arrays: [
      { x: [1, 2, 3], y: [4, 5, 6] },
      { x: [7, 8, 9], y: [0, 1, 2] },
    ],
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testEmptyInlineTables", () => {
  // https://github.com/BinaryMuse/toml-node/issues/24
  const input = "a = { }";
  const expected = {
    a: {},
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testKeyNamesWithWhitespaceAroundStartAndFinish", () => {
  const input = "[ a ]\nb = 1";
  const expected = {
    a: {
      b: 1,
    },
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testKeyNamesWithWhitespaceAroundDots", () => {
  const input = "[ a . b . c]\nd = 1";
  const expected = {
    a: {
      b: {
        c: {
          d: 1,
        },
      },
    },
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testSimpleQuotedKeyNames", () => {
  const input = '["ʞ"]\na = 1';
  const expected = {
    "ʞ": {
      a: 1,
    },
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testComplexQuotedKeyNames", () => {
  const input = '[ a . "ʞ" . c ]\nd = 1';
  const expected = {
    a: {
      "ʞ": {
        c: {
          d: 1,
        },
      },
    },
  };
  const actual = parse(input);
  assertEquals(actual, expected);
});

Deno.test("testEscapedQuotesInQuotedKeyNames", () => {
  const expected = {
    'the "thing"': {
      a: true,
    },
  };
  const actual = parse('["the \\"thing\\""]\na = true');
  assertEquals(actual, expected);
});

Deno.test("testMoreComplexQuotedKeyNames", () => {
  // https://github.com/BinaryMuse/toml-node/issues/21
  const expected = {
    "the\\ key": {
      one: "one",
      two: 2,
      three: false,
    },
  };
  const actual = parse('["the\\ key"]\n\none = "one"\ntwo = 2\nthree = false');
  assertEquals(actual, expected);

  const expected2 = {
    a: {
      "the\\ key": {
        one: "one",
        two: 2,
        three: false,
      },
    },
  };
  const actual2 = parse(
    '[a."the\\ key"]\n\none = "one"\ntwo = 2\nthree = false',
  );
  assertEquals(actual2, expected2);

  const expected3 = {
    a: {
      "the-key": {
        one: "one",
        two: 2,
        three: false,
      },
    },
  };
  const actual3 = parse('[a."the-key"]\n\none = "one"\ntwo = 2\nthree = false');
  assertEquals(actual3, expected3);

  const expected4 = {
    a: {
      "the.key": {
        one: "one",
        two: 2,
        three: false,
      },
    },
  };
  const actual4 = parse('[a."the.key"]\n\none = "one"\ntwo = 2\nthree = false');
  // https://github.com/BinaryMuse/toml-node/issues/34
  assertEquals(actual4, expected4);

  const expected5 = {
    table: {
      'a "quoted value"': "value",
    },
  };
  const actual5 = parse('[table]\n\'a "quoted value"\' = "value"');
  assertEquals(actual5, expected5);

  // https://github.com/BinaryMuse/toml-node/issues/33
  const expected6 = {
    module: {
      "foo=bar": "zzz",
    },
  };
  const actual6 = parse('[module]\n"foo=bar" = "zzz"');
  assertEquals(actual6, expected6);
});

Deno.test("testErrorOnBadUnicode", () => {
  const input = 'str = "My name is Jos\\uD800"';
  assertThrows(() => {
    parse(input);
  });
});

Deno.test("testErrorOnDotAtStartOfKey", () => {
  assertThrows(() => {
    const input = "[.a]\nb = 1";
    parse(input);
  });
});

Deno.test("testErrorOnDotAtEndOfKey", () => {
  assertThrows(() => {
    const input = "[.a]\nb = 1";
    parse(input);
  });
});

Deno.test("testErrorOnTableOverride", () => {
  assertThrows(() => {
    const input = "[a]\nb = 1\n\n[a]\nc = 2";
    parse(input);
  });
});

Deno.test("testErrorOnKeyOverride", () => {
  assertThrows(() => {
    const input = "[a]\nb = 1\n[a.b]\nc = 2";
    parse(input);
  });
});

Deno.test("testErrorOnKeyOverrideWithNested", () => {
  // https://github.com/BinaryMuse/toml-node/issues/23
  assertThrows(() => {
    const input = '[a]\nb = "a"\n[a.b.c]';
    parse(input);
  }, "existing key 'a.b'");
});

Deno.test("testErrorOnKeyOverrideWithArrayTable", () => {
  assertThrows(() => {
    const input = "[a]\nb = 1\n[[a]]\nc = 2";
    parse(input);
  });
});

Deno.test("testErrorOnKeyReplace", () => {
  assertThrows(() => {
    const input = "[a]\nb = 1\nb = 2";
    parse(input);
  });
});

Deno.test("testErrorOnInlineTableReplace", () => {
  // https://github.com/BinaryMuse/toml-node/issues/25
  assertThrows(() => {
    const input = "a = { b = 1 }\n[a]\nc = 2";
    parse(input);
  }, "existing key 'a'");
});

Deno.test("testErrorOnArrayMismatch", () => {
  assertThrows(() => {
    const input = 'data = [1, 2, "test"]';
    parse(input);
  });
});

Deno.test("testErrorOnBadInputs", () => {
  assertThrows(() => {
    const input = "[error]   if you didn't catch this, your parser is broken";
    parse(input);
  });
  assertThrows(() => {
    const input =
      'string = "Anything other than tabs, spaces and newline after a table or key value pair has ended should produce an error unless it is a comment"   like this';
    parse(input);
  });
  assertThrows(() => {
    const input =
      'array = [\n           "This might most likely happen in multiline arrays",\n           Like here,\n           "or here,\n           and here"\n           ]     End of array comment, forgot the #';
    parse(input);
  });
  assertThrows(() => {
    const input = "number = 3.14  pi <--again forgot the #";
    parse(input);
  });
});

Deno.test.ignore("testErrorsHaveCorrectLineAndColumn", () => {
  const input = "[a]\nb = 1\n [a.b]\nc = 2";
  try {
    parse(input);
  } catch (e) {
    assertEquals(e, { line: 3, column: 2 });
  }
});

Deno.test("testUsingConstructorAsKey", () => {
  const expected = {
    "empty": {},
    "emptier": {},
    "constructor": { "constructor": 1 },
    "emptiest": {},
  };
  const actual = parse(
    "[empty]\n[emptier]\n[constructor]\nconstructor = 1\n[emptiest]",
  );
  assertEquals(expected, actual);
});

testParsesHardExample => ./toml/_parse_test.ts:76:6
testDefineOnSuperkey => ./toml/_parse_test.ts:258:6
testUnicode => ./toml/_parse_test.ts:272:6
testEmptyInlineTables => ./toml/_parse_test.ts:491:6
testMoreComplexQuotedKeyNames => ./toml/_parse_test.ts:563:6
testErrorOnBadUnicode => ./toml/_parse_test.ts:632:6
testErrorOnKeyReplace => ./toml/_parse_test.ts:682:6
testErrorOnArrayMismatch => ./toml/_parse_test.ts:697:6
testUsingConstructorAsKey => ./toml/_parse_test.ts:734:6

FAILED | 84 passed | 9 failed | 1 ignored (32ms)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working needs triage
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants