This is an automated email from the ASF dual-hosted git repository.
CurtHagenlocher pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-adbc.git
The following commit(s) were added to refs/heads/main by this push:
new e471d5366 fix(csharp): improved TOML parsing (#4340)
e471d5366 is described below
commit e471d5366759147faaa69a72845dce1badd564f8
Author: Curt Hagenlocher <[email protected]>
AuthorDate: Wed May 20 15:07:29 2026 -0700
fix(csharp): improved TOML parsing (#4340)
Improved TOML parsing.
Closes #4328
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
.../Apache.Arrow.Adbc/DriverManager/TomlParser.cs | 411 ++++++++++++++++++---
.../DriverManager/TomlParserTests.cs | 176 ++++++++-
2 files changed, 534 insertions(+), 53 deletions(-)
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlParser.cs
b/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlParser.cs
index 0854e7fe4..c8ca80ce4 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlParser.cs
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/TomlParser.cs
@@ -25,13 +25,18 @@ namespace Apache.Arrow.Adbc.DriverManager
/// A minimal TOML parser that handles the subset of TOML used by ADBC
driver
/// manifests and connection profiles:
/// <list type="bullet">
- /// <item><description>Root-level key = value
assignments</description></item>
- /// <item><description>Table section headers:
<c>[section]</c></description></item>
- /// <item><description>String values (double-quoted), integer values,
floating-point
- /// values, and boolean values
(<c>true</c>/<c>false</c>)</description></item>
+ /// <item><description>Root-level key = value assignments (bare keys
only)</description></item>
+ /// <item><description>Table section headers: <c>[section]</c> and
dotted <c>[a.b]</c></description></item>
+ /// <item><description>Basic strings (<c>"..."</c>) with <c>\"</c>,
<c>\\</c>, <c>\n</c>, <c>\r</c>, <c>\t</c> escapes</description></item>
+ /// <item><description>Literal strings (<c>'...'</c>) with no escape
processing</description></item>
+ /// <item><description>Single-line arrays of supported
scalars</description></item>
+ /// <item><description>Integer, floating-point, and lowercase boolean
(<c>true</c>/<c>false</c>) values</description></item>
/// <item><description>Line comments beginning with
<c>#</c></description></item>
/// </list>
- /// This parser intentionally does not support the full TOML specification.
+ /// Unsupported but recognized TOML productions -- multi-line strings,
inline tables,
+ /// dates, multi-line/nested arrays, underscored/hex/oct/bin integers,
dotted keys, etc.
+ /// -- are rejected with a <see cref="FormatException"/> rather than
silently misread.
+ ///
/// A full-featured TOML library (e.g. Tomlyn) was considered but cannot
be used here
/// because the assembly is strongly-named and Tomlyn does not publish a
strongly-named
/// package that is compatible with the project's pinned dependency
versions.
@@ -44,7 +49,7 @@ namespace Apache.Arrow.Adbc.DriverManager
/// Parses <paramref name="content"/> and returns a dictionary keyed
by section name.
/// Root-level keys are stored under the empty string key.
/// Values are typed as <see cref="string"/>, <see cref="long"/>, <see
cref="double"/>,
- /// or <see cref="bool"/>.
+ /// <see cref="bool"/>, or <see cref="List{T}"/> of those.
/// </summary>
internal static Dictionary<string, Dictionary<string, object>>
Parse(string content)
{
@@ -69,8 +74,13 @@ namespace Apache.Arrow.Adbc.DriverManager
continue;
}
- if (line.StartsWith("[", StringComparison.Ordinal) &&
line.EndsWith("]", StringComparison.Ordinal))
+ if (line[0] == '[')
{
+ if (line[line.Length - 1] != ']')
+ {
+ throw new FormatException(
+ "Invalid TOML section header '" + line + "':
missing closing ']' or invalid trailing content.");
+ }
string sectionName = line.Substring(1, line.Length -
2).Trim();
ValidateSectionName(sectionName);
currentSection = sectionName;
@@ -84,10 +94,12 @@ namespace Apache.Arrow.Adbc.DriverManager
int eqIndex = line.IndexOf('=');
if (eqIndex <= 0)
{
- continue;
+ throw new FormatException(
+ "Invalid TOML line '" + line + "': expected 'key =
value', section header, or comment.");
}
string key = line.Substring(0, eqIndex).Trim();
+ ValidateKeyName(key);
string valueRaw = line.Substring(eqIndex + 1).Trim();
object value = ParseValue(valueRaw);
@@ -140,6 +152,38 @@ namespace Apache.Arrow.Adbc.DriverManager
}
}
+ private static void ValidateKeyName(string keyName)
+ {
+ // ADBC connection profiles and driver manifests use
dot-namespaced option
+ // names (e.g. 'adbc.snowflake.sql.warehouse') as flat keys, not
as TOML
+ // dotted keys that nest tables. The Rust and C++ driver managers
parse
+ // those as nested tables and then flatten the result back to
dotted
+ // strings before handing them to AdbcDatabaseSetOption; this
minimal
+ // parser short-circuits that by accepting '.' as part of a bare
key, so
+ // the dictionary entries match what the other implementations
produce.
+ // Quoted and whitespace-containing keys are still rejected.
+ if (keyName.Length == 0)
+ {
+ throw new FormatException("Invalid TOML key: key is empty.");
+ }
+
+ for (int i = 0; i < keyName.Length; i++)
+ {
+ char c = keyName[i];
+ bool isAllowed =
+ (c >= 'A' && c <= 'Z') ||
+ (c >= 'a' && c <= 'z') ||
+ (c >= '0' && c <= '9') ||
+ c == '_' || c == '-' || c == '.';
+ if (!isAllowed)
+ {
+ throw new FormatException(
+ "Invalid TOML key '" + keyName +
+ "': only bare keys (A-Z, a-z, 0-9, '_', '-', '.') are
supported.");
+ }
+ }
+ }
+
private static object ParseValue(string raw)
{
if (raw.Length == 0)
@@ -147,22 +191,25 @@ namespace Apache.Arrow.Adbc.DriverManager
throw new FormatException("Invalid TOML value: value is
empty.");
}
- // Double-quoted string. Multi-line triple-quoted strings
("""...""") are
- // explicitly rejected so that they don't get misread as a basic
string with
- // empty quotes around the content.
if (raw[0] == '"')
{
- if (raw.Length >= 3 && raw[1] == '"' && raw[2] == '"')
- {
- throw new FormatException(
- "Invalid TOML value '" + raw + "': multi-line strings
are not supported.");
- }
- if (raw.Length < 2 || raw[raw.Length - 1] != '"')
- {
- throw new FormatException("Invalid TOML value '" + raw +
"': unterminated double-quoted string.");
- }
- string inner = raw.Substring(1, raw.Length - 2);
- return UnescapeString(inner);
+ return ParseBasicString(raw);
+ }
+
+ if (raw[0] == '\'')
+ {
+ return ParseLiteralString(raw);
+ }
+
+ if (raw[0] == '[')
+ {
+ return ParseArray(raw);
+ }
+
+ if (raw[0] == '{')
+ {
+ throw new FormatException(
+ "Invalid TOML value '" + raw + "': inline tables are not
supported.");
}
// Boolean (TOML booleans are lowercase; be strict.)
@@ -175,51 +222,285 @@ namespace Apache.Arrow.Adbc.DriverManager
return false;
}
- // Integer (try before float, since integers are a subset)
- if (long.TryParse(raw, NumberStyles.Integer,
CultureInfo.InvariantCulture, out long intValue))
+ // Integer (try before float, since integers are a subset). Reject
TOML
+ // integer extensions (underscores, hex/oct/bin prefixes) by
restricting
+ // the allowed NumberStyles and explicitly checking the input.
+ if (IsPlainInteger(raw) &&
+ long.TryParse(raw, NumberStyles.Integer,
CultureInfo.InvariantCulture, out long intValue))
{
return intValue;
}
// Float
- if (double.TryParse(raw, NumberStyles.Float |
NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out double
dblValue))
+ if (IsPlainFloat(raw) &&
+ double.TryParse(raw, NumberStyles.Float,
CultureInfo.InvariantCulture, out double dblValue))
{
return dblValue;
}
- // Per the ADBC driver manifest spec, values are TOML scalars.
This parser
- // intentionally implements only the subset of TOML productions
documented on
- // the type (double-quoted strings, integers, floats, and
lowercase booleans).
- // Anything else -- TOML literal strings ('foo'), multi-line
strings ("""..."""),
- // arrays, inline tables, dates, hex/oct/bin/underscored integers,
etc. -- is
- // rejected with a clear error rather than being silently treated
as an unquoted
- // string. This matches the strict-by-default policy used for
section names.
+ // Per the ADBC driver manifest spec, values are a bounded set of
TOML scalars
+ // plus single-line arrays of those. Anything else -- multi-line
strings,
+ // inline tables, dates, hex/oct/bin/underscored integers, bare
unquoted
+ // strings, etc. -- is rejected with a clear error rather than
silently
+ // treated as a string. This matches the strict-by-default policy
used for
+ // section and key names.
throw new FormatException(
"Invalid TOML value '" + raw +
- "': only double-quoted strings, integers, floats, and
'true'/'false' are supported.");
+ "': only basic strings (\"...\"), literal strings ('...'),
arrays of those, integers, floats, and 'true'/'false' are supported.");
}
- private static string UnescapeString(string s)
+ private static string ParseBasicString(string raw)
{
- System.Text.StringBuilder sb = new
System.Text.StringBuilder(s.Length);
- for (int i = 0; i < s.Length; i++)
+ // Reject multi-line basic strings ("""...""").
+ if (raw.Length >= 3 && raw[1] == '"' && raw[2] == '"')
+ {
+ throw new FormatException(
+ "Invalid TOML value '" + raw + "': multi-line strings are
not supported.");
+ }
+
+ int close = -1;
+ for (int i = 1; i < raw.Length; i++)
{
- if (s[i] == '\\' && i + 1 < s.Length)
+ char c = raw[i];
+ if (c == '\\')
{
+ if (i + 1 >= raw.Length)
+ {
+ throw new FormatException(
+ "Invalid TOML value '" + raw + "': dangling escape
in basic string.");
+ }
i++;
- switch (s[i])
+ continue;
+ }
+ if (c == '"')
+ {
+ close = i;
+ break;
+ }
+ }
+ if (close < 0)
+ {
+ throw new FormatException(
+ "Invalid TOML value '" + raw + "': unterminated basic
string.");
+ }
+ if (close != raw.Length - 1)
+ {
+ throw new FormatException(
+ "Invalid TOML value '" + raw + "': trailing content after
basic string.");
+ }
+ return UnescapeString(raw.Substring(1, close - 1));
+ }
+
+ private static string ParseLiteralString(string raw)
+ {
+ // Reject multi-line literal strings ('''...''').
+ if (raw.Length >= 3 && raw[1] == '\'' && raw[2] == '\'')
+ {
+ throw new FormatException(
+ "Invalid TOML value '" + raw + "': multi-line literal
strings are not supported.");
+ }
+
+ int close = raw.IndexOf('\'', 1);
+ if (close < 0)
+ {
+ throw new FormatException(
+ "Invalid TOML value '" + raw + "': unterminated literal
string.");
+ }
+ if (close != raw.Length - 1)
+ {
+ throw new FormatException(
+ "Invalid TOML value '" + raw + "': trailing content after
literal string.");
+ }
+ return raw.Substring(1, close - 1);
+ }
+
+ private static List<object> ParseArray(string raw)
+ {
+ if (raw[raw.Length - 1] != ']')
+ {
+ throw new FormatException(
+ "Invalid TOML value '" + raw +
+ "': arrays must open and close on the same line
(multi-line arrays are not supported).");
+ }
+
+ string inner = raw.Substring(1, raw.Length - 2).Trim();
+ List<object> result = new List<object>();
+ if (inner.Length == 0)
+ {
+ return result;
+ }
+
+ foreach (string element in SplitArrayElements(inner, raw))
+ {
+ string trimmed = element.Trim();
+ if (trimmed.Length == 0)
+ {
+ throw new FormatException(
+ "Invalid TOML array '" + raw + "': empty array
element.");
+ }
+ if (trimmed[0] == '[' || trimmed[0] == '{')
+ {
+ throw new FormatException(
+ "Invalid TOML array '" + raw +
+ "': nested arrays and inline tables are not
supported.");
+ }
+ result.Add(ParseValue(trimmed));
+ }
+ return result;
+ }
+
+ private static List<string> SplitArrayElements(string s, string raw)
+ {
+ List<string> tokens = new List<string>();
+ bool inBasic = false;
+ bool inLiteral = false;
+ int start = 0;
+ for (int i = 0; i < s.Length; i++)
+ {
+ char c = s[i];
+ if (inBasic)
+ {
+ if (c == '\\' && i + 1 < s.Length)
+ {
+ i++;
+ continue;
+ }
+ if (c == '"')
+ {
+ inBasic = false;
+ }
+ }
+ else if (inLiteral)
+ {
+ if (c == '\'')
{
- case '"': sb.Append('"'); break;
- case '\\': sb.Append('\\'); break;
- case 'n': sb.Append('\n'); break;
- case 'r': sb.Append('\r'); break;
- case 't': sb.Append('\t'); break;
- default: sb.Append('\\'); sb.Append(s[i]); break;
+ inLiteral = false;
}
}
else
+ {
+ if (c == '"')
+ {
+ inBasic = true;
+ }
+ else if (c == '\'')
+ {
+ inLiteral = true;
+ }
+ else if (c == ',')
+ {
+ tokens.Add(s.Substring(start, i - start));
+ start = i + 1;
+ }
+ }
+ }
+ if (inBasic || inLiteral)
+ {
+ throw new FormatException(
+ "Invalid TOML array '" + raw + "': unterminated string in
array element.");
+ }
+ string tail = s.Substring(start);
+ // Allow (but don't require) a trailing comma per TOML spec: if
tokens were
+ // produced and the tail is whitespace-only, treat it as a
trailing comma.
+ if (tokens.Count == 0 || tail.Trim().Length > 0)
+ {
+ tokens.Add(tail);
+ }
+ return tokens;
+ }
+
+ private static bool IsPlainInteger(string raw)
+ {
+ // TOML allows a single optional leading sign followed by ASCII
digits, with
+ // no underscores and no 0x/0o/0b prefixes. Anything else is a
recognized
+ // production this parser doesn't support.
+ int i = 0;
+ if (i < raw.Length && (raw[i] == '+' || raw[i] == '-'))
+ {
+ i++;
+ }
+ if (i >= raw.Length)
+ {
+ return false;
+ }
+ for (; i < raw.Length; i++)
+ {
+ if (raw[i] < '0' || raw[i] > '9')
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static bool IsPlainFloat(string raw)
+ {
+ // Reject TOML float extensions (underscores, inf, nan) and shapes
that
+ // double.TryParse would otherwise accept (hex, thousand
separators). A plain
+ // float here is sign? digits ('.' digits)? ([eE] sign? digits)?
with at least
+ // one '.' or exponent so that IsPlainInteger covers the
pure-integer case.
+ int i = 0;
+ if (i < raw.Length && (raw[i] == '+' || raw[i] == '-'))
+ {
+ i++;
+ }
+ bool seenDigit = false;
+ bool seenDot = false;
+ bool seenExp = false;
+ for (; i < raw.Length; i++)
+ {
+ char c = raw[i];
+ if (c >= '0' && c <= '9')
+ {
+ seenDigit = true;
+ }
+ else if (c == '.' && !seenDot && !seenExp)
+ {
+ seenDot = true;
+ }
+ else if ((c == 'e' || c == 'E') && !seenExp && seenDigit)
+ {
+ seenExp = true;
+ seenDigit = false;
+ if (i + 1 < raw.Length && (raw[i + 1] == '+' || raw[i + 1]
== '-'))
+ {
+ i++;
+ }
+ }
+ else
+ {
+ return false;
+ }
+ }
+ return seenDigit && (seenDot || seenExp);
+ }
+
+ private static string UnescapeString(string s)
+ {
+ System.Text.StringBuilder sb = new
System.Text.StringBuilder(s.Length);
+ for (int i = 0; i < s.Length; i++)
+ {
+ if (s[i] != '\\')
{
sb.Append(s[i]);
+ continue;
+ }
+ if (i + 1 >= s.Length)
+ {
+ throw new FormatException("Invalid TOML basic string:
dangling escape '\\'.");
+ }
+ i++;
+ switch (s[i])
+ {
+ case '"': sb.Append('"'); break;
+ case '\\': sb.Append('\\'); break;
+ case 'n': sb.Append('\n'); break;
+ case 'r': sb.Append('\r'); break;
+ case 't': sb.Append('\t'); break;
+ default:
+ throw new FormatException(
+ "Invalid TOML basic string escape '\\" + s[i] +
+ "': only \\\", \\\\, \\n, \\r, and \\t are
supported.");
}
}
return sb.ToString();
@@ -227,18 +508,48 @@ namespace Apache.Arrow.Adbc.DriverManager
private static string StripComment(string line)
{
- // Only strip # that is not inside a quoted string
- bool inString = false;
+ // Strip a trailing '#' comment, but not when the '#' falls inside
a
+ // double-quoted basic string or a single-quoted literal string.
Literal
+ // strings perform no escape processing, so backslashes inside
them are
+ // taken literally and do not affect string termination.
+ bool inBasic = false;
+ bool inLiteral = false;
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
- if (c == '"' && (i == 0 || line[i - 1] != '\\'))
+ if (inBasic)
{
- inString = !inString;
+ if (c == '\\' && i + 1 < line.Length)
+ {
+ i++;
+ continue;
+ }
+ if (c == '"')
+ {
+ inBasic = false;
+ }
}
- if (c == '#' && !inString)
+ else if (inLiteral)
{
- return line.Substring(0, i);
+ if (c == '\'')
+ {
+ inLiteral = false;
+ }
+ }
+ else
+ {
+ if (c == '"')
+ {
+ inBasic = true;
+ }
+ else if (c == '\'')
+ {
+ inLiteral = true;
+ }
+ else if (c == '#')
+ {
+ return line.Substring(0, i);
+ }
}
}
return line;
diff --git
a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlParserTests.cs
b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlParserTests.cs
index 11bc440e8..4940ebbbd 100644
--- a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlParserTests.cs
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlParserTests.cs
@@ -61,10 +61,8 @@ namespace Apache.Arrow.Adbc.Tests.DriverManager
}
[Theory]
- [InlineData("key = 'foo'")] // TOML literal
(single-quoted) string
[InlineData("key = \"\"\"foo\"\"\"")] // multi-line basic string
[InlineData("key = '''foo'''")] // multi-line literal string
- [InlineData("key = [1, 2, 3]")] // array
[InlineData("key = { a = 1 }")] // inline table
[InlineData("key = 1_000")] // underscored integer
[InlineData("key = 0xff")] // hex integer
@@ -73,34 +71,206 @@ namespace Apache.Arrow.Adbc.Tests.DriverManager
[InlineData("key = 2024-01-01")] // date
[InlineData("key = bareword")] // bare unquoted string
[InlineData("key = \"unterminated")] // unterminated double-quoted
string
+ [InlineData("key = 'unterminated")] // unterminated single-quoted
string
[InlineData("key = TRUE")] // non-lowercase boolean
[InlineData("key = False")] // non-lowercase boolean
+ [InlineData("key = \"foo\"trailing")] // trailing content after basic
string
+ [InlineData("key = 'foo'trailing")] // trailing content after
literal string
+ [InlineData("key = \"bad\\xescape\"")] // unsupported basic-string
escape
+ [InlineData("key = [[1, 2], [3]]")] // nested arrays
+ [InlineData("key = [{ a = 1 }]")] // array containing inline table
+ [InlineData("key = [,1]")] // leading comma / empty
element
+ [InlineData("key = [1,,2]")] // double comma / empty element
+ [InlineData("key = [")] // multi-line / unterminated
array
public void Parse_RejectsUnsupportedValueProductions(string line)
{
Assert.Throws<FormatException>(() => TomlParser.Parse(line +
"\n"));
}
+ [Theory]
+ [InlineData("[foo")] // missing closing bracket
+ [InlineData("key value")] // missing '='
+ [InlineData("= value")] // missing key
+ [InlineData("\"quoted-key\" = 1")] // quoted keys are not supported
+ [InlineData("key with spaces = 1")] // whitespace-containing keys
are not supported
+ public void Parse_RejectsMalformedLines(string line)
+ {
+ Assert.Throws<FormatException>(() => TomlParser.Parse(line +
"\n"));
+ }
+
+ [Fact]
+ public void Parse_AcceptsDotNamespacedKeyAsFlatKey()
+ {
+ // ADBC connection profiles use dot-namespaced option names as
flat keys.
+ // The Rust and C++ driver managers reach the same result by
parsing them
+ // as nested tables and then flattening; this minimal parser
accepts them
+ // as bare keys directly. The dictionary entry must be the literal
dotted
+ // string -- not nested -- so it can be passed to
AdbcDatabaseSetOption.
+ Dictionary<string, Dictionary<string, object>> result =
+ TomlParser.Parse("adbc.snowflake.sql.warehouse =
\"COMPUTE_WH\"\n");
+ Assert.Equal("COMPUTE_WH",
result[""]["adbc.snowflake.sql.warehouse"]);
+ }
+
[Theory]
[InlineData("key = \"hello\"", "hello")]
[InlineData("key = \"\"", "")]
+ [InlineData("key = \"say \\\"hi\\\"\"", "say \"hi\"")]
public void Parse_AcceptsDoubleQuotedStrings(string line, string
expected)
{
Dictionary<string, Dictionary<string, object>> result =
TomlParser.Parse(line + "\n");
Assert.Equal(expected, result[""]["key"]);
}
+ [Theory]
+ [InlineData("key = 'hello'", "hello")]
+ [InlineData("key = ''", "")]
+ [InlineData("key = 'Driver Display Name'", "Driver Display Name")]
+ [InlineData("key = 'C:\\\\path\\\\to\\\\driver.dll'",
"C:\\\\path\\\\to\\\\driver.dll")]
+ public void Parse_AcceptsSingleQuotedLiteralStrings(string line,
string expected)
+ {
+ Dictionary<string, Dictionary<string, object>> result =
TomlParser.Parse(line + "\n");
+ Assert.Equal(expected, result[""]["key"]);
+ }
+
+ [Fact]
+ public void Parse_LiteralString_PreservesHashCharacter()
+ {
+ // A '#' inside a literal string must not be treated as a comment
marker.
+ Dictionary<string, Dictionary<string, object>> result =
+ TomlParser.Parse("key = 'has#hash' # actual comment\n");
+ Assert.Equal("has#hash", result[""]["key"]);
+ }
+
+ [Fact]
+ public void Parse_LiteralString_BackslashesAreLiteral()
+ {
+ // In TOML literal strings, backslashes are taken literally with
no escape
+ // processing -- this is the standard idiom for Windows paths in
manifests.
+ Dictionary<string, Dictionary<string, object>> result =
+ TomlParser.Parse(@"key = 'C:\path\to\driver.dll'" + "\n");
+ Assert.Equal(@"C:\path\to\driver.dll", result[""]["key"]);
+ }
+
+ [Fact]
+ public void Parse_AcceptsEmptyArray()
+ {
+ Dictionary<string, Dictionary<string, object>> result =
+ TomlParser.Parse("supported = []\n");
+ List<object> arr =
Assert.IsType<List<object>>(result[""]["supported"]);
+ Assert.Empty(arr);
+ }
+
+ [Fact]
+ public void Parse_AcceptsArrayOfStrings()
+ {
+ Dictionary<string, Dictionary<string, object>> result =
+ TomlParser.Parse("features = ['bulk insert', \"async\"]\n");
+ List<object> arr =
Assert.IsType<List<object>>(result[""]["features"]);
+ Assert.Equal(new object[] { "bulk insert", "async" }, arr);
+ }
+
+ [Fact]
+ public void Parse_AcceptsArrayOfMixedScalars()
+ {
+ Dictionary<string, Dictionary<string, object>> result =
+ TomlParser.Parse("mixed = [1, 2.5, true, 'x']\n");
+ List<object> arr =
Assert.IsType<List<object>>(result[""]["mixed"]);
+ Assert.Equal(new object[] { 1L, 2.5, true, "x" }, arr);
+ }
+
+ [Fact]
+ public void Parse_AcceptsTrailingCommaInArray()
+ {
+ Dictionary<string, Dictionary<string, object>> result =
+ TomlParser.Parse("arr = ['a', 'b',]\n");
+ List<object> arr = Assert.IsType<List<object>>(result[""]["arr"]);
+ Assert.Equal(new object[] { "a", "b" }, arr);
+ }
+
+ [Fact]
+ public void Parse_Array_HashInsideStringElementIsLiteral()
+ {
+ // The strip-comment pass must respect strings inside array bodies
too.
+ Dictionary<string, Dictionary<string, object>> result =
+ TomlParser.Parse("arr = ['a#b', 'c'] # comment\n");
+ List<object> arr = Assert.IsType<List<object>>(result[""]["arr"]);
+ Assert.Equal(new object[] { "a#b", "c" }, arr);
+ }
+
[Fact]
public void Parse_AcceptsRecognizedScalarValues()
{
- string content = "s = \"x\"\ni = 42\nf = 3.14\nb1 = true\nb2 =
false\n";
+ string content = "s = \"x\"\nlit = 'y'\ni = 42\nf = 3.14\nb1 =
true\nb2 = false\n";
Dictionary<string, Dictionary<string, object>> result =
TomlParser.Parse(content);
Assert.Equal("x", result[""]["s"]);
+ Assert.Equal("y", result[""]["lit"]);
Assert.Equal(42L, result[""]["i"]);
Assert.Equal(3.14, result[""]["f"]);
Assert.Equal(true, result[""]["b1"]);
Assert.Equal(false, result[""]["b2"]);
}
+
+ [Fact]
+ public void Parse_DriverManifestExample_ParsesAllFields()
+ {
+ // This is the canonical ADBC driver-manifest layout from the spec
docs.
+ // Combines literal strings, integers, empty arrays, inline
comments, and
+ // both top-level and dotted section headers -- if any of those
regress,
+ // this test catches it.
+ const string toml = @"manifest_version = 1
+
+name = 'Driver Display Name'
+version = '1.0.0' # driver version
+publisher = 'string to identify the publisher'
+license = 'Apache-2.0' # or otherwise
+url = 'https://example.com' # URL with more info about the driver
+ # such as a github link or documentation.
+
+[ADBC]
+version = '1.1.0' # Maximum supported ADBC spec version
+
+[ADBC.features]
+supported = [] # list of strings such as 'bulk insert'
+unsupported = [] # list of strings such as 'async'
+
+[Driver]
+entrypoint = 'AdbcDriverInit' # entrypoint to use if not using default
+# You can provide just a single path
+# shared = '/path/to/libadbc_driver.so'
+
+# or you can provide platform-specific paths for scenarios where the driver
+# is distributed with multiple platforms supported by a single package.
+[Driver.shared]
+# paths to shared libraries to load based on platform tuple
+linux_amd64 = '/path/to/libadbc_driver.so'
+osx_amd64 = '/path/to/libadbc_driver.dylib'
+windows_amd64 = 'C:\\path\\to\\adbc_driver.dll'
+";
+ Dictionary<string, Dictionary<string, object>> result =
TomlParser.Parse(toml);
+
+ Assert.Equal(1L, result[""]["manifest_version"]);
+ Assert.Equal("Driver Display Name", result[""]["name"]);
+ Assert.Equal("1.0.0", result[""]["version"]);
+ Assert.Equal("string to identify the publisher",
result[""]["publisher"]);
+ Assert.Equal("Apache-2.0", result[""]["license"]);
+ Assert.Equal("https://example.com", result[""]["url"]);
+
+ Assert.Equal("1.1.0", result["ADBC"]["version"]);
+
+ List<object> supported =
Assert.IsType<List<object>>(result["ADBC.features"]["supported"]);
+ List<object> unsupported =
Assert.IsType<List<object>>(result["ADBC.features"]["unsupported"]);
+ Assert.Empty(supported);
+ Assert.Empty(unsupported);
+
+ Assert.Equal("AdbcDriverInit", result["Driver"]["entrypoint"]);
+
+ Assert.Equal("/path/to/libadbc_driver.so",
result["Driver.shared"]["linux_amd64"]);
+ Assert.Equal("/path/to/libadbc_driver.dylib",
result["Driver.shared"]["osx_amd64"]);
+ // Literal strings preserve backslashes verbatim: '\\' in source
means two
+ // literal backslash characters in the parsed value.
+ Assert.Equal(@"C:\\path\\to\\adbc_driver.dll",
result["Driver.shared"]["windows_amd64"]);
+ }
}
}