Skip to content

Commit 8a598b1

Browse files
committed
feat: Filter Selector chars by allowlist or blocklilst
1 parent 6b1dbec commit 8a598b1

7 files changed

Lines changed: 550 additions & 107 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System;
2+
using System.Linq;
3+
using NUnit.Framework;
4+
using SmartFormat.Core.Parsing;
5+
6+
namespace SmartFormat.Tests.Core;
7+
8+
[TestFixture]
9+
internal class CharSetTests
10+
{
11+
[Test]
12+
public void CharSet_Add_Remove()
13+
{
14+
char[] asciiChars = ['A', 'B', 'C'];
15+
char[] nonAsciiChars = ['Ā', 'Б', '中'];
16+
var charSet = new CharSet();
17+
charSet.AddRange(asciiChars.AsEnumerable());
18+
charSet.AddRange(nonAsciiChars.AsSpan());
19+
var countBeforeRemoval = charSet.Count;
20+
var existingRemoved = charSet.Remove('C');
21+
charSet.Remove('中');
22+
// trying to remove a not existing char returns false
23+
var nonExistingRemoved = charSet.Remove('?');
24+
var count = charSet.Count;
25+
26+
Assert.Multiple(() =>
27+
{
28+
Assert.That(countBeforeRemoval, Is.EqualTo(asciiChars.Length + nonAsciiChars.Length));
29+
Assert.That(count, Is.EqualTo(countBeforeRemoval - 2));
30+
Assert.That(existingRemoved, Is.True);
31+
Assert.That(nonExistingRemoved, Is.False);
32+
});
33+
}
34+
35+
[Test]
36+
public void CharSet_CreateFromSpan_GetCharacters_Contains()
37+
{
38+
char[] asciiAndNonAscii = ['\0', 'A', 'B', 'C', 'Ā', 'Б', '中'];
39+
var charSet = new CharSet(asciiAndNonAscii.AsSpan());
40+
41+
Assert.Multiple(() =>
42+
{
43+
Assert.That(charSet, Has.Count.EqualTo(7));
44+
Assert.That(charSet.Contains('A'), Is.True); // ASCII
45+
Assert.That(charSet.Contains('\0'), Is.True); // control character
46+
Assert.That(charSet.Contains('中'), Is.True); // non-ASCII
47+
Assert.That(charSet.Contains('?'), Is.False);
48+
Assert.That(charSet.GetCharacters(), Is.EquivalentTo(asciiAndNonAscii));
49+
charSet.Clear();
50+
Assert.That(charSet, Has.Count.EqualTo(0));
51+
Assert.That(charSet.GetCharacters(), Is.Empty);
52+
});
53+
}
54+
}

src/SmartFormat.Tests/Core/ParserTests.cs

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ public void Parser_Error_Action_Ignore()
155155
// | Literal | Erroneous | | Okay |
156156
var invalidTemplate = "Hello, I'm {Name from {City} {Street}";
157157

158+
// settings must be set before parser instantiation
158159
var parser = GetRegularParser(new SmartSettings {Parser = new ParserSettings {ErrorAction = ParseErrorAction.Ignore}});
159160
using var parsed = parser.ParseFormat(invalidTemplate);
160161

@@ -177,6 +178,7 @@ public void Parser_Error_Action_Ignore()
177178
[TestCase("Hello, I'm {Name from {City} {Street", false)]
178179
public void Parser_Error_Action_MaintainTokens(string invalidTemplate, bool lastItemIsPlaceholder)
179180
{
181+
// settings must be set before parser instantiation
180182
var parser = GetRegularParser(new SmartSettings {Parser = new ParserSettings {ErrorAction = ParseErrorAction.MaintainTokens}});
181183
using var parsed = parser.ParseFormat(invalidTemplate);
182184

@@ -203,14 +205,21 @@ public void Parser_Error_Action_MaintainTokens(string invalidTemplate, bool last
203205
public void Parser_Error_Action_OutputErrorInResult()
204206
{
205207
// | Literal | Erroneous |
206-
// ▼ Selector must not contain {
207208
var invalidTemplate = "Hello, I'm {Name from {City}";
208-
209-
var parser = GetRegularParser(new SmartSettings {Parser = new ParserSettings {ErrorAction = ParseErrorAction.OutputErrorInResult}});
209+
210+
var parser = GetRegularParser(new SmartSettings
211+
{
212+
Parser = new ParserSettings
213+
{
214+
SelectorCharFilter = FilterType.Allowlist, // default
215+
ErrorAction = ParseErrorAction.OutputErrorInResult
216+
}
217+
});
218+
210219
using var parsed = parser.ParseFormat(invalidTemplate);
211220

212221
Assert.That(parsed.Items, Has.Count.EqualTo(1));
213-
Assert.That(parsed.Items[0].RawText, Does.StartWith("The format string has 1 issue"));
222+
Assert.That(parsed.Items[0].RawText, Does.StartWith("The format string has 3 issues"));
214223
}
215224

216225
[Test]
@@ -414,11 +423,11 @@ public void Parser_NotifyParsingError()
414423
});
415424

416425
formatter.Parser.OnParsingFailure += (o, args) => parsingError = args.Errors;
417-
var res = formatter.Format("{NoName {Other} {Same", default(object)!);
426+
var res = formatter.Format("{NoName {Other} {Same");
418427
Assert.Multiple(() =>
419428
{
420-
Assert.That(parsingError!.Issues, Has.Count.EqualTo(2));
421-
Assert.That(parsingError.Issues[1].Issue, Is.EqualTo(new Parser.ParsingErrorText()[SmartFormat.Core.Parsing.Parser.ParsingError.MissingClosingBrace]));
429+
Assert.That(parsingError!.Issues, Has.Count.EqualTo(3));
430+
Assert.That(parsingError.Issues[2].Issue, Is.EqualTo(new Parser.ParsingErrorText()[Parser.ParsingError.MissingClosingBrace]));
422431
});
423432
}
424433

@@ -459,6 +468,18 @@ public void Escaping_TheEscapingCharacter_ShouldWork()
459468
Assert.That(result, Is.EqualTo(@"\\aaa\{}bbb ccc\x{}ddd\\"));
460469
}
461470

471+
[Test]
472+
public void Parsing_Selector_With_CharFromBlocklist_ShouldThrow()
473+
{
474+
var settings = new SmartSettings { Parser = new ParserSettings { SelectorCharFilter = FilterType.Blocklist } };
475+
var parser = GetRegularParser(settings);
476+
477+
// The newline character is in the default blocklist of disallowed characters
478+
Assert.That(() => parser.ParseFormat("{A\nB}"),
479+
Throws.Exception.InstanceOf<ParsingErrors>().And.Message
480+
.Contains(new Parser.ParsingErrorText()[Parser.ParsingError.InvalidCharactersInSelector]));
481+
}
482+
462483
[Test]
463484
public void StringFormat_Escaping_In_Literal()
464485
{
@@ -536,8 +557,10 @@ public void Parse_Unicode(string formatString, string unicodeLiteral, int itemIn
536557
[TestCase("{%C}", '%')]
537558
public void Selector_With_Custom_Selector_Character(string formatString, char customChar)
538559
{
560+
// settings must be set before parser instantiation
539561
var settings = new SmartSettings();
540-
settings.Parser.AddCustomSelectorChars(new[]{customChar});
562+
settings.Parser.AddCustomSelectorChars([customChar]);
563+
var x = settings.Parser.GetSelectorChars();
541564
var parser = GetRegularParser(settings);
542565
var result = parser.ParseFormat(formatString);
543566

@@ -546,7 +569,7 @@ public void Selector_With_Custom_Selector_Character(string formatString, char cu
546569
Assert.That(placeholder!.Selectors, Has.Count.EqualTo(1));
547570
Assert.Multiple(() =>
548571
{
549-
Assert.That(placeholder!.Selectors, Has.Count.EqualTo(placeholder!.GetSelectors().Count));
572+
Assert.That(placeholder.Selectors, Has.Count.EqualTo(placeholder.GetSelectors().Count));
550573
Assert.That(placeholder.Selectors[0].ToString(), Is.EqualTo(formatString.Substring(1, 2)));
551574
});
552575
}
@@ -555,8 +578,10 @@ public void Selector_With_Custom_Selector_Character(string formatString, char cu
555578
[TestCase("{a°b}", '°')]
556579
public void Selectors_With_Custom_Operator_Character(string formatString, char customChar)
557580
{
558-
var parser = GetRegularParser();
559-
parser.Settings.Parser.AddCustomOperatorChars(new[]{customChar});
581+
// settings must be set before parser instantiation
582+
var settings = new SmartSettings();
583+
settings.Parser.AddCustomOperatorChars([customChar]);
584+
var parser = GetRegularParser(settings);
560585
var result = parser.ParseFormat(formatString);
561586

562587
var placeholder = result.Items[0] as Placeholder;
@@ -583,10 +608,12 @@ public void Selector_WorksWithAllUnicodeChars(string selector)
583608
{
584609
// See https://github.com/axuno/SmartFormat/issues/454
585610

611+
// settings must be set before parser instantiation
612+
var settings = new SmartSettings { Parser = { SelectorCharFilter = FilterType.Blocklist } };
586613
const string expected = "The Value";
587614
// The default formatter with default settings should be able to handle any
588615
// Unicode characters in selectors except the "magic" disallowed ones
589-
var formatter = Smart.CreateDefaultSmartFormat();
616+
var formatter = Smart.CreateDefaultSmartFormat(settings);
590617
// Use the Unicode string as a selector of the placeholder
591618
var template = $"{{{selector}}}";
592619
var result = formatter.Format(template, new Dictionary<string, string> { { selector, expected } });
@@ -647,10 +674,11 @@ public void Selector_With_Nullable_Operator_Character(string formatString)
647674
public void Selector_With_Other_Contiguous_Operator_Characters(string formatString, char customChar)
648675
{
649676
// contiguous operator characters are parsed as "ONE operator string"
650-
651-
var parser = GetRegularParser();
677+
var settings = new SmartSettings();
678+
settings.Parser.AddCustomOperatorChars([customChar]);
679+
var parser = GetRegularParser(settings);
652680
// adding '.' is ignored, as it's a standard operator
653-
parser.Settings.Parser.AddCustomOperatorChars(new[]{customChar});
681+
parser.Settings.Parser.AddCustomOperatorChars([customChar]);
654682
var result = parser.ParseFormat(formatString);
655683

656684
var placeholder = result.Items[0] as Placeholder;
@@ -706,6 +734,12 @@ public void ParseInputAsHtml(string input)
706734
Assert.That(literalText!.RawText, Is.EqualTo(input));
707735
}
708736

737+
#region * Parse HTML input without ParserSetting 'IsHtml'
738+
739+
/// <summary>
740+
/// <see cref="ParserSettings.SelectorCharFilter"/> is <see cref="FilterType.Blocklist"/>:
741+
/// all characters are allowed in selectors
742+
/// </summary>
709743
[TestCase("<script>{Placeholder}</script>", "{Placeholder}")]
710744
[TestCase("<style>{Placeholder}</style>", "{Placeholder}")]
711745
[TestCase("Something <style>h1 { color : #000; }</style>! nice", "{ color : #000; }")]
@@ -715,7 +749,12 @@ public void ParseHtmlInput_Without_ParserSetting_IsHtml(string input, string sel
715749
var parser = GetRegularParser(new SmartSettings
716750
{
717751
StringFormatCompatibility = false,
718-
Parser = new ParserSettings { ErrorAction = ParseErrorAction.ThrowError, ParseInputAsHtml = false }
752+
Parser = new ParserSettings
753+
{
754+
SelectorCharFilter = FilterType.Blocklist,
755+
ErrorAction = ParseErrorAction.ThrowError,
756+
ParseInputAsHtml = false
757+
}
719758
});
720759

721760
var result = parser.ParseFormat(input);
@@ -724,9 +763,45 @@ public void ParseHtmlInput_Without_ParserSetting_IsHtml(string input, string sel
724763
Assert.That(result.Items, Has.Count.EqualTo(3));
725764
Assert.That(((Placeholder) result.Items[1]).RawText, Is.EqualTo(selector));
726765
});
766+
}
727767

768+
/// <summary>
769+
/// <see cref="ParserSettings.SelectorCharFilter"/> is <see cref="FilterType.Allowlist"/>:
770+
/// Predefined set of allowed characters in selectors
771+
/// </summary>
772+
[TestCase("<script>{Placeholder}</script>", false)] // should parse a placeholder
773+
[TestCase("<style>{Placeholder}</style>", false)] // should parse a placeholder
774+
[TestCase("Something <style>h1 { color : #000; }</style>! nice", true)] // illegal selector chars
775+
[TestCase("Something <script>{const a = '</script>';}</script>! nice", true)] // illegal selector chars
776+
public void ParseHtmlInput_Without_ParserSetting_IsHtml(string input, bool shouldThrow)
777+
{
778+
var parser = GetRegularParser(new SmartSettings
779+
{
780+
StringFormatCompatibility = false,
781+
Parser = new ParserSettings
782+
{
783+
SelectorCharFilter = FilterType.Allowlist,
784+
ErrorAction = ParseErrorAction.ThrowError,
785+
ParseInputAsHtml = false
786+
}
787+
});
788+
789+
switch (shouldThrow)
790+
{
791+
case true:
792+
Assert.That(() => _ = parser.ParseFormat(input), Throws.TypeOf<ParsingErrors>());
793+
break;
794+
case false:
795+
{
796+
var result = parser.ParseFormat(input);
797+
Assert.That(result.Items, Has.Count.EqualTo(3));
798+
break;
799+
}
800+
}
728801
}
729802

803+
#endregion
804+
730805
/// <summary>
731806
/// SmartFormat is able to parse script tags, if <see cref="ParserSettings.ParseInputAsHtml"/> is <see langword="true"/>
732807
/// </summary>

src/SmartFormat.Tests/Core/SettingsTests.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ public void ExistingSelectorCharacter_Should_Not_Be_Added()
2323
Assert.Multiple(() =>
2424
{
2525
Assert.That(settings.Parser.CustomSelectorChars.Count(c => c == 'A'), Is.EqualTo(0));
26-
Assert.That(settings.Parser.CustomSelectorChars.Count(c => c == ' '), Is.EqualTo(0));
26+
Assert.That(settings.Parser.CustomSelectorChars.Count(c => c == ' '), Is.EqualTo(1));
2727
});
2828
}
2929

30-
[Test]
31-
public void ControlCharacters_Should_Be_Added_As_SelectorChars()
30+
[TestCase(FilterType.Allowlist)]
31+
[TestCase(FilterType.Blocklist)]
32+
public void ControlCharacters_Should_Be_Added_As_SelectorChars(FilterType filterType)
3233
{
33-
var settings = new SmartSettings();
34+
var settings = new SmartSettings { Parser = { SelectorCharFilter = filterType } };
3435
var controlChars = ParserSettings.ControlChars().ToList();
3536
settings.Parser.AddCustomSelectorChars(controlChars);
3637

@@ -39,8 +40,8 @@ public void ControlCharacters_Should_Be_Added_As_SelectorChars()
3940
Assert.That(settings.Parser.CustomSelectorChars, Has.Count.EqualTo(controlChars.Count));
4041
foreach (var c in settings.Parser.CustomSelectorChars)
4142
{
42-
Assert.That(settings.Parser.DisallowedSelectorChars(), Does.Not.Contain(c),
43-
$"Control char U+{(int)c:X4} should be allowed as selector char.");
43+
Assert.That(settings.Parser.GetSelectorChars(), filterType == FilterType.Allowlist ? Does.Contain(c) : Does.Not.Contain(c),
44+
$"Control char U+{(int) c:X4} should be allowed as selector char.");
4445
}
4546
});
4647
}

0 commit comments

Comments
 (0)