diff --git a/TwitchDownloaderCore/Extensions/ReadOnlySpanExtensions.cs b/TwitchDownloaderCore/Extensions/ReadOnlySpanExtensions.cs index 5a3fcaf3..433553bd 100644 --- a/TwitchDownloaderCore/Extensions/ReadOnlySpanExtensions.cs +++ b/TwitchDownloaderCore/Extensions/ReadOnlySpanExtensions.cs @@ -4,44 +4,91 @@ namespace TwitchDownloaderCore.Extensions { public static class ReadOnlySpanExtensions { - /// Replaces all occurrences of not prepended by a backslash with . - public static bool TryReplaceNonEscaped(this ReadOnlySpan str, Span destination, out int charsWritten, char oldChar, char newChar) + /// Replaces all occurrences of not prepended by a backslash or contained within quotation marks with . + public static bool TryReplaceNonEscaped(this ReadOnlySpan str, Span destination, char oldChar, char newChar) { + const string ESCAPE_CHARS = @"\'"""; + if (destination.Length < str.Length) - { - charsWritten = 0; return false; - } str.CopyTo(destination); - charsWritten = str.Length; var firstIndex = destination.IndexOf(oldChar); - if (firstIndex == -1) - { return true; - } - firstIndex = Math.Min(firstIndex, destination.IndexOf('\\')); + var firstEscapeIndex = destination.IndexOfAny(ESCAPE_CHARS); + if (firstEscapeIndex != -1 && firstEscapeIndex < firstIndex) + firstIndex = firstEscapeIndex; - for (var i = firstIndex; i < str.Length; i++) + var lastIndex = destination.LastIndexOf(oldChar); + var lastEscapeIndex = destination.LastIndexOfAny(ESCAPE_CHARS); + if (lastEscapeIndex != -1 && lastEscapeIndex > lastIndex) + lastIndex = lastEscapeIndex; + + lastIndex++; + for (var i = firstIndex; i < lastIndex; i++) { var readChar = destination[i]; - if (readChar == '\\' && i + 1 < str.Length) + switch (readChar) + { + case '\\': + i++; + break; + case '\'': + case '\"': + { + i = FindCloseQuoteMark(destination, i, lastIndex, readChar); + + if (i == -1) + { + destination.Clear(); + return false; + } + + break; + } + default: + { + if (readChar == oldChar) + { + destination[i] = newChar; + } + + break; + } + } + } + + return true; + } + + private static int FindCloseQuoteMark(ReadOnlySpan destination, int openQuoteIndex, int endIndex, char readChar) + { + var i = openQuoteIndex + 1; + var quoteFound = false; + while (i < endIndex) + { + var readCharQuote = destination[i]; + i++; + + if (readCharQuote == '\\') { i++; continue; } - if (readChar == oldChar) + if (readCharQuote == readChar) { - destination[i] = newChar; + i--; + quoteFound = true; + break; } } - return true; + return quoteFound ? i : -1; } } } \ No newline at end of file diff --git a/TwitchDownloaderCore/Tools/TimeSpanHFormat.cs b/TwitchDownloaderCore/Tools/TimeSpanHFormat.cs index f9fbb120..531f04cb 100644 --- a/TwitchDownloaderCore/Tools/TimeSpanHFormat.cs +++ b/TwitchDownloaderCore/Tools/TimeSpanHFormat.cs @@ -49,76 +49,121 @@ public string Format(string format, TimeSpan timeSpan, IFormatProvider formatPro if (timeSpan.Days == 0) { var newFormat = format.Length <= 256 ? stackalloc char[format.Length] : new char[format.Length]; - if (!format.AsSpan().TryReplaceNonEscaped(newFormat, out var charsWritten, 'H', 'h')) + if (!format.AsSpan().TryReplaceNonEscaped(newFormat, 'H', 'h')) { - throw new Exception("Failed to generate ToString() compatible format. This should not have been possible."); + throw new FormatException($"Invalid character escaping in the format string: {format}"); } // If the format contains more than 2 sequential unescaped h's, it will throw a format exception. If so, we can fallback to our parser. if (newFormat.IndexOf("hhh") == -1) { - return HandleOtherFormats(newFormat[..charsWritten].ToString(), timeSpan, formatProvider); + return HandleOtherFormats(newFormat.ToString(), timeSpan, formatProvider); } } - var sb = new StringBuilder(format.Length); + return HandleBigHFormat(format.AsSpan(), timeSpan); + } + + private static string HandleBigHFormat(ReadOnlySpan format, TimeSpan timeSpan) + { + var formatLength = format.Length; + var sb = new StringBuilder(formatLength); var regularFormatCharStart = -1; var bigHStart = -1; - var formatSpan = format.AsSpan(); - for (var i = 0; i < formatSpan.Length; i++) + for (var i = 0; i < formatLength; i++) { - var readChar = formatSpan[i]; + var readChar = format[i]; if (readChar == 'H') { if (bigHStart == -1) - { bigHStart = i; - } if (regularFormatCharStart != -1) { - AppendRegularFormat(sb, timeSpan, format, regularFormatCharStart, i - regularFormatCharStart); + var formatEnd = i - regularFormatCharStart; + AppendRegularFormat(sb, timeSpan, format.Slice(regularFormatCharStart, formatEnd)); regularFormatCharStart = -1; } } else { if (regularFormatCharStart == -1) - { regularFormatCharStart = i; - } if (bigHStart != -1) { - AppendBigHFormat(sb, timeSpan, i - bigHStart); + var bigHCount = i - bigHStart; + AppendBigHFormat(sb, timeSpan, bigHCount); bigHStart = -1; } - // If the current char is an escape we can skip the next char - if (readChar == '\\' && i + 1 < formatSpan.Length) + switch (readChar) { - i++; + // If the current char is an escape we can skip the next char + case '\\' when i + 1 < formatLength: + i++; + continue; + // If the current char is a quote we can skip the next quote, if it exists + case '\'' when i + 1 < formatLength: + case '\"' when i + 1 < formatLength: + { + i = FindCloseQuoteMark(format, i, formatLength, readChar); + + if (i == -1) + { + throw new FormatException($"Invalid character escaping in the format string: {format}"); + } + + continue; + } } } } if (regularFormatCharStart != -1) { - AppendRegularFormat(sb, timeSpan, format, regularFormatCharStart, formatSpan.Length - regularFormatCharStart); + var formatEnd = format.Length - regularFormatCharStart; + AppendRegularFormat(sb, timeSpan, format.Slice(regularFormatCharStart, formatEnd)); } else if (bigHStart != -1) { - AppendBigHFormat(sb, timeSpan, formatSpan.Length - bigHStart); + var bigHCount = format.Length - bigHStart; + AppendBigHFormat(sb, timeSpan, bigHCount); } return sb.ToString(); } - private static void AppendRegularFormat(StringBuilder sb, TimeSpan timeSpan, string formatString, int start, int length) + private static int FindCloseQuoteMark(ReadOnlySpan format, int openQuoteIndex, int endIndex, char readChar) + { + var i = openQuoteIndex + 1; + var quoteFound = false; + while (i < endIndex) + { + var readCharQuote = format[i]; + i++; + + if (readCharQuote == '\\') + { + i++; + continue; + } + + if (readCharQuote == readChar) + { + i--; + quoteFound = true; + break; + } + } + + return quoteFound ? i : -1; + } + + private static void AppendRegularFormat(StringBuilder sb, TimeSpan timeSpan, ReadOnlySpan format) { Span destination = stackalloc char[256]; - var format = formatString.AsSpan(start, length); if (timeSpan.TryFormat(destination, out var charsWritten, format)) { @@ -132,7 +177,8 @@ private static void AppendRegularFormat(StringBuilder sb, TimeSpan timeSpan, str private static void AppendBigHFormat(StringBuilder sb, TimeSpan timeSpan, int count) { - Span destination = stackalloc char[8]; + const int TIMESPAN_MAX_HOURS_LENGTH = 9; // The maximum integer hours a TimeSpan can hold is 256204778. + Span destination = stackalloc char[TIMESPAN_MAX_HOURS_LENGTH]; Span format = stackalloc char[count]; format.Fill('0');