Skip to content

Commit e404c6e

Browse files
authored
Fix: Handle Escaped Escape Character (#494)
* fix: Handle Escaped Escape Character correctly when `SmartSettings.Parser.ConvertCharacterStringLiterals` is `false` * chore: Refactor `ExcapedLiteral` class Resolves #493
1 parent 3c7d3ec commit e404c6e

File tree

8 files changed

+83
-37
lines changed

8 files changed

+83
-37
lines changed

src/SmartFormat.Tests/Core/EscapedLiteralTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public void UnEscapeCharLiterals_General_Test(string input, string expected, boo
3535
{
3636
try
3737
{
38-
EscapedLiteral.UnEscapeCharLiterals('\\', input.AsSpan(0, input.Length), false, resultBuffer);
38+
EscapedLiteral.UnEscapeCharLiterals('\\', input.AsSpan(0, input.Length), false, true, resultBuffer);
3939
Assert.Fail("Failure expected.");
4040
}
4141
catch (Exception e)
@@ -45,7 +45,7 @@ public void UnEscapeCharLiterals_General_Test(string input, string expected, boo
4545
}
4646
else
4747
{
48-
var result = EscapedLiteral.UnEscapeCharLiterals('\\', input.AsSpan(0, input.Length), false, resultBuffer);
48+
var result = EscapedLiteral.UnEscapeCharLiterals('\\', input.AsSpan(0, input.Length), false, true, resultBuffer);
4949
Assert.That(result.ToString(), Is.EqualTo(expected));
5050
}
5151
}
@@ -59,7 +59,7 @@ public void UnEscapeCharLiterals_FormatterOption_Test(string input, string expec
5959
{
6060
try
6161
{
62-
EscapedLiteral.UnEscapeCharLiterals('\\', input.AsSpan(0, input.Length), true, resultBuffer);
62+
EscapedLiteral.UnEscapeCharLiterals('\\', input.AsSpan(0, input.Length), true, true, resultBuffer);
6363
Assert.Fail("Failure expected.");
6464
}
6565
catch (Exception e)
@@ -69,7 +69,7 @@ public void UnEscapeCharLiterals_FormatterOption_Test(string input, string expec
6969
}
7070
else
7171
{
72-
var result = EscapedLiteral.UnEscapeCharLiterals('\\', input.AsSpan(0, input.Length), true, resultBuffer);
72+
var result = EscapedLiteral.UnEscapeCharLiterals('\\', input.AsSpan(0, input.Length), true, true, resultBuffer);
7373
Assert.That(result.ToString(), Is.EqualTo(expected));
7474
}
7575
}
@@ -100,7 +100,7 @@ public void UnEscape_Escaped_Special_Characters(string pattern)
100100
{
101101
var resultBuffer = new Span<char>(new char[pattern.Length]);
102102
var optionsEscaped = new string(EscapedLiteral.EscapeCharLiterals('\\', pattern, 0, pattern.Length, true).ToArray());
103-
Assert.That(EscapedLiteral.UnEscapeCharLiterals('\\', optionsEscaped.AsSpan(0, optionsEscaped.Length), true, resultBuffer).ToString(), Is.EqualTo(pattern));
103+
Assert.That(EscapedLiteral.UnEscapeCharLiterals('\\', optionsEscaped.AsSpan(0, optionsEscaped.Length), true, true, resultBuffer).ToString(), Is.EqualTo(pattern));
104104
}
105105

106106
[Test]
@@ -109,6 +109,6 @@ public void UnEscape_With_StartIndex_not_zero()
109109
var full = "abc(de";
110110
var startIndex = 3;
111111
var resultBuffer = new Span<char>(new char[full.Length]);
112-
Assert.That(EscapedLiteral.UnEscapeCharLiterals('\\', full.AsSpan(startIndex, full.Length - startIndex), true, resultBuffer).ToString(), Is.EqualTo("(de"));
112+
Assert.That(EscapedLiteral.UnEscapeCharLiterals('\\', full.AsSpan(startIndex, full.Length - startIndex), true, true, resultBuffer).ToString(), Is.EqualTo("(de"));
113113
}
114114
}

src/SmartFormat.Tests/Core/LiteralTextTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,4 @@ public void IllegalEscapeSequenceThrowsException()
9393
smart.Format(@"Illegal escape sequence starts at end of line = \");
9494
},Throws.ArgumentException.And.Message.Contains("escape sequence"));
9595
}
96-
}
96+
}

src/SmartFormat.Tests/Core/ParserTests.cs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -462,11 +462,30 @@ public void Missing_Curly_Brace_Should_Throw()
462462
.Contains(new Parser.ParsingErrorText()[Parser.ParsingError.MissingClosingBrace]));
463463
}
464464

465+
[TestCase(true, "{}")]
466+
[TestCase(false, "{}")]
467+
public void Literal_Escaping_In_Literal(bool convert, string expected)
468+
{
469+
var parser = GetRegularParser(new SmartSettings { Parser = new ParserSettings { ConvertCharacterStringLiterals = convert } });
470+
Assert.That(parser.ParseFormat(@"\{\}").ToString(), Is.EqualTo(expected));
471+
}
472+
465473
[Test]
466-
public void Literal_Escaping_In_Literal()
474+
public void Escaping_TheEscapingCharacter_ShouldWork()
467475
{
468-
var parser = GetRegularParser(new SmartSettings {StringFormatCompatibility = false});
469-
Assert.That(parser.ParseFormat("\\{\\}").ToString(), Is.EqualTo("{}"));
476+
// Issue https://github.com/axuno/SmartFormat/issues/493 fixed:
477+
// Escaping the escaping character, i.e. "\\", now works correctly
478+
// when ConvertCharacterStringLiterals is false.
479+
var parser = GetRegularParser(new SmartSettings
480+
{ Parser = new ParserSettings { ConvertCharacterStringLiterals = false } });
481+
482+
// Edge case where an escaping character is
483+
// immediately followed by another escaping character:
484+
const string input = @"\\\\aaa\\\{\}bbb ccc\x\{\}ddd\\\\";
485+
var format = parser.ParseFormat(input);
486+
var result = format.ToString();
487+
488+
Assert.That(result, Is.EqualTo(@"\\aaa\{}bbb ccc\x{}ddd\\"));
470489
}
471490

472491
[Test]
@@ -476,7 +495,6 @@ public void StringFormat_Escaping_In_Literal()
476495
Assert.That(parser.ParseFormat("{{}}").ToString(), Is.EqualTo("{}"));
477496
}
478497

479-
480498
[Test]
481499
public void Nested_format_with_literal_escaping()
482500
{

src/SmartFormat/Core/Parsing/EscapedLiteral.cs

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,33 @@
1010
namespace SmartFormat.Core.Parsing;
1111

1212
/// <summary>
13-
/// Handles escaped literals, like \\ or \n
13+
/// Handles escaped literals, like \\ \{ \n \u2022
1414
/// </summary>
1515
public static class EscapedLiteral
1616
{
1717
private static readonly Dictionary<char, char> GeneralLookupTable = new() {
18-
// General
18+
// Escaping the escape character itself
1919
{'\\', '\\'},
20+
// Placeholder related
2021
{'{', '{'},
2122
{'}', '}'},
23+
// escaped colons can be used anywhere in the format string
24+
{':', ':'}
25+
};
26+
27+
private static readonly Dictionary<char, char> CharLiteralLookupTable = new() {
2228
{'0', '\0'},
2329
{'a', '\a'},
2430
{'b', '\b'},
2531
{'f', '\f'},
2632
{'n', '\n'},
2733
{'r', '\r'},
2834
{'t', '\t'},
29-
{'v', '\v'},
30-
{':', ':'} // escaped colons can be used anywhere in the format string
35+
{'v', '\v'}
3136
};
3237

3338
private static readonly Dictionary<char, char> FormatterOptionsLookupTable = new() {
34-
// Smart.Format characters used in formatter options
39+
// characters used in formatter options
3540
{'(', '('},
3641
{')', ')'}
3742
};
@@ -42,13 +47,13 @@ public static class EscapedLiteral
4247
/// <param name="input">The input character.</param>
4348
/// <param name="result">The matching character.</param>
4449
/// <param name="includeFormatterOptionChars">If <see langword="true"/>, (){}: will be escaped, else not.</param>
50+
/// <param name="includeCharacterLiterals">If <see langword="true"/>, \n, \t etc. will be escaped, else not.</param>
4551
/// <returns><see langword="true"/>, if a matching character was found.</returns>
46-
public static bool TryGetChar(char input, out char result, bool includeFormatterOptionChars)
52+
public static bool TryGetChar(char input, out char result, bool includeFormatterOptionChars, bool includeCharacterLiterals = true)
4753
{
48-
return includeFormatterOptionChars
49-
? GeneralLookupTable.TryGetValue(input, out result) ||
50-
FormatterOptionsLookupTable.TryGetValue(input, out result)
51-
: GeneralLookupTable.TryGetValue(input, out result);
54+
return GeneralLookupTable.TryGetValue(input, out result) ||
55+
(includeCharacterLiterals && CharLiteralLookupTable.TryGetValue(input, out result)) ||
56+
(includeFormatterOptionChars && FormatterOptionsLookupTable.TryGetValue(input, out result));
5257
}
5358

5459
private static char GetUnicode(ReadOnlySpan<char> input, int startIndex)
@@ -74,9 +79,10 @@ private static char GetUnicode(ReadOnlySpan<char> input, int startIndex)
7479
/// <param name="escapingSequenceStart"></param>
7580
/// <param name="input"></param>
7681
/// <param name="includeFormatterOptionChars">If <see langword="true"/>, (){}: will be escaped, else not.</param>
82+
/// <param name="includeCharacterLiterals">If <see langword="true"/>, \n \t etc. will be escaped, else not.</param>
7783
/// <param name="resultBuffer">The buffer to fill. It's enough to have a buffer with the same size as the input length.</param>
7884
/// <returns>The input having escaped characters replaced with their real value.</returns>
79-
public static ReadOnlySpan<char> UnEscapeCharLiterals(char escapingSequenceStart, ReadOnlySpan<char> input, bool includeFormatterOptionChars, Span<char> resultBuffer)
85+
public static ReadOnlySpan<char> UnEscapeCharLiterals(char escapingSequenceStart, ReadOnlySpan<char> input, bool includeFormatterOptionChars, bool includeCharacterLiterals, Span<char> resultBuffer)
8086
{
8187
var max = input.Length;
8288
var resultIndex = 0;
@@ -104,7 +110,7 @@ public static ReadOnlySpan<char> UnEscapeCharLiterals(char escapingSequenceStart
104110
resultBuffer[resultIndex++] = GetUnicode(input, nextInputIndex + 1);
105111
inputIndex += 6; // move to last unicode character
106112
}
107-
else if (TryGetChar(input[nextInputIndex], out var realChar, includeFormatterOptionChars))
113+
else if (TryGetChar(input[nextInputIndex], out var realChar, includeFormatterOptionChars, includeCharacterLiterals))
108114
{
109115
resultBuffer[resultIndex++] = realChar;
110116
inputIndex += 2;
@@ -132,8 +138,9 @@ public static ReadOnlySpan<char> UnEscapeCharLiterals(char escapingSequenceStart
132138
/// <param name="startIndex"></param>
133139
/// <param name="length"></param>
134140
/// <param name="includeFormatterOptionChars"><see langword="true"/>, if characters for formatter options should be included. Default is <see langword="false"/>.</param>
141+
/// <param name="includeCharLiterals"><see langword="true"/>, if character literals should be included. Default is <see langword="true"/>.</param>
135142
/// <returns>Returns the escaped characters.</returns>
136-
public static IEnumerable<char> EscapeCharLiterals(char escapeSequenceStart, string input, int startIndex, int length, bool includeFormatterOptionChars)
143+
public static IEnumerable<char> EscapeCharLiterals(char escapeSequenceStart, string input, int startIndex, int length, bool includeFormatterOptionChars, bool includeCharLiterals = true)
137144
{
138145
var max = startIndex + length;
139146
for (var index = startIndex; index < max; index++)
@@ -146,6 +153,13 @@ public static IEnumerable<char> EscapeCharLiterals(char escapeSequenceStart, str
146153
continue;
147154
}
148155

156+
if (CharLiteralLookupTable.ContainsValue(c))
157+
{
158+
yield return escapeSequenceStart;
159+
yield return CharLiteralLookupTable.First(kv => kv.Value == c).Key;
160+
continue;
161+
}
162+
149163
if (includeFormatterOptionChars && FormatterOptionsLookupTable.ContainsValue(c))
150164
{
151165
yield return escapeSequenceStart;

src/SmartFormat/Core/Parsing/Format.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// Licensed under the MIT license.
44

55
using System;
6-
using System.Buffers;
76
using System.Collections;
87
using System.Collections.Generic;
98
using SmartFormat.Core.Settings;
@@ -349,7 +348,7 @@ public string GetLiteralText()
349348
return _literalTextCache;
350349
}
351350

352-
/// <summary>
351+
/// <summary>
353352
/// Reconstructs the format string, but doesn't include escaped chars
354353
/// and tries to reconstruct placeholders.
355354
/// </summary>

src/SmartFormat/Core/Parsing/LiteralText.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,26 @@ public override ReadOnlySpan<char> AsSpan()
7272
{
7373
if (Length == 0) return ReadOnlySpan<char>.Empty;
7474

75-
// The buffer is only for 1 character - each escaped char goes into its own LiteralText
76-
return SmartSettings.Parser.ConvertCharacterStringLiterals &&
77-
BaseString.AsSpan(StartIndex)[0] == SmartSettings.Parser.CharLiteralEscapeChar
78-
? EscapedLiteral.UnEscapeCharLiterals(SmartSettings.Parser.CharLiteralEscapeChar,
79-
BaseString.AsSpan(StartIndex, Length),
80-
false, new char[1])
81-
: BaseString.AsSpan(StartIndex, Length);
75+
var span = BaseString.AsSpan(StartIndex, Length);
76+
77+
return SmartSettings.Parser.ConvertCharacterStringLiterals switch
78+
{
79+
// Convert escaped literals, e.g. \n, \t, or \u2022
80+
// Each escaped char goes into its own LiteralText object.
81+
true when span[0] == SmartSettings.Parser.CharLiteralEscapeChar
82+
=> EscapedLiteral.UnEscapeCharLiterals(
83+
SmartSettings.Parser.CharLiteralEscapeChar,
84+
span,
85+
false,
86+
true,
87+
// Each escaped literal has just 1 character in size.
88+
new char[1]),
89+
// Special case: Escaped escape char, i.e. "\\", when ConvertCharacterStringLiterals is false.
90+
false when span.Length == 2 && span[0] == span[1] && span[0] == SmartSettings.Parser.CharLiteralEscapeChar
91+
=> span.Slice(1), // simplify instead of calling UnEscapeCharLiterals
92+
// No conversion
93+
_ => span
94+
};
8295
}
8396

8497
/// <summary>
@@ -90,4 +103,4 @@ public override void Clear()
90103
base.Clear();
91104
_toStringCache = null;
92105
}
93-
}
106+
}

src/SmartFormat/Core/Parsing/Parser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,7 @@ private void ParseFormatOptions(ParserState state)
634634
// Skip escaped terminating characters
635635
if (state.InputFormat[state.Index.Current] == _parserSettings.CharLiteralEscapeChar &&
636636
(_formatOptionsTerminatorChars.Contains(nextChar) ||
637-
EscapedLiteral.TryGetChar(nextChar, out _, true)))
637+
EscapedLiteral.TryGetChar(nextChar, out _, true, false)))
638638
{
639639
state.Index.Current = state.Index.SafeAdd(state.Index.Current, 1);
640640
if (_formatOptionsTerminatorChars.Contains(

src/SmartFormat/Core/Parsing/Placeholder.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,16 @@ public string FormatterOptions
197197
try
198198
{
199199
_formatterOptionsCache = EscapedLiteral
200-
.UnEscapeCharLiterals(SmartSettings.Parser.CharLiteralEscapeChar, BaseString.AsSpan(FormatterOptionsStartIndex, FormatterOptionsLength), true, resultBuffer).ToString();
200+
.UnEscapeCharLiterals(SmartSettings.Parser.CharLiteralEscapeChar,
201+
BaseString.AsSpan(FormatterOptionsStartIndex, FormatterOptionsLength), true,
202+
SmartSettings.Parser.ConvertCharacterStringLiterals, resultBuffer).ToString();
201203

202204
}
203205
finally
204206
{
205207
pool.Return(resultBuffer);
206208
}
207-
209+
208210
return _formatterOptionsCache;
209211
}
210212
}

0 commit comments

Comments
 (0)