src/Microsoft.Azure.NotificationHubs/Messaging/ExpressionEvaluator.cs (247 lines of code) (raw):

//---------------------------------------------------------------- // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for // license information. //---------------------------------------------------------------- using System; using System.Collections.Generic; using System.Runtime.Serialization; using System.Text; using System.Text.RegularExpressions; namespace Microsoft.Azure.NotificationHubs.Messaging { static class ExpressionEvaluator { public const string InternalBodyProperty = "READ_ONCE_BODY_PUSHNOTIFICATION"; public const string InternalJsonNavigationProperty = "FIRST_LEVEL_JSON_NAVIGATION_PUSHNOTIFICATION"; const string BodyExpression = @"$body"; public const int MaxLengthOfPropertyName = 120; public static readonly Regex PropertyNameRegEx = new Regex("^[A-Za-z0-9_]+$"); enum TokenType { None, Dollar, Hash, Dot, Percentage, SingleLiteral, DoubleLiteral, Body } public enum ExpressionType { Literal, Numeric, String, Composite, None } public static ExpressionType Validate(string expression) { ExpressionType expressionType; List<Token> tokens = ValidateAndTokenize(expression, out expressionType); Token bodyToken = tokens.Find((t) => { return t.Type == TokenType.Body; }); if (bodyToken != null) { throw new InvalidDataContractException(SRClient.BodyIsNotSupportedExpression); } return expressionType; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")] static List<Token> ValidateAndTokenize(string expression, out ExpressionType expressionType) { expressionType = ExpressionEvaluator.PeekExpressionType(expression); if (expressionType == ExpressionType.Literal) { return new List<Token>(); } List<Token> tokens = new List<Token>(); string workingInput = expression; int tokenEndIndex; if (expressionType == ExpressionType.Composite) { if (expression[expression.Length - 1] != '}') { throw new InvalidDataContractException(string.Format(SRClient.ExpressionMissingClosingParenthesesNoToken, expression)); } workingInput = expression.Substring(1, expression.Length - 2).TrimEnd(); } while (true) { Token token = new Token(); workingInput = workingInput.TrimStart(); if (workingInput.Length < 3) { throw new InvalidDataContractException(string.Format(SRClient.ExpressionInvalidToken, expression, workingInput)); } switch (workingInput[0]) { case '.': token.Type = TokenType.Dot; tokenEndIndex = ExpressionEvaluator.ExtractToken(expression, workingInput, token); break; case '%': token.Type = TokenType.Percentage; tokenEndIndex = ExpressionEvaluator.ExtractToken(expression, workingInput, token); break; case '\'': token.Type = TokenType.SingleLiteral; tokenEndIndex = ExpressionEvaluator.ExtractLiteral(expression, workingInput, token); break; case '"': token.Type = TokenType.DoubleLiteral; tokenEndIndex = ExpressionEvaluator.ExtractLiteral(expression, workingInput, token); break; case '#': token.Type = TokenType.Hash; tokenEndIndex = ExpressionEvaluator.ExtractToken(expression, workingInput, token); break; case '$': if (workingInput.ToLowerInvariant().StartsWith(BodyExpression, StringComparison.OrdinalIgnoreCase)) { tokenEndIndex = BodyExpression.Length - 1; token.Type = TokenType.Body; } else { token.Type = TokenType.Dollar; tokenEndIndex = ExpressionEvaluator.ExtractToken(expression, workingInput, token); } break; default: throw new InvalidDataContractException(string.Format(SRClient.ExpressionInvalidTokenType, expression, workingInput)); } if (token.Type != TokenType.SingleLiteral && token.Type != TokenType.DoubleLiteral && token.Type != TokenType.Body && !string.Equals(token.Property, ExpressionEvaluator.BodyExpression, StringComparison.OrdinalIgnoreCase)) { if (!ExpressionEvaluator.PropertyNameRegEx.IsMatch(token.Property)) { throw new InvalidDataContractException(string.Format(SRClient.PropertyNameIsBad, token.Property)); } if (token.Property.Length > ExpressionEvaluator.MaxLengthOfPropertyName) { throw new InvalidDataContractException(string.Format(SRClient.PropertyTooLong, token.Property.Length, ExpressionEvaluator.MaxLengthOfPropertyName)); } } tokens.Add(token); if (workingInput.Length == tokenEndIndex + 1) { break; } workingInput = workingInput.Substring(tokenEndIndex + 1).TrimStart(); if (workingInput[0] != '+') { throw new InvalidDataContractException(string.Format(SRClient.ExpressionInvalidCompositionOperator, expression, workingInput)); } workingInput = workingInput.Substring(1); } if (tokens.Count > 1 && tokens.Find((token) => token.Type == TokenType.Hash) != null) { throw new InvalidDataContractException(SRClient.ExpressionHashInComposite); } return tokens; } static string Evaluate(List<string> values) { if (values.Count == 1) { return values[0]; } int expectedStringSize = 0; values.ForEach(s => expectedStringSize += s.Length); StringBuilder finalString = new StringBuilder(expectedStringSize); values.ForEach(s => finalString.Append(s)); return finalString.ToString(); } static ExpressionType PeekExpressionType(string expression) { if (string.IsNullOrWhiteSpace(expression)) { return ExpressionType.Literal; } char firstChar = expression[0]; switch (firstChar) { case '$': case '.': case '%': return ExpressionType.String; case '#': return ExpressionType.Numeric; case '{': return ExpressionType.Composite; default: return ExpressionType.Literal; } } static int ExtractLiteral(string fullExpression, string tokenBegin, Token token) { int tokenEndIndex = 0; int startLookingFromIndex = 1; char literalSeperator = token.Type == TokenType.SingleLiteral ? '\'' : '\"'; bool escapeCharsFound = false; while (true) { tokenEndIndex = tokenBegin.IndexOf(literalSeperator, startLookingFromIndex); if (tokenEndIndex == -1) { throw new InvalidDataContractException(string.Format(SRClient.ExpressionLiteralMissingClosingNotation, fullExpression, tokenBegin)); } if (tokenEndIndex + 1 < tokenBegin.Length && tokenBegin[tokenEndIndex + 1] == literalSeperator) { escapeCharsFound = true; startLookingFromIndex = tokenEndIndex + 2; continue; } else { string literal = tokenBegin.Substring(1, tokenEndIndex - 1); if (escapeCharsFound) { token.Property = token.Type == TokenType.DoubleLiteral ? literal.Replace(@"""""", @"""") : literal.Replace(@"''", @"'"); } else { token.Property = literal; } break; } } return tokenEndIndex; } static int ExtractToken(string fullExpression, string tokenBegin, Token token) { // Cant find opening Parentheses if (tokenBegin[1] != '(') { throw new InvalidDataContractException(string.Format(SRClient.ExpressionMissingOpenParentheses, fullExpression, tokenBegin)); } int indexOfClose = tokenBegin.IndexOf(')'); int indexOfComma = tokenBegin.IndexOf(','); int indexOfDefaultBegin = tokenBegin.IndexOf(":{", StringComparison.InvariantCultureIgnoreCase); int indexOfDefaultEnd = indexOfDefaultBegin + 2; int defaultFullLength = 0; // Here we 'read' default value which may contain any character if (indexOfDefaultBegin > 1 && indexOfDefaultBegin < indexOfClose) { if (indexOfDefaultBegin < 3) { throw new InvalidDataContractException(string.Format(SRClient.ExpressionMissingProperty, fullExpression, tokenBegin)); } var escape = false; while (true) { if (indexOfDefaultEnd >= tokenBegin.Length) { throw new InvalidDataContractException(string.Format(SRClient.ExpressionMissingDefaultEnd, fullExpression, tokenBegin)); } if (tokenBegin[indexOfDefaultEnd] == '}' && !escape) { break; } escape = tokenBegin[indexOfDefaultEnd] == '\\' && !escape; indexOfDefaultEnd++; } defaultFullLength = indexOfDefaultEnd - indexOfDefaultBegin + 1; token.DefaultValue = tokenBegin.Substring(indexOfDefaultBegin + 2, defaultFullLength - 3); // Fix indexes if default value contains ')' or ',' indexOfComma = tokenBegin.IndexOf(',', indexOfDefaultEnd); indexOfClose = tokenBegin.IndexOf(')', indexOfDefaultEnd); } // Cant find closing Parentheses if (indexOfClose == -1) { throw new InvalidDataContractException(string.Format(SRClient.ExpressionMissingClosingParentheses, fullExpression, tokenBegin)); } // When percentage or hash is used comma is prohibited if ((token.Type == TokenType.Percentage || token.Type == TokenType.Hash) && indexOfComma != -1 && indexOfComma < indexOfClose) { throw new InvalidDataContractException(string.Format(SRClient.ExpressionErrorParsePercentFormat, fullExpression, tokenBegin)); } // When dot is used comma is mandatory if (token.Type == TokenType.Dot && indexOfComma == -1) { throw new InvalidDataContractException(string.Format(SRClient.ExpressionErrorParseDotFormat, fullExpression, tokenBegin)); } int shortLength = 0; if (indexOfComma != -1 && indexOfComma < indexOfClose) { string shortLengthString = tokenBegin.Substring(indexOfComma + 1, indexOfClose - indexOfComma - 1); if (!int.TryParse(shortLengthString, out shortLength) || shortLength < 0) { throw new InvalidDataContractException(string.Format(SRClient.ExpressionIsNotPositiveInteger, fullExpression, tokenBegin, shortLengthString)); } if (shortLength == 0) { token.EmptyString = true; } token.Length = shortLength; token.Property = tokenBegin.Substring(2, indexOfComma - 2 - defaultFullLength); } else { token.Property = tokenBegin.Substring(2, indexOfClose - 2 - defaultFullLength); } token.Property = token.Property.Trim(); return indexOfClose; } class Token { public bool EmptyString { get; set; } public int Length { get; set; } public string Property { get; set; } public string DefaultValue { get; set; } public TokenType Type { get; set; } } } }