Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issue where custom datetime format strings result in binding failure #3505

Merged
merged 5 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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