Skip to content

Commit 4883c3a

Browse files
committed
feat: FilterSelector chars by allowlist or blocklilst
1 parent 6b1dbec commit 4883c3a

File tree

7 files changed

+530
-107
lines changed

7 files changed

+530
-107
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
25+
Assert.Multiple(() =>
26+
{
27+
Assert.That(countBeforeRemoval, Is.EqualTo(asciiChars.Length + nonAsciiChars.Length));
28+
Assert.That(charSet.Count, Is.EqualTo(countBeforeRemoval - 2));
29+
Assert.That(existingRemoved, Is.True);
30+
Assert.That(nonExistingRemoved, Is.False);
31+
});
32+
}
33+
34+
[Test]
35+
public void CharSet_CreateFromSpan_GetCharacters_Contains()
36+
{
37+
char[] asciiAndNonAscii = ['\0', 'A', 'B', 'C', 'Ā', 'Б', '中'];
38+
var charSet = new CharSet(asciiAndNonAscii.AsSpan());
39+
40+
Assert.Multiple(() =>
41+
{
42+
Assert.That(charSet, Has.Count.EqualTo(7));
43+
Assert.That(charSet.Contains('A'), Is.True); // ASCII
44+
Assert.That(charSet.Contains('\0'), Is.True); // control character
45+
Assert.That(charSet.Contains('中'), Is.True); // non-ASCII
46+
Assert.That(charSet.Contains('?'), Is.False);
47+
Assert.That(charSet.GetCharacters(), Is.EquivalentTo(asciiAndNonAscii));
48+
charSet.Clear();
49+
Assert.That(charSet, Has.Count.EqualTo(0));
50+
Assert.That(charSet.GetCharacters(), Is.Empty);
51+
});
52+
}
53+
}

src/SmartFormat.Tests/Core/ParserTests.cs

Lines changed: 79 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

@@ -536,8 +545,10 @@ public void Parse_Unicode(string formatString, string unicodeLiteral, int itemIn
536545
[TestCase("{%C}", '%')]
537546
public void Selector_With_Custom_Selector_Character(string formatString, char customChar)
538547
{
548+
// settings must be set before parser instantiation
539549
var settings = new SmartSettings();
540-
settings.Parser.AddCustomSelectorChars(new[]{customChar});
550+
settings.Parser.AddCustomSelectorChars([customChar]);
551+
var x = settings.Parser.GetSelectorChars();
541552
var parser = GetRegularParser(settings);
542553
var result = parser.ParseFormat(formatString);
543554

@@ -546,7 +557,7 @@ public void Selector_With_Custom_Selector_Character(string formatString, char cu
546557
Assert.That(placeholder!.Selectors, Has.Count.EqualTo(1));
547558
Assert.Multiple(() =>
548559
{
549-
Assert.That(placeholder!.Selectors, Has.Count.EqualTo(placeholder!.GetSelectors().Count));
560+
Assert.That(placeholder.Selectors, Has.Count.EqualTo(placeholder.GetSelectors().Count));
550561
Assert.That(placeholder.Selectors[0].ToString(), Is.EqualTo(formatString.Substring(1, 2)));
551562
});
552563
}
@@ -555,8 +566,10 @@ public void Selector_With_Custom_Selector_Character(string formatString, char cu
555566
[TestCase("{a°b}", '°')]
556567
public void Selectors_With_Custom_Operator_Character(string formatString, char customChar)
557568
{
558-
var parser = GetRegularParser();
559-
parser.Settings.Parser.AddCustomOperatorChars(new[]{customChar});
569+
// settings must be set before parser instantiation
570+
var settings = new SmartSettings();
571+
settings.Parser.AddCustomOperatorChars([customChar]);
572+
var parser = GetRegularParser(settings);
560573
var result = parser.ParseFormat(formatString);
561574

562575
var placeholder = result.Items[0] as Placeholder;
@@ -583,10 +596,12 @@ public void Selector_WorksWithAllUnicodeChars(string selector)
583596
{
584597
// See https://github.com/axuno/SmartFormat/issues/454
585598

599+
// settings must be set before parser instantiation
600+
var settings = new SmartSettings { Parser = { SelectorCharFilter = FilterType.Blocklist } };
586601
const string expected = "The Value";
587602
// The default formatter with default settings should be able to handle any
588603
// Unicode characters in selectors except the "magic" disallowed ones
589-
var formatter = Smart.CreateDefaultSmartFormat();
604+
var formatter = Smart.CreateDefaultSmartFormat(settings);
590605
// Use the Unicode string as a selector of the placeholder
591606
var template = $"{{{selector}}}";
592607
var result = formatter.Format(template, new Dictionary<string, string> { { selector, expected } });
@@ -647,10 +662,11 @@ public void Selector_With_Nullable_Operator_Character(string formatString)
647662
public void Selector_With_Other_Contiguous_Operator_Characters(string formatString, char customChar)
648663
{
649664
// contiguous operator characters are parsed as "ONE operator string"
650-
651-
var parser = GetRegularParser();
665+
var settings = new SmartSettings();
666+
settings.Parser.AddCustomOperatorChars([customChar]);
667+
var parser = GetRegularParser(settings);
652668
// adding '.' is ignored, as it's a standard operator
653-
parser.Settings.Parser.AddCustomOperatorChars(new[]{customChar});
669+
parser.Settings.Parser.AddCustomOperatorChars([customChar]);
654670
var result = parser.ParseFormat(formatString);
655671

656672
var placeholder = result.Items[0] as Placeholder;
@@ -706,6 +722,12 @@ public void ParseInputAsHtml(string input)
706722
Assert.That(literalText!.RawText, Is.EqualTo(input));
707723
}
708724

725+
#region * Parse HTML input without ParserSetting 'IsHtml'
726+
727+
/// <summary>
728+
/// <see cref="ParserSettings.SelectorCharFilter"/> is <see cref="FilterType.Blocklist"/>:
729+
/// all characters are allowed in selectors
730+
/// </summary>
709731
[TestCase("<script>{Placeholder}</script>", "{Placeholder}")]
710732
[TestCase("<style>{Placeholder}</style>", "{Placeholder}")]
711733
[TestCase("Something <style>h1 { color : #000; }</style>! nice", "{ color : #000; }")]
@@ -715,7 +737,12 @@ public void ParseHtmlInput_Without_ParserSetting_IsHtml(string input, string sel
715737
var parser = GetRegularParser(new SmartSettings
716738
{
717739
StringFormatCompatibility = false,
718-
Parser = new ParserSettings { ErrorAction = ParseErrorAction.ThrowError, ParseInputAsHtml = false }
740+
Parser = new ParserSettings
741+
{
742+
SelectorCharFilter = FilterType.Blocklist,
743+
ErrorAction = ParseErrorAction.ThrowError,
744+
ParseInputAsHtml = false
745+
}
719746
});
720747

721748
var result = parser.ParseFormat(input);
@@ -724,9 +751,45 @@ public void ParseHtmlInput_Without_ParserSetting_IsHtml(string input, string sel
724751
Assert.That(result.Items, Has.Count.EqualTo(3));
725752
Assert.That(((Placeholder) result.Items[1]).RawText, Is.EqualTo(selector));
726753
});
754+
}
755+
756+
/// <summary>
757+
/// <see cref="ParserSettings.SelectorCharFilter"/> is <see cref="FilterType.Allowlist"/>:
758+
/// Predefined set of allowed characters in selectors
759+
/// </summary>
760+
[TestCase("<script>{Placeholder}</script>", false)] // should parse a placeholder
761+
[TestCase("<style>{Placeholder}</style>", false)] // should parse a placeholder
762+
[TestCase("Something <style>h1 { color : #000; }</style>! nice", true)] // illegal selector chars
763+
[TestCase("Something <script>{const a = '</script>';}</script>! nice", true)] // illegal selector chars
764+
public void ParseHtmlInput_Without_ParserSetting_IsHtml(string input, bool shouldThrow)
765+
{
766+
var parser = GetRegularParser(new SmartSettings
767+
{
768+
StringFormatCompatibility = false,
769+
Parser = new ParserSettings
770+
{
771+
SelectorCharFilter = FilterType.Allowlist,
772+
ErrorAction = ParseErrorAction.ThrowError,
773+
ParseInputAsHtml = false
774+
}
775+
});
727776

777+
switch (shouldThrow)
778+
{
779+
case true:
780+
Assert.That(() => _ = parser.ParseFormat(input), Throws.TypeOf<ParsingErrors>());
781+
break;
782+
case false:
783+
{
784+
var result = parser.ParseFormat(input);
785+
Assert.That(result.Items, Has.Count.EqualTo(3));
786+
break;
787+
}
788+
}
728789
}
729790

791+
#endregion
792+
730793
/// <summary>
731794
/// SmartFormat is able to parse script tags, if <see cref="ParserSettings.ParseInputAsHtml"/> is <see langword="true"/>
732795
/// </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)