• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SamboyCoding / Tomlet / 14562845545

20 Apr 2025 08:00PM UTC coverage: 90.93% (-0.5%) from 91.406%
14562845545

push

github

SamboyCoding
Fix: Handling of "line-ending-backslash" in multiline basic strings

970 of 1140 branches covered (85.09%)

18 of 24 new or added lines in 1 file covered. (75.0%)

6 existing lines in 1 file now uncovered.

1935 of 2128 relevant lines covered (90.93%)

217.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.71
/Tomlet/TomlParser.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Globalization;
4
using System.IO;
5
using System.Linq;
6
using System.Text;
7
using Tomlet.Attributes;
8
using Tomlet.Exceptions;
9
using Tomlet.Extensions;
10
using Tomlet.Models;
11

12
namespace Tomlet;
13

14
public class TomlParser
15
{
16
    private static readonly char[] TrueChars = {'t', 'r', 'u', 'e'};
1✔
17
    private static readonly char[] FalseChars = {'f', 'a', 'l', 's', 'e'};
1✔
18

19
    private int _lineNumber = 1;
141✔
20

21
    private string[] _tableNames = new string[0];
141✔
22
    private TomlTable? _currentTable;
23

24
    // ReSharper disable once UnusedMember.Global
25
    [ExcludeFromCodeCoverage]
26
    public static TomlDocument ParseFile(string filePath)
27
    {
28
        var fileContent = File.ReadAllText(filePath);
29
        TomlParser parser = new();
30
        return parser.Parse(fileContent);
31
    }
32

33
    public TomlDocument Parse(string input)
34
    {
141✔
35
        try
36
        {
141✔
37
            var document = new TomlDocument();
141✔
38
            using var reader = new TomletStringReader(input);
141✔
39

40
            string? lastPrecedingComment = null;
141✔
41
            while (reader.TryPeek(out _))
476✔
42
            {
379✔
43
                //We have more to read.
44
                //By the time we get back to this position in the main loop, we've fully consumed any structure.
45
                //So that means we're at the start of a line - which could be a comment, table-array or table header, a key-value pair, or just whitespace.
46
                _lineNumber += reader.SkipAnyNewlineOrWhitespace();
379✔
47

48
                lastPrecedingComment = ReadAnyPotentialMultilineComment(reader);
379✔
49

50
                if (!reader.TryPeek(out var nextChar))
379✔
51
                    break;
3✔
52

53
                if (nextChar == '[')
376✔
54
                {
59✔
55
                    reader.Read(); //Consume the [
59✔
56

57
                    //Table or table-array?
58
                    if (!reader.TryPeek(out var potentialSecondBracket))
59!
59
                        throw new TomlEndOfFileException(_lineNumber);
×
60

61
                    TomlValue valueFromSquareBracket;
62
                    if (potentialSecondBracket != '[')
59✔
63
                        valueFromSquareBracket = ReadTableStatement(reader, document);
35✔
64
                    else
65
                        valueFromSquareBracket = ReadTableArrayStatement(reader, document);
24✔
66

67
                    valueFromSquareBracket.Comments.PrecedingComment = lastPrecedingComment;
50✔
68

69
                    continue; //Restart loop.
50✔
70
                }
71

72
                //Read a key-value pair
73
                ReadKeyValuePair(reader, out var key, out var value);
317✔
74

75
                value.Comments.PrecedingComment = lastPrecedingComment;
287✔
76
                lastPrecedingComment = null;
287✔
77

78
                if (_currentTable != null)
287✔
79
                    //Insert into current table
80
                    _currentTable.ParserPutValue(key, value, _lineNumber);
87✔
81
                else
82
                    //Insert into the document
83
                    document.ParserPutValue(key, value, _lineNumber);
200✔
84

85
                //Read up until the end of the line, ignoring any comments or whitespace
86
                reader.SkipWhitespace();
286✔
87

88
                //Ensure we have a newline
89
                reader.SkipPotentialCarriageReturn();
286✔
90
                if (!reader.ExpectAndConsume('\n') && reader.TryPeek(out var shouldHaveBeenLf))
286✔
91
                    //Not EOF and found a non-newline char
92
                    throw new TomlMissingNewlineException(_lineNumber, (char) shouldHaveBeenLf);
1✔
93

94
                _lineNumber++; //We've consumed a newline, move to the next line number.
285✔
95
            }
285✔
96

97
            document.TrailingComment = lastPrecedingComment;
100✔
98

99
            return document;
100✔
100
        }
101
        catch (Exception e) when (e is not TomlException)
41✔
102
        {
×
103
            throw new TomlInternalException(_lineNumber, e);
×
104
        }
105
    }
100✔
106

107
    private void ReadKeyValuePair(TomletStringReader reader, out string key, out TomlValue value)
108
    {
352✔
109
        //Read the key
110
        key = ReadKey(reader);
352✔
111

112
        //Consume the equals sign, potentially with whitespace either side.
113
        reader.SkipWhitespace();
344✔
114
        if (!reader.ExpectAndConsume('='))
344✔
115
        {
1✔
116
            if (reader.TryPeek(out var shouldHaveBeenEquals))
1!
117
                throw new TomlMissingEqualsException(_lineNumber, (char) shouldHaveBeenEquals);
1✔
118

119
            throw new TomlEndOfFileException(_lineNumber);
×
120
        }
121

122
        reader.SkipWhitespace();
343✔
123

124
        //Read the value
125
        value = ReadValue(reader);
343✔
126
    }
321✔
127

128
    private string ReadKey(TomletStringReader reader)
129
    {
352✔
130
        reader.SkipWhitespace();
352✔
131

132
        if (!reader.TryPeek(out var nextChar))
352!
133
            return "";
×
134

135
        if (nextChar.IsEquals())
352✔
136
            throw new NoTomlKeyException(_lineNumber);
2✔
137

138
        //Read a key
139
        reader.SkipWhitespace();
350✔
140

141
        string key;
142
        if (nextChar.IsDoubleQuote())
350✔
143
        {
11✔
144
            //Read double-quoted key
145
            reader.Read();
11✔
146
            if (reader.TryPeek(out var maybeSecondDoubleQuote) && maybeSecondDoubleQuote.IsDoubleQuote())
11!
147
            {
2✔
148
                reader.Read(); //Consume second double quote.
2✔
149

150
                //Check for third quote => invalid key
151
                //Else => empty key
152
                if (reader.TryPeek(out var maybeThirdDoubleQuote) && maybeThirdDoubleQuote.IsDoubleQuote())
2!
153
                    throw new TomlTripleQuotedKeyException(_lineNumber);
1✔
154

155
                return string.Empty;
1✔
156
            }
157

158
            //We delegate to the dedicated string reading function here because a double-quoted key can contain everything a double-quoted string can. 
159
            key = '"' + ReadSingleLineBasicString(reader, false).StringValue + '"';
9✔
160

161
            if (!reader.ExpectAndConsume('"'))
9✔
162
                throw new UnterminatedTomlKeyException(_lineNumber);
1✔
163
        }
8✔
164
        else if (nextChar.IsSingleQuote())
339✔
165
        {
2✔
166
            reader.Read(); //Consume opening quote.
2✔
167

168
            //Read single-quoted key
169
            key = "'" + ReadSingleLineLiteralString(reader, false).StringValue + "'";
2✔
170
            if (!reader.ExpectAndConsume('\''))
2!
171
                throw new UnterminatedTomlKeyException(_lineNumber);
×
172
        }
2✔
173
        else
174
            //Read unquoted key
175
            key = ReadKeyInternal(reader, keyChar => keyChar.IsEquals() || keyChar.IsHashSign());
3,471✔
176

177
        key = key.Replace("\\n", "\n")
343✔
178
            .Replace("\\t", "\t");
343✔
179

180
        return key;
343✔
181
    }
344✔
182

183
    private string ReadKeyInternal(TomletStringReader reader, Func<int, bool> charSignalsEndOfKey)
184
    {
337✔
185
        var parts = new List<string>();
337✔
186

187
        //Parts loop
188
        while (reader.TryPeek(out var nextChar))
687!
189
        {
687✔
190
            if (charSignalsEndOfKey(nextChar))
687✔
191
                return string.Join(".", parts.ToArray());
333✔
192

193
            if (nextChar.IsPeriod())
354✔
194
                throw new TomlDoubleDottedKeyException(_lineNumber);
1✔
195

196
            var thisPart = new StringBuilder();
353✔
197
            //Part loop
198
            while (reader.TryPeek(out nextChar))
2,463✔
199
            {
2,463✔
200
                nextChar.EnsureLegalChar(_lineNumber);
2,463✔
201
                    
202
                var numLeadingWhitespace = reader.SkipWhitespace();
2,462✔
203
                reader.TryPeek(out var charAfterWhitespace);
2,462✔
204
                if (charAfterWhitespace.IsPeriod())
2,462✔
205
                {
17✔
206
                    //Whitespace is permitted in keys only around periods
207
                    parts.Add(thisPart.ToString()); //Add this part
17✔
208

209
                    //Consume period and any trailing whitespace
210
                    reader.ExpectAndConsume('.');
17✔
211
                    reader.SkipWhitespace();
17✔
212
                    break; //End of part, move to next
17✔
213
                }
214

215
                if (numLeadingWhitespace > 0 && charSignalsEndOfKey(charAfterWhitespace))
2,445✔
216
                {
333✔
217
                    //Add this part to the list of parts and break out of the loop, without consuming the char (it'll be picked up by the outer loop)
218
                    parts.Add(thisPart.ToString());
333✔
219
                    break;
333✔
220
                }
221

222
                //Un-skip the whitespace
223
                reader.Backtrack(numLeadingWhitespace);
2,112✔
224

225
                //NextChar is still the whitespace itself
226
                if (charSignalsEndOfKey(nextChar))
2,112!
227
                {
×
228
                    //Add this part to the list of parts and break out of the loop, without consuming the char (it'll be picked up by the outer loop)
229
                    parts.Add(thisPart.ToString());
×
230
                    break;
×
231
                }
232

233
                if (numLeadingWhitespace > 0)
2,112✔
234
                    //Whitespace is not allowed outside of the area immediately around a period in a dotted key
235
                    throw new TomlWhitespaceInKeyException(_lineNumber);
2✔
236

237
                //Append this char to the part
238
                thisPart.Append((char) reader.Read());
2,110✔
239
            }
2,110✔
240
        }
350✔
241

242
        throw new TomlEndOfFileException(_lineNumber);
×
243
    }
333✔
244

245
    private TomlValue ReadValue(TomletStringReader reader)
246
    {
430✔
247
        if (!reader.TryPeek(out var startOfValue))
430✔
248
            throw new TomlEndOfFileException(_lineNumber);
1✔
249

250
        TomlValue value;
251
        switch (startOfValue)
429✔
252
        {
253
            case '[':
254
                //Array
255
                value = ReadArray(reader);
32✔
256
                break;
31✔
257
            case '{':
258
                //Inline table
259
                value = ReadInlineTable(reader);
29✔
260
                break;
26✔
261
            case '"':
262
            case '\'':
263
                //Basic or Literal String, maybe multiline
264
                var startQuote = reader.Read();
232✔
265
                var maybeSecondQuote = reader.Peek();
232✔
266
                if (maybeSecondQuote != startQuote)
232✔
267
                    //Second char is not first, this is a single-line string.
268
                    value = startQuote.IsSingleQuote() ? ReadSingleLineLiteralString(reader) : ReadSingleLineBasicString(reader);
186✔
269
                else
270
                {
46✔
271
                    reader.Read(); //Consume second char
46✔
272

273
                    //Check the third char. If it's another quote, we have a multiline string. If it's whitespace, a newline, part of an inline array, or a #, we have an empty string.
274
                    //Anything else is an error.
275
                    var maybeThirdQuote = reader.Peek();
46✔
276
                    if (maybeThirdQuote == startQuote)
46✔
277
                    {
15✔
278
                        reader.Read(); //Consume the third opening quote, for simplicity's sake.
15✔
279
                        value = startQuote.IsSingleQuote() ? ReadMultiLineLiteralString(reader) : ReadMultiLineBasicString(reader);
15✔
280
                    }
13✔
281
                    else if (maybeThirdQuote.IsWhitespace() || maybeThirdQuote.IsNewline() || maybeThirdQuote.IsHashSign() || maybeThirdQuote.IsComma() || maybeThirdQuote.IsEndOfArrayChar() || maybeThirdQuote == -1)
31✔
282
                    {
30✔
283
                        value = TomlString.Empty;
30✔
284
                    }
30✔
285
                    else
286
                    {
1✔
287
                        throw new TomlStringException(_lineNumber);
1✔
288
                    }
289
                }
43✔
290

291
                break;
227✔
292
            case '+':
293
            case '-':
294
            case '0':
295
            case '1':
296
            case '2':
297
            case '3':
298
            case '4':
299
            case '5':
300
            case '6':
301
            case '7':
302
            case '8':
303
            case '9':
304
            case 'i':
305
            case 'n':
306
                //I kind of hate that but it's probably fast.
307
                //Number. Maybe floating-point.
308
                //i and n indicate special floating point values (inf and nan).
309

310
                //Read a string, stopping if we hit an equals, whitespace, newline, or comment.
311
                var stringValue = reader.ReadWhile(valueChar => !valueChar.IsEquals() && !valueChar.IsNewline() && !valueChar.IsHashSign() && !valueChar.IsComma() && !valueChar.IsEndOfArrayChar() && !valueChar.IsEndOfInlineObjectChar())
1,104✔
312
                    .ToLowerInvariant().Trim();
118✔
313

314
                if (stringValue.Contains(':') || stringValue.Contains('t') || stringValue.Contains(' ') || stringValue.Contains('z'))
118✔
315
                    value = TomlDateTimeUtils.ParseDateString(stringValue, _lineNumber) ?? throw new InvalidTomlDateTimeException(_lineNumber, stringValue);
25✔
316
                else if (stringValue.Contains('.') || (stringValue.Contains('e') && !stringValue.StartsWith("0x")) || stringValue.Contains('n') || stringValue.Contains('i'))
93✔
317
                    //Try parse as a double, then fall back to a date/time.
318
                    value = TomlDouble.Parse(stringValue) ?? TomlDateTimeUtils.ParseDateString(stringValue, _lineNumber) ?? throw new InvalidTomlNumberException(_lineNumber, stringValue);
32✔
319
                else
320
                    //Try parse as a long, then fall back to a date/time.
321
                    value = TomlLong.Parse(stringValue) ?? TomlDateTimeUtils.ParseDateString(stringValue, _lineNumber) ?? throw new InvalidTomlNumberException(_lineNumber, stringValue);
61✔
322

323
                break;
107✔
324
            case 't':
325
            {
14✔
326
                //Either "true" or an error
327
                var charsRead = reader.ReadChars(4);
14✔
328

329
                if (!TrueChars.SequenceEqual(charsRead))
14!
330
                    throw new TomlInvalidValueException(_lineNumber, (char) startOfValue);
×
331

332
                value = TomlBoolean.True;
14✔
333
                break;
14✔
334
            }
335
            case 'f':
336
            {
3✔
337
                //Either "false" or an error
338
                var charsRead = reader.ReadChars(5);
3✔
339

340
                if (!FalseChars.SequenceEqual(charsRead))
3!
341
                    throw new TomlInvalidValueException(_lineNumber, (char) startOfValue);
×
342

343
                value = TomlBoolean.False;
3✔
344
                break;
3✔
345
            }
346
            default:
347
                throw new TomlInvalidValueException(_lineNumber, (char) startOfValue);
1✔
348
        }
349

350
        reader.SkipWhitespace();
408✔
351
        value.Comments.InlineComment = ReadAnyPotentialInlineComment(reader);
408✔
352

353
        return value;
408✔
354
    }
408✔
355

356
    private TomlValue ReadSingleLineBasicString(TomletStringReader reader, bool consumeClosingQuote = true)
357
    {
185✔
358
        //No simple read here, we have to accomodate escaped double quotes.
359
        var content = new StringBuilder();
185✔
360

361
        var escapeMode = false;
185✔
362
        var fourDigitUnicodeMode = false;
185✔
363
        var eightDigitUnicodeMode = false;
185✔
364

365
        var unicodeStringBuilder = new StringBuilder();
185✔
366
        while (reader.TryPeek(out var nextChar))
1,702✔
367
        {
1,700✔
368
            nextChar.EnsureLegalChar(_lineNumber);
1,700✔
369
            if (nextChar == '"' && !escapeMode)
1,700✔
370
                break;
182✔
371

372
            reader.Read(); //Consume the next char
1,518✔
373

374
            if (nextChar == '\\' && !escapeMode)
1,518✔
375
            {
20✔
376
                escapeMode = true;
20✔
377
                continue; //Don't append
20✔
378
            }
379

380
            if (escapeMode)
1,498✔
381
            {
20✔
382
                escapeMode = false;
20✔
383
                var toAppend = HandleEscapedChar(nextChar, out fourDigitUnicodeMode, out eightDigitUnicodeMode);
20✔
384

385
                if (toAppend.HasValue)
19✔
386
                    content.Append(toAppend.Value);
16✔
387
                continue;
19✔
388
            }
389

390
            if (fourDigitUnicodeMode || eightDigitUnicodeMode)
1,478✔
391
            {
16✔
392
                //Handle \u1234 and \U12345678
393
                unicodeStringBuilder.Append((char) nextChar);
16✔
394

395
                if (fourDigitUnicodeMode && unicodeStringBuilder.Length == 4 || eightDigitUnicodeMode && unicodeStringBuilder.Length == 8)
16✔
396
                {
3✔
397
                    var unicodeString = unicodeStringBuilder.ToString();
3✔
398

399
                    content.Append(DecipherUnicodeEscapeSequence(unicodeString, fourDigitUnicodeMode));
3✔
400

401
                    fourDigitUnicodeMode = false;
3✔
402
                    eightDigitUnicodeMode = false;
3✔
403
                    unicodeStringBuilder = new StringBuilder();
3✔
404
                }
3✔
405

406
                continue;
16✔
407
            }
408

409
            if (nextChar.IsNewline())
1,462!
410
                throw new UnterminatedTomlStringException(_lineNumber);
×
411

412
            content.Append((char) nextChar);
1,462✔
413
        }
1,462✔
414

415
        if (consumeClosingQuote)
184✔
416
        {
175✔
417
            if (!reader.ExpectAndConsume('"'))
175✔
418
                throw new UnterminatedTomlStringException(_lineNumber);
1✔
419
        }
174✔
420

421
        return new TomlString(content.ToString());
183✔
422
    }
183✔
423

424
    private string DecipherUnicodeEscapeSequence(string unicodeString, bool fourDigitMode)
425
    {
3✔
426
        if (unicodeString.Any(c => !c.IsHexDigit()))
19!
427
            throw new InvalidTomlEscapeException(_lineNumber, $"\\{(fourDigitMode ? 'u' : 'U')}{unicodeString}");
×
428

429
        if (fourDigitMode)
3✔
430
        {
2✔
431
            //16-bit char
432
            var decodedChar = short.Parse(unicodeString, NumberStyles.HexNumber);
2✔
433
            return ((char) decodedChar).ToString();
2✔
434
        }
435

436
        //32-bit char
437
        var decodedChars = int.Parse(unicodeString, NumberStyles.HexNumber);
1✔
438
        return char.ConvertFromUtf32(decodedChars);
1✔
439
    }
3✔
440

441
    private char? HandleEscapedChar(int escapedChar, out bool fourDigitUnicodeMode, out bool eightDigitUnicodeMode, bool allowNewline = false)
442
    {
26✔
443
        eightDigitUnicodeMode = false;
26✔
444
        fourDigitUnicodeMode = false;
26✔
445

446
        char toAppend;
447
        switch (escapedChar)
26✔
448
        {
449
            case 'b':
450
                toAppend = '\b';
1✔
451
                break;
1✔
452
            case 't':
453
                toAppend = '\t';
3✔
454
                break;
3✔
455
            case 'n':
456
                toAppend = '\n';
4✔
457
                break;
4✔
458
            case 'f':
459
                toAppend = '\f';
1✔
460
                break;
1✔
461
            case 'r':
462
                toAppend = '\r';
1✔
463
                break;
1✔
464
            case '"':
465
                toAppend = '"';
11✔
466
                break;
11✔
467
            case '\\':
468
                toAppend = '\\';
1✔
469
                break;
1✔
470
            case 'u':
471
                fourDigitUnicodeMode = true;
2✔
472
                return null;
2✔
473
            case 'U':
474
                eightDigitUnicodeMode = true;
1✔
475
                return null;
1✔
476
            default:
477
                if (allowNewline && escapedChar.IsNewline())
1!
UNCOV
478
                    return null;
×
479
                throw new InvalidTomlEscapeException(_lineNumber, $"\\{(char) escapedChar}");
1✔
480
        }
481

482
        return toAppend;
22✔
483
    }
25✔
484

485
    private TomlValue ReadSingleLineLiteralString(TomletStringReader reader, bool consumeClosingQuote = true)
486
    {
12✔
487
        //Literally (hah) just read until a single-quote
488
        var stringContent = reader.ReadWhile(valueChar => !valueChar.IsSingleQuote() && !valueChar.IsNewline());
154✔
489
            
490
        foreach (var i in stringContent.Select(c => (int) c)) 
426✔
491
            i.EnsureLegalChar(_lineNumber);
130✔
492

493
        if (!reader.TryPeek(out var terminatingChar))
12!
494
            //Unexpected EOF
495
            throw new TomlEndOfFileException(_lineNumber);
×
496

497
        if (!terminatingChar.IsSingleQuote())
12!
498
            throw new UnterminatedTomlStringException(_lineNumber);
×
499

500
        if (consumeClosingQuote)
12✔
501
            reader.Read(); //Consume terminating quote.
10✔
502

503
        return new TomlString(stringContent);
12✔
504
    }
12✔
505

506
    private TomlValue ReadMultiLineLiteralString(TomletStringReader reader)
507
    {
6✔
508
        var content = new StringBuilder();
6✔
509
        //Ignore any first-line newlines
510
        _lineNumber += reader.SkipAnyNewline();
6✔
511
        while (reader.TryPeek(out _))
239✔
512
        {
239✔
513
            var nextChar = reader.Read();
239✔
514
            nextChar.EnsureLegalChar(_lineNumber);
239✔
515

516
            if (!nextChar.IsSingleQuote())
239✔
517
            {
229✔
518
                content.Append((char) nextChar);
229✔
519

520
                if (nextChar == '\n')
229✔
521
                    _lineNumber++; //We've wrapped to a new line.
4✔
522

523
                continue;
229✔
524
            }
525

526
            //We have a single quote.
527
            //Is it alone? if so, just continue.
528
            if (!reader.TryPeek(out var potentialSecondQuote) || !potentialSecondQuote.IsSingleQuote())
10!
529
            {
4✔
530
                content.Append('\'');
4✔
531
                continue;
4✔
532
            }
533

534
            //We have two quotes in a row. Consume the second one
535
            reader.Read();
6✔
536

537
            //Do we have three?
538
            if (!reader.TryPeek(out var potentialThirdQuote) || !potentialThirdQuote.IsSingleQuote())
6!
539
            {
×
540
                content.Append('\'');
×
541
                content.Append('\'');
×
542
                continue;
×
543
            }
544

545
            //Ok we have at least three quotes. Consume the third.
546
            reader.Read();
6✔
547

548
            if (!reader.TryPeek(out var afterThirdQuote) || !afterThirdQuote.IsSingleQuote())
6✔
549
                //And ONLY three quotes. End of literal.
550
                break;
4✔
551

552
            //We're at 4 single quotes back-to-back at this point, and the max is 5. I'm just going to do this without a loop because it's probably actually less code.
553
            //Consume the fourth.
554
            reader.Read();
2✔
555
            //And we have to append one single quote to our string.
556
            content.Append('\'');
2✔
557

558
            //Check for a 5th.
559
            if (!reader.TryPeek(out var potentialFifthQuote) || !potentialFifthQuote.IsSingleQuote())
2✔
560
                //Four in total, so we bail out here.
561
                break;
1✔
562

563
            //We have a 5th. Consume it.
564
            reader.Read();
1✔
565
            //And append to output
566
            content.Append('\'');
1✔
567

568
            //Check for sixth
569
            if (!reader.TryPeek(out var potentialSixthQuote) || !potentialSixthQuote.IsSingleQuote())
1!
570
                //Five in total, so we bail out here.
571
                break;
×
572

573
            //We have a sixth. This is a syntax error.
574
            throw new TripleQuoteInTomlMultilineLiteralException(_lineNumber);
1✔
575
        }
576

577
        return new TomlString(content.ToString());
5✔
578
    }
5✔
579

580
    private TomlValue ReadMultiLineBasicString(TomletStringReader reader)
581
    {
9✔
582
        var content = new StringBuilder();
9✔
583

584
        var escapeMode = false;
9✔
585
        var fourDigitUnicodeMode = false;
9✔
586
        var eightDigitUnicodeMode = false;
9✔
587

588
        var unicodeStringBuilder = new StringBuilder();
9✔
589

590
        //Leading newlines are ignored
591
        _lineNumber += reader.SkipAnyNewline();
9✔
592

593
        while (reader.TryPeek(out _))
358✔
594
        {
358✔
595
            var nextChar = reader.Read();
358✔
596
            nextChar.EnsureLegalChar(_lineNumber);
358✔
597

598
            if (nextChar == '\\' && !escapeMode)
358✔
599
            {
12✔
600
                escapeMode = true;
12✔
601
                continue; //Don't append
12✔
602
            }
603

604
            if (escapeMode)
346✔
605
            {
12✔
606
                escapeMode = false;
12✔
607
                
608
                //If the escaped char is whitespace, and there's no other non-whitespace on this line, we skip all whitespace until the next non-whitespace char.
609
                if (nextChar.IsWhitespace() || nextChar.IsNewline())
12!
610
                {
6✔
611
                    //Check that everything else on this line is whitespace
612
                    var backtrack = 0;
6✔
613
                    var skipWhitespace = false;
6✔
614
                    while (reader.TryPeek(out var nextCharOnLine))
6✔
615
                    {
6✔
616
                        backtrack++;
6✔
617
                        reader.Read();
6✔
618

619
                        if (nextCharOnLine.IsNewline())
6!
620
                        {
6✔
621
                            skipWhitespace = true;
6✔
622
                            break;
6✔
623
                        }
624

NEW
625
                        if (!nextCharOnLine.IsWhitespace())
×
NEW
626
                        {
×
627
                            //Backslash-whitespace but then non-whitespace char before newline, this is invalid, we can break here and let HandleEscapedChar deal with it
NEW
628
                            break;
×
629
                        }
NEW
630
                    }
×
631
                    
632
                    //Return back to where we were
633
                    reader.Backtrack(backtrack);
6✔
634

635
                    if (skipWhitespace)
6!
636
                    {
6✔
637
                        _lineNumber += reader.SkipAnyNewlineOrWhitespace();
6✔
638
                        continue; //The main while loop
6✔
639
                    }
640
                    
641
                    //Otherwise we drop down to HandleEscapedChar, which will throw due to the whitespace
NEW
642
                }
×
643
                
644
                var toAppend = HandleEscapedChar(nextChar, out fourDigitUnicodeMode, out eightDigitUnicodeMode, true);
6✔
645

646
                if (toAppend.HasValue)
6!
647
                    content.Append(toAppend.Value);
6✔
UNCOV
648
                else if (nextChar.IsNewline())
×
UNCOV
649
                {
×
650
                    //Ensure we've fully consumed the newline
UNCOV
651
                    if (nextChar == '\r' && !reader.ExpectAndConsume('\n'))
×
652
                        throw new Exception($"Found a CR without an LF on line {_lineNumber}");
×
653

654
                    //Increment line number
UNCOV
655
                    _lineNumber++;
×
656

657
                    //Escaped newline indicates we skip this newline and any whitespace at the start of the next line
NEW
658
                    _lineNumber += reader.SkipAnyNewlineOrWhitespace();
×
UNCOV
659
                }
×
660

661
                continue;
6✔
662
            }
663

664
            if (fourDigitUnicodeMode || eightDigitUnicodeMode)
334!
665
            {
×
666
                //Handle \u1234 and \U12345678
667
                unicodeStringBuilder.Append((char) nextChar);
×
668

669
                if (fourDigitUnicodeMode && unicodeStringBuilder.Length == 4 || eightDigitUnicodeMode && unicodeStringBuilder.Length == 8)
×
670
                {
×
671
                    var unicodeString = unicodeStringBuilder.ToString();
×
672

673
                    content.Append(DecipherUnicodeEscapeSequence(unicodeString, fourDigitUnicodeMode));
×
674

675
                    fourDigitUnicodeMode = false;
×
676
                    eightDigitUnicodeMode = false;
×
677
                    unicodeStringBuilder = new StringBuilder();
×
678
                }
×
679

680
                continue;
×
681
            }
682

683
            if (!nextChar.IsDoubleQuote())
334✔
684
            {
315✔
685
                if (nextChar == '\n')
315✔
686
                    _lineNumber++;
1✔
687

688
                content.Append((char) nextChar);
315✔
689
                continue;
315✔
690
            }
691

692
            //Like above, check for up to 6 quotes.
693

694
            //We have a double quote.
695
            //Is it alone? if so, just continue.
696
            if (!reader.TryPeek(out var potentialSecondQuote) || !potentialSecondQuote.IsDoubleQuote())
19!
697
            {
3✔
698
                content.Append('"');
3✔
699
                continue;
3✔
700
            }
701

702
            //We have two quotes in a row. Consume the second one
703
            reader.Read();
16✔
704

705
            //Do we have three?
706
            if (!reader.TryPeek(out var potentialThirdQuote) || !potentialThirdQuote.IsDoubleQuote())
16!
707
            {
7✔
708
                content.Append('"');
7✔
709
                content.Append('"');
7✔
710
                continue;
7✔
711
            }
712

713
            //Ok we have at least three quotes. Consume the third.
714
            reader.Read();
9✔
715

716
            if (!reader.TryPeek(out var afterThirdQuote) || !afterThirdQuote.IsDoubleQuote())
9✔
717
                //And ONLY three quotes. End of literal.
718
                break;
7✔
719

720
            //Like above, just going to bruteforce this out instead of writing a loop.
721
            //Consume the fourth.
722
            reader.Read();
2✔
723
            //And we have to append one double quote to our string.
724
            content.Append('"');
2✔
725

726
            //Check for a 5th.
727
            if (!reader.TryPeek(out var potentialFifthQuote) || !potentialFifthQuote.IsDoubleQuote())
2✔
728
                //Four in total, so we bail out here.
729
                break;
1✔
730

731
            //We have a 5th. Consume it.
732
            reader.Read();
1✔
733
            //And append to output
734
            content.Append('"');
1✔
735

736
            //Check for sixth
737
            if (!reader.TryPeek(out var potentialSixthQuote) || !potentialSixthQuote.IsDoubleQuote())
1!
738
                //Five in total, so we bail out here.
739
                break;
×
740

741
            //We have a sixth. This is a syntax error.
742
            throw new TripleQuoteInTomlMultilineSimpleStringException(_lineNumber);
1✔
743
        }
744

745
        return new TomlString(content.ToString());
8✔
746
    }
8✔
747

748
    private TomlArray ReadArray(TomletStringReader reader)
749
    {
32✔
750
        //Consume the opening bracket
751
        if (!reader.ExpectAndConsume('['))
32!
752
            throw new ArgumentException("Internal Tomlet Bug: ReadArray called and first char is not a [");
×
753

754
        //Move to the first value
755
        _lineNumber += reader.SkipAnyCommentNewlineWhitespaceEtc();
32✔
756

757
        var result = new TomlArray();
32✔
758

759
        while (reader.TryPeek(out _))
95✔
760
        {
95✔
761
            //Skip any empty lines
762
            _lineNumber += reader.SkipAnyCommentNewlineWhitespaceEtc();
95✔
763

764
            if (!reader.TryPeek(out var nextChar))
95!
765
                throw new TomlEndOfFileException(_lineNumber);
×
766

767
            //Check for end of array here (helps with trailing commas, which are legal)
768
            if (nextChar.IsEndOfArrayChar())
95✔
769
                break;
8✔
770

771
            //Read a value
772
            result.ArrayValues.Add(ReadValue(reader));
87✔
773

774
            //Skip any whitespace or newlines, NOT comments - that would be a syntax error
775
            _lineNumber += reader.SkipAnyNewlineOrWhitespace();
87✔
776

777
            if (!reader.TryPeek(out var postValueChar))
87!
778
                throw new TomlEndOfFileException(_lineNumber);
×
779

780
            if (postValueChar.IsEndOfArrayChar())
87✔
781
                break; //end of array
23✔
782

783
            if (!postValueChar.IsComma())
64✔
784
                throw new TomlArraySyntaxException(_lineNumber, (char) postValueChar);
1✔
785

786
            reader.ExpectAndConsume(','); //We've already verified we have one.
63✔
787
        }
63✔
788

789
        reader.ExpectAndConsume(']');
31✔
790

791
        return result;
31✔
792
    }
31✔
793

794
    private TomlTable ReadInlineTable(TomletStringReader reader)
795
    {
29✔
796
        //Consume the opening brace
797
        if (!reader.ExpectAndConsume('{'))
29!
798
            throw new ArgumentException("Internal Tomlet Bug: ReadInlineTable called and first char is not a {");
×
799

800
        //Move to the first key
801
        _lineNumber += reader.SkipAnyCommentNewlineWhitespaceEtc();
29✔
802

803
        var result = new TomlTable {Defined = true};
29✔
804

805
        while (reader.TryPeek(out _))
37✔
806
        {
37✔
807
            //Skip any whitespace. Do not skip comments or newlines, those aren't allowed. 
808
            reader.SkipWhitespace();
37✔
809

810
            if (!reader.TryPeek(out var nextChar))
37!
811
                throw new TomlEndOfFileException(_lineNumber);
×
812

813
            //Note that this is only needed when we first enter the loop, in case of an empty inline table
814
            if (nextChar.IsEndOfInlineObjectChar())
37✔
815
                break;
1✔
816

817
            //Newlines are not permitted
818
            if (nextChar.IsNewline())
36✔
819
                throw new NewLineInTomlInlineTableException(_lineNumber);
1✔
820

821
            //Note that unlike in the above case, we do not check for the end of the value here. Trailing commas aren't permitted
822
            //and so all cases where the table ends should be handled at the end of this look
823
            try
824
            {
35✔
825
                //Read a key-value pair
826
                ReadKeyValuePair(reader, out var key, out var value);
35✔
827
                //Insert into the table
828
                result.ParserPutValue(key, value, _lineNumber);
34✔
829
            }
34✔
830
            catch (TomlException ex) when (ex is TomlMissingEqualsException or NoTomlKeyException or TomlWhitespaceInKeyException)
1✔
831
            {
1✔
832
                //Wrap missing keys or equals signs in a parent exception.
833
                throw new InvalidTomlInlineTableException(_lineNumber, ex);
1✔
834
            }
835

836
            if (!reader.TryPeek(out var postValueChar))
34!
837
                throw new TomlEndOfFileException(_lineNumber);
×
838

839
            if (reader.ExpectAndConsume(','))
34✔
840
                continue; //Comma, we have more.
8✔
841

842
            //Non-comma, consume any whitespace
843
            reader.SkipWhitespace();
26✔
844

845
            if (!reader.TryPeek(out postValueChar))
26!
846
                throw new TomlEndOfFileException(_lineNumber);
×
847

848
            if (postValueChar.IsEndOfInlineObjectChar())
26✔
849
                break; //end of table
25✔
850

851
            throw new TomlInlineTableSeparatorException(_lineNumber, (char) postValueChar);
1✔
852
        }
853

854
        reader.ExpectAndConsume('}');
26✔
855

856
        result.Locked = true; //Defined inline, cannot be later modified
26✔
857
        return result;
26✔
858
    }
26✔
859

860
    private TomlTable ReadTableStatement(TomletStringReader reader, TomlDocument document)
861
    {
35✔
862
        //Table name
863
        var currentTableKey = reader.ReadWhile(c => !c.IsEndOfArrayChar() && !c.IsNewline());
425✔
864

865
        var parent = (TomlTable) document;
35✔
866
        var relativeKey = currentTableKey;
35✔
867
        FindParentAndRelativeKey(ref parent, ref relativeKey);
35✔
868

869
        TomlTable table;
870
        try
871
        {
35✔
872
            if (parent.ContainsKey(relativeKey))
35✔
873
            {
4✔
874
                try
875
                {
4✔
876
                    table = (TomlTable) parent.GetValue(relativeKey);
4✔
877

878
                    //The cast succeeded - we are defining an existing table
879
                    if (table.Defined)
2✔
880
                    {
1✔
881
                        // The table was not one created automatically
882
                        throw new TomlTableRedefinitionException(_lineNumber, currentTableKey);
1✔
883
                    }
884
                }
1✔
885
                catch (InvalidCastException)
2✔
886
                {
2✔
887
                    //The cast failed, we are re-defining a non-table.
888
                    throw new TomlKeyRedefinitionException(_lineNumber, currentTableKey);
2✔
889
                }
890
            }
1✔
891
            else
892
            {
30✔
893
                table = new TomlTable {Defined = true};
30✔
894
                parent.ParserPutValue(relativeKey, table, _lineNumber);
30✔
895
            }
30✔
896
        }
31✔
897
        catch (TomlContainsDottedKeyNonTableException e)
1✔
898
        {
1✔
899
            //Re-throw with correct line number and exception type.
900
            //To be clear - here we're re-defining a NON-TABLE key as a table, so this is a dotted key exception
901
            //while the one above is a TableRedefinition exception because it's re-defining a key which is already a table.
902
            throw new TomlDottedKeyParserException(_lineNumber, e.Key);
1✔
903
        }
904

905
        if (!reader.TryPeek(out _))
31!
906
            throw new TomlEndOfFileException(_lineNumber);
×
907

908
        if (!reader.ExpectAndConsume(']'))
31✔
909
            throw new UnterminatedTomlTableNameException(_lineNumber);
1✔
910

911
        reader.SkipWhitespace();
30✔
912
        table.Comments.InlineComment = ReadAnyPotentialInlineComment(reader);
30✔
913
        reader.SkipPotentialCarriageReturn();
30✔
914

915
        if (!reader.TryPeek(out var shouldBeNewline))
30!
916
            throw new TomlEndOfFileException(_lineNumber);
×
917

918
        if (!shouldBeNewline.IsNewline())
30!
919
            throw new TomlMissingNewlineException(_lineNumber, (char) shouldBeNewline);
×
920

921
        _currentTable = table;
30✔
922

923
        //Save table names
924
        _tableNames = currentTableKey.Split('.');
30✔
925

926
        return table;
30✔
927
    }
30✔
928

929
    private TomlArray ReadTableArrayStatement(TomletStringReader reader, TomlDocument document)
930
    {
24✔
931
        //Consume the (second) opening bracket
932
        if (!reader.ExpectAndConsume('['))
24!
933
            throw new ArgumentException("Internal Tomlet Bug: ReadTableArrayStatement called and first char is not a [");
×
934

935
        //Array
936
        var arrayName = reader.ReadWhile(c => !c.IsEndOfArrayChar() && !c.IsNewline());
301✔
937

938
        if (!reader.ExpectAndConsume(']') || !reader.ExpectAndConsume(']'))
24✔
939
            throw new UnterminatedTomlTableArrayException(_lineNumber);
1✔
940

941
        TomlTable parentTable = document;
23✔
942
        var relativeKey = arrayName;
23✔
943
        FindParentAndRelativeKey(ref parentTable, ref relativeKey);
23✔
944

945
        if (parentTable == document)
23✔
946
        {
15✔
947
            if (relativeKey.Contains('.'))
15✔
948
                throw new MissingIntermediateInTomlTableArraySpecException(_lineNumber, relativeKey);
1✔
949
        }
14✔
950

951
        //Find existing array or make new one
952
        TomlArray array;
953
        if (parentTable.ContainsKey(relativeKey))
22✔
954
        {
10✔
955
            var value = parentTable.GetValue(relativeKey);
10✔
956
            if (value is TomlArray arr)
10✔
957
                array = arr;
9✔
958
            else
959
                throw new TomlTableArrayAlreadyExistsAsNonArrayException(_lineNumber, arrayName);
1✔
960

961
            if (!array.IsLockedToBeTableArray)
9✔
962
            {
1✔
963
                throw new TomlNonTableArrayUsedAsTableArrayException(_lineNumber, arrayName);
1✔
964
            }
965
        }
8✔
966
        else
967
        {
12✔
968
            array = new TomlArray {IsLockedToBeTableArray = true};
12✔
969
            //Insert into parent table
970
            parentTable.ParserPutValue(relativeKey, array, _lineNumber);
12✔
971
        }
12✔
972

973
        // Create new table and add it to the array
974
        _currentTable = new TomlTable {Defined = true};
20✔
975
        array.ArrayValues.Add(_currentTable);
20✔
976

977
        //Save table names
978
        _tableNames = arrayName.Split('.');
20✔
979
            
980
        return array;
20✔
981
    }
20✔
982

983
    private void FindParentAndRelativeKey(ref TomlTable parent, ref string relativeName)
984
    {
58✔
985
        for (var index = 0; index < _tableNames.Length; index++)
154✔
986
        {
38✔
987
            var rootTableName = _tableNames[index];
38✔
988
            if (!relativeName.StartsWith(rootTableName + "."))
38✔
989
            {
19✔
990
                break;
19✔
991
            }
992

993
            var value = parent.GetValue(rootTableName);
19✔
994
            if (value is TomlTable subTable)
19✔
995
            {
4✔
996
                parent = subTable;
4✔
997
            }
4✔
998
            else if (value is TomlArray array)
15!
999
            {
15✔
1000
                parent = (TomlTable) array.Last();
15✔
1001
            }
15✔
1002
            else
1003
            {
×
1004
                // Note: Expects either TomlArray or TomlTable
1005
                throw new TomlTypeMismatchException(typeof(TomlArray), value.GetType(), typeof(TomlArray));
×
1006
            }
1007

1008
            relativeName = relativeName.Substring(rootTableName.Length + 1);
19✔
1009
        }
19✔
1010
    }
58✔
1011

1012
    private string? ReadAnyPotentialInlineComment(TomletStringReader reader)
1013
    {
438✔
1014
        if (!reader.ExpectAndConsume('#'))
438✔
1015
            return null; //No comment
411✔
1016
            
1017
        var ret = reader.ReadWhile(c => !c.IsNewline()).Trim();
615✔
1018

1019
        if (ret.Length < 1) 
27✔
1020
            return null;
2✔
1021
            
1022
        if(ret[0] == ' ')
25!
1023
            ret = ret.Substring(1);
×
1024
            
1025
        foreach (var i in ret.Select(c => (int) c)) 
1,698✔
1026
            i.EnsureLegalChar(_lineNumber);
541✔
1027

1028
        return ret;
25✔
1029

1030
    }
438✔
1031
        
1032
    private string? ReadAnyPotentialMultilineComment(TomletStringReader reader)
1033
    {
379✔
1034
        var ret = new StringBuilder();
379✔
1035
        while (reader.ExpectAndConsume('#'))
396✔
1036
        {
17✔
1037
            var line = reader.ReadWhile(c => !c.IsNewline());
450✔
1038
                
1039
            if(line.Length > 0 && line[0] == ' ')
17✔
1040
                line = line.Substring(1);
15✔
1041
                
1042
            foreach (var i in line.Select(c => (int) c)) 
1,254✔
1043
                i.EnsureLegalChar(_lineNumber);
401✔
1044
                
1045
            ret.Append(line);
17✔
1046

1047
            _lineNumber += reader.SkipAnyNewlineOrWhitespace();
17✔
1048
        }
17✔
1049

1050
        if (ret.Length == 0)
379✔
1051
            return null;
364✔
1052

1053
        return ret.ToString();
15✔
1054
    }
379✔
1055
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc