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');