Skip to content

Commit

Permalink
Fix issue where custom datetime format strings result in binding fail…
Browse files Browse the repository at this point in the history
…ure (#3505)

* Fix issue where custom datetime format strings result in binding failure

Also fix issue with always using UtcNow, even with $localDatetime.

Add tests

* Use delegate type for GetDateTimeFunc, more code cleanup

* make tests void instead of async Task

* Fix additional tests

Require $datetime and $localDatetime to have either a custom format, or the rfc/iso options
Just specifying $datetime or #localDatetime should result in a IncorrectDateTimeFormat binding failure

Fix error strings to include proper escaping for {{$localDatetime ...}} strings, this was being output with only single set of braces which is incorrect.

* More test updates
  • Loading branch information
phenning authored Apr 4, 2024
1 parent 0d5f733 commit af05eb5
Show file tree
Hide file tree
Showing 4 changed files with 345 additions and 129 deletions.
Original file line number Diff line number Diff line change
@@ -1,30 +1,49 @@
using Microsoft.DotNet.Interactive.Http;
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.DotNet.Interactive.Http.Parsing
{
#nullable enable
internal static class DynamicExpressionUtilites
{

const string DateTime = "$datetime";
const string LocalDateTime = "$localDatetime";
const string DateTimeMacroName = "$datetime";
const string LocalDateTimeMacroName = "$localDatetime";
const string OffsetRegex = """(?:\s+(?<offset>[-+]?[^\s]+)\s+(?<option>[^\s]+))?""";
const string TypeRegex = """(?:\s+(?<type>rfc1123|iso8601|'.+'|".+"))?""";

internal static Regex guidPattern = new Regex(@$"^\$guid$", RegexOptions.Compiled);
internal static Regex dateTimePattern = new Regex(@$"^\{DateTime}{TypeRegex}{OffsetRegex}$", RegexOptions.Compiled);
internal static Regex localDateTimePattern = new Regex(@$"^\{LocalDateTime}{TypeRegex}{OffsetRegex}$", RegexOptions.Compiled);
internal static Regex dateTimePattern = new Regex(@$"^\{DateTimeMacroName}{TypeRegex}{OffsetRegex}$", RegexOptions.Compiled);
internal static Regex localDateTimePattern = new Regex(@$"^\{LocalDateTimeMacroName}{TypeRegex}{OffsetRegex}$", RegexOptions.Compiled);
internal static Regex randomIntPattern = new Regex(@$"^\$randomInt(?:\s+(?<arguments>-?[^\s]+)){{0,2}}$", RegexOptions.Compiled);
internal static Regex timestampPattern = new Regex($@"^\$timestamp{OffsetRegex}$", RegexOptions.Compiled);
internal static Regex timestampPattern = new Regex($@"^\$timestamp{OffsetRegex}$", RegexOptions.Compiled);

private delegate DateTimeOffset GetDateTimeOffsetDelegate(bool isLocal);
private static GetDateTimeOffsetDelegate GetDateTimeOffset = DefaultGetDateTimeOffset;

private static DateTimeOffset DefaultGetDateTimeOffset(bool isLocal)
{
return isLocal ? DateTimeOffset.Now : DateTimeOffset.UtcNow;
}

// For Unit Tests, pass in a known date time, use it for all time related funcs, and then reset to default time handling
internal static HttpBindingResult<object?> ResolveExpressionBinding(HttpExpressionNode node, Func<DateTimeOffset> dateTimeFunc, string expression)
{
try
{
GetDateTimeOffset = delegate (bool _) { return dateTimeFunc(); };
return ResolveExpressionBinding(node, expression);
}
finally
{
GetDateTimeOffset = DefaultGetDateTimeOffset;
}
}

internal static HttpBindingResult<object?> ResolveExpressionBinding(HttpExpressionNode node, string expression)
{
Expand All @@ -34,34 +53,34 @@ internal static class DynamicExpressionUtilites
return node.CreateBindingSuccess(Guid.NewGuid().ToString());
}

if (expression.Contains(DateTime))
if (expression.Contains(DateTimeMacroName))
{
var dateTimeMatches = dateTimePattern.Matches(expression);
if (dateTimeMatches.Count == 1)
{
return GetDateTime(node, DateTime, expression, dateTimeMatches[0]);
return GetDateTime(node, DateTimeMacroName, GetDateTimeOffset(isLocal: false), expression, dateTimeMatches[0]);
}

return node.CreateBindingFailure(HttpDiagnostics.IncorrectDateTimeFormat(expression, DateTime));
return node.CreateBindingFailure(HttpDiagnostics.IncorrectDateTimeFormat(expression, DateTimeMacroName));
}

if (expression.Contains(LocalDateTime))
if (expression.Contains(LocalDateTimeMacroName))
{
var localDateTimeMatches = localDateTimePattern.Matches(expression);
if (localDateTimeMatches.Count == 1)
{
return GetDateTime(node, LocalDateTime, expression, localDateTimeMatches[0]);
return GetDateTime(node, LocalDateTimeMacroName, GetDateTimeOffset(isLocal: false), expression, localDateTimeMatches[0]);
}

return node.CreateBindingFailure(HttpDiagnostics.IncorrectDateTimeFormat(expression, LocalDateTime));
return node.CreateBindingFailure(HttpDiagnostics.IncorrectDateTimeFormat(expression, LocalDateTimeMacroName));
}

if (expression.Contains("$timestamp"))
{
var timestampMatches = timestampPattern.Matches(expression);
if (timestampMatches.Count == 1)
{
return GetTimestamp(node, expression, timestampMatches[0]);
return GetTimestamp(node, GetDateTimeOffset(isLocal: false), expression, timestampMatches[0]);
}

return node.CreateBindingFailure(HttpDiagnostics.IncorrectTimestampFormat(expression));
Expand All @@ -82,12 +101,10 @@ internal static class DynamicExpressionUtilites
}


private static HttpBindingResult<object?> GetTimestamp(HttpExpressionNode node, string expressionText, Match match)
private static HttpBindingResult<object?> GetTimestamp(HttpExpressionNode node, DateTimeOffset currentDateTimeOffset, string expressionText, Match match)
{
if (match.Groups.Count == 3)
{
var currentDateTimeOffset = DateTimeOffset.UtcNow;

if (string.Equals(expressionText, "$timestamp", StringComparison.InvariantCulture))
{
return node.CreateBindingSuccess(currentDateTimeOffset.ToUnixTimeSeconds().ToString());
Expand Down Expand Up @@ -119,14 +136,12 @@ internal static class DynamicExpressionUtilites

}
return node.CreateBindingFailure(HttpDiagnostics.IncorrectTimestampFormat(expressionText));

}

private static HttpBindingResult<object?> GetDateTime(HttpExpressionNode node, string dateTimeType, string expressionText, Match match)
private static HttpBindingResult<object?> GetDateTime(HttpExpressionNode node, string dateTimeType, DateTimeOffset currentDateTimeOffset, string expressionText, Match match)
{
if (match.Groups.Count == 4)
{
var currentDateTimeOffset = DateTimeOffset.UtcNow;
if (match.Groups["offset"].Success && match.Groups["option"].Success)
{
var offsetString = match.Groups["offset"].Value;
Expand All @@ -149,14 +164,11 @@ internal static class DynamicExpressionUtilites
}
string format;
var formatProvider = Thread.CurrentThread.CurrentUICulture;
var type = match.Groups["type"];

string text;
if (string.IsNullOrWhiteSpace(type.Value))
{
text = currentDateTimeOffset.ToString();
}
else
var type = match.Groups["type"];

// $datetime and $localDatetime MUST have either rfc1123, iso8601 or some other parameter.
// $datetime or $localDatetime alone should result in a binding error.
if (type is not null && !string.IsNullOrWhiteSpace(type.Value))
{
if (string.Equals(type.Value, "rfc1123", StringComparison.OrdinalIgnoreCase))
{
Expand All @@ -174,14 +186,17 @@ internal static class DynamicExpressionUtilites
{
// This substring exists to strip out the double quotes that are expected in a custom format
format = type.Value.Substring(1, type.Value.Length - 2);
}

try
{
string text = currentDateTimeOffset.ToString(format, formatProvider);
return node.CreateBindingSuccess(text);
}
catch(FormatException)
{
return node.CreateBindingFailure(HttpDiagnostics.IncorrectDateTimeCustomFormat(format));
}

text = currentDateTimeOffset.ToString(format, formatProvider);
}

if (DateTimeOffset.TryParse(text, out _))
{
return node.CreateBindingSuccess(text);
}
}
return node.CreateBindingFailure(HttpDiagnostics.IncorrectDateTimeFormat(expressionText, dateTimeType));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,14 @@ internal static HttpDiagnosticInfo UnableToEvaluateExpression(string symbol)
var severity = DiagnosticSeverity.Error;
var messageFormat = "Unable to evaluate expression '{0}'.";
return new HttpDiagnosticInfo(id, messageFormat, severity, symbol);
}

}

internal static HttpDiagnosticInfo IncorrectDateTimeFormat(string expression, string dateTimeType)
{
var id = $"HTTP0013";
var severity = DiagnosticSeverity.Error;
var messageFormat =
"""The supplied expression '{0}' does not follow the correct pattern. The expression should adhere to the following pattern: '{{{1} [rfc1123|iso8601|"custom format"] [offset option]}}' where offset (if specified) must be a valid integer and option must be one of the following: ms, s, m, h, d, w, M, Q, y. See https://aka.ms/http-date-time-format for more details.""";
"""The supplied expression '{0}' does not follow the correct pattern. The expression should adhere to the following pattern: '{{{{{1} [rfc1123|iso8601|"custom format"] [offset option]}}}}' where offset (if specified) must be a valid integer and option must be one of the following: ms, s, m, h, d, w, M, Q, y. See https://aka.ms/http-date-time-format for more details.""";
return new HttpDiagnosticInfo(id, messageFormat, severity, expression, dateTimeType);
}

Expand All @@ -119,7 +119,7 @@ internal static HttpDiagnosticInfo IncorrectTimestampFormat(string timestamp)
var id = $"HTTP0014";
var severity = DiagnosticSeverity.Error;
var messageFormat =
"The supplied expression '{0}' does not follow the correct pattern. The expression should adhere to the following pattern: '{{$timestamp [offset option]}}' where offset (if specified) must be a valid integer and option must be one of the following: ms, s, m, h, d, w, M, Q, y. See https://aka.ms/http-date-time-format for more details.";
"The supplied expression '{0}' does not follow the correct pattern. The expression should adhere to the following pattern: '{{{{$timestamp [offset option]}}}}' where offset (if specified) must be a valid integer and option must be one of the following: ms, s, m, h, d, w, M, Q, y. See https://aka.ms/http-date-time-format for more details.";
return new HttpDiagnosticInfo(id, messageFormat, severity, timestamp);
}

Expand All @@ -144,7 +144,7 @@ internal static HttpDiagnosticInfo IncorrectRandomIntFormat(string expression)
var id = $"HTTP0017";
var severity = DiagnosticSeverity.Error;
var messageFormat =
"""The supplied expression '{0}' does not follow the correct pattern. The expression should adhere to the following pattern: '{{$randomInt [min] [max]]}}' where min and max (if specified) must be valid integers.""";
"""The supplied expression '{0}' does not follow the correct pattern. The expression should adhere to the following pattern: '{{{{$randomInt [min] [max]]}}}}' where min and max (if specified) must be valid integers.""";
return new HttpDiagnosticInfo(id, messageFormat, severity, expression);
}

Expand All @@ -163,6 +163,14 @@ internal static HttpDiagnosticInfo InvalidRandomIntArgument(string expression, s
var severity = DiagnosticSeverity.Error;
var messageFormat = "The supplied argument '{1}' in the expression '{0}' is not a valid integer.";
return new HttpDiagnosticInfo(id, messageFormat, severity, expression, argument);
}

internal static HttpDiagnosticInfo IncorrectDateTimeCustomFormat(string format)
{
var id = $"HTTP0020";
var severity = DiagnosticSeverity.Error;
var messageFormat =
"""The supplied format '{0}' is invalid.""";
return new HttpDiagnosticInfo(id, messageFormat, severity, format);
}

}
Loading

0 comments on commit af05eb5

Please sign in to comment.