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"]);
+        }
     }
 }

Reply via email to