Skip to content

Commit

Permalink
Constrain custom selectors and operators (axuno#172)
Browse files Browse the repository at this point in the history
* Custom selector chars can be added, if not disallowed or in use as an operator char
* Custom operator chars can be added, if not disallowed or in use as a selector char
* Added corresponding unit tests
* Alphanumeric selector chars are the only option now and cannot be degraded to pure numeric chars
* `PlaceholderBeginChar`, `PlaceholderEndChar`, `FormatterOptionsBeginChar` and `FormatterOptionsEndChar` now only have getters
* Removed obsolete `formatter.Parser.AddOperators("[]");` from the `ListFormatter` CTOR.
  • Loading branch information
axunonb committed Mar 9, 2022
1 parent 9fdbb83 commit 280226a
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 47 deletions.
50 changes: 50 additions & 0 deletions src/SmartFormat.Tests/Core/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -617,5 +617,55 @@ public void Parse_Unicode(string formatString, string unicodeLiteral, int itemIn
else
Assert.That(() => literal.ToString(), Throws.ArgumentException.And.Message.Contains(unicodeLiteral));
}

[TestCase("{A }", ' ')]
[TestCase("{B§}", '§')]
[TestCase("{?C}", '?')]
public void Selector_With_Custom_Selector_Character(string formatString, char customChar)
{
var parser = GetRegularParser();
parser.Settings.Parser.AddCustomSelectorChars(new[]{customChar});
var result = parser.ParseFormat(formatString, new[] {"d"});

var placeholder = result.Items[0] as Placeholder;
Assert.That(placeholder, Is.Not.Null);
Assert.That(placeholder.Selectors.Count, Is.EqualTo(1));
Assert.That(placeholder.Selectors[0].ToString(), Is.EqualTo(formatString.Substring(1,2)));
}

[TestCase("{a b}", ' ')]
[TestCase("{a°b}", '°')]
public void Selectors_With_Custom_Operator_Character(string formatString, char customChar)
{
var parser = GetRegularParser();
parser.Settings.Parser.AddCustomOperatorChars(new[]{customChar});
var result = parser.ParseFormat(formatString, new[] {"d"});

var placeholder = result.Items[0] as Placeholder;
Assert.That(placeholder, Is.Not.Null);
Assert.That(placeholder.Selectors.Count, Is.EqualTo(2));
Assert.That(placeholder.Selectors[0].ToString(), Is.EqualTo(formatString.Substring(1,1)));
Assert.That(placeholder.Selectors[1].ToString(), Is.EqualTo(formatString.Substring(3, 1)));
Assert.That(placeholder.Selectors[1].Operator, Is.EqualTo(formatString.Substring(2,1)));
}

[TestCase("{C?.D}", '?')]
[TestCase("{C..D}", '.')]
public void Selector_With_Contiguous_Operator_Characters(string formatString, char customChar)
{
// contiguous operators '?.' are parsed as ONE

var parser = GetRegularParser();
// adding '.' is ignored, as it's the standard operator
parser.Settings.Parser.AddCustomOperatorChars(new[]{customChar});
var result = parser.ParseFormat(formatString, new[] {"d"});

var placeholder = result.Items[0] as Placeholder;
Assert.That(placeholder, Is.Not.Null);
Assert.That(placeholder.Selectors.Count, Is.EqualTo(2));
Assert.That(placeholder.Selectors[0].ToString(), Is.EqualTo(formatString.Substring(1,1)));
Assert.That(placeholder.Selectors[1].ToString(), Is.EqualTo(formatString.Substring(4,1)));
Assert.That(placeholder.Selectors[1].Operator, Is.EqualTo(formatString.Substring(2,2)));
}
}
}
104 changes: 104 additions & 0 deletions src/SmartFormat.Tests/Core/SettingsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NUnit.Framework;
using SmartFormat.Core.Settings;

namespace SmartFormat.Tests.Core
{
[TestFixture]
public class SettingsTests
{
[Test]
public void TryingToAddDisallowedSelectorCharacters_Should_Throw()
{
var settings = new SmartSettings();
Assert.That(() => settings.Parser.AddCustomSelectorChars(new[] {settings.Parser.PlaceholderBeginChar}),
Throws.ArgumentException.And.Message.Contains($"{settings.Parser.PlaceholderBeginChar}"));
}

[Test]
public void ExistingSelectorCharacter_Should_Not_Be_Added()
{
var settings = new SmartSettings();
settings.Parser.AddCustomSelectorChars(new[] {'A', ' '});
settings.Parser.AddCustomSelectorChars(new[] {' '});

Assert.That(settings.Parser.CustomSelectorChars.Count(c => c == 'A'), Is.EqualTo(0));
Assert.That(settings.Parser.CustomSelectorChars.Count(c => c == ' '), Is.EqualTo(1));
}

[Test]
public void TryingToAddDisallowedOperatorCharacters_Should_Throw()
{
var settings = new SmartSettings();
Assert.That(() => settings.Parser.AddCustomOperatorChars(new[] {settings.Parser.PlaceholderBeginChar}),
Throws.ArgumentException.And.Message.Contains($"{settings.Parser.PlaceholderBeginChar}"));
}

[Test]
public void ExistingOperatorCharacter_Should_Not_Be_Added()
{
var settings = new SmartSettings();
settings.Parser.AddCustomOperatorChars(new[] {settings.Parser.OperatorChars[0], '°'});
settings.Parser.AddCustomOperatorChars(new[] {'°'});

Assert.That(settings.Parser.CustomOperatorChars.Count(c => c == settings.Parser.OperatorChars[0]), Is.EqualTo(0));
Assert.That(settings.Parser.CustomOperatorChars.Count(c => c == '°'), Is.EqualTo(1));
}

[Test]
public void GetCaseSensitivityComparison()
{
var settings = new SmartSettings();
foreach (var name in Enum.GetNames(typeof(CaseSensitivityType)))
{
settings.CaseSensitivity = (CaseSensitivityType) Enum.Parse(typeof(CaseSensitivityType), name);
Assert.DoesNotThrow(() => settings.GetCaseSensitivityComparison());
}

settings.CaseSensitivity = (CaseSensitivityType) int.MaxValue;
Assert.Throws<InvalidOperationException>(() => settings.GetCaseSensitivityComparison());
}

[Test]
public void CaseSensitivityComparer()
{
var settings = new SmartSettings();
foreach (var name in Enum.GetNames(typeof(CaseSensitivityType)))
{
settings.CaseSensitivity = (CaseSensitivityType) Enum.Parse(typeof(CaseSensitivityType), name);
Assert.DoesNotThrow(() => settings.GetCaseSensitivityComparer());
}

settings.CaseSensitivity = (CaseSensitivityType) int.MaxValue;
Assert.Throws<InvalidOperationException>(() => settings.GetCaseSensitivityComparer());
}

[TestCase('°')] // a custom char
[TestCase('A')] // a standard selector char
public void Add_CustomOperator_Used_As_Seperator_Should_Throw(char operatorChar)
{
var settings = new SmartSettings();
settings.Parser.AddCustomSelectorChars(new[] {operatorChar}); // reserve as selector char

// try to add the same char as operator
Assert.That(() => settings.Parser.AddCustomOperatorChars(new[] {operatorChar}),
Throws.ArgumentException.And.Message.Contains($"{operatorChar}"));
}

[TestCase('°')] // a custom char
[TestCase('.')] // a standard operator char
public void Add_CustomSelector_Used_As_Operator_Should_Throw(char selectorChar)
{
var settings = new SmartSettings();
settings.Parser.AddCustomOperatorChars(new[] {selectorChar}); // reserve as operator char

// try to add the same char as selector
Assert.That(() => settings.Parser.AddCustomSelectorChars(new[] {selectorChar}),
Throws.ArgumentException.And.Message.Contains($"{selectorChar}"));
}
}
}
19 changes: 8 additions & 11 deletions src/SmartFormat/Core/Parsing/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ internal Parser(SmartSettings smartSettings)
/// <summary>
/// Includes a-z and A-Z in the list of allowed selector chars.
/// </summary>
[Obsolete("Use 'ParserSettings.AllowAlphanumericSelectors' instead.")]
[Obsolete("Alphanumeric selectors are always enabled")]
public void AddAlphanumericSelectors()
{
_parserSettings.AllowAlphanumericSelectors = true;
// Do nothing - this is the standard behavior
}

/// <summary>
Expand Down Expand Up @@ -116,8 +116,7 @@ public void UseBraceEscaping()
[Obsolete("This feature has been removed", true)]
public void UseAlternativeBraces(char opening, char closing)
{
_parserSettings.PlaceholderBeginChar = opening;
_parserSettings.PlaceholderEndChar = opening;
throw new NotImplementedException("This feature has been removed");
}

#endregion
Expand Down Expand Up @@ -565,7 +564,7 @@ private void ParseSelector(string inputFormat, ref Format currentFormat, ref Ind
}

var inputChar = inputFormat[index.Current];
if (_parserSettings.OperatorChars.Contains(inputChar))
if (_parserSettings.OperatorChars.Contains(inputChar) || _parserSettings.CustomOperatorChars.Contains(inputChar))
{
// Add the selector:
if (index.Current != index.LastEnd)
Expand Down Expand Up @@ -675,8 +674,7 @@ private void ParseFormatOptions(string inputFormat, ref IndexContainer index)
}

/// <summary>
/// Checks whether the selector character is valid,
/// depending on <see cref="ParserSettings.AllowAlphanumericSelectors"/> and <see cref="ParserSettings.CustomSelectorChars"/>.
/// Checks whether the selector character is valid.
/// </summary>
/// <param name="c">The character to check.</param>
/// <returns><see langword="true"/> if the character is valid, else <see langword="false"/>.</returns>
Expand All @@ -685,11 +683,10 @@ internal bool IsValidSelectorChar(char c)
{
// C# variables: LetterOrDigit and "_"
// Required for Alignment: "-" and ","
return _parserSettings.AllowAlphanumericSelectors
? _parserSettings.AlphanumericSelectorChars.Contains(c) || _parserSettings.OperatorChars.Contains(c) || _parserSettings.CustomSelectorChars.Contains(c)
: _parserSettings.NumericSelectorChars.Contains(c) || _parserSettings.OperatorChars.Contains(c);
return _parserSettings.AlphanumericSelectorChars.Contains(c) || _parserSettings.OperatorChars.Contains(c) ||
_parserSettings.CustomSelectorChars.Contains(c);
}

private static bool FormatterNameExists(string name, string[] formatterExtensionNames)
{
return formatterExtensionNames.Any(n => n == name);
Expand Down
76 changes: 41 additions & 35 deletions src/SmartFormat/Core/Settings/ParserSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Licensed under the MIT license.
//

using System;
using System.Collections.Generic;
using System.Linq;
using SmartFormat.Core.Parsing;
Expand All @@ -15,7 +16,6 @@ namespace SmartFormat.Core.Settings
public class ParserSettings
{
private readonly IList<char> _alphanumericSelectorChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-".ToCharArray();
private readonly IList<char> _numericSelectorChars = "0123456789-".ToCharArray();

private readonly IList<char> _customSelectorChars = new List<char>();
private readonly IList<char> _customOperatorChars = new List<char>();
Expand All @@ -26,64 +26,69 @@ public class ParserSettings
/// </summary>
public ParseErrorAction ErrorAction { get; set; } = ParseErrorAction.ThrowError;

/// <summary>
/// If <see langword="true"/>, selectors can be alpha-numeric: They may contain
/// letters, digits, and the underscore character (_), like it is for C# variable names.
/// If <see langword="false"/>, only digits are allowed as selectors. Default is <see langword="true"/>.
/// </summary>
public bool AllowAlphanumericSelectors { get; set; } = true;

/// <summary>
/// The list of alphanumeric selector characters.
/// </summary>
public IReadOnlyList<char> AlphanumericSelectorChars => (IReadOnlyList<char>) _alphanumericSelectorChars;

/// <summary>
/// The list of numeric selector characters.
/// Gets a read-only list of the custom selector characters, which were set with <see cref="AddCustomSelectorChars"/>.
/// </summary>
public IReadOnlyList<char> NumericSelectorChars => (IReadOnlyList<char>) _numericSelectorChars;
public IReadOnlyList<char> CustomSelectorChars => (IReadOnlyList<char>) _customSelectorChars;

/// <summary>
/// Gets a read-only list of the custom selector characters, which were set with <see cref="AddCustomSelectorChars"/>.
/// Gets a list of characters which are not allowed in a selector.
/// </summary>
public IReadOnlyList<char> CustomSelectorChars => (IReadOnlyList<char>) _customSelectorChars;
public IReadOnlyList<char> DisallowedSelectorChars
{
get
{
var chars = new List<char> {
CharLiteralEscapeChar, FormatterNameSeparator, AlignmentOperator, SelectorOperator,
PlaceholderBeginChar, PlaceholderEndChar, FormatterOptionsBeginChar, FormatterOptionsEndChar
};
chars.AddRange(OperatorChars);
return chars;
}
}

/// <summary>
/// Gets a read-only list of the custom operator characters, which were set with <see cref="AddCustomSelectorChars"/>.
/// Contiguous operator characters are parsed as one operator (e.g. '?.').
/// </summary>
public IReadOnlyList<char> CustomOperatorChars => (IReadOnlyList<char>) _customOperatorChars;

/// <summary>
/// Add a list of allowable selector characters on top of the <see cref="AllowAlphanumericSelectors"/> setting.
/// Add a list of allowable selector characters on top of the <see cref="AlphanumericSelectorChars"/> setting.
/// This can be useful to support additional selector syntax such as math.
/// </summary>
/// Characters in <see cref="DisallowedSelectorChars"/> cannot be added.
/// Operator chars and selector chars must be different.
/// </summary>
public void AddCustomSelectorChars(IList<char> characters)
{
if (AllowAlphanumericSelectors)
{
foreach (var c in characters)
{
if (!_customSelectorChars.Contains(c) && !_alphanumericSelectorChars.Contains(c))
_customSelectorChars.Add(c);
}
}
else
foreach (var c in characters)
{
foreach (var c in characters)
{
if (!_customSelectorChars.Contains(c) && !_numericSelectorChars.Contains(c))
_customSelectorChars.Add(c);
}
if (DisallowedSelectorChars.Contains(c) || _customOperatorChars.Contains(c))
throw new ArgumentException($"Cannot add '{c}' as a custom selector character. It is disallowed or in use as an operator.");

if (!_customSelectorChars.Contains(c) && !_alphanumericSelectorChars.Contains(c))
_customSelectorChars.Add(c);
}
}

/// <summary>
/// Add a list of allowable operator characters on top of the standard <see cref="OperatorChars"/> setting.
/// Characters in <see cref="DisallowedSelectorChars"/> cannot be added.
/// Operator chars and selector chars must be different.
/// </summary>
public void AddCustomOperatorChars(IList<char> characters)
{
foreach (var c in characters)
{
if (DisallowedSelectorChars.Where(_ => OperatorChars.All(ch => ch != c)).Contains(c) ||
_alphanumericSelectorChars.Contains(c) || _customSelectorChars.Contains(c))
throw new ArgumentException($"Cannot add '{c}' as a custom operator character. It is disallowed or in use as a selector.");

if (!OperatorChars.Contains(c) && !_customOperatorChars.Contains(c))
_customOperatorChars.Add(c);
}
Expand Down Expand Up @@ -114,42 +119,43 @@ public void AddCustomOperatorChars(IList<char> characters)
/// The character which separates the formatter name (if any exists) from other parts of the placeholder.
/// E.g.: {Variable:FormatterName:argument} or {Variable:FormatterName}
/// </summary>
internal char FormatterNameSeparator { get; set; } = ':';
internal char FormatterNameSeparator { get; } = ':';

/// <summary>
/// The standard operator characters.
/// Contiguous operator characters are parsed as one operator (e.g. '?.').
/// </summary>
internal IReadOnlyList<char> OperatorChars => new List<char> {SelectorOperator, AlignmentOperator, '[', ']'};

/// <summary>
/// The character which separates the selector for alignment. <c>E.g.: Smart.Format("Name: {name,10}")</c>
/// </summary>
internal char AlignmentOperator { get; set; } = ',';
internal char AlignmentOperator { get; } = ',';

/// <summary>
/// The character which separates two or more selectors <c>E.g.: "First.Second.Third"</c>
/// </summary>
internal char SelectorOperator { get; set; } = '.';
internal char SelectorOperator { get; } = '.';

/// <summary>
/// Gets the character indicating the start of a <see cref="Placeholder"/>.
/// </summary>
public char PlaceholderBeginChar { get; internal set; } = '{';
public char PlaceholderBeginChar { get; } = '{';

/// <summary>
/// Gets the character indicating the end of a <see cref="Placeholder"/>.
/// </summary>
public char PlaceholderEndChar { get; internal set; } = '}';
public char PlaceholderEndChar { get; } = '}';

/// <summary>
/// Gets the character indicating the begin of formatter options.
/// </summary>
public char FormatterOptionsBeginChar { get; internal set; } = '(';
public char FormatterOptionsBeginChar { get; } = '(';

/// <summary>
/// Gets the character indicating the end of formatter options.
/// </summary>
public char FormatterOptionsEndChar { get; internal set; } = ')';
public char FormatterOptionsEndChar { get; } = ')';

/// <summary>
/// Characters which terminate parsing of format options.
Expand Down
2 changes: 1 addition & 1 deletion src/SmartFormat/Extensions/ListFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public class ListFormatter : IFormatter, ISource

public ListFormatter(SmartFormatter formatter)
{
formatter.Parser.AddOperators("[]()");
formatter.Parser.AddOperators("[]");
_smartSettings = formatter.Settings;
}

Expand Down

0 comments on commit 280226a

Please sign in to comment.